From 64d5905f63c169597ecf4439513a2d443b5093cb Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 15 Sep 2026 09:56:04 +0700 Subject: [PATCH 01/94] feat(platform)!: token shielded pools A token can own its own Orchard shielded pool from protocol version 14. `TokenConfiguration::V1` adds `hasShieldedPool`; contracts with the flag get a pool at `[Tokens, TOKEN_SHIELDED_POOLS_KEY, token_id]` laid out like the credit pool, and three batch token transitions (TokenShield, TokenUnshield, TokenShieldedTransfer) move tokens into, out of and inside it. The identity signs and pays the fee in credits; the token id, owner id and, for an unshield, recipient and amount are bound into the Orchard sighash; pool balances are a term of the token conservation check; touched pools have their anchors recorded and pruned at block end. The six shielded queries take an optional token_id, the proof verifier and SDK route on it, and DPP builders plus wasm bindings expose the new transitions. Co-Authored-By: Claude Fable 5.1 --- book/src/SUMMARY.md | 1 + book/src/data-model/token-shielded-pools.md | 163 +++ book/src/fees/shielded-fees.md | 16 + .../protos/platform/v0/platform.proto | 9 + .../src/balances/total_tokens_balance/mod.rs | 38 +- .../token_configuration/accessors/mod.rs | 207 ++-- .../token_configuration/accessors/v1/mod.rs | 14 + .../apply_token_configuration_item/mod.rs | 1 + .../mod.rs | 14 +- .../can_apply_token_configuration_item/mod.rs | 18 +- .../v1/mod.rs | 2 +- .../v0/mod.rs | 15 + .../token_configuration/mod.rs | 49 + .../token_configuration/v1/accessors/mod.rs | 194 +++ .../token_configuration/v1/mod.rs | 54 + packages/rs-dpp/src/errors/consensus/codes.rs | 1 + .../src/errors/consensus/state/state_error.rs | 5 +- .../src/errors/consensus/state/token/mod.rs | 2 + .../token_shielded_pool_not_enabled_error.rs | 45 + packages/rs-dpp/src/shielded/builder/mod.rs | 59 + .../src/shielded/builder/token_shield.rs | 151 +++ .../builder/token_shielded_transfer.rs | 187 +++ .../src/shielded/builder/token_unshield.rs | 195 +++ packages/rs-dpp/src/shielded/mod.rs | 5 +- packages/rs-dpp/src/shielded/sighash.rs | 131 +++ packages/rs-dpp/src/state_transition/mod.rs | 7 + .../batched_transition/document_transition.rs | 14 +- .../batched_transition/mod.rs | 3 + .../batched_transition/resolvers.rs | 46 +- .../token_shield_transition/mod.rs | 102 ++ .../token_shield_transition/v0/mod.rs | 63 + .../token_shield_transition/v0/v0_methods.rs | 65 + .../token_shield_transition/v0_methods.rs | 64 + .../validate_structure/mod.rs | 36 + .../validate_structure/v0/mod.rs | 197 ++++ .../token_shielded_transfer_transition/mod.rs | 112 ++ .../v0/mod.rs | 57 + .../v0/v0_methods.rs | 50 + .../v0_methods.rs | 51 + .../validate_structure/mod.rs | 36 + .../validate_structure/v0/mod.rs | 142 +++ .../batched_transition/token_transition.rs | 90 +- .../token_transition_action_type.rs | 20 + .../token_unshield_transition/mod.rs | 106 ++ .../token_unshield_transition/v0/mod.rs | 68 ++ .../v0/v0_methods.rs | 80 ++ .../token_unshield_transition/v0_methods.rs | 77 ++ .../validate_structure/mod.rs | 36 + .../validate_structure/v0/mod.rs | 198 ++++ .../document/batch_transition/methods/mod.rs | 168 +++ .../batch_transition/methods/v1/mod.rs | 68 ++ .../document/batch_transition/mod.rs | 8 +- .../batch_transition/resolvers/v0/mod.rs | 6 +- .../batch_transition/v1/v1_methods.rs | 223 ++++ .../validate_basic_structure/v0/mod.rs | 12 + packages/rs-dpp/src/tokens/token_event.rs | 64 + .../src/execution/check_tx/v0/mod.rs | 29 +- .../engine/run_block_proposal/v0/mod.rs | 8 + .../block_processing_end_events/mod.rs | 1 + .../record_token_shielded_pool_anchors/mod.rs | 44 + .../v0/mod.rs | 57 + .../create_genesis_state/test/tokens.rs | 2 +- .../v0/mod.rs | 6 + .../process_raw_state_transitions/v0/mod.rs | 43 + .../v0/mod.rs | 2 +- .../processor/traits/is_allowed.rs | 38 +- .../processor/traits/shielded_proof.rs | 173 ++- .../batch/action_validation/token/mod.rs | 4 + .../token_shield_transition_action/mod.rs | 66 ++ .../state_v0/mod.rs | 142 +++ .../token/token_shielded_pool_common/mod.rs | 293 +++++ .../mod.rs | 66 ++ .../state_v0/mod.rs | 144 +++ .../token_unshield_transition_action/mod.rs | 66 ++ .../state_v0/mod.rs | 230 ++++ .../state_transitions/batch/mod.rs | 3 +- .../state_transitions/batch/state/v0/mod.rs | 37 + .../distribution/perpetual/block_based.rs | 2 +- .../batch/tests/token/mod.rs | 1 + .../batch/tests/token/shielded/mod.rs | 1043 +++++++++++++++++ .../batch/transformer/v0/mod.rs | 33 + .../basic_structure/v0/mod.rs | 12 + .../basic_structure/v0/mod.rs | 12 + .../mod.rs | 18 +- .../group_queries/group_actions/v0/mod.rs | 7 +- .../src/query/shielded/anchors/mod.rs | 2 + .../src/query/shielded/anchors/v0/mod.rs | 41 +- .../src/query/shielded/encrypted_notes/mod.rs | 4 + .../query/shielded/encrypted_notes/v0/mod.rs | 26 +- .../rs-drive-abci/src/query/shielded/mod.rs | 135 +++ .../query/shielded/most_recent_anchor/mod.rs | 2 + .../shielded/most_recent_anchor/v0/mod.rs | 11 +- .../src/query/shielded/notes_count/mod.rs | 2 + .../src/query/shielded/notes_count/v0/mod.rs | 28 +- .../src/query/shielded/nullifiers/mod.rs | 1 + .../src/query/shielded/nullifiers/v0/mod.rs | 25 +- .../src/query/shielded/pool_state/mod.rs | 2 + .../src/query/shielded/pool_state/v0/mod.rs | 17 +- packages/rs-drive-proof-verifier/src/proof.rs | 204 +++- packages/rs-drive-proof-verifier/src/types.rs | 43 + .../contract/insert/insert_contract/v1/mod.rs | 11 + .../contract/update/update_contract/v1/mod.rs | 11 + .../src/drive/initialization/v4/mod.rs | 4 + .../src/drive/shielded/has_anchor/mod.rs | 32 + .../src/drive/shielded/has_anchor/v0/mod.rs | 20 +- .../src/drive/shielded/has_nullifier/mod.rs | 32 + .../drive/shielded/has_nullifier/v0/mod.rs | 20 +- .../src/drive/shielded/insert_note/mod.rs | 30 + .../src/drive/shielded/insert_note/v0/mod.rs | 23 +- .../drive/shielded/insert_nullifiers/mod.rs | 23 + .../shielded/insert_nullifiers/v0/mod.rs | 18 +- .../src/drive/shielded/notes_count/mod.rs | 30 + .../src/drive/shielded/notes_count/v0/mod.rs | 14 +- packages/rs-drive/src/drive/shielded/paths.rs | 247 +++- .../src/drive/shielded/prune_anchors/mod.rs | 36 + .../drive/shielded/prune_anchors/v0/mod.rs | 31 +- .../drive/shielded/read_total_balance/mod.rs | 32 + .../shielded/read_total_balance/v0/mod.rs | 14 +- .../shielded/record_anchor_if_changed/mod.rs | 72 +- .../record_anchor_if_changed/v0/mod.rs | 76 +- .../shielded/update_total_balance/mod.rs | 23 + .../shielded/update_total_balance/v0/mod.rs | 10 +- .../calculate_total_tokens_balance/mod.rs | 4 +- .../calculate_total_tokens_balance/v0/mod.rs | 2 + .../calculate_total_tokens_balance/v1/mod.rs | 58 + packages/rs-drive/src/drive/tokens/mod.rs | 5 + packages/rs-drive/src/drive/tokens/paths.rs | 22 + .../shielded_pool/create_pool_trees/mod.rs | 50 + .../shielded_pool/create_pool_trees/v0/mod.rs | 114 ++ .../tokens/shielded_pool/estimated_costs.rs | 145 +++ .../tokens/shielded_pool/insert_root_tree.rs | 32 + .../src/drive/tokens/shielded_pool/mod.rs | 638 ++++++++++ .../drive/tokens/shielded_pool/shield/mod.rs | 46 + .../tokens/shielded_pool/shield/v0/mod.rs | 50 + .../shielded_pool/shielded_transfer/mod.rs | 49 + .../shielded_pool/shielded_transfer/v0/mod.rs | 35 + .../tokens/shielded_pool/unshield/mod.rs | 48 + .../tokens/shielded_pool/unshield/v0/mod.rs | 50 + .../batch/token/mod.rs | 3 + .../batch/token/token_shield_transition.rs | 59 + .../token_shielded_transfer_transition.rs | 59 + .../batch/token/token_transition.rs | 17 + .../batch/token/token_unshield_transition.rs | 60 + .../token_transition/mod.rs | 32 + .../token_shield_transition_action/mod.rs | 62 + .../transformer.rs | 53 + .../token_shield_transition_action/v0/mod.rs | 130 ++ .../v0/transformer.rs | 111 ++ .../mod.rs | 55 + .../transformer.rs | 53 + .../v0/mod.rs | 120 ++ .../v0/transformer.rs | 111 ++ .../token_transition_action_type.rs | 5 + .../token_unshield_transition_action/mod.rs | 69 ++ .../transformer.rs | 53 + .../v0/mod.rs | 139 +++ .../v0/transformer.rs | 113 ++ .../src/util/batch/drive_op_batch/token.rs | 76 ++ .../verify_most_recent_shielded_anchor/mod.rs | 32 + .../v0/mod.rs | 18 +- .../shielded/verify_shielded_anchors/mod.rs | 32 + .../verify_shielded_anchors/v0/mod.rs | 18 +- .../verify_shielded_encrypted_notes/mod.rs | 39 + .../verify_shielded_encrypted_notes/v0/mod.rs | 26 +- .../verify_shielded_notes_count/mod.rs | 32 + .../verify_shielded_notes_count/v0/mod.rs | 18 +- .../verify_shielded_nullifiers/mod.rs | 35 + .../verify_shielded_nullifiers/v0/mod.rs | 21 +- .../verify_shielded_pool_state/mod.rs | 32 + .../verify_shielded_pool_state/v0/mod.rs | 18 +- .../token_transition.rs | 24 + .../v0/mod.rs | 66 ++ .../dpp_versions/dpp_contract_versions/mod.rs | 4 + .../dpp_versions/dpp_contract_versions/v1.rs | 5 + .../dpp_versions/dpp_contract_versions/v2.rs | 5 + .../dpp_versions/dpp_contract_versions/v3.rs | 5 + .../dpp_versions/dpp_contract_versions/v4.rs | 5 + .../dpp_versions/dpp_contract_versions/v5.rs | 5 + .../dpp_versions/dpp_contract_versions/v6.rs | 7 + .../drive_abci_method_versions/mod.rs | 3 + .../drive_abci_method_versions/v1.rs | 1 + .../drive_abci_method_versions/v10.rs | 1 + .../drive_abci_method_versions/v2.rs | 1 + .../drive_abci_method_versions/v3.rs | 1 + .../drive_abci_method_versions/v4.rs | 1 + .../drive_abci_method_versions/v5.rs | 1 + .../drive_abci_method_versions/v6.rs | 1 + .../drive_abci_method_versions/v7.rs | 1 + .../drive_abci_method_versions/v8.rs | 1 + .../drive_abci_method_versions/v9.rs | 1 + .../drive_abci_validation_versions/mod.rs | 12 + .../drive_abci_validation_versions/v1.rs | 7 + .../drive_abci_validation_versions/v10.rs | 7 + .../drive_abci_validation_versions/v2.rs | 7 + .../drive_abci_validation_versions/v3.rs | 7 + .../drive_abci_validation_versions/v4.rs | 7 + .../drive_abci_validation_versions/v5.rs | 7 + .../drive_abci_validation_versions/v6.rs | 7 + .../drive_abci_validation_versions/v7.rs | 7 + .../drive_abci_validation_versions/v8.rs | 7 + .../drive_abci_validation_versions/v9.rs | 7 + .../mod.rs | 3 + .../v1.rs | 3 + .../v2.rs | 3 + .../v3.rs | 3 + .../v4.rs | 3 + .../drive_token_method_versions/mod.rs | 9 + .../drive_token_method_versions/v1.rs | 4 + .../drive_token_method_versions/v2.rs | 1 + .../feature_initial_protocol_versions.rs | 4 + .../src/version/mocks/v3_test.rs | 1 + .../rs-platform-version/src/version/v14.rs | 19 +- packages/rs-sdk/src/platform/query.rs | 153 ++- .../batch_transition/token_transition/mod.rs | 21 + .../token_transition/shield.rs | 41 + .../token_transition/shielded_transfer.rs | 36 + .../token_transition/unshield.rs | 47 + .../src/errors/consensus/consensus_error.rs | 5 +- packages/wasm-dpp2/src/group/token_event.rs | 9 + .../batch/token_transition.rs | 38 +- .../batch/token_transitions/mod.rs | 3 + .../batch/token_transitions/token_shield.rs | 146 +++ .../token_shielded_transfer.rs | 137 +++ .../batch/token_transitions/token_unshield.rs | 164 +++ .../configuration/token_configuration.rs | 88 +- 225 files changed, 11633 insertions(+), 357 deletions(-) create mode 100644 book/src/data-model/token-shielded-pools.md create mode 100644 packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/v1/mod.rs create mode 100644 packages/rs-dpp/src/data_contract/associated_token/token_configuration/v1/accessors/mod.rs create mode 100644 packages/rs-dpp/src/data_contract/associated_token/token_configuration/v1/mod.rs create mode 100644 packages/rs-dpp/src/errors/consensus/state/token/token_shielded_pool_not_enabled_error.rs create mode 100644 packages/rs-dpp/src/shielded/builder/token_shield.rs create mode 100644 packages/rs-dpp/src/shielded/builder/token_shielded_transfer.rs create mode 100644 packages/rs-dpp/src/shielded/builder/token_unshield.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/v0/v0_methods.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/v0_methods.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/validate_structure/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/validate_structure/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/v0/v0_methods.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/v0_methods.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/validate_structure/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/validate_structure/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/v0/v0_methods.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/v0_methods.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/validate_structure/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/validate_structure/v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/record_token_shielded_pool_anchors/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/record_token_shielded_pool_anchors/v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_transfer_transition_action/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_transfer_transition_action/state_v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/state_v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs create mode 100644 packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v1/mod.rs create mode 100644 packages/rs-drive/src/drive/tokens/shielded_pool/create_pool_trees/mod.rs create mode 100644 packages/rs-drive/src/drive/tokens/shielded_pool/create_pool_trees/v0/mod.rs create mode 100644 packages/rs-drive/src/drive/tokens/shielded_pool/estimated_costs.rs create mode 100644 packages/rs-drive/src/drive/tokens/shielded_pool/insert_root_tree.rs create mode 100644 packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs create mode 100644 packages/rs-drive/src/drive/tokens/shielded_pool/shield/mod.rs create mode 100644 packages/rs-drive/src/drive/tokens/shielded_pool/shield/v0/mod.rs create mode 100644 packages/rs-drive/src/drive/tokens/shielded_pool/shielded_transfer/mod.rs create mode 100644 packages/rs-drive/src/drive/tokens/shielded_pool/shielded_transfer/v0/mod.rs create mode 100644 packages/rs-drive/src/drive/tokens/shielded_pool/unshield/mod.rs create mode 100644 packages/rs-drive/src/drive/tokens/shielded_pool/unshield/v0/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_shield_transition.rs create mode 100644 packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_shielded_transfer_transition.rs create mode 100644 packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_unshield_transition.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/transformer.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/v0/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/v0/transformer.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/transformer.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/v0/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/v0/transformer.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/transformer.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/v0/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/v0/transformer.rs create mode 100644 packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/shield.rs create mode 100644 packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/shielded_transfer.rs create mode 100644 packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/unshield.rs create mode 100644 packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_shield.rs create mode 100644 packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_shielded_transfer.rs create mode 100644 packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_unshield.rs diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index c41f5122fa5..00608c60f57 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -60,6 +60,7 @@ - [Contested Documents](data-model/contested-documents.md) - [Identities](data-model/identities.md) - [Key Budgets and Expiry](data-model/key-limits.md) +- [Token Shielded Pools](data-model/token-shielded-pools.md) # Drive diff --git a/book/src/data-model/token-shielded-pools.md b/book/src/data-model/token-shielded-pools.md new file mode 100644 index 00000000000..b03ddf458e9 --- /dev/null +++ b/book/src/data-model/token-shielded-pools.md @@ -0,0 +1,163 @@ +# Token Shielded Pools + +From protocol version 14 a token can own a shielded pool: an Orchard pool that holds that token +instead of credits. Holders move tokens between their identity balance and the pool with three +token transitions inside a batch, and transfer inside the pool without revealing amounts or +counterparties. This chapter describes the storage, the configuration flag, the transitions, the +validation rules, the block end bookkeeping, the queries and the client builders. + +## Why one pool per token + +The Orchard construction Platform uses for credits has no asset base: a note carries a value +but not an asset id, and the value balance the circuit proves is a single number. Mixing tokens +in one pool would let a spend of token A create a note of token B. A token therefore gets its +own pool, and each pool is a copy of the credit pool's layout rooted under the token. + +## Storage layout + +The credit shielded pool lives at `[ShieldedBalances(52)]/"M"`. Token pools live under the +tokens tree: + +```text +[Tokens(16)] + [TOKEN_SHIELDED_POOLS_KEY(224)] BigSumTree + [token_id] SumTree (the pool) + NOTES[128] CommitmentTree, chunk power 11 (the note commitments and ciphertexts) + NULLIFIERS[64] ProvableCountTree (spent nullifiers) + ANCHORS_IN_POOL[192] anchor -> block height + TOTAL_BALANCE[32] SumItem (tokens currently shielded) + ANCHORS_BY_HEIGHT[96] block height -> anchor (for pruning) +``` + +The five children use the same keys as the credit pool, so the drive primitives that insert +notes and nullifiers, read balances and record anchors are shared: every credit pool method has +a pool-agnostic form taking the pool path, and a token twin that supplies the token's path. The +root of all token pools is a BigSumTree so the amount of every token that is shielded is one +sum, which the token conservation check reads. + +The root tree is created by the version 14 upgrade transition (`transition_to_version_14`) on +an existing chain and by `create_initial_state_structure` version 4 on a new one. A pool's five +trees are created when a contract with the flag is inserted or updated. + +## Configuration + +`TokenConfiguration` gains a format version 1 whose only addition over version 0 is +`hasShieldedPool: bool`. A version 0 configuration behaves as `hasShieldedPool: false`. +The format version is admitted by +`dpp.contract_versions.token_versions.token_configuration_format`: protocol versions 13 and +below allow only version 0, protocol version 14 allows versions 0 and 1. Contract create and +update reject a token configuration outside the bounds with `UnsupportedVersionError`, so a +pre-14 network never stores the flag. + +The flag is immutable. A contract update that changes it is rejected with +`DataContractTokenConfigurationUpdateError` for `hasShieldedPool`, because a pool that was +enabled can hold notes that would become unspendable, and a pool that is enabled late would +need a tree created under an existing token. + +## Transitions + +The three operations are `TokenTransition` variants inside a `Batch` transition, like every +other token operation. The identity signs the batch and pays the fee in credits. Tokens cannot +pay fees, so unlike the credit pool nothing is carved from the bundle's value balance. + +| Transition | Flags | Value balance | Extra sighash data | Effect | +|---|---|---|---|---| +| `TokenShield` | outputs only | `-amount` | none | `amount` leaves the owner's balance and enters the pool as new notes. | +| `TokenUnshield` | spends and outputs | `+amount` | `token_id, owner_id, recipient_id, amount` | Notes are spent; `amount` is credited to `recipient_id`; change comes back as new notes. | +| `TokenShieldedTransfer` | spends and outputs | `0` | `token_id, owner_id` | Notes are spent and recreated; the pool balance is unchanged. | + +Each transition carries the Orchard bundle (`actions`, `anchor`, `proof`, `binding_signature`) +next to the token base transition (`token_id`, contract id, contract position, identity +contract nonce). The extra sighash data is bound into the Orchard sighash by the client and +recomputed by consensus from the transition's own fields, so a bundle proven for one token, +owner, recipient or amount cannot be replayed with another. The layouts are in +`dpp::shielded::sighash` (`token_unshield_extra_sighash_data`, +`token_shielded_transfer_extra_sighash_data`). + +The shield bundle has no spends, so its anchor is not checked against the pool; the client +builds it against the empty tree. Spending bundles must name an anchor the pool has recorded. + +## Validation + +Structure validation checks the amount bounds, the action count against +`SystemLimits::max_shielded_transition_actions`, the encrypted note sizes, a non-empty proof and +a non-zero anchor. + +State validation runs in this order, and the first failure is returned: + +1. The token base transition (contract exists, position valid, nonce). +2. `hasShieldedPool` on the token's configuration, else `TokenShieldedPoolNotEnabledError`. +3. Shield: the owner holds `amount`, the owner's account is not frozen, the token is not + paused. Unshield: the token is not paused, the recipient identity exists, the recipient's + account is not frozen unless the token allows transfers to frozen balances. Shielded + transfer: the token is not paused. +4. Spending bundles: the anchor is recorded in the pool (`InvalidAnchorError`), no nullifier + repeats within the bundle or is already spent (`NullifierAlreadySpentError`), and for an + unshield the pool holds `amount`. +5. Proof verification. The fee for it, `compute_shielded_verification_fee(actions)`, is added + as a precalculated operation before the Halo 2 proof and binding signature are checked, so a + failed proof is a paid failure: the identity is charged, its nonce advances, and nothing + else moves. + +Batch state validation does not run in `CheckTx`. The mempool admission path +(`CheckTxProofVerifier`) verifies the proofs of batch token transitions keyed by the identity +contract nonce, so a proof is verified once per nonce before the block and not again for the +same submission. + +The transitions are gated on the protocol version: below 14 `validate_is_allowed` rejects a +batch carrying any of them with `StateTransitionNotActiveError`. + +## Execution and conservation + +The drive operations are composites of the credit pool primitives re-rooted under the token: + +- shield: remove `amount` from the owner's token balance, append the notes, add `amount` to + the pool's `TOTAL_BALANCE`; +- unshield: insert the nullifiers, append the notes, subtract `amount` from the pool balance, + add `amount` to the recipient's token balance; +- shielded transfer: insert the nullifiers, append the notes. + +The total supply of a token never changes. `calculate_total_tokens_balance` version 1 reads +the token pools BigSumTree and the block end conservation check requires +`identity balances + pool balances == total supply`. + +## Block end + +Every successful or paid token pool transition records its pool in +`StateTransitionsProcessingResult::token_shielded_pools_touched`. At block end +`record_token_shielded_pool_anchors` (enabled by `DRIVE_ABCI_METHOD_VERSIONS_V10`) records +each touched pool's current anchor if the commitment tree changed and prunes that pool's +anchors older than `shielded_anchor_retention_blocks`, always keeping the newest one. Pruning +is driven by touches rather than by an interval because there can be many pools; an idle pool +keeps a valid anchor to spend against. + +## Queries + +The six shielded pool queries (`getShieldedPoolState`, `getShieldedNotesCount`, +`getShieldedAnchors`, `getMostRecentShieldedAnchor`, `getShieldedEncryptedNotes`, +`getShieldedNullifiers`) take an optional `token_id`. Without it they target the credit pool; +with a 32-byte token id they target that token's pool and answer with the same response shape. +A token id is rejected with `InvalidArgument` before protocol version 14 or when it is not 32 +bytes. The proof verifier routes on the same field to the token twins of the verify functions +(`verify_token_shielded_pool_state` and the rest), and the Rust SDK exposes +`TokenShieldedPoolQuery`, `TokenShieldedEncryptedNotesQuery` and `TokenShieldedNullifiersQuery`. + +A token pool transition proves its execution like the token transfer it resembles: a shield +proves the owner's balance, an unshield proves the recipient's balance, and a shielded transfer +proves the spent nullifiers in the token's pool. + +## Client builders + +`dpp::shielded::builder` provides `build_token_shield_transition`, +`build_token_unshield_transition` and `build_token_shielded_transfer_transition`. They prove +the bundle, bind the extra sighash data, and call the batch constructors +(`new_token_shield_transition` and siblings) which sign with the identity key. The wasm +bindings expose `TokenShieldTransition`, `TokenUnshieldTransition` and +`TokenShieldedTransferTransition`, and `TokenConfiguration` accepts `hasShieldedPool` and +reports `formatVersion`. + +## Fees + +See [Shielded Transaction Fees](../fees/shielded-fees.md#token-shielded-pool-fees). In short: +the identity pays the metered cost of the writes plus the proof verification fee, exactly like +`ShieldFromIdentity`, for all three transitions. diff --git a/book/src/fees/shielded-fees.md b/book/src/fees/shielded-fees.md index b9df1216525..57b9bf1c12d 100644 --- a/book/src/fees/shielded-fees.md +++ b/book/src/fees/shielded-fees.md @@ -46,6 +46,22 @@ The fee is derived differently depending on the shielded transition type: | **ShieldFromIdentity** | `fee = metered(storage + processing) + shielded_verification_fee`, paid from the funding identity's balance | Identity balance to pool (protocol version 14). Charged exactly like `Shield`, but on the identity side: the identity signature covers the whole outputs-only bundle, the metered note writes and identity writes go through the standard identity-paid path (`IdentityCreditTransferToAddresses` model), and only the ZK compute fee is added as `additional_fixed_fee_cost`. `user_fee_increase` applies. The identity must hold `amount + fee`; consensus rejects a short balance with `IdentityInsufficientBalanceError`. The pool and the identity are both balance trees, so no system-credit adjustment is emitted. See [Entry-Transition Fees](#entry-transition-fees-shield-shieldfromassetlock-and-shieldfromidentity). | | **IdentityTopUpFromShieldedPool** | `fee = compute_shielded_identity_top_up_fee(num_actions)` = `compute_minimum_shielded_fee(num_actions) + identity_balance_storage_fee`, carved from `value_balance` | Shielded pool to an EXISTING identity's balance (protocol version 14). `value_balance` (the transition's `topUpAmount`) is the gross amount leaving the pool; the identity receives `topUpAmount - fee` and validation requires `topUpAmount >= fee`. Same flat pool-paid model as `Unshield`, with the identity balance write as a flat component built like `Unshield`'s address write but calibrated to its measured cost: the top-up rewrites the existing identity's balance element and its Merk path (320 replaced bytes, 175,320 credits of processing, no storage), folded into one flat figure with headroom like the other shielded components, so `identity_balance_storage_fee = 8 x per_byte_rate` (`SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES`). The target identity and gross amount are bound into the Orchard sighash; the identity must already exist; no system-credit adjustment. | +### Token shielded pool fees + +Token pools (protocol version 14, see [Token Shielded Pools](../data-model/token-shielded-pools.md)) +hold tokens, and tokens cannot pay fees, so none of the three token pool transitions carves a +fee from the bundle. They are `TokenTransition` variants inside a `Batch`, and the batch's +signing identity pays in credits through the standard identity-paid path. + +| Transition | Fee Formula | Explanation | +|---|---|---| +| **TokenShield** | `fee = metered(storage + processing) + shielded_verification_fee`, paid by the signing identity | Same model as `ShieldFromIdentity`: the note appends, the identity token balance write and the pool balance write are metered, and `compute_shielded_verification_fee(num_actions)` is added as a precalculated operation before the proof is verified. `value_balance` is `-amount` in tokens and carries no fee. | +| **TokenUnshield** | `fee = metered(storage + processing) + shielded_verification_fee`, paid by the signing identity | `value_balance` equals the unshielded token amount exactly; the recipient receives the full amount. Nullifier inserts, note appends and the two balance writes are metered. | +| **TokenShieldedTransfer** | `fee = metered(storage + processing) + shielded_verification_fee`, paid by the signing identity | `value_balance` is exactly zero; consensus rejects any other value. Only the nullifier inserts and note appends are metered. | + +Because the verification fee is charged before the proof is checked, an invalid proof is a paid +failure: the identity is charged, its identity contract nonce advances, and no token moves. + For `ShieldedTransfer`, the client constructs the bundle so that `total_spent − total_output = desired_fee`. The Orchard circuit proves that value is conserved (inputs = outputs + value_balance), and the binding signature cryptographically diff --git a/packages/dapi-grpc/protos/platform/v0/platform.proto b/packages/dapi-grpc/protos/platform/v0/platform.proto index 07c3e8dfc04..971c15c5827 100644 --- a/packages/dapi-grpc/protos/platform/v0/platform.proto +++ b/packages/dapi-grpc/protos/platform/v0/platform.proto @@ -3830,11 +3830,15 @@ message GetRecentCompactedAddressBalanceChangesResponse { // --- Shielded Pool Queries --- +// Every shielded pool query below targets the credit shielded pool unless `token_id` is set, +// in which case it targets that token's own shielded pool (protocol version 14+; the 32-byte +// token id). Responses have the same shape for both pools. message GetShieldedEncryptedNotesRequest { message GetShieldedEncryptedNotesRequestV0 { uint64 start_index = 1; uint32 count = 2; bool prove = 3; + optional bytes token_id = 4; // target a token's shielded pool instead of the credit pool } oneof version { GetShieldedEncryptedNotesRequestV0 v0 = 1; } } @@ -3862,6 +3866,7 @@ message GetShieldedEncryptedNotesResponse { message GetShieldedAnchorsRequest { message GetShieldedAnchorsRequestV0 { bool prove = 1; + optional bytes token_id = 2; // target a token's shielded pool instead of the credit pool } oneof version { GetShieldedAnchorsRequestV0 v0 = 1; } } @@ -3883,6 +3888,7 @@ message GetShieldedAnchorsResponse { message GetMostRecentShieldedAnchorRequest { message GetMostRecentShieldedAnchorRequestV0 { bool prove = 1; + optional bytes token_id = 2; // target a token's shielded pool instead of the credit pool } oneof version { GetMostRecentShieldedAnchorRequestV0 v0 = 1; } } @@ -3901,6 +3907,7 @@ message GetMostRecentShieldedAnchorResponse { message GetShieldedPoolStateRequest { message GetShieldedPoolStateRequestV0 { bool prove = 1; + optional bytes token_id = 2; // target a token's shielded pool instead of the credit pool } oneof version { GetShieldedPoolStateRequestV0 v0 = 1; } } @@ -3926,6 +3933,7 @@ message GetShieldedPoolStateResponse { message GetShieldedNotesCountRequest { message GetShieldedNotesCountRequestV0 { bool prove = 1; + optional bytes token_id = 2; // target a token's shielded pool instead of the credit pool } oneof version { GetShieldedNotesCountRequestV0 v0 = 1; } } @@ -3945,6 +3953,7 @@ message GetShieldedNullifiersRequest { message GetShieldedNullifiersRequestV0 { repeated bytes nullifiers = 1; bool prove = 2; + optional bytes token_id = 3; // target a token's shielded pool instead of the credit pool } oneof version { GetShieldedNullifiersRequestV0 v0 = 1; } } diff --git a/packages/rs-dpp/src/balances/total_tokens_balance/mod.rs b/packages/rs-dpp/src/balances/total_tokens_balance/mod.rs index 84aa157643a..7f920242e54 100644 --- a/packages/rs-dpp/src/balances/total_tokens_balance/mod.rs +++ b/packages/rs-dpp/src/balances/total_tokens_balance/mod.rs @@ -9,6 +9,8 @@ pub struct TotalTokensBalance { pub total_tokens_in_platform: SumTokenAmount, /// all the tokens in identity token balances pub total_identity_token_balances: SumTokenAmount, + /// all the tokens held in token shielded pools (0 before protocol version 14) + pub total_token_shielded_pool_balances: SumTokenAmount, } impl fmt::Display for TotalTokensBalance { @@ -21,9 +23,14 @@ impl fmt::Display for TotalTokensBalance { )?; writeln!( f, - " total_identity_token_balances: {}", + " total_identity_token_balances: {},", self.total_identity_token_balances )?; + writeln!( + f, + " total_token_shielded_pool_balances: {}", + self.total_token_shielded_pool_balances + )?; write!(f, "}}") } } @@ -34,6 +41,7 @@ impl TotalTokensBalance { let TotalTokensBalance { total_tokens_in_platform, total_identity_token_balances, + total_token_shielded_pool_balances, } = *self; if total_tokens_in_platform < 0 { @@ -48,6 +56,32 @@ impl TotalTokensBalance { )); } - Ok(total_tokens_in_platform == total_identity_token_balances) + if total_token_shielded_pool_balances < 0 { + return Err(ProtocolError::CriticalCorruptedCreditsCodeExecution( + "Tokens in shielded pools are less than 0".to_string(), + )); + } + + let total_balances = total_identity_token_balances + .checked_add(total_token_shielded_pool_balances) + .ok_or_else(|| { + ProtocolError::CriticalCorruptedCreditsCodeExecution( + "Overflow adding identity and shielded pool token balances".to_string(), + ) + })?; + + Ok(total_tokens_in_platform == total_balances) + } + + /// The balance side of the conservation equation: identity balances plus shielded pool + /// balances. Errors on overflow. + pub fn total_balances(&self) -> Result { + self.total_identity_token_balances + .checked_add(self.total_token_shielded_pool_balances) + .ok_or_else(|| { + ProtocolError::CriticalCorruptedCreditsCodeExecution( + "Overflow adding identity and shielded pool token balances".to_string(), + ) + }) } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/mod.rs index 54066c361a4..6ed02bc2e40 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/mod.rs @@ -1,9 +1,15 @@ pub mod v0; +pub mod v1; use crate::balances::credits::TokenAmount; use crate::data_contract::associated_token::token_configuration::accessors::v0::{ TokenConfigurationV0Getters, TokenConfigurationV0Setters, }; +use crate::data_contract::associated_token::token_configuration::accessors::v1::{ + TokenConfigurationV1Getters, TokenConfigurationV1Setters, +}; +use crate::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; +use crate::data_contract::associated_token::token_configuration::v1::TokenConfigurationV1; use crate::data_contract::associated_token::token_configuration::TokenConfiguration; use crate::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention; use crate::data_contract::associated_token::token_distribution_rules::TokenDistributionRules; @@ -17,157 +23,111 @@ use std::collections::BTreeSet; impl TokenConfigurationV0Getters for TokenConfiguration { /// Returns a reference to the conventions. fn conventions(&self) -> &TokenConfigurationConvention { - match self { - TokenConfiguration::V0(v0) => v0.conventions(), - } + self.as_v0().conventions() } /// Returns a mutable reference to the conventions. fn conventions_mut(&mut self) -> &mut TokenConfigurationConvention { - match self { - TokenConfiguration::V0(v0) => v0.conventions_mut(), - } + self.as_v0_mut().conventions_mut() } fn conventions_change_rules(&self) -> &ChangeControlRules { - match self { - TokenConfiguration::V0(v0) => v0.conventions_change_rules(), - } + self.as_v0().conventions_change_rules() } /// Returns the base supply. fn base_supply(&self) -> TokenAmount { - match self { - TokenConfiguration::V0(v0) => v0.base_supply(), - } + self.as_v0().base_supply() } /// Returns if we keep history. fn keeps_history(&self) -> &TokenKeepsHistoryRules { - match self { - TokenConfiguration::V0(v0) => v0.keeps_history(), - } + self.as_v0().keeps_history() } /// Returns if we keep history. fn keeps_history_mut(&mut self) -> &mut TokenKeepsHistoryRules { - match self { - TokenConfiguration::V0(v0) => v0.keeps_history_mut(), - } + self.as_v0_mut().keeps_history_mut() } /// Returns if we start as paused. fn start_as_paused(&self) -> bool { - match self { - TokenConfiguration::V0(v0) => v0.start_as_paused(), - } + self.as_v0().start_as_paused() } fn is_allowed_transfer_to_frozen_balance(&self) -> bool { - match self { - TokenConfiguration::V0(v0) => v0.is_allowed_transfer_to_frozen_balance(), - } + self.as_v0().is_allowed_transfer_to_frozen_balance() } /// Returns the maximum supply. fn max_supply(&self) -> Option { - match self { - TokenConfiguration::V0(v0) => v0.max_supply(), - } + self.as_v0().max_supply() } /// Returns the max supply change rules. fn max_supply_change_rules(&self) -> &ChangeControlRules { - match self { - TokenConfiguration::V0(v0) => v0.max_supply_change_rules(), - } + self.as_v0().max_supply_change_rules() } fn distribution_rules(&self) -> &TokenDistributionRules { - match self { - TokenConfiguration::V0(v0) => v0.distribution_rules(), - } + self.as_v0().distribution_rules() } fn distribution_rules_mut(&mut self) -> &mut TokenDistributionRules { - match self { - TokenConfiguration::V0(v0) => v0.distribution_rules_mut(), - } + self.as_v0_mut().distribution_rules_mut() } /// Returns the manual minting rules. fn manual_minting_rules(&self) -> &ChangeControlRules { - match self { - TokenConfiguration::V0(v0) => v0.manual_minting_rules(), - } + self.as_v0().manual_minting_rules() } /// Returns the manual burning rules. fn manual_burning_rules(&self) -> &ChangeControlRules { - match self { - TokenConfiguration::V0(v0) => v0.manual_burning_rules(), - } + self.as_v0().manual_burning_rules() } /// Returns the freeze rules. fn freeze_rules(&self) -> &ChangeControlRules { - match self { - TokenConfiguration::V0(v0) => v0.freeze_rules(), - } + self.as_v0().freeze_rules() } /// Returns the unfreeze rules. fn unfreeze_rules(&self) -> &ChangeControlRules { - match self { - TokenConfiguration::V0(v0) => v0.unfreeze_rules(), - } + self.as_v0().unfreeze_rules() } fn destroy_frozen_funds_rules(&self) -> &ChangeControlRules { - match self { - TokenConfiguration::V0(v0) => v0.destroy_frozen_funds_rules(), - } + self.as_v0().destroy_frozen_funds_rules() } fn emergency_action_rules(&self) -> &ChangeControlRules { - match self { - TokenConfiguration::V0(v0) => v0.emergency_action_rules(), - } + self.as_v0().emergency_action_rules() } /// Returns the main control group. fn main_control_group(&self) -> Option { - match self { - TokenConfiguration::V0(v0) => v0.main_control_group(), - } + self.as_v0().main_control_group() } /// Returns the main control group can be modified. fn main_control_group_can_be_modified(&self) -> &AuthorizedActionTakers { - match self { - TokenConfiguration::V0(v0) => v0.main_control_group_can_be_modified(), - } + self.as_v0().main_control_group_can_be_modified() } /// Returns all group positions used in the token configuration fn all_used_group_positions(&self) -> (BTreeSet, bool) { - match self { - TokenConfiguration::V0(v0) => v0.all_used_group_positions(), - } + self.as_v0().all_used_group_positions() } /// Returns all the change contract rules, including those from the distribution rules fn all_change_control_rules(&self) -> Vec<(&str, &ChangeControlRules)> { - match self { - TokenConfiguration::V0(v0) => v0.all_change_control_rules(), - } + self.as_v0().all_change_control_rules() } /// Returns the token description. fn description(&self) -> &Option { - match self { - TokenConfiguration::V0(v0) => v0.description(), - } + self.as_v0().description() } } @@ -175,117 +135,128 @@ impl TokenConfigurationV0Getters for TokenConfiguration { impl TokenConfigurationV0Setters for TokenConfiguration { /// Sets the conventions. fn set_conventions(&mut self, conventions: TokenConfigurationConvention) { - match self { - TokenConfiguration::V0(v0) => v0.set_conventions(conventions), - } + self.as_v0_mut().set_conventions(conventions) } /// Sets the conventions change rules. fn set_conventions_change_rules(&mut self, rules: ChangeControlRules) { - match self { - TokenConfiguration::V0(v0) => v0.set_conventions_change_rules(rules), - } + self.as_v0_mut().set_conventions_change_rules(rules) } /// Allow or not a transfer and mint tokens to frozen identity token balances fn allow_transfer_to_frozen_balance(&mut self, allow: bool) { - match self { - TokenConfiguration::V0(v0) => v0.allow_transfer_to_frozen_balance(allow), - } + self.as_v0_mut().allow_transfer_to_frozen_balance(allow) } /// Sets the base supply. fn set_base_supply(&mut self, base_supply: u64) { - match self { - TokenConfiguration::V0(v0) => v0.set_base_supply(base_supply), - } + self.as_v0_mut().set_base_supply(base_supply) } /// Sets if we should start as paused. Meaning transfers will not work till unpaused fn set_start_as_paused(&mut self, start_as_paused: bool) { - match self { - TokenConfiguration::V0(v0) => v0.set_start_as_paused(start_as_paused), - } + self.as_v0_mut().set_start_as_paused(start_as_paused) } /// Sets the maximum supply. fn set_max_supply(&mut self, max_supply: Option) { - match self { - TokenConfiguration::V0(v0) => v0.set_max_supply(max_supply), - } + self.as_v0_mut().set_max_supply(max_supply) } /// Sets the max supply change rules. fn set_max_supply_change_rules(&mut self, rules: ChangeControlRules) { - match self { - TokenConfiguration::V0(v0) => v0.set_max_supply_change_rules(rules), - } + self.as_v0_mut().set_max_supply_change_rules(rules) } fn set_distribution_rules(&mut self, rules: TokenDistributionRules) { - match self { - TokenConfiguration::V0(v0) => v0.set_distribution_rules(rules), - } + self.as_v0_mut().set_distribution_rules(rules) } /// Sets the manual minting rules. fn set_manual_minting_rules(&mut self, rules: ChangeControlRules) { - match self { - TokenConfiguration::V0(v0) => v0.set_manual_minting_rules(rules), - } + self.as_v0_mut().set_manual_minting_rules(rules) } /// Sets the manual burning rules. fn set_manual_burning_rules(&mut self, rules: ChangeControlRules) { - match self { - TokenConfiguration::V0(v0) => v0.set_manual_burning_rules(rules), - } + self.as_v0_mut().set_manual_burning_rules(rules) } /// Sets the freeze rules. fn set_freeze_rules(&mut self, rules: ChangeControlRules) { - match self { - TokenConfiguration::V0(v0) => v0.set_freeze_rules(rules), - } + self.as_v0_mut().set_freeze_rules(rules) } /// Sets the unfreeze rules. fn set_unfreeze_rules(&mut self, rules: ChangeControlRules) { - match self { - TokenConfiguration::V0(v0) => v0.set_unfreeze_rules(rules), - } + self.as_v0_mut().set_unfreeze_rules(rules) } fn set_destroy_frozen_funds_rules(&mut self, rules: ChangeControlRules) { - match self { - TokenConfiguration::V0(v0) => v0.set_destroy_frozen_funds_rules(rules), - } + self.as_v0_mut().set_destroy_frozen_funds_rules(rules) } fn set_emergency_action_rules(&mut self, rules: ChangeControlRules) { - match self { - TokenConfiguration::V0(v0) => v0.set_emergency_action_rules(rules), - } + self.as_v0_mut().set_emergency_action_rules(rules) } /// Sets the main control group. fn set_main_control_group(&mut self, group: Option) { - match self { - TokenConfiguration::V0(v0) => v0.set_main_control_group(group), - } + self.as_v0_mut().set_main_control_group(group) } /// Sets the main control group can be modified. fn set_main_control_group_can_be_modified(&mut self, action_takers: AuthorizedActionTakers) { - match self { - TokenConfiguration::V0(v0) => v0.set_main_control_group_can_be_modified(action_takers), - } + self.as_v0_mut() + .set_main_control_group_can_be_modified(action_takers) } /// Sets the token description. fn set_description(&mut self, description: Option) { + self.as_v0_mut().set_description(description) + } +} + +impl TokenConfiguration { + /// The V0 part of the configuration, whichever version wraps it. + pub fn as_v0(&self) -> &TokenConfigurationV0 { match self { - TokenConfiguration::V0(v0) => v0.set_description(description), + TokenConfiguration::V0(v0) => v0, + TokenConfiguration::V1(v1) => &v1.base, + } + } + + /// The V0 part of the configuration, mutably, whichever version wraps it. + pub fn as_v0_mut(&mut self) -> &mut TokenConfigurationV0 { + match self { + TokenConfiguration::V0(v0) => v0, + TokenConfiguration::V1(v1) => &mut v1.base, + } + } +} + +impl TokenConfigurationV1Getters for TokenConfiguration { + fn has_shielded_pool(&self) -> bool { + match self { + TokenConfiguration::V0(_) => false, + TokenConfiguration::V1(v1) => v1.has_shielded_pool(), + } + } +} + +impl TokenConfigurationV1Setters for TokenConfiguration { + /// Enabling the pool on a V0 configuration upgrades it in place to V1; disabling it on a + /// V0 configuration is a no-op (a V0 never has a pool). + fn set_has_shielded_pool(&mut self, has_shielded_pool: bool) { + match self { + TokenConfiguration::V0(v0) => { + if has_shielded_pool { + let base = + std::mem::replace(v0, TokenConfigurationV0::default_most_restrictive()); + *self = TokenConfiguration::V1(TokenConfigurationV1::from_v0(base, true)); + } + } + TokenConfiguration::V1(v1) => v1.set_has_shielded_pool(has_shielded_pool), } } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/v1/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/v1/mod.rs new file mode 100644 index 00000000000..4ccdffa4c18 --- /dev/null +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/v1/mod.rs @@ -0,0 +1,14 @@ +/// Getters introduced by `TokenConfigurationV1`. +/// +/// Implemented for every configuration version: a V0 configuration answers `false`, since +/// the shielded pool opt-in did not exist before V1. +pub trait TokenConfigurationV1Getters { + /// Whether the token has its own shielded pool. + fn has_shielded_pool(&self) -> bool; +} + +/// Setters introduced by `TokenConfigurationV1`. +pub trait TokenConfigurationV1Setters { + /// Sets whether the token has its own shielded pool. + fn set_has_shielded_pool(&mut self, has_shielded_pool: bool); +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/mod.rs index 9468c91ca76..ffc9186eac1 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/mod.rs @@ -13,6 +13,7 @@ impl TokenConfiguration { pub fn apply_token_configuration_item(&mut self, change_item: TokenConfigurationChangeItem) { match self { TokenConfiguration::V0(v0) => v0.apply_token_configuration_item(change_item), + TokenConfiguration::V1(v1) => v1.base.apply_token_configuration_item(change_item), } } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/mod.rs index d6d5fbdad99..1ef16659c4d 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/mod.rs @@ -10,11 +10,8 @@ impl TokenConfiguration { &self, change_item: &TokenConfigurationChangeItem, ) -> AuthorizedActionTakers { - match self { - TokenConfiguration::V0(v0) => { - v0.controlling_action_takers_for_configuration_item(change_item) - } - } + self.as_v0() + .controlling_action_takers_for_configuration_item(change_item) } /// Returns the authorized action takers for a specific `TokenConfigurationChangeItem`. @@ -28,10 +25,7 @@ impl TokenConfiguration { &self, change_item: &TokenConfigurationChangeItem, ) -> AuthorizedActionTakers { - match self { - TokenConfiguration::V0(v0) => { - v0.authorized_action_takers_for_configuration_item(change_item) - } - } + self.as_v0() + .authorized_action_takers_for_configuration_item(change_item) } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/mod.rs index 8aa4d41a4ba..b203f78bc96 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/mod.rs @@ -24,15 +24,13 @@ impl TokenConfiguration { action_taker: &ActionTaker, goal: ActionGoal, ) -> bool { - match self { - TokenConfiguration::V0(v0) => v0.can_apply_token_configuration_item( - change_item, - contract_owner_id, - main_group, - groups, - action_taker, - goal, - ), - } + self.as_v0().can_apply_token_configuration_item( + change_item, + contract_owner_id, + main_group, + groups, + action_taker, + goal, + ) } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_groups_exist/v1/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_groups_exist/v1/mod.rs index 943450779e4..88732208e9e 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_groups_exist/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_groups_exist/v1/mod.rs @@ -20,7 +20,7 @@ impl TokenConfiguration { return legacy_result; } - let TokenConfiguration::V0(configuration) = self; + let configuration = self.as_v0(); for (_, rules) in configuration.all_change_control_rules() { for action_takers in [ diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/v0/mod.rs index 97b3a1a9789..46bbd1503ce 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/v0/mod.rs @@ -1,5 +1,6 @@ use crate::consensus::basic::data_contract::DataContractTokenConfigurationUpdateError; use crate::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters; use crate::data_contract::associated_token::token_configuration::TokenConfiguration; use crate::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; use crate::data_contract::associated_token::token_marketplace_rules::accessors::v0::TokenMarketplaceRulesV0Getters; @@ -448,6 +449,20 @@ impl TokenConfiguration { ); } + // The shielded pool opt-in is decided at creation: enabling it later would need the + // pool subtree created by the update, and a pool holding notes can never be removed. + if self.has_shielded_pool() != new_config.has_shielded_pool() { + return SimpleConsensusValidationResult::new_with_error( + DataContractTokenConfigurationUpdateError::new( + "update".to_string(), + "hasShieldedPool".to_string(), + self.clone(), + new_config.clone(), + ) + .into(), + ); + } + // If we reach here with no errors, return an empty result SimpleConsensusValidationResult::new() } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs index 8c0f076f7d1..b8d6bd79816 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs @@ -1,10 +1,14 @@ +use crate::consensus::basic::unsupported_version_error::UnsupportedVersionError; use crate::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; +use crate::data_contract::associated_token::token_configuration::v1::TokenConfigurationV1; #[cfg(feature = "json-conversion")] use crate::serialization::JsonConvertible; #[cfg(feature = "value-conversion")] use crate::serialization::ValueConvertible; +use crate::validation::SimpleConsensusValidationResult; use bincode::{Decode, DecodeUntrusted, Encode}; use derive_more::From; +use platform_version::version::PlatformVersion; use serde::{Deserialize, Serialize}; use std::borrow::Cow; use std::fmt; @@ -12,6 +16,7 @@ use std::fmt; pub mod accessors; mod methods; pub mod v0; +pub mod v1; #[cfg_attr(feature = "json-conversion", derive(JsonConvertible))] #[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] @@ -22,11 +27,54 @@ pub mod v0; pub enum TokenConfiguration { #[serde(rename = "0")] V0(TokenConfigurationV0), + /// V0 plus the per-token shielded pool opt-in. Admitted from protocol version 14 + /// (`token_versions.token_configuration_format`). + #[serde(rename = "1")] + V1(TokenConfigurationV1), } impl TokenConfiguration { pub fn as_cow_v0(&self) -> Cow<'_, TokenConfigurationV0> { match self { TokenConfiguration::V0(v0) => Cow::Borrowed(v0), + TokenConfiguration::V1(v1) => Cow::Borrowed(&v1.base), + } + } + + /// The `$formatVersion` this configuration is serialized with. + pub fn format_version(&self) -> u16 { + match self { + TokenConfiguration::V0(_) => 0, + TokenConfiguration::V1(_) => 1, + } + } + + /// Checks the configuration's format version against the bounds the platform version + /// admits (`dpp.contract_versions.token_versions.token_configuration_format`). + /// + /// A format above the bound is a consensus error, not a decode failure: nodes running + /// software that knows the newer variant must still refuse it until the protocol version + /// that introduces it activates, so a mixed-version network agrees. + pub fn validate_format_version( + &self, + platform_version: &PlatformVersion, + ) -> SimpleConsensusValidationResult { + let bounds = &platform_version + .dpp + .contract_versions + .token_versions + .token_configuration_format; + let format_version = self.format_version(); + if format_version < bounds.min_version || format_version > bounds.max_version { + SimpleConsensusValidationResult::new_with_error( + UnsupportedVersionError::new( + format_version, + bounds.min_version, + bounds.max_version, + ) + .into(), + ) + } else { + SimpleConsensusValidationResult::new() } } } @@ -35,6 +83,7 @@ impl fmt::Display for TokenConfiguration { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { TokenConfiguration::V0(v0) => write!(f, "{}", v0), + TokenConfiguration::V1(v1) => write!(f, "{}", v1), } } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v1/accessors/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v1/accessors/mod.rs new file mode 100644 index 00000000000..79b17a16c7d --- /dev/null +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v1/accessors/mod.rs @@ -0,0 +1,194 @@ +use crate::balances::credits::TokenAmount; +use crate::data_contract::associated_token::token_configuration::accessors::v0::{ + TokenConfigurationV0Getters, TokenConfigurationV0Setters, +}; +use crate::data_contract::associated_token::token_configuration::accessors::v1::{ + TokenConfigurationV1Getters, TokenConfigurationV1Setters, +}; +use crate::data_contract::associated_token::token_configuration::v1::TokenConfigurationV1; +use crate::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention; +use crate::data_contract::associated_token::token_distribution_rules::TokenDistributionRules; +use crate::data_contract::associated_token::token_keeps_history_rules::TokenKeepsHistoryRules; +use crate::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; +use crate::data_contract::change_control_rules::ChangeControlRules; +use crate::data_contract::GroupContractPosition; +use std::collections::BTreeSet; + +/// Every V0 getter reads through to the nested `base` configuration. +impl TokenConfigurationV0Getters for TokenConfigurationV1 { + fn conventions(&self) -> &TokenConfigurationConvention { + self.base.conventions() + } + + fn conventions_mut(&mut self) -> &mut TokenConfigurationConvention { + self.base.conventions_mut() + } + + fn conventions_change_rules(&self) -> &ChangeControlRules { + self.base.conventions_change_rules() + } + + fn base_supply(&self) -> TokenAmount { + self.base.base_supply() + } + + fn keeps_history(&self) -> &TokenKeepsHistoryRules { + self.base.keeps_history() + } + + fn keeps_history_mut(&mut self) -> &mut TokenKeepsHistoryRules { + self.base.keeps_history_mut() + } + + fn start_as_paused(&self) -> bool { + self.base.start_as_paused() + } + + fn is_allowed_transfer_to_frozen_balance(&self) -> bool { + self.base.is_allowed_transfer_to_frozen_balance() + } + + fn max_supply(&self) -> Option { + self.base.max_supply() + } + + fn max_supply_change_rules(&self) -> &ChangeControlRules { + self.base.max_supply_change_rules() + } + + fn distribution_rules(&self) -> &TokenDistributionRules { + self.base.distribution_rules() + } + + fn distribution_rules_mut(&mut self) -> &mut TokenDistributionRules { + self.base.distribution_rules_mut() + } + + fn manual_minting_rules(&self) -> &ChangeControlRules { + self.base.manual_minting_rules() + } + + fn manual_burning_rules(&self) -> &ChangeControlRules { + self.base.manual_burning_rules() + } + + fn freeze_rules(&self) -> &ChangeControlRules { + self.base.freeze_rules() + } + + fn unfreeze_rules(&self) -> &ChangeControlRules { + self.base.unfreeze_rules() + } + + fn destroy_frozen_funds_rules(&self) -> &ChangeControlRules { + self.base.destroy_frozen_funds_rules() + } + + fn emergency_action_rules(&self) -> &ChangeControlRules { + self.base.emergency_action_rules() + } + + fn main_control_group(&self) -> Option { + self.base.main_control_group() + } + + fn main_control_group_can_be_modified(&self) -> &AuthorizedActionTakers { + self.base.main_control_group_can_be_modified() + } + + fn all_used_group_positions(&self) -> (BTreeSet, bool) { + self.base.all_used_group_positions() + } + + fn all_change_control_rules(&self) -> Vec<(&str, &ChangeControlRules)> { + self.base.all_change_control_rules() + } + + fn description(&self) -> &Option { + self.base.description() + } +} + +/// Every V0 setter writes through to the nested `base` configuration. +impl TokenConfigurationV0Setters for TokenConfigurationV1 { + fn set_conventions(&mut self, conventions: TokenConfigurationConvention) { + self.base.set_conventions(conventions) + } + + fn set_conventions_change_rules(&mut self, rules: ChangeControlRules) { + self.base.set_conventions_change_rules(rules) + } + + fn allow_transfer_to_frozen_balance(&mut self, allow: bool) { + self.base.allow_transfer_to_frozen_balance(allow) + } + + fn set_base_supply(&mut self, base_supply: TokenAmount) { + self.base.set_base_supply(base_supply) + } + + fn set_start_as_paused(&mut self, start_as_paused: bool) { + self.base.set_start_as_paused(start_as_paused) + } + + fn set_max_supply(&mut self, max_supply: Option) { + self.base.set_max_supply(max_supply) + } + + fn set_max_supply_change_rules(&mut self, rules: ChangeControlRules) { + self.base.set_max_supply_change_rules(rules) + } + + fn set_distribution_rules(&mut self, rules: TokenDistributionRules) { + self.base.set_distribution_rules(rules) + } + + fn set_manual_minting_rules(&mut self, rules: ChangeControlRules) { + self.base.set_manual_minting_rules(rules) + } + + fn set_manual_burning_rules(&mut self, rules: ChangeControlRules) { + self.base.set_manual_burning_rules(rules) + } + + fn set_freeze_rules(&mut self, rules: ChangeControlRules) { + self.base.set_freeze_rules(rules) + } + + fn set_unfreeze_rules(&mut self, rules: ChangeControlRules) { + self.base.set_unfreeze_rules(rules) + } + + fn set_destroy_frozen_funds_rules(&mut self, rules: ChangeControlRules) { + self.base.set_destroy_frozen_funds_rules(rules) + } + + fn set_emergency_action_rules(&mut self, rules: ChangeControlRules) { + self.base.set_emergency_action_rules(rules) + } + + fn set_main_control_group(&mut self, group: Option) { + self.base.set_main_control_group(group) + } + + fn set_main_control_group_can_be_modified(&mut self, action_takers: AuthorizedActionTakers) { + self.base + .set_main_control_group_can_be_modified(action_takers) + } + + fn set_description(&mut self, description: Option) { + self.base.set_description(description) + } +} + +impl TokenConfigurationV1Getters for TokenConfigurationV1 { + fn has_shielded_pool(&self) -> bool { + self.has_shielded_pool + } +} + +impl TokenConfigurationV1Setters for TokenConfigurationV1 { + fn set_has_shielded_pool(&mut self, has_shielded_pool: bool) { + self.has_shielded_pool = has_shielded_pool; + } +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v1/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v1/mod.rs new file mode 100644 index 00000000000..a7b32af0f7a --- /dev/null +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v1/mod.rs @@ -0,0 +1,54 @@ +mod accessors; + +use crate::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; +#[cfg(feature = "json-conversion")] +use crate::serialization::json_safe_fields; +use bincode::{Decode, DecodeUntrusted, Encode}; +use serde::{Deserialize, Serialize}; +use std::fmt; + +/// Version 1 of the token configuration: everything `TokenConfigurationV0` carries, plus the +/// per-token shielded pool opt-in. +/// +/// The V0 fields are nested as `base` and flattened on the JSON / Value wire, so a V1 +/// configuration reads exactly like a V0 one with `$formatVersion: "1"` and the extra +/// `hasShieldedPool` key. On the bincode wire it is the V0 bytes followed by the flag, under +/// the enum's variant index 1, so stored V0 configurations decode unchanged. +/// +/// `has_shielded_pool` is decided at token creation and is immutable afterwards: the pool +/// subtree (an Orchard note commitment tree, nullifier set, anchors and a balance) is created +/// together with the token's other trees, and a pool holding notes can never be removed. +/// Enabling a pool on an existing token is not supported by this version. +#[cfg_attr(feature = "json-conversion", json_safe_fields)] +#[derive(Serialize, Deserialize, Decode, Encode, Debug, Clone, PartialEq, Eq, DecodeUntrusted)] +#[serde(rename_all = "camelCase")] +pub struct TokenConfigurationV1 { + /// The V0 configuration this version extends. + #[serde(flatten)] + pub base: TokenConfigurationV0, + /// Whether this token has its own shielded pool. When `true`, holders may move balance + /// into the pool (`TokenShield`), transfer privately inside it (`TokenShieldedTransfer`) + /// and move balance back out to an identity (`TokenUnshield`). + #[serde(default)] + pub has_shielded_pool: bool, +} + +impl TokenConfigurationV1 { + /// Wraps a V0 configuration, opting the token into a shielded pool when `has_shielded_pool`. + pub fn from_v0(base: TokenConfigurationV0, has_shielded_pool: bool) -> Self { + Self { + base, + has_shielded_pool, + } + } +} + +impl fmt::Display for TokenConfigurationV1 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "TokenConfigurationV1 {{\n base: {},\n has_shielded_pool: {}\n}}", + self.base, self.has_shielded_pool + ) + } +} diff --git a/packages/rs-dpp/src/errors/consensus/codes.rs b/packages/rs-dpp/src/errors/consensus/codes.rs index 8048f541b18..4427884aa6b 100644 --- a/packages/rs-dpp/src/errors/consensus/codes.rs +++ b/packages/rs-dpp/src/errors/consensus/codes.rs @@ -431,6 +431,7 @@ impl ErrorWithCode for StateError { Self::TokenAmountUnderMinimumSaleAmount(_) => 40720, Self::TokenNotForDirectSale(_) => 40721, Self::TokenOncePerIdentityDistributionAlreadyClaimedError(_) => 40722, + Self::TokenShieldedPoolNotEnabledError(_) => 40723, // Group errors: 40800-40899 Self::IdentityNotMemberOfGroupError(_) => 40800, diff --git a/packages/rs-dpp/src/errors/consensus/state/state_error.rs b/packages/rs-dpp/src/errors/consensus/state/state_error.rs index a2793685e97..5578daa6736 100644 --- a/packages/rs-dpp/src/errors/consensus/state/state_error.rs +++ b/packages/rs-dpp/src/errors/consensus/state/state_error.rs @@ -93,7 +93,7 @@ use crate::consensus::state::identity::missing_transfer_key_error::MissingTransf use crate::consensus::state::identity::no_transfer_key_for_core_withdrawal_available_error::NoTransferKeyForCoreWithdrawalAvailableError; use crate::consensus::state::prefunded_specialized_balances::prefunded_specialized_balance_insufficient_error::PrefundedSpecializedBalanceInsufficientError; use crate::consensus::state::prefunded_specialized_balances::prefunded_specialized_balance_not_found_error::PrefundedSpecializedBalanceNotFoundError; -use crate::consensus::state::token::{IdentityDoesNotHaveEnoughTokenBalanceError, IdentityTokenAccountFrozenError, IdentityTokenAccountNotFrozenError, InvalidGroupPositionError, NewAuthorizedActionTakerGroupDoesNotExistError, NewAuthorizedActionTakerIdentityDoesNotExistError, NewAuthorizedActionTakerMainGroupNotSetError, NewTokensDestinationIdentityDoesNotExistError, TokenMintPastMaxSupplyError, TokenSettingMaxSupplyToLessThanCurrentSupplyError, UnauthorizedTokenActionError, IdentityTokenAccountAlreadyFrozenError, TokenAlreadyPausedError, TokenIsPausedError, TokenNotPausedError, InvalidTokenClaimPropertyMismatch, InvalidTokenClaimNoCurrentRewards, InvalidTokenClaimWrongClaimant, PreProgrammedDistributionTimestampInPastError, TokenTransferRecipientIdentityNotExistError, IdentityHasNotAgreedToPayRequiredTokenAmountError, RequiredTokenPaymentInfoNotSetError, IdentityTryingToPayWithWrongTokenError, TokenDirectPurchaseUserPriceTooLow, TokenAmountUnderMinimumSaleAmount, TokenNotForDirectSale, InvalidTokenPositionStateError, TokenOncePerIdentityDistributionAlreadyClaimedError}; +use crate::consensus::state::token::{IdentityDoesNotHaveEnoughTokenBalanceError, IdentityTokenAccountFrozenError, IdentityTokenAccountNotFrozenError, InvalidGroupPositionError, NewAuthorizedActionTakerGroupDoesNotExistError, NewAuthorizedActionTakerIdentityDoesNotExistError, NewAuthorizedActionTakerMainGroupNotSetError, NewTokensDestinationIdentityDoesNotExistError, TokenMintPastMaxSupplyError, TokenSettingMaxSupplyToLessThanCurrentSupplyError, UnauthorizedTokenActionError, IdentityTokenAccountAlreadyFrozenError, TokenAlreadyPausedError, TokenIsPausedError, TokenNotPausedError, InvalidTokenClaimPropertyMismatch, InvalidTokenClaimNoCurrentRewards, InvalidTokenClaimWrongClaimant, PreProgrammedDistributionTimestampInPastError, TokenTransferRecipientIdentityNotExistError, IdentityHasNotAgreedToPayRequiredTokenAmountError, RequiredTokenPaymentInfoNotSetError, IdentityTryingToPayWithWrongTokenError, TokenDirectPurchaseUserPriceTooLow, TokenAmountUnderMinimumSaleAmount, TokenNotForDirectSale, InvalidTokenPositionStateError, TokenOncePerIdentityDistributionAlreadyClaimedError, TokenShieldedPoolNotEnabledError}; use crate::consensus::state::voting::masternode_incorrect_voter_identity_id_error::MasternodeIncorrectVoterIdentityIdError; use crate::consensus::state::voting::masternode_incorrect_voting_address_error::MasternodeIncorrectVotingAddressError; use crate::consensus::state::voting::masternode_not_found_error::MasternodeNotFoundError; @@ -338,6 +338,9 @@ pub enum StateError { #[error(transparent)] TokenTransferRecipientIdentityNotExistError(TokenTransferRecipientIdentityNotExistError), + #[error(transparent)] + TokenShieldedPoolNotEnabledError(TokenShieldedPoolNotEnabledError), + #[error(transparent)] PreProgrammedDistributionTimestampInPastError(PreProgrammedDistributionTimestampInPastError), diff --git a/packages/rs-dpp/src/errors/consensus/state/token/mod.rs b/packages/rs-dpp/src/errors/consensus/state/token/mod.rs index 8b8a4cb3e22..3e3b9e720ef 100644 --- a/packages/rs-dpp/src/errors/consensus/state/token/mod.rs +++ b/packages/rs-dpp/src/errors/consensus/state/token/mod.rs @@ -26,6 +26,7 @@ mod token_not_for_direct_sale; mod token_not_paused_error; mod token_once_per_identity_distribution_already_claimed_error; mod token_setting_max_supply_to_less_than_current_supply_error; +mod token_shielded_pool_not_enabled_error; mod token_transfer_recipient_identity_not_exist_error; mod unauthorized_token_action_error; @@ -57,5 +58,6 @@ pub use token_not_for_direct_sale::*; pub use token_not_paused_error::*; pub use token_once_per_identity_distribution_already_claimed_error::*; pub use token_setting_max_supply_to_less_than_current_supply_error::*; +pub use token_shielded_pool_not_enabled_error::*; pub use token_transfer_recipient_identity_not_exist_error::*; pub use unauthorized_token_action_error::*; diff --git a/packages/rs-dpp/src/errors/consensus/state/token/token_shielded_pool_not_enabled_error.rs b/packages/rs-dpp/src/errors/consensus/state/token/token_shielded_pool_not_enabled_error.rs new file mode 100644 index 00000000000..b0dd38fd838 --- /dev/null +++ b/packages/rs-dpp/src/errors/consensus/state/token/token_shielded_pool_not_enabled_error.rs @@ -0,0 +1,45 @@ +use crate::consensus::state::state_error::StateError; +use crate::consensus::ConsensusError; +use crate::ProtocolError; +use bincode::{Decode, DecodeUntrusted, Encode}; +use platform_serialization_derive::{ + PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, +}; +use platform_value::Identifier; +use thiserror::Error; + +/// A shielded token operation targeted a token whose configuration has no shielded pool. +#[derive( + Error, + Debug, + Clone, + PartialEq, + Eq, + Encode, + Decode, + PlatformSerialize, + PlatformDeserializeTrusted, + PlatformDeserializeUntrusted, + DecodeUntrusted, +)] +#[error("Token {} does not have a shielded pool.", token_id)] +#[platform_serialize(unversioned)] +pub struct TokenShieldedPoolNotEnabledError { + token_id: Identifier, +} + +impl TokenShieldedPoolNotEnabledError { + pub fn new(token_id: Identifier) -> Self { + Self { token_id } + } + + pub fn token_id(&self) -> &Identifier { + &self.token_id + } +} + +impl From for ConsensusError { + fn from(err: TokenShieldedPoolNotEnabledError) -> Self { + Self::StateError(StateError::TokenShieldedPoolNotEnabledError(err)) + } +} diff --git a/packages/rs-dpp/src/shielded/builder/mod.rs b/packages/rs-dpp/src/shielded/builder/mod.rs index 04a4bc2f2bc..d26b49348bd 100644 --- a/packages/rs-dpp/src/shielded/builder/mod.rs +++ b/packages/rs-dpp/src/shielded/builder/mod.rs @@ -36,6 +36,9 @@ mod shield_from_asset_lock; mod shield_from_identity; mod shielded_transfer; mod shielded_withdrawal; +mod token_shield; +mod token_shielded_transfer; +mod token_unshield; mod unshield; pub use self::shield::build_shield_transition; @@ -49,6 +52,9 @@ pub use shield_from_asset_lock::build_shield_from_asset_lock_transition_with_sig pub use shield_from_identity::build_shield_from_identity_transition; pub use shielded_transfer::build_shielded_transfer_transition; pub use shielded_withdrawal::build_shielded_withdrawal_transition; +pub use token_shield::build_token_shield_transition; +pub use token_shielded_transfer::build_token_shielded_transfer_transition; +pub use token_unshield::build_token_unshield_transition; pub use unshield::build_unshield_transition; use grovedb_commitment_tree::{ @@ -380,10 +386,17 @@ where #[cfg(test)] pub(crate) mod test_helpers { use super::*; + use crate::address_funds::AddressWitness; + use crate::identity::signer::Signer; + use crate::identity::{IdentityPublicKey, KeyType, Purpose, SecurityLevel}; use grovedb_commitment_tree::{ FullViewingKey, Hashable, MerkleHashOrchard, Note, NoteValue, ProvingKey, RandomSeed, Rho, Scope, SpendingKey, NOTE_COMMITMENT_TREE_DEPTH, }; + use platform_value::BinaryData; + use platform_version::version::PlatformVersion; + use rand::rngs::StdRng; + use rand::SeedableRng; use std::sync::OnceLock; static PROVING_KEY: OnceLock = OnceLock::new(); @@ -411,6 +424,52 @@ pub(crate) mod test_helpers { .expect("valid orchard address bytes") } + /// An identity signer that never verifies: it returns a fixed 65-byte signature so batch + /// constructors can be exercised without real keys. + #[derive(Debug)] + pub struct DummyIdentitySigner; + + #[async_trait::async_trait] + impl Signer for DummyIdentitySigner { + async fn sign( + &self, + _key: &IdentityPublicKey, + _data: &[u8], + ) -> Result { + Ok(BinaryData::new(vec![0u8; 65])) + } + + async fn sign_create_witness( + &self, + _key: &IdentityPublicKey, + _data: &[u8], + ) -> Result { + Err(ProtocolError::ShieldedBuildError( + "identity signer never creates address witnesses".to_string(), + )) + } + + fn can_sign_with(&self, _key: &IdentityPublicKey) -> bool { + true + } + } + + /// A critical authentication key an identity could sign a batch transition with. + pub fn test_identity_key() -> IdentityPublicKey { + let mut rng = StdRng::seed_from_u64(42); + let (key, _) = IdentityPublicKey::random_key_with_known_attributes( + 0, + &mut rng, + Purpose::AUTHENTICATION, + SecurityLevel::CRITICAL, + KeyType::ECDSA_SECP256K1, + None, + PlatformVersion::latest(), + ) + .expect("authentication key"); + key + } + /// Creates a SpendableNote with the given value. /// /// The note is cryptographically valid (has a valid commitment) but uses diff --git a/packages/rs-dpp/src/shielded/builder/token_shield.rs b/packages/rs-dpp/src/shielded/builder/token_shield.rs new file mode 100644 index 00000000000..ad347ba39f4 --- /dev/null +++ b/packages/rs-dpp/src/shielded/builder/token_shield.rs @@ -0,0 +1,151 @@ +use grovedb_commitment_tree::OutgoingViewingKey; + +use crate::address_funds::OrchardAddress; +use crate::balances::credits::TokenAmount; +use crate::identity::signer::Signer; +use crate::identity::IdentityPublicKey; +use crate::prelude::{Identifier, IdentityNonce, UserFeeIncrease}; +use crate::shielded::OrchardBundleParams; +use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1; +use crate::state_transition::batch_transition::BatchTransition; +use crate::state_transition::StateTransition; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +use super::{build_output_only_bundle, serialize_authorized_bundle, OrchardProver}; + +/// Builds a `TokenShield` batch transition: proves an outputs-only Orchard bundle paying +/// `amount` of the token to `recipient` inside the token's own shielded pool, then wraps it in +/// a batch transition signed by `owner_id`, the identity whose token balance funds the shield. +/// +/// The identity pays the fee in credits; `amount` tokens leave its balance for the pool at +/// execution. The bundle has no spends, so it carries no anchor of the pool and no extra +/// sighash data: the identity signature over the whole batch binds it. `sender_ovk` lets the +/// sending wallet recover the note it created (recipient, value, memo) from chain data. +#[allow(clippy::too_many_arguments)] +pub async fn build_token_shield_transition, P: OrchardProver>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + recipient: &OrchardAddress, + amount: TokenAmount, + memo: [u8; 36], + sender_ovk: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + prover: &P, + platform_version: &PlatformVersion, +) -> Result { + if amount == 0 { + return Err(ProtocolError::ShieldedBuildError( + "token shield amount must be greater than zero".to_string(), + )); + } + + let bundle = build_output_only_bundle(recipient, amount, memo, sender_ovk, 0, prover)?; + let sb = serialize_authorized_bundle(&bundle); + + BatchTransition::new_token_shield_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + sb.value_balance.unsigned_abs(), + OrchardBundleParams { + actions: sb.actions, + anchor: sb.anchor, + proof: sb.proof, + binding_signature: sb.binding_signature, + }, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + None, + ) + .await +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::shielded::builder::test_helpers::{ + test_identity_key, test_orchard_address, DummyIdentitySigner, TestProver, + }; + use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; + use crate::state_transition::batch_transition::batched_transition::token_transition::TokenTransition; + use crate::state_transition::batch_transition::batched_transition::BatchedTransitionRef; + use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; + use crate::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; + use crate::state_transition::batch_transition::token_shield_transition::v0::v0_methods::TokenShieldTransitionV0Methods; + + #[tokio::test] + async fn rejects_zero_amount() { + let key = test_identity_key(); + let err = build_token_shield_transition( + Identifier::from([1u8; 32]), + Identifier::from([2u8; 32]), + Identifier::from([3u8; 32]), + 0, + &test_orchard_address(), + 0, + [0u8; 36], + None, + &key, + 1, + 0, + &DummyIdentitySigner, + &TestProver, + PlatformVersion::latest(), + ) + .await + .expect_err("zero amount must be rejected") + .to_string(); + assert!(err.contains("greater than zero"), "unexpected error: {err}"); + } + + #[tokio::test] + async fn builds_a_signed_batch_carrying_the_proved_bundle() { + let key = test_identity_key(); + let token_id = Identifier::from([1u8; 32]); + let owner_id = Identifier::from([2u8; 32]); + let state_transition = build_token_shield_transition( + token_id, + owner_id, + Identifier::from([3u8; 32]), + 0, + &test_orchard_address(), + 50_000, + [0u8; 36], + None, + &key, + 7, + 0, + &DummyIdentitySigner, + &TestProver, + PlatformVersion::latest(), + ) + .await + .expect("shield transition"); + + assert_eq!(state_transition.owner_id(), Some(owner_id)); + let StateTransition::Batch(batch) = state_transition else { + panic!("expected a batch transition"); + }; + let transitions: Vec<_> = batch.transitions_iter().collect(); + assert_eq!(transitions.len(), 1); + let BatchedTransitionRef::Token(TokenTransition::Shield(shield)) = transitions[0] else { + panic!("expected a token shield transition"); + }; + assert_eq!(shield.amount(), 50_000); + assert_eq!(shield.base().token_id(), token_id); + assert_eq!(shield.base().identity_contract_nonce(), 7); + // Orchard pads an outputs-only bundle to its two-action minimum. + assert_eq!(shield.actions().len(), 2); + assert!(!shield.proof().is_empty()); + } +} diff --git a/packages/rs-dpp/src/shielded/builder/token_shielded_transfer.rs b/packages/rs-dpp/src/shielded/builder/token_shielded_transfer.rs new file mode 100644 index 00000000000..b67369d6a38 --- /dev/null +++ b/packages/rs-dpp/src/shielded/builder/token_shielded_transfer.rs @@ -0,0 +1,187 @@ +use grovedb_commitment_tree::{ + Anchor, Builder, BundleType, DashMemo, FullViewingKey, NoteValue, PaymentAddress, Scope, + SpendAuthorizingKey, +}; + +use crate::address_funds::OrchardAddress; +use crate::balances::credits::TokenAmount; +use crate::identity::signer::Signer; +use crate::identity::IdentityPublicKey; +use crate::prelude::{Identifier, IdentityNonce, UserFeeIncrease}; +use crate::shielded::{token_shielded_transfer_extra_sighash_data, OrchardBundleParams}; +use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1; +use crate::state_transition::batch_transition::BatchTransition; +use crate::state_transition::StateTransition; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +use super::{prove_and_sign_bundle, serialize_authorized_bundle, OrchardProver, SpendableNote}; + +/// Builds a `TokenShieldedTransfer` batch transition: spends `spends` inside the token's +/// shielded pool, pays `transfer_amount` to `recipient` and any remainder to `change_address`, +/// and wraps the bundle in a batch transition signed by `owner_id`, which pays the fee in +/// credits. +/// +/// Nothing leaves the pool, so the bundle's value balance is exactly zero; consensus rejects +/// anything else. Both outputs are encrypted under the sender's external outgoing viewing key so +/// the sending wallet can recover its own send history. The token id and owner id are bound into +/// the Orchard sighash, so the bundle is tied to this token and this signing identity. +#[allow(clippy::too_many_arguments)] +pub async fn build_token_shielded_transfer_transition< + S: Signer, + P: OrchardProver, +>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + spends: Vec, + recipient: &OrchardAddress, + transfer_amount: TokenAmount, + change_address: &OrchardAddress, + fvk: &FullViewingKey, + ask: &SpendAuthorizingKey, + anchor: Anchor, + memo: [u8; 36], + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + prover: &P, + platform_version: &PlatformVersion, +) -> Result { + if transfer_amount == 0 { + return Err(ProtocolError::ShieldedBuildError( + "token shielded transfer amount must be greater than zero".to_string(), + )); + } + + let total_spent: u64 = spends.iter().map(|s| s.note.value().inner()).sum(); + if transfer_amount > total_spent { + return Err(ProtocolError::ShieldedBuildError(format!( + "token shielded transfer amount {} exceeds total spendable value {}", + transfer_amount, total_spent + ))); + } + let change_amount = total_spent - transfer_amount; + + let mut builder = Builder::::new(BundleType::DEFAULT, anchor); + + for spend in spends { + builder + .add_spend(fvk.clone(), spend.note, spend.merkle_path) + .map_err(|e| { + ProtocolError::ShieldedBuildError(format!("failed to add spend: {:?}", e)) + })?; + } + + let sender_ovk = fvk.to_ovk(Scope::External); + + builder + .add_output( + Some(sender_ovk.clone()), + PaymentAddress::from(recipient), + NoteValue::from_raw(transfer_amount), + memo, + ) + .map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to add output: {:?}", e)))?; + + if change_amount > 0 { + builder + .add_output( + Some(sender_ovk), + PaymentAddress::from(change_address), + NoteValue::from_raw(change_amount), + [0u8; 36], + ) + .map_err(|e| { + ProtocolError::ShieldedBuildError(format!("failed to add change output: {:?}", e)) + })?; + } + + let extra_sighash_data = token_shielded_transfer_extra_sighash_data( + &token_id.to_buffer(), + &owner_id.to_buffer(), + platform_version, + )?; + + let bundle = prove_and_sign_bundle( + builder, + prover, + std::slice::from_ref(ask), + &extra_sighash_data, + )?; + let sb = serialize_authorized_bundle(&bundle); + + if sb.value_balance != 0 { + return Err(ProtocolError::ShieldedBuildError(format!( + "token shielded transfer bundle value balance must be zero, got {}", + sb.value_balance + ))); + } + + BatchTransition::new_token_shielded_transfer_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + OrchardBundleParams { + actions: sb.actions, + anchor: sb.anchor, + proof: sb.proof, + binding_signature: sb.binding_signature, + }, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + None, + ) + .await +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::shielded::builder::test_helpers::{ + test_identity_key, test_orchard_address, test_spendable_note, DummyIdentitySigner, + TestProver, + }; + use grovedb_commitment_tree::SpendingKey; + + #[tokio::test] + async fn rejects_amount_above_spendable_value() { + let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes"); + let fvk = FullViewingKey::from(&sk); + let ask = SpendAuthorizingKey::from(&sk); + let key = test_identity_key(); + let err = build_token_shielded_transfer_transition( + Identifier::from([1u8; 32]), + Identifier::from([2u8; 32]), + Identifier::from([3u8; 32]), + 0, + vec![test_spendable_note(100)], + &test_orchard_address(), + 1_000, + &test_orchard_address(), + &fvk, + &ask, + Anchor::empty_tree(), + [0u8; 36], + &key, + 1, + 0, + &DummyIdentitySigner, + &TestProver, + PlatformVersion::latest(), + ) + .await + .expect_err("overspend must be rejected") + .to_string(); + assert!( + err.contains("exceeds total spendable value"), + "unexpected error: {err}" + ); + } +} diff --git a/packages/rs-dpp/src/shielded/builder/token_unshield.rs b/packages/rs-dpp/src/shielded/builder/token_unshield.rs new file mode 100644 index 00000000000..c377f8eac88 --- /dev/null +++ b/packages/rs-dpp/src/shielded/builder/token_unshield.rs @@ -0,0 +1,195 @@ +use grovedb_commitment_tree::{Anchor, FullViewingKey, SpendAuthorizingKey}; + +use crate::address_funds::OrchardAddress; +use crate::balances::credits::TokenAmount; +use crate::identity::signer::Signer; +use crate::identity::IdentityPublicKey; +use crate::prelude::{Identifier, IdentityNonce, UserFeeIncrease}; +use crate::shielded::{token_unshield_extra_sighash_data, OrchardBundleParams}; +use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1; +use crate::state_transition::batch_transition::BatchTransition; +use crate::state_transition::StateTransition; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +use super::{build_spend_bundle, serialize_authorized_bundle, OrchardProver, SpendableNote}; + +/// Builds a `TokenUnshield` batch transition: spends `spends` from the token's shielded pool, +/// credits `amount` tokens to `recipient_id`, returns the remainder to `change_address` as a +/// new note, and wraps the bundle in a batch transition signed by `owner_id`. +/// +/// Tokens cannot pay fees, so unlike the credit pool's `Unshield` nothing is carved from the +/// value balance: `value_balance == amount` exactly, and the signing identity pays the fee in +/// credits. The token id, owner id, recipient id and amount are bound into the Orchard sighash +/// so the bundle cannot be replayed against a different token, recipient or amount. +#[allow(clippy::too_many_arguments)] +pub async fn build_token_unshield_transition, P: OrchardProver>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + spends: Vec, + recipient_id: Identifier, + amount: TokenAmount, + change_address: &OrchardAddress, + fvk: &FullViewingKey, + ask: &SpendAuthorizingKey, + anchor: Anchor, + memo: [u8; 36], + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + prover: &P, + platform_version: &PlatformVersion, +) -> Result { + if amount == 0 { + return Err(ProtocolError::ShieldedBuildError( + "token unshield amount must be greater than zero".to_string(), + )); + } + if amount > i64::MAX as u64 { + return Err(ProtocolError::ShieldedBuildError(format!( + "token unshield amount {} exceeds maximum allowed value {}", + amount, + i64::MAX as u64 + ))); + } + + let total_spent: u64 = spends.iter().map(|s| s.note.value().inner()).sum(); + if amount > total_spent { + return Err(ProtocolError::ShieldedBuildError(format!( + "token unshield amount {} exceeds total spendable value {}", + amount, total_spent + ))); + } + let change_amount = total_spent - amount; + + let extra_sighash_data = token_unshield_extra_sighash_data( + &token_id.to_buffer(), + &owner_id.to_buffer(), + &recipient_id.to_buffer(), + amount, + platform_version, + )?; + + let bundle = build_spend_bundle( + spends, + change_address, + change_amount, + memo, + fvk, + ask, + anchor, + prover, + &extra_sighash_data, + )?; + let sb = serialize_authorized_bundle(&bundle); + + if sb.value_balance != amount as i64 { + return Err(ProtocolError::ShieldedBuildError(format!( + "token unshield bundle value balance {} does not equal the amount {}", + sb.value_balance, amount + ))); + } + + BatchTransition::new_token_unshield_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + amount, + recipient_id, + OrchardBundleParams { + actions: sb.actions, + anchor: sb.anchor, + proof: sb.proof, + binding_signature: sb.binding_signature, + }, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + None, + ) + .await +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::shielded::builder::test_helpers::{ + test_identity_key, test_orchard_address, test_spendable_note, DummyIdentitySigner, + TestProver, + }; + use grovedb_commitment_tree::SpendingKey; + + fn keys() -> (FullViewingKey, SpendAuthorizingKey) { + let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes"); + (FullViewingKey::from(&sk), SpendAuthorizingKey::from(&sk)) + } + + #[tokio::test] + async fn rejects_amount_above_spendable_value() { + let (fvk, ask) = keys(); + let key = test_identity_key(); + let err = build_token_unshield_transition( + Identifier::from([1u8; 32]), + Identifier::from([2u8; 32]), + Identifier::from([3u8; 32]), + 0, + vec![test_spendable_note(100)], + Identifier::from([4u8; 32]), + 1_000, + &test_orchard_address(), + &fvk, + &ask, + Anchor::empty_tree(), + [0u8; 36], + &key, + 1, + 0, + &DummyIdentitySigner, + &TestProver, + PlatformVersion::latest(), + ) + .await + .expect_err("overspend must be rejected") + .to_string(); + assert!( + err.contains("exceeds total spendable value"), + "unexpected error: {err}" + ); + } + + #[tokio::test] + async fn rejects_zero_amount() { + let (fvk, ask) = keys(); + let key = test_identity_key(); + let err = build_token_unshield_transition( + Identifier::from([1u8; 32]), + Identifier::from([2u8; 32]), + Identifier::from([3u8; 32]), + 0, + vec![test_spendable_note(100)], + Identifier::from([4u8; 32]), + 0, + &test_orchard_address(), + &fvk, + &ask, + Anchor::empty_tree(), + [0u8; 36], + &key, + 1, + 0, + &DummyIdentitySigner, + &TestProver, + PlatformVersion::latest(), + ) + .await + .expect_err("zero amount must be rejected") + .to_string(); + assert!(err.contains("greater than zero"), "unexpected error: {err}"); + } +} diff --git a/packages/rs-dpp/src/shielded/mod.rs b/packages/rs-dpp/src/shielded/mod.rs index 2298376f250..ba9accde943 100644 --- a/packages/rs-dpp/src/shielded/mod.rs +++ b/packages/rs-dpp/src/shielded/mod.rs @@ -28,7 +28,9 @@ pub use sighash::{ identity_create_from_shielded_extra_sighash_data_v0, identity_top_up_from_shielded_extra_sighash_data, identity_top_up_from_shielded_extra_sighash_data_v0, shielded_withdrawal_extra_sighash_data, - shielded_withdrawal_extra_sighash_data_v0, unshield_extra_sighash_data, + shielded_withdrawal_extra_sighash_data_v0, token_shielded_transfer_extra_sighash_data, + token_shielded_transfer_extra_sighash_data_v0, token_unshield_extra_sighash_data, + token_unshield_extra_sighash_data_v0, unshield_extra_sighash_data, unshield_extra_sighash_data_v0, }; @@ -128,6 +130,7 @@ pub const SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES: u64 = 20; /// the serialized actions, Sinsemilla anchor, Halo 2 proof, and RedPallas /// binding signature. Using this struct reduces parameter counts in SDK /// helper functions from 10-12 down to 5-8. +#[derive(Debug, Clone, PartialEq)] pub struct OrchardBundleParams { /// The serialized Orchard actions (spends + outputs). pub actions: Vec, diff --git a/packages/rs-dpp/src/shielded/sighash.rs b/packages/rs-dpp/src/shielded/sighash.rs index edbd5ce9dca..95617294330 100644 --- a/packages/rs-dpp/src/shielded/sighash.rs +++ b/packages/rs-dpp/src/shielded/sighash.rs @@ -298,6 +298,89 @@ pub fn identity_create_from_shielded_extra_sighash_data_v0( data } +/// Builds the transparent `extra_data` bound into a `TokenUnshield`'s platform sighash, with the +/// byte layout `token_id (32) || owner_id (32) || recipient_id (32) || amount (u64 LE)`. +/// +/// A token unshield rides inside an identity-signed batch, but the note owner who authorizes +/// the Orchard spend is not necessarily that identity. The spend-auth and binding signatures +/// must therefore commit to the pool the notes leave (`token_id`), the identity paying the +/// credits fee and submitting the batch (`owner_id`), the identity credited (`recipient_id`) +/// and the gross amount, so a bundle observed in the mempool cannot be re-wrapped by another +/// submitter to a different recipient or against another token's pool (every token pool +/// starts from the same empty-tree anchor). +pub fn token_unshield_extra_sighash_data( + token_id: &[u8; 32], + owner_id: &[u8; 32], + recipient_id: &[u8; 32], + amount: u64, + platform_version: &PlatformVersion, +) -> Result, ProtocolError> { + match platform_version.dpp.methods.shielded_extra_sighash_data { + 0 => Ok(token_unshield_extra_sighash_data_v0( + token_id, + owner_id, + recipient_id, + amount, + )), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "token_unshield_extra_sighash_data".to_string(), + known_versions: vec![0], + received: version, + }), + } +} + +/// v0 byte layout of [`token_unshield_extra_sighash_data`]. Frozen: never mutate; a layout +/// change requires a new `_v1` + version bump. +pub fn token_unshield_extra_sighash_data_v0( + token_id: &[u8; 32], + owner_id: &[u8; 32], + recipient_id: &[u8; 32], + amount: u64, +) -> Vec { + let mut data = Vec::with_capacity(32 + 32 + 32 + 8); + data.extend_from_slice(token_id); + data.extend_from_slice(owner_id); + data.extend_from_slice(recipient_id); + data.extend_from_slice(&amount.to_le_bytes()); + data +} + +/// Builds the transparent `extra_data` bound into a `TokenShieldedTransfer`'s platform sighash, +/// with the byte layout `token_id (32) || owner_id (32)`. +/// +/// Nothing leaves the pool, so there is no amount or destination to bind, but the bundle is +/// still pinned to one token pool and to the identity that pays for it: the same bundle +/// cannot be resubmitted under another fee payer, and the pool it spends from is explicit. +pub fn token_shielded_transfer_extra_sighash_data( + token_id: &[u8; 32], + owner_id: &[u8; 32], + platform_version: &PlatformVersion, +) -> Result, ProtocolError> { + match platform_version.dpp.methods.shielded_extra_sighash_data { + 0 => Ok(token_shielded_transfer_extra_sighash_data_v0( + token_id, owner_id, + )), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "token_shielded_transfer_extra_sighash_data".to_string(), + known_versions: vec![0], + received: version, + }), + } +} + +/// v0 byte layout of [`token_shielded_transfer_extra_sighash_data`]. Frozen: never mutate; a +/// layout change requires a new `_v1` + version bump. +pub fn token_shielded_transfer_extra_sighash_data_v0( + token_id: &[u8; 32], + owner_id: &[u8; 32], +) -> Vec { + let mut data = Vec::with_capacity(64); + data.extend_from_slice(token_id); + data.extend_from_slice(owner_id); + data +} + #[cfg(test)] mod tests { use super::*; @@ -346,6 +429,54 @@ mod tests { assert_eq!(d[14], Pooling::Never as u8); } + #[test] + fn token_unshield_sighash_data_layout() { + use crate::shielded::token_unshield_extra_sighash_data_v0; + // token_id(32) || owner_id(32) || recipient_id(32) || amount(8) + let d = + token_unshield_extra_sighash_data_v0(&[0xAAu8; 32], &[0xBBu8; 32], &[0xCCu8; 32], 7); + assert_eq!(d.len(), 32 + 32 + 32 + 8); + assert_eq!(&d[0..32], &[0xAAu8; 32]); + assert_eq!(&d[32..64], &[0xBBu8; 32]); + assert_eq!(&d[64..96], &[0xCCu8; 32]); + assert_eq!(&d[96..104], &7u64.to_le_bytes()); + // Every bound field changes the preimage. + assert_ne!( + d, + token_unshield_extra_sighash_data_v0(&[0xADu8; 32], &[0xBBu8; 32], &[0xCCu8; 32], 7) + ); + assert_ne!( + d, + token_unshield_extra_sighash_data_v0(&[0xAAu8; 32], &[0xB0u8; 32], &[0xCCu8; 32], 7) + ); + assert_ne!( + d, + token_unshield_extra_sighash_data_v0(&[0xAAu8; 32], &[0xBBu8; 32], &[0xC0u8; 32], 7) + ); + assert_ne!( + d, + token_unshield_extra_sighash_data_v0(&[0xAAu8; 32], &[0xBBu8; 32], &[0xCCu8; 32], 8) + ); + } + + #[test] + fn token_shielded_transfer_sighash_data_layout() { + use crate::shielded::token_shielded_transfer_extra_sighash_data_v0; + // token_id(32) || owner_id(32) + let d = token_shielded_transfer_extra_sighash_data_v0(&[0x11u8; 32], &[0x22u8; 32]); + assert_eq!(d.len(), 64); + assert_eq!(&d[0..32], &[0x11u8; 32]); + assert_eq!(&d[32..64], &[0x22u8; 32]); + assert_ne!( + d, + token_shielded_transfer_extra_sighash_data_v0(&[0x12u8; 32], &[0x22u8; 32]) + ); + assert_ne!( + d, + token_shielded_transfer_extra_sighash_data_v0(&[0x11u8; 32], &[0x23u8; 32]) + ); + } + #[test] fn unshield_sighash_data_layout() { // output_address || unshielding_amount(8) diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index 3c338489596..c750134e58b 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -1163,6 +1163,13 @@ impl StateTransition { BatchedTransitionRef::Token( TokenTransition::SetPriceForDirectPurchase(_), ) => "SetPriceForDirectPurchase", + BatchedTransitionRef::Token(TokenTransition::Shield(_)) => "TokenShield", + BatchedTransitionRef::Token(TokenTransition::Unshield(_)) => { + "TokenUnshield" + } + BatchedTransitionRef::Token(TokenTransition::ShieldedTransfer(_)) => { + "TokenShieldedTransfer" + } }; document_transition_types.push(document_transition_name); } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs index 1b9c13ddea9..d02cad104a0 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs @@ -5,7 +5,7 @@ use derive_more::{Display, From}; use serde::{Deserialize, Serialize}; use bincode::{Encode, Decode, DecodeUntrusted}; use crate::prelude::{IdentityNonce, Revision}; -use crate::state_transition::batch_transition::{DocumentCreateTransition, DocumentDeleteTransition, DocumentReplaceTransition, TokenBurnTransition, TokenConfigUpdateTransition, TokenDestroyFrozenFundsTransition, TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintTransition, TokenClaimTransition, TokenTransferTransition, TokenUnfreezeTransition, TokenDirectPurchaseTransition, TokenSetPriceForDirectPurchaseTransition}; +use crate::state_transition::batch_transition::{DocumentCreateTransition, DocumentDeleteTransition, DocumentReplaceTransition, TokenBurnTransition, TokenConfigUpdateTransition, TokenDestroyFrozenFundsTransition, TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintTransition, TokenClaimTransition, TokenTransferTransition, TokenUnfreezeTransition, TokenDirectPurchaseTransition, TokenSetPriceForDirectPurchaseTransition, TokenShieldTransition, TokenShieldedTransferTransition, TokenUnshieldTransition}; use crate::state_transition::batch_transition::batched_transition::{DocumentIndexOnlyDeleteTransition, DocumentPurchaseTransition, DocumentTransferTransition, DocumentUpdatePriceTransition}; use crate::state_transition::batch_transition::batched_transition::document_index_only_delete_transition::v0::v0_methods::DocumentIndexOnlyDeleteTransitionV0Methods; use crate::state_transition::batch_transition::batched_transition::document_purchase_transition::v0::v0_methods::DocumentPurchaseTransitionV0Methods; @@ -272,6 +272,18 @@ impl BatchTransitionResolversV0 for DocumentTransition { ) -> Option<&TokenSetPriceForDirectPurchaseTransition> { None } + + fn as_transition_token_shield(&self) -> Option<&TokenShieldTransition> { + None + } + + fn as_transition_token_unshield(&self) -> Option<&TokenUnshieldTransition> { + None + } + + fn as_transition_token_shielded_transfer(&self) -> Option<&TokenShieldedTransferTransition> { + None + } } pub trait DocumentTransitionV0Methods { diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs index a71a4271563..3bce73e6f2f 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs @@ -25,10 +25,13 @@ pub mod token_emergency_action_transition; pub mod token_freeze_transition; pub mod token_mint_transition; pub mod token_set_price_for_direct_purchase_transition; +pub mod token_shield_transition; +pub mod token_shielded_transfer_transition; pub mod token_transfer_transition; pub mod token_transition; pub mod token_transition_action_type; pub mod token_unfreeze_transition; +pub mod token_unshield_transition; use crate::prelude::IdentityNonce; use crate::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/resolvers.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/resolvers.rs index d8dabc14ca0..794e6133bd4 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/resolvers.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/resolvers.rs @@ -7,7 +7,9 @@ use crate::state_transition::batch_transition::{ TokenBurnTransition, TokenClaimTransition, TokenConfigUpdateTransition, TokenDestroyFrozenFundsTransition, TokenDirectPurchaseTransition, TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintTransition, - TokenSetPriceForDirectPurchaseTransition, TokenTransferTransition, TokenUnfreezeTransition, + TokenSetPriceForDirectPurchaseTransition, TokenShieldTransition, + TokenShieldedTransferTransition, TokenTransferTransition, TokenUnfreezeTransition, + TokenUnshieldTransition, }; impl BatchTransitionResolversV0 for BatchedTransition { @@ -128,6 +130,27 @@ impl BatchTransitionResolversV0 for BatchedTransition { } } } + + fn as_transition_token_shield(&self) -> Option<&TokenShieldTransition> { + match self { + BatchedTransition::Document(_) => None, + BatchedTransition::Token(token) => token.as_transition_token_shield(), + } + } + + fn as_transition_token_unshield(&self) -> Option<&TokenUnshieldTransition> { + match self { + BatchedTransition::Document(_) => None, + BatchedTransition::Token(token) => token.as_transition_token_unshield(), + } + } + + fn as_transition_token_shielded_transfer(&self) -> Option<&TokenShieldedTransferTransition> { + match self { + BatchedTransition::Document(_) => None, + BatchedTransition::Token(token) => token.as_transition_token_shielded_transfer(), + } + } } impl BatchTransitionResolversV0 for BatchedTransitionRef<'_> { @@ -248,6 +271,27 @@ impl BatchTransitionResolversV0 for BatchedTransitionRef<'_> { } } } + + fn as_transition_token_shield(&self) -> Option<&TokenShieldTransition> { + match self { + BatchedTransitionRef::Document(_) => None, + BatchedTransitionRef::Token(token) => token.as_transition_token_shield(), + } + } + + fn as_transition_token_unshield(&self) -> Option<&TokenUnshieldTransition> { + match self { + BatchedTransitionRef::Document(_) => None, + BatchedTransitionRef::Token(token) => token.as_transition_token_unshield(), + } + } + + fn as_transition_token_shielded_transfer(&self) -> Option<&TokenShieldedTransferTransition> { + match self { + BatchedTransitionRef::Document(_) => None, + BatchedTransitionRef::Token(token) => token.as_transition_token_shielded_transfer(), + } + } } #[cfg(test)] diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/mod.rs new file mode 100644 index 00000000000..a6b8a8869d6 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/mod.rs @@ -0,0 +1,102 @@ +pub mod v0; +mod v0_methods; +pub mod validate_structure; + +use bincode::{Decode, DecodeUntrusted, Encode}; +use derive_more::{Display, From}; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; +pub use v0::TokenShieldTransitionV0; + +/// Moves `amount` of a token from the batch owner's identity token balance into the token's +/// shielded pool. +/// +/// The Orchard bundle is outputs-only (spends disabled) with a value balance of `-amount`: the +/// new notes are created for whoever the owner addressed them to, and nothing is spent from the +/// pool. The identity signature over the batch binds the bundle, the token and the amount, so +/// the bundle carries no extra sighash data of its own. +#[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From, DecodeUntrusted)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] +pub enum TokenShieldTransition { + #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] + V0(TokenShieldTransitionV0), +} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenShieldTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenShieldTransition {} + +impl Default for TokenShieldTransition { + fn default() -> Self { + TokenShieldTransition::V0(TokenShieldTransitionV0::default()) + } +} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::shielded::SerializedAction; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::TokenBaseTransition; + use platform_value::Identifier; + + pub(crate) fn fixture() -> TokenShieldTransition { + TokenShieldTransition::V0(TokenShieldTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 15, + token_contract_position: 1, + data_contract_id: Identifier::new([0xa1; 32]), + token_id: Identifier::new([0xb2; 32]), + using_group_info: None, + }), + amount: 4_000, + actions: vec![SerializedAction { + nullifier: [0x11; 32], + rk: [0x22; 32], + cmx: [0x33; 32], + encrypted_note: vec![0x44; 216], + cv_net: [0x55; 32], + spend_auth_sig: [0x66; 64], + }], + anchor: [0x77; 32], + proof: vec![0x88; 8], + binding_signature: [0x99; 64], + }) + } + + #[test] + fn json_round_trip_keeps_envelope_and_bundle() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + assert_eq!(json["$formatVersion"], "0"); + assert_eq!(json["$baseFormatVersion"], "0"); + assert_eq!(json["$tokenContractPosition"], 1); + assert_eq!(json["amount"], 4_000); + assert!(json["actions"].is_array()); + assert!(json["anchor"].is_string(), "byte fields are base64 in JSON"); + let recovered = TokenShieldTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + let recovered = TokenShieldTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/v0/mod.rs new file mode 100644 index 00000000000..031c36e84cb --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/v0/mod.rs @@ -0,0 +1,63 @@ +pub mod v0_methods; + +use crate::balances::credits::TokenAmount; +use crate::shielded::SerializedAction; +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +use bincode::{Decode, DecodeUntrusted, Encode}; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; +use std::fmt; + +/// The Identifier fields in [`TokenShieldTransition`] +pub use super::super::document_base_transition::IDENTIFIER_FIELDS; + +#[derive(Debug, Clone, Encode, Decode, PartialEq, DecodeUntrusted)] +// Auto-injects `json_safe_u64` on `amount` and `serde_bytes` on the fixed byte arrays / +// `serde_bytes_var` on `proof` (base64 strings in JSON, raw bytes in Value). +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +pub struct TokenShieldTransitionV0 { + /// Token base transition (nonce, contract, token position, token id). + #[cfg_attr(feature = "serde-conversion", serde(flatten))] + pub base: TokenBaseTransition, + /// Tokens leaving the owner's identity balance and entering the token's shielded pool. + /// Must equal the absolute value of the bundle's value balance. + pub amount: TokenAmount, + /// Orchard actions (spend-output pairs; spends disabled). + pub actions: Vec, + /// Sinsemilla root of the token pool's note commitment tree (Orchard anchor). + pub anchor: [u8; 32], + /// Halo 2 proof bytes. + pub proof: Vec, + /// RedPallas binding signature. + pub binding_signature: [u8; 64], +} + +impl Default for TokenShieldTransitionV0 { + fn default() -> Self { + Self { + base: TokenBaseTransition::default(), + amount: 0, + actions: vec![], + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + } + } +} + +impl fmt::Display for TokenShieldTransitionV0 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Token Shield, base: {}, amount: {}, actions: {}", + self.base, + self.amount, + self.actions.len() + ) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/v0/v0_methods.rs new file mode 100644 index 00000000000..165b5f6f2fc --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/v0/v0_methods.rs @@ -0,0 +1,65 @@ +use crate::balances::credits::TokenAmount; +use crate::shielded::SerializedAction; +use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +use crate::state_transition::batch_transition::token_shield_transition::TokenShieldTransitionV0; + +impl TokenBaseTransitionAccessors for TokenShieldTransitionV0 { + fn base(&self) -> &TokenBaseTransition { + &self.base + } + + fn base_mut(&mut self) -> &mut TokenBaseTransition { + &mut self.base + } + + fn set_base(&mut self, base: TokenBaseTransition) { + self.base = base; + } +} + +pub trait TokenShieldTransitionV0Methods: TokenBaseTransitionAccessors { + /// Tokens entering the shielded pool. + fn amount(&self) -> TokenAmount; + + /// Sets the amount entering the shielded pool. + fn set_amount(&mut self, amount: TokenAmount); + + /// The Orchard actions. + fn actions(&self) -> &[SerializedAction]; + + /// The Orchard anchor the bundle was built against. + fn anchor(&self) -> &[u8; 32]; + + /// The Halo 2 proof bytes. + fn proof(&self) -> &[u8]; + + /// The RedPallas binding signature. + fn binding_signature(&self) -> &[u8; 64]; +} + +impl TokenShieldTransitionV0Methods for TokenShieldTransitionV0 { + fn amount(&self) -> TokenAmount { + self.amount + } + + fn set_amount(&mut self, amount: TokenAmount) { + self.amount = amount; + } + + fn actions(&self) -> &[SerializedAction] { + &self.actions + } + + fn anchor(&self) -> &[u8; 32] { + &self.anchor + } + + fn proof(&self) -> &[u8] { + &self.proof + } + + fn binding_signature(&self) -> &[u8; 64] { + &self.binding_signature + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/v0_methods.rs new file mode 100644 index 00000000000..e21a09acec2 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/v0_methods.rs @@ -0,0 +1,64 @@ +use crate::balances::credits::TokenAmount; +use crate::shielded::SerializedAction; +use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +use crate::state_transition::batch_transition::token_shield_transition::v0::v0_methods::TokenShieldTransitionV0Methods; +use crate::state_transition::batch_transition::TokenShieldTransition; + +impl TokenBaseTransitionAccessors for TokenShieldTransition { + fn base(&self) -> &TokenBaseTransition { + match self { + TokenShieldTransition::V0(v0) => &v0.base, + } + } + + fn base_mut(&mut self) -> &mut TokenBaseTransition { + match self { + TokenShieldTransition::V0(v0) => &mut v0.base, + } + } + + fn set_base(&mut self, base: TokenBaseTransition) { + match self { + TokenShieldTransition::V0(v0) => v0.base = base, + } + } +} + +impl TokenShieldTransitionV0Methods for TokenShieldTransition { + fn amount(&self) -> TokenAmount { + match self { + TokenShieldTransition::V0(v0) => v0.amount(), + } + } + + fn set_amount(&mut self, amount: TokenAmount) { + match self { + TokenShieldTransition::V0(v0) => v0.set_amount(amount), + } + } + + fn actions(&self) -> &[SerializedAction] { + match self { + TokenShieldTransition::V0(v0) => v0.actions(), + } + } + + fn anchor(&self) -> &[u8; 32] { + match self { + TokenShieldTransition::V0(v0) => v0.anchor(), + } + } + + fn proof(&self) -> &[u8] { + match self { + TokenShieldTransition::V0(v0) => v0.proof(), + } + } + + fn binding_signature(&self) -> &[u8; 64] { + match self { + TokenShieldTransition::V0(v0) => v0.binding_signature(), + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/validate_structure/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/validate_structure/mod.rs new file mode 100644 index 00000000000..252688fddaa --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/validate_structure/mod.rs @@ -0,0 +1,36 @@ +use crate::state_transition::batch_transition::token_shield_transition::validate_structure::v0::TokenShieldTransitionActionStructureValidationV0; +use crate::state_transition::batch_transition::TokenShieldTransition; +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +mod v0; + +pub trait TokenShieldTransitionStructureValidation { + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenShieldTransitionStructureValidation for TokenShieldTransition { + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .token_shield_transition_structure_validation + { + 0 => self.validate_structure_v0(platform_version), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "TokenShieldTransition::validate_structure".to_string(), + known_versions: vec![0], + received: version, + }), + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/validate_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/validate_structure/v0/mod.rs new file mode 100644 index 00000000000..ccc1e673542 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/validate_structure/v0/mod.rs @@ -0,0 +1,197 @@ +use crate::consensus::basic::token::InvalidTokenAmountError; +use crate::consensus::basic::BasicError; +use crate::consensus::ConsensusError; +use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::MAX_DISTRIBUTION_PARAM; +use crate::state_transition::batch_transition::token_shield_transition::v0::v0_methods::TokenShieldTransitionV0Methods; +use crate::state_transition::batch_transition::TokenShieldTransition; +use crate::state_transition::state_transitions::shielded::common_validation::{ + validate_actions_count, validate_anchor_not_zero, validate_encrypted_note_sizes, + validate_proof_not_empty, +}; +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +pub(super) trait TokenShieldTransitionActionStructureValidationV0 { + fn validate_structure_v0( + &self, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenShieldTransitionActionStructureValidationV0 for TokenShieldTransition { + /// The amount bounds match the other token transitions; the bundle checks are the same + /// stateless checks the credit-pool transitions run (action count, note sizes, non-empty + /// proof, non-zero anchor). The proof itself is verified against state, where its cost + /// can be charged. + fn validate_structure_v0( + &self, + platform_version: &PlatformVersion, + ) -> Result { + if self.amount() > MAX_DISTRIBUTION_PARAM || self.amount() == 0 { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::BasicError(BasicError::InvalidTokenAmountError( + InvalidTokenAmountError::new(MAX_DISTRIBUTION_PARAM, self.amount()), + )), + )); + } + + let result = validate_actions_count( + self.actions(), + platform_version + .system_limits + .max_shielded_transition_actions, + ); + if !result.is_valid() { + return Ok(result); + } + + let result = validate_encrypted_note_sizes(self.actions()); + if !result.is_valid() { + return Ok(result); + } + + let result = validate_proof_not_empty(self.proof()); + if !result.is_valid() { + return Ok(result); + } + + Ok(validate_anchor_not_zero(self.anchor())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::shielded::SerializedAction; + use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::token_shield_transition::v0::TokenShieldTransitionV0; + use platform_value::Identifier; + + fn action() -> SerializedAction { + SerializedAction { + nullifier: [1u8; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + } + } + + fn make_transition(amount: u64) -> TokenShieldTransition { + TokenShieldTransition::V0(TokenShieldTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::default(), + token_id: Identifier::default(), + using_group_info: None, + }), + amount, + actions: vec![action()], + anchor: [9u8; 32], + proof: vec![1u8; 10], + binding_signature: [7u8; 64], + }) + } + + #[test] + fn valid_shield_passes() { + let result = make_transition(100) + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(result.is_valid(), "{:?}", result.errors); + } + + #[test] + fn zero_amount_is_rejected() { + let result = make_transition(0) + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::InvalidTokenAmountError(_) + )) + )); + } + + #[test] + fn amount_above_max_is_rejected() { + let result = make_transition(MAX_DISTRIBUTION_PARAM + 1) + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::InvalidTokenAmountError(_) + )) + )); + } + + #[test] + fn empty_actions_are_rejected() { + let mut transition = make_transition(100); + let TokenShieldTransition::V0(v0) = &mut transition; + v0.actions.clear(); + let result = transition + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::ShieldedNoActionsError(_) + )) + )); + } + + #[test] + fn wrong_note_size_is_rejected() { + let mut transition = make_transition(100); + let TokenShieldTransition::V0(v0) = &mut transition; + v0.actions[0].encrypted_note = vec![0u8; 10]; + let result = transition + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::ShieldedEncryptedNoteSizeMismatchError(_) + )) + )); + } + + #[test] + fn empty_proof_is_rejected() { + let mut transition = make_transition(100); + let TokenShieldTransition::V0(v0) = &mut transition; + v0.proof.clear(); + let result = transition + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::ShieldedEmptyProofError(_) + )) + )); + } + + #[test] + fn zero_anchor_is_rejected() { + let mut transition = make_transition(100); + let TokenShieldTransition::V0(v0) = &mut transition; + v0.anchor = [0u8; 32]; + let result = transition + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::ShieldedZeroAnchorError(_) + )) + )); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/mod.rs new file mode 100644 index 00000000000..ba05dfbcfe8 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/mod.rs @@ -0,0 +1,112 @@ +pub mod v0; +mod v0_methods; +pub mod validate_structure; + +use bincode::{Decode, DecodeUntrusted, Encode}; +use derive_more::{Display, From}; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; +pub use v0::TokenShieldedTransferTransitionV0; + +/// A transfer inside a token's shielded pool: notes are spent and new notes created, with a +/// value balance of zero, so no token leaves or enters the pool and nothing about the parties +/// or the amount is public. +/// +/// The batch owner pays the credits fee for proof verification and note storage; the note +/// owner's spend authorization binds the token and the batch owner through the sighash extra +/// data (`token_shielded_transfer_extra_sighash_data`). +#[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From, DecodeUntrusted)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] +pub enum TokenShieldedTransferTransition { + #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] + V0(TokenShieldedTransferTransitionV0), +} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenShieldedTransferTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenShieldedTransferTransition {} + +impl Default for TokenShieldedTransferTransition { + fn default() -> Self { + TokenShieldedTransferTransition::V0(TokenShieldedTransferTransitionV0::default()) + } +} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::shielded::SerializedAction; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::TokenBaseTransition; + use platform_value::Identifier; + + pub(crate) fn fixture() -> TokenShieldedTransferTransition { + TokenShieldedTransferTransition::V0(TokenShieldedTransferTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 17, + token_contract_position: 1, + data_contract_id: Identifier::new([0xa1; 32]), + token_id: Identifier::new([0xb2; 32]), + using_group_info: None, + }), + actions: vec![ + SerializedAction { + nullifier: [0x11; 32], + rk: [0x22; 32], + cmx: [0x33; 32], + encrypted_note: vec![0x44; 216], + cv_net: [0x55; 32], + spend_auth_sig: [0x66; 64], + }, + SerializedAction { + nullifier: [0x12; 32], + rk: [0x23; 32], + cmx: [0x34; 32], + encrypted_note: vec![0x45; 216], + cv_net: [0x56; 32], + spend_auth_sig: [0x67; 64], + }, + ], + anchor: [0x77; 32], + proof: vec![0x88; 8], + binding_signature: [0x99; 64], + }) + } + + #[test] + fn json_round_trip_keeps_envelope_and_bundle() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + assert_eq!(json["$formatVersion"], "0"); + assert_eq!(json["$baseFormatVersion"], "0"); + assert_eq!(json["actions"].as_array().map(|a| a.len()), Some(2)); + assert!( + json.get("amount").is_none(), + "a shielded transfer has no public amount" + ); + let recovered = TokenShieldedTransferTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + let recovered = TokenShieldedTransferTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/v0/mod.rs new file mode 100644 index 00000000000..babe4a25382 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/v0/mod.rs @@ -0,0 +1,57 @@ +pub mod v0_methods; + +use crate::shielded::SerializedAction; +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +use bincode::{Decode, DecodeUntrusted, Encode}; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; +use std::fmt; + +/// The Identifier fields in [`TokenShieldedTransferTransition`] +pub use super::super::document_base_transition::IDENTIFIER_FIELDS; + +#[derive(Debug, Clone, Encode, Decode, PartialEq, DecodeUntrusted)] +// Auto-injects `serde_bytes` on the fixed byte arrays / `serde_bytes_var` on `proof` +// (base64 strings in JSON, raw bytes in Value). +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +pub struct TokenShieldedTransferTransitionV0 { + /// Token base transition (nonce, contract, token position, token id). + #[cfg_attr(feature = "serde-conversion", serde(flatten))] + pub base: TokenBaseTransition, + /// Orchard actions (spend-output pairs). The bundle's value balance is zero. + pub actions: Vec, + /// Sinsemilla root of the token pool's note commitment tree (Orchard anchor). + pub anchor: [u8; 32], + /// Halo 2 proof bytes. + pub proof: Vec, + /// RedPallas binding signature. + pub binding_signature: [u8; 64], +} + +impl Default for TokenShieldedTransferTransitionV0 { + fn default() -> Self { + Self { + base: TokenBaseTransition::default(), + actions: vec![], + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + } + } +} + +impl fmt::Display for TokenShieldedTransferTransitionV0 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Token Shielded Transfer, base: {}, actions: {}", + self.base, + self.actions.len() + ) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/v0/v0_methods.rs new file mode 100644 index 00000000000..b78bc5a3167 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/v0/v0_methods.rs @@ -0,0 +1,50 @@ +use crate::shielded::SerializedAction; +use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +use crate::state_transition::batch_transition::token_shielded_transfer_transition::TokenShieldedTransferTransitionV0; + +impl TokenBaseTransitionAccessors for TokenShieldedTransferTransitionV0 { + fn base(&self) -> &TokenBaseTransition { + &self.base + } + + fn base_mut(&mut self) -> &mut TokenBaseTransition { + &mut self.base + } + + fn set_base(&mut self, base: TokenBaseTransition) { + self.base = base; + } +} + +pub trait TokenShieldedTransferTransitionV0Methods: TokenBaseTransitionAccessors { + /// The Orchard actions. + fn actions(&self) -> &[SerializedAction]; + + /// The Orchard anchor the bundle was built against. + fn anchor(&self) -> &[u8; 32]; + + /// The Halo 2 proof bytes. + fn proof(&self) -> &[u8]; + + /// The RedPallas binding signature. + fn binding_signature(&self) -> &[u8; 64]; +} + +impl TokenShieldedTransferTransitionV0Methods for TokenShieldedTransferTransitionV0 { + fn actions(&self) -> &[SerializedAction] { + &self.actions + } + + fn anchor(&self) -> &[u8; 32] { + &self.anchor + } + + fn proof(&self) -> &[u8] { + &self.proof + } + + fn binding_signature(&self) -> &[u8; 64] { + &self.binding_signature + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/v0_methods.rs new file mode 100644 index 00000000000..8ada53af4e3 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/v0_methods.rs @@ -0,0 +1,51 @@ +use crate::shielded::SerializedAction; +use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +use crate::state_transition::batch_transition::token_shielded_transfer_transition::v0::v0_methods::TokenShieldedTransferTransitionV0Methods; +use crate::state_transition::batch_transition::TokenShieldedTransferTransition; + +impl TokenBaseTransitionAccessors for TokenShieldedTransferTransition { + fn base(&self) -> &TokenBaseTransition { + match self { + TokenShieldedTransferTransition::V0(v0) => &v0.base, + } + } + + fn base_mut(&mut self) -> &mut TokenBaseTransition { + match self { + TokenShieldedTransferTransition::V0(v0) => &mut v0.base, + } + } + + fn set_base(&mut self, base: TokenBaseTransition) { + match self { + TokenShieldedTransferTransition::V0(v0) => v0.base = base, + } + } +} + +impl TokenShieldedTransferTransitionV0Methods for TokenShieldedTransferTransition { + fn actions(&self) -> &[SerializedAction] { + match self { + TokenShieldedTransferTransition::V0(v0) => v0.actions(), + } + } + + fn anchor(&self) -> &[u8; 32] { + match self { + TokenShieldedTransferTransition::V0(v0) => v0.anchor(), + } + } + + fn proof(&self) -> &[u8] { + match self { + TokenShieldedTransferTransition::V0(v0) => v0.proof(), + } + } + + fn binding_signature(&self) -> &[u8; 64] { + match self { + TokenShieldedTransferTransition::V0(v0) => v0.binding_signature(), + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/validate_structure/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/validate_structure/mod.rs new file mode 100644 index 00000000000..9fa74052f8f --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/validate_structure/mod.rs @@ -0,0 +1,36 @@ +use crate::state_transition::batch_transition::token_shielded_transfer_transition::validate_structure::v0::TokenShieldedTransferTransitionActionStructureValidationV0; +use crate::state_transition::batch_transition::TokenShieldedTransferTransition; +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +mod v0; + +pub trait TokenShieldedTransferTransitionStructureValidation { + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenShieldedTransferTransitionStructureValidation for TokenShieldedTransferTransition { + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .token_shielded_transfer_transition_structure_validation + { + 0 => self.validate_structure_v0(platform_version), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "TokenShieldedTransferTransition::validate_structure".to_string(), + known_versions: vec![0], + received: version, + }), + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/validate_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/validate_structure/v0/mod.rs new file mode 100644 index 00000000000..7036a02ec5e --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shielded_transfer_transition/validate_structure/v0/mod.rs @@ -0,0 +1,142 @@ +use crate::state_transition::batch_transition::token_shielded_transfer_transition::v0::v0_methods::TokenShieldedTransferTransitionV0Methods; +use crate::state_transition::batch_transition::TokenShieldedTransferTransition; +use crate::state_transition::state_transitions::shielded::common_validation::{ + validate_actions_count, validate_anchor_not_zero, validate_encrypted_note_sizes, + validate_proof_not_empty, +}; +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +pub(super) trait TokenShieldedTransferTransitionActionStructureValidationV0 { + fn validate_structure_v0( + &self, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenShieldedTransferTransitionActionStructureValidationV0 + for TokenShieldedTransferTransition +{ + /// The same stateless bundle checks the credit-pool transitions run (action count, note + /// sizes, non-empty proof, non-zero anchor). There is no amount to bound: the value + /// balance is fixed at zero by the state validator. + fn validate_structure_v0( + &self, + platform_version: &PlatformVersion, + ) -> Result { + let result = validate_actions_count( + self.actions(), + platform_version + .system_limits + .max_shielded_transition_actions, + ); + if !result.is_valid() { + return Ok(result); + } + + let result = validate_encrypted_note_sizes(self.actions()); + if !result.is_valid() { + return Ok(result); + } + + let result = validate_proof_not_empty(self.proof()); + if !result.is_valid() { + return Ok(result); + } + + Ok(validate_anchor_not_zero(self.anchor())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::consensus::basic::BasicError; + use crate::consensus::ConsensusError; + use crate::shielded::SerializedAction; + use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::token_shielded_transfer_transition::v0::TokenShieldedTransferTransitionV0; + use platform_value::Identifier; + + fn action() -> SerializedAction { + SerializedAction { + nullifier: [1u8; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + } + } + + fn make_transition(actions: usize) -> TokenShieldedTransferTransition { + TokenShieldedTransferTransition::V0(TokenShieldedTransferTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::default(), + token_id: Identifier::default(), + using_group_info: None, + }), + actions: vec![action(); actions], + anchor: [9u8; 32], + proof: vec![1u8; 10], + binding_signature: [7u8; 64], + }) + } + + #[test] + fn valid_transfer_passes() { + let result = make_transition(2) + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(result.is_valid(), "{:?}", result.errors); + } + + #[test] + fn empty_actions_are_rejected() { + let result = make_transition(0) + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::ShieldedNoActionsError(_) + )) + )); + } + + #[test] + fn too_many_actions_are_rejected() { + let max = PlatformVersion::latest() + .system_limits + .max_shielded_transition_actions as usize; + let result = make_transition(max + 1) + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::ShieldedTooManyActionsError(_) + )) + )); + } + + #[test] + fn zero_anchor_is_rejected() { + let mut transition = make_transition(1); + let TokenShieldedTransferTransition::V0(v0) = &mut transition; + v0.anchor = [0u8; 32]; + let result = transition + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::ShieldedZeroAnchorError(_) + )) + )); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs index 78b358d4c50..d4869026cf5 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs @@ -19,7 +19,7 @@ use crate::data_contract::document_type::DocumentTypeRef; use crate::document::Document; use crate::prelude::IdentityNonce; use crate::ProtocolError; -use crate::state_transition::batch_transition::{DocumentCreateTransition, DocumentDeleteTransition, DocumentReplaceTransition, TokenBurnTransition, TokenConfigUpdateTransition, TokenDestroyFrozenFundsTransition, TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintTransition, TokenClaimTransition, TokenTransferTransition, TokenSetPriceForDirectPurchaseTransition}; +use crate::state_transition::batch_transition::{DocumentCreateTransition, DocumentDeleteTransition, DocumentReplaceTransition, TokenBurnTransition, TokenConfigUpdateTransition, TokenDestroyFrozenFundsTransition, TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintTransition, TokenClaimTransition, TokenTransferTransition, TokenSetPriceForDirectPurchaseTransition, TokenShieldTransition, TokenShieldedTransferTransition, TokenUnshieldTransition}; use crate::state_transition::batch_transition::batched_transition::{DocumentPurchaseTransition, DocumentTransferTransition}; use crate::state_transition::batch_transition::batched_transition::multi_party_action::AllowedAsMultiPartyAction; use crate::state_transition::batch_transition::batched_transition::token_unfreeze_transition::TokenUnfreezeTransition; @@ -38,6 +38,8 @@ use crate::state_transition::batch_transition::token_direct_purchase_transition: use crate::state_transition::batch_transition::token_direct_purchase_transition::v0::v0_methods::TokenDirectPurchaseTransitionV0Methods; use crate::state_transition::batch_transition::token_set_price_for_direct_purchase_transition::v0::v0_methods::TokenSetPriceForDirectPurchaseTransitionV0Methods; use crate::state_transition::batch_transition::token_transfer_transition::v0::v0_methods::TokenTransferTransitionV0Methods; +use crate::state_transition::batch_transition::token_shield_transition::v0::v0_methods::TokenShieldTransitionV0Methods; +use crate::state_transition::batch_transition::token_unshield_transition::v0::v0_methods::TokenUnshieldTransitionV0Methods; use crate::state_transition::batch_transition::token_unfreeze_transition::v0::v0_methods::TokenUnfreezeTransitionV0Methods; use crate::tokens::token_event::TokenEvent; @@ -90,6 +92,15 @@ pub enum TokenTransition { #[display("TokenSetPriceForDirectPurchaseTransition({})", "_0")] SetPriceForDirectPurchase(TokenSetPriceForDirectPurchaseTransition), + + #[display("TokenShieldTransition({})", "_0")] + Shield(TokenShieldTransition), + + #[display("TokenUnshieldTransition({})", "_0")] + Unshield(TokenUnshieldTransition), + + #[display("TokenShieldedTransferTransition({})", "_0")] + ShieldedTransfer(TokenShieldedTransferTransition), } #[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] @@ -110,8 +121,9 @@ pub(crate) mod json_convertible_tests { token_burn_transition, token_claim_transition, token_config_update_transition, token_destroy_frozen_funds_transition, token_direct_purchase_transition, token_emergency_action_transition, token_freeze_transition, token_mint_transition, - token_set_price_for_direct_purchase_transition, token_transfer_transition, - token_unfreeze_transition, + token_set_price_for_direct_purchase_transition, token_shield_transition, + token_shielded_transfer_transition, token_transfer_transition, token_unfreeze_transition, + token_unshield_transition, }; /// Wrapping helper — drives a single `TokenTransition::*` variant through @@ -243,6 +255,32 @@ pub(crate) mod json_convertible_tests { "setPriceForDirectPurchase", ); } + + #[test] + fn umbrella_shield() { + assert_umbrella_round_trip( + TokenTransition::Shield(token_shield_transition::json_convertible_tests::fixture()), + "shield", + ); + } + + #[test] + fn umbrella_unshield() { + assert_umbrella_round_trip( + TokenTransition::Unshield(token_unshield_transition::json_convertible_tests::fixture()), + "unshield", + ); + } + + #[test] + fn umbrella_shielded_transfer() { + assert_umbrella_round_trip( + TokenTransition::ShieldedTransfer( + token_shielded_transfer_transition::json_convertible_tests::fixture(), + ), + "shieldedTransfer", + ); + } } impl BatchTransitionResolversV0 for TokenTransition { @@ -355,6 +393,30 @@ impl BatchTransitionResolversV0 for TokenTransition { None } } + + fn as_transition_token_shield(&self) -> Option<&TokenShieldTransition> { + if let Self::Shield(ref t) = self { + Some(t) + } else { + None + } + } + + fn as_transition_token_unshield(&self) -> Option<&TokenUnshieldTransition> { + if let Self::Unshield(ref t) = self { + Some(t) + } else { + None + } + } + + fn as_transition_token_shielded_transfer(&self) -> Option<&TokenShieldedTransferTransition> { + if let Self::ShieldedTransfer(ref t) = self { + Some(t) + } else { + None + } + } } pub trait TokenTransitionV0Methods { @@ -423,6 +485,9 @@ impl TokenTransitionV0Methods for TokenTransition { TokenTransition::ConfigUpdate(t) => t.base(), TokenTransition::DirectPurchase(t) => t.base(), TokenTransition::SetPriceForDirectPurchase(t) => t.base(), + TokenTransition::Shield(t) => t.base(), + TokenTransition::Unshield(t) => t.base(), + TokenTransition::ShieldedTransfer(t) => t.base(), } } @@ -439,6 +504,9 @@ impl TokenTransitionV0Methods for TokenTransition { TokenTransition::ConfigUpdate(t) => t.base_mut(), TokenTransition::DirectPurchase(t) => t.base_mut(), TokenTransition::SetPriceForDirectPurchase(t) => t.base_mut(), + TokenTransition::Shield(t) => t.base_mut(), + TokenTransition::Unshield(t) => t.base_mut(), + TokenTransition::ShieldedTransfer(t) => t.base_mut(), } } @@ -471,6 +539,9 @@ impl TokenTransitionV0Methods for TokenTransition { TokenTransition::SetPriceForDirectPurchase(t) => { Some(t.calculate_action_id(owner_id, platform_version)) } + TokenTransition::Shield(_) + | TokenTransition::Unshield(_) + | TokenTransition::ShieldedTransfer(_) => None, } } @@ -486,7 +557,10 @@ impl TokenTransitionV0Methods for TokenTransition { | TokenTransition::SetPriceForDirectPurchase(_) => true, TokenTransition::Transfer(_) | TokenTransition::Claim(_) - | TokenTransition::DirectPurchase(_) => false, + | TokenTransition::DirectPurchase(_) + | TokenTransition::Shield(_) + | TokenTransition::Unshield(_) + | TokenTransition::ShieldedTransfer(_) => false, } } @@ -524,6 +598,9 @@ impl TokenTransitionV0Methods for TokenTransition { TokenTransition::Claim(_) => "claim", TokenTransition::DirectPurchase(_) => "directPurchase", TokenTransition::SetPriceForDirectPurchase(_) => "directPricing", + TokenTransition::Shield(_) => "shield", + TokenTransition::Unshield(_) => "unshield", + TokenTransition::ShieldedTransfer(_) => "shieldedTransfer", } } @@ -663,6 +740,11 @@ impl TokenTransitionV0Methods for TokenTransition { claim.public_note().cloned(), ) } + TokenTransition::Shield(shield) => TokenEvent::Shield(shield.amount()), + TokenTransition::Unshield(unshield) => { + TokenEvent::Unshield(unshield.recipient_id(), unshield.amount()) + } + TokenTransition::ShieldedTransfer(_) => TokenEvent::ShieldedTransfer, TokenTransition::DirectPurchase(direct_purchase) => TokenEvent::DirectPurchase( direct_purchase.token_count(), direct_purchase.total_agreed_price(), diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition_action_type.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition_action_type.rs index afe766c47f9..e95a042ab44 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition_action_type.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition_action_type.rs @@ -45,6 +45,15 @@ pub enum TokenTransitionActionType { /// Indicates that the transition sets or updates the price for direct token purchases. SetPriceForDirectPurchase, + + /// Indicates tokens moving from an identity balance into the token's shielded pool. + Shield, + + /// Indicates tokens moving from the token's shielded pool to an identity balance. + Unshield, + + /// Indicates a transfer inside the token's shielded pool. + ShieldedTransfer, } impl fmt::Display for TokenTransitionActionType { @@ -61,6 +70,9 @@ impl fmt::Display for TokenTransitionActionType { TokenTransitionActionType::ConfigUpdate => "ConfigUpdate", TokenTransitionActionType::DirectPurchase => "DirectPurchase", TokenTransitionActionType::SetPriceForDirectPurchase => "SetPriceForDirectPurchase", + TokenTransitionActionType::Shield => "Shield", + TokenTransitionActionType::Unshield => "Unshield", + TokenTransitionActionType::ShieldedTransfer => "ShieldedTransfer", }; write!(f, "{}", action_str) } @@ -86,6 +98,9 @@ impl TokenTransitionActionTypeGetter for TokenTransition { TokenTransitionActionType::SetPriceForDirectPurchase } TokenTransition::DirectPurchase(_) => TokenTransitionActionType::DirectPurchase, + TokenTransition::Shield(_) => TokenTransitionActionType::Shield, + TokenTransition::Unshield(_) => TokenTransitionActionType::Unshield, + TokenTransition::ShieldedTransfer(_) => TokenTransitionActionType::ShieldedTransfer, } } } @@ -112,6 +127,11 @@ impl TryFrom<&str> for TokenTransitionActionType { "set_price_for_direct_purchase" | "setPriceForDirectPurchase" => { Ok(TokenTransitionActionType::SetPriceForDirectPurchase) } + "shield" => Ok(TokenTransitionActionType::Shield), + "unshield" => Ok(TokenTransitionActionType::Unshield), + "shielded_transfer" | "shieldedTransfer" => { + Ok(TokenTransitionActionType::ShieldedTransfer) + } action_type => Err(ProtocolError::Generic(format!( "unknown token transition action type {action_type}" ))), diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/mod.rs new file mode 100644 index 00000000000..0296c981ec0 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/mod.rs @@ -0,0 +1,106 @@ +pub mod v0; +mod v0_methods; +pub mod validate_structure; + +use bincode::{Decode, DecodeUntrusted, Encode}; +use derive_more::{Display, From}; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; +pub use v0::TokenUnshieldTransitionV0; + +/// Moves `amount` of a token out of the token's shielded pool into `recipient_id`'s identity +/// token balance. +/// +/// The Orchard bundle spends pool notes (revealing their nullifiers) with a value balance of +/// `+amount`; any change goes back into the pool as new notes. The batch owner pays the credits +/// fee and need not be the note owner or the recipient: the note owner's spend authorization +/// binds the token, the batch owner, the recipient and the amount through the sighash extra +/// data (`token_unshield_extra_sighash_data`), so the bundle cannot be redirected. +#[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From, DecodeUntrusted)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] +pub enum TokenUnshieldTransition { + #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] + V0(TokenUnshieldTransitionV0), +} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenUnshieldTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenUnshieldTransition {} + +impl Default for TokenUnshieldTransition { + fn default() -> Self { + TokenUnshieldTransition::V0(TokenUnshieldTransitionV0::default()) + } +} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::shielded::SerializedAction; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::TokenBaseTransition; + use platform_value::Identifier; + + pub(crate) fn fixture() -> TokenUnshieldTransition { + TokenUnshieldTransition::V0(TokenUnshieldTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 16, + token_contract_position: 1, + data_contract_id: Identifier::new([0xa1; 32]), + token_id: Identifier::new([0xb2; 32]), + using_group_info: None, + }), + amount: 2_500, + recipient_id: Identifier::new([0xc3; 32]), + actions: vec![SerializedAction { + nullifier: [0x11; 32], + rk: [0x22; 32], + cmx: [0x33; 32], + encrypted_note: vec![0x44; 216], + cv_net: [0x55; 32], + spend_auth_sig: [0x66; 64], + }], + anchor: [0x77; 32], + proof: vec![0x88; 8], + binding_signature: [0x99; 64], + }) + } + + #[test] + fn json_round_trip_keeps_envelope_and_bundle() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + assert_eq!(json["$formatVersion"], "0"); + assert_eq!(json["$baseFormatVersion"], "0"); + assert_eq!(json["amount"], 2_500); + assert_eq!( + json["recipientId"], + serde_json::json!(Identifier::new([0xc3; 32])) + ); + assert!(json["actions"].is_array()); + let recovered = TokenUnshieldTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + let recovered = TokenUnshieldTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/v0/mod.rs new file mode 100644 index 00000000000..4766d3e7054 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/v0/mod.rs @@ -0,0 +1,68 @@ +pub mod v0_methods; + +use crate::balances::credits::TokenAmount; +use crate::shielded::SerializedAction; +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +use bincode::{Decode, DecodeUntrusted, Encode}; +use platform_value::Identifier; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; +use std::fmt; + +/// The Identifier fields in [`TokenUnshieldTransition`] +pub use super::super::document_base_transition::IDENTIFIER_FIELDS; + +#[derive(Debug, Clone, Encode, Decode, PartialEq, DecodeUntrusted)] +// Auto-injects `json_safe_u64` on `amount` and `serde_bytes` on the fixed byte arrays / +// `serde_bytes_var` on `proof` (base64 strings in JSON, raw bytes in Value). +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +pub struct TokenUnshieldTransitionV0 { + /// Token base transition (nonce, contract, token position, token id). + #[cfg_attr(feature = "serde-conversion", serde(flatten))] + pub base: TokenBaseTransition, + /// Tokens leaving the shielded pool and credited to `recipient_id`. Must equal the bundle's + /// value balance. + pub amount: TokenAmount, + /// The identity whose token balance receives the unshielded amount. + pub recipient_id: Identifier, + /// Orchard actions (spend-output pairs). + pub actions: Vec, + /// Sinsemilla root of the token pool's note commitment tree (Orchard anchor). + pub anchor: [u8; 32], + /// Halo 2 proof bytes. + pub proof: Vec, + /// RedPallas binding signature. + pub binding_signature: [u8; 64], +} + +impl Default for TokenUnshieldTransitionV0 { + fn default() -> Self { + Self { + base: TokenBaseTransition::default(), + amount: 0, + recipient_id: Identifier::default(), + actions: vec![], + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + } + } +} + +impl fmt::Display for TokenUnshieldTransitionV0 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Token Unshield, base: {}, amount: {}, recipient: {}, actions: {}", + self.base, + self.amount, + self.recipient_id, + self.actions.len() + ) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/v0/v0_methods.rs new file mode 100644 index 00000000000..0bd64ca008c --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/v0/v0_methods.rs @@ -0,0 +1,80 @@ +use crate::balances::credits::TokenAmount; +use crate::shielded::SerializedAction; +use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +use crate::state_transition::batch_transition::token_unshield_transition::TokenUnshieldTransitionV0; +use platform_value::Identifier; + +impl TokenBaseTransitionAccessors for TokenUnshieldTransitionV0 { + fn base(&self) -> &TokenBaseTransition { + &self.base + } + + fn base_mut(&mut self) -> &mut TokenBaseTransition { + &mut self.base + } + + fn set_base(&mut self, base: TokenBaseTransition) { + self.base = base; + } +} + +pub trait TokenUnshieldTransitionV0Methods: TokenBaseTransitionAccessors { + /// Tokens leaving the shielded pool. + fn amount(&self) -> TokenAmount; + + /// Sets the amount leaving the shielded pool. + fn set_amount(&mut self, amount: TokenAmount); + + /// The identity credited with the unshielded amount. + fn recipient_id(&self) -> Identifier; + + /// Sets the identity credited with the unshielded amount. + fn set_recipient_id(&mut self, recipient_id: Identifier); + + /// The Orchard actions. + fn actions(&self) -> &[SerializedAction]; + + /// The Orchard anchor the bundle was built against. + fn anchor(&self) -> &[u8; 32]; + + /// The Halo 2 proof bytes. + fn proof(&self) -> &[u8]; + + /// The RedPallas binding signature. + fn binding_signature(&self) -> &[u8; 64]; +} + +impl TokenUnshieldTransitionV0Methods for TokenUnshieldTransitionV0 { + fn amount(&self) -> TokenAmount { + self.amount + } + + fn set_amount(&mut self, amount: TokenAmount) { + self.amount = amount; + } + + fn recipient_id(&self) -> Identifier { + self.recipient_id + } + + fn set_recipient_id(&mut self, recipient_id: Identifier) { + self.recipient_id = recipient_id; + } + + fn actions(&self) -> &[SerializedAction] { + &self.actions + } + + fn anchor(&self) -> &[u8; 32] { + &self.anchor + } + + fn proof(&self) -> &[u8] { + &self.proof + } + + fn binding_signature(&self) -> &[u8; 64] { + &self.binding_signature + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/v0_methods.rs new file mode 100644 index 00000000000..536de9d8750 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/v0_methods.rs @@ -0,0 +1,77 @@ +use crate::balances::credits::TokenAmount; +use platform_value::Identifier; +use crate::shielded::SerializedAction; +use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +use crate::state_transition::batch_transition::token_unshield_transition::v0::v0_methods::TokenUnshieldTransitionV0Methods; +use crate::state_transition::batch_transition::TokenUnshieldTransition; + +impl TokenBaseTransitionAccessors for TokenUnshieldTransition { + fn base(&self) -> &TokenBaseTransition { + match self { + TokenUnshieldTransition::V0(v0) => &v0.base, + } + } + + fn base_mut(&mut self) -> &mut TokenBaseTransition { + match self { + TokenUnshieldTransition::V0(v0) => &mut v0.base, + } + } + + fn set_base(&mut self, base: TokenBaseTransition) { + match self { + TokenUnshieldTransition::V0(v0) => v0.base = base, + } + } +} + +impl TokenUnshieldTransitionV0Methods for TokenUnshieldTransition { + fn recipient_id(&self) -> Identifier { + match self { + TokenUnshieldTransition::V0(v0) => v0.recipient_id(), + } + } + + fn set_recipient_id(&mut self, recipient_id: Identifier) { + match self { + TokenUnshieldTransition::V0(v0) => v0.set_recipient_id(recipient_id), + } + } + + fn amount(&self) -> TokenAmount { + match self { + TokenUnshieldTransition::V0(v0) => v0.amount(), + } + } + + fn set_amount(&mut self, amount: TokenAmount) { + match self { + TokenUnshieldTransition::V0(v0) => v0.set_amount(amount), + } + } + + fn actions(&self) -> &[SerializedAction] { + match self { + TokenUnshieldTransition::V0(v0) => v0.actions(), + } + } + + fn anchor(&self) -> &[u8; 32] { + match self { + TokenUnshieldTransition::V0(v0) => v0.anchor(), + } + } + + fn proof(&self) -> &[u8] { + match self { + TokenUnshieldTransition::V0(v0) => v0.proof(), + } + } + + fn binding_signature(&self) -> &[u8; 64] { + match self { + TokenUnshieldTransition::V0(v0) => v0.binding_signature(), + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/validate_structure/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/validate_structure/mod.rs new file mode 100644 index 00000000000..02c8f3d8f61 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/validate_structure/mod.rs @@ -0,0 +1,36 @@ +use crate::state_transition::batch_transition::token_unshield_transition::validate_structure::v0::TokenUnshieldTransitionActionStructureValidationV0; +use crate::state_transition::batch_transition::TokenUnshieldTransition; +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +mod v0; + +pub trait TokenUnshieldTransitionStructureValidation { + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenUnshieldTransitionStructureValidation for TokenUnshieldTransition { + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .token_unshield_transition_structure_validation + { + 0 => self.validate_structure_v0(platform_version), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "TokenUnshieldTransition::validate_structure".to_string(), + known_versions: vec![0], + received: version, + }), + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/validate_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/validate_structure/v0/mod.rs new file mode 100644 index 00000000000..24e8f315958 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_unshield_transition/validate_structure/v0/mod.rs @@ -0,0 +1,198 @@ +use crate::consensus::basic::token::InvalidTokenAmountError; +use crate::consensus::basic::BasicError; +use crate::consensus::ConsensusError; +use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::MAX_DISTRIBUTION_PARAM; +use crate::state_transition::batch_transition::token_unshield_transition::v0::v0_methods::TokenUnshieldTransitionV0Methods; +use crate::state_transition::batch_transition::TokenUnshieldTransition; +use crate::state_transition::state_transitions::shielded::common_validation::{ + validate_actions_count, validate_anchor_not_zero, validate_encrypted_note_sizes, + validate_proof_not_empty, +}; +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +pub(super) trait TokenUnshieldTransitionActionStructureValidationV0 { + fn validate_structure_v0( + &self, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenUnshieldTransitionActionStructureValidationV0 for TokenUnshieldTransition { + /// The amount bounds match the other token transitions; the bundle checks are the same + /// stateless checks the credit-pool transitions run (action count, note sizes, non-empty + /// proof, non-zero anchor). The proof itself is verified against state, where its cost + /// can be charged. + fn validate_structure_v0( + &self, + platform_version: &PlatformVersion, + ) -> Result { + if self.amount() > MAX_DISTRIBUTION_PARAM || self.amount() == 0 { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::BasicError(BasicError::InvalidTokenAmountError( + InvalidTokenAmountError::new(MAX_DISTRIBUTION_PARAM, self.amount()), + )), + )); + } + + let result = validate_actions_count( + self.actions(), + platform_version + .system_limits + .max_shielded_transition_actions, + ); + if !result.is_valid() { + return Ok(result); + } + + let result = validate_encrypted_note_sizes(self.actions()); + if !result.is_valid() { + return Ok(result); + } + + let result = validate_proof_not_empty(self.proof()); + if !result.is_valid() { + return Ok(result); + } + + Ok(validate_anchor_not_zero(self.anchor())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::shielded::SerializedAction; + use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::token_unshield_transition::v0::TokenUnshieldTransitionV0; + use platform_value::Identifier; + + fn action() -> SerializedAction { + SerializedAction { + nullifier: [1u8; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + } + } + + fn make_transition(amount: u64) -> TokenUnshieldTransition { + TokenUnshieldTransition::V0(TokenUnshieldTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::default(), + token_id: Identifier::default(), + using_group_info: None, + }), + amount, + recipient_id: Identifier::new([2u8; 32]), + actions: vec![action()], + anchor: [9u8; 32], + proof: vec![1u8; 10], + binding_signature: [7u8; 64], + }) + } + + #[test] + fn valid_unshield_passes() { + let result = make_transition(100) + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(result.is_valid(), "{:?}", result.errors); + } + + #[test] + fn zero_amount_is_rejected() { + let result = make_transition(0) + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::InvalidTokenAmountError(_) + )) + )); + } + + #[test] + fn amount_above_max_is_rejected() { + let result = make_transition(MAX_DISTRIBUTION_PARAM + 1) + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::InvalidTokenAmountError(_) + )) + )); + } + + #[test] + fn empty_actions_are_rejected() { + let mut transition = make_transition(100); + let TokenUnshieldTransition::V0(v0) = &mut transition; + v0.actions.clear(); + let result = transition + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::ShieldedNoActionsError(_) + )) + )); + } + + #[test] + fn wrong_note_size_is_rejected() { + let mut transition = make_transition(100); + let TokenUnshieldTransition::V0(v0) = &mut transition; + v0.actions[0].encrypted_note = vec![0u8; 10]; + let result = transition + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::ShieldedEncryptedNoteSizeMismatchError(_) + )) + )); + } + + #[test] + fn empty_proof_is_rejected() { + let mut transition = make_transition(100); + let TokenUnshieldTransition::V0(v0) = &mut transition; + v0.proof.clear(); + let result = transition + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::ShieldedEmptyProofError(_) + )) + )); + } + + #[test] + fn zero_anchor_is_rejected() { + let mut transition = make_transition(100); + let TokenUnshieldTransition::V0(v0) = &mut transition; + v0.anchor = [0u8; 32]; + let result = transition + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::ShieldedZeroAnchorError(_) + )) + )); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs index 65b1f437bb0..c125acdc7e9 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs @@ -24,6 +24,8 @@ use crate::prelude::UserFeeIncrease; use crate::state_transition::batch_transition::batched_transition::document_transition::{ DocumentTransition, DocumentTransitionV0Methods, }; +#[cfg(feature = "state-transition-signing")] +use crate::shielded::OrchardBundleParams; use crate::state_transition::batch_transition::batched_transition::BatchedTransition; use crate::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1; @@ -680,6 +682,172 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { } } + #[cfg(feature = "state-transition-signing")] + async fn new_token_shield_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + amount: TokenAmount, + bundle: OrchardBundleParams, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let resolved_options = options.unwrap_or_default(); + match resolved_options.batch_feature_version.unwrap_or( + platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .default_current_version, + ) { + 1 | 0 + if platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .max_version + >= 1 => + { + BatchTransitionV1::new_token_shield_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + amount, + bundle, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + options, + ) + .await + } + version => Err(ProtocolError::UnknownVersionMismatch { + method: "DocumentsBatchTransition::new_token_shield_transition".to_string(), + known_versions: vec![1], + received: version, + }), + } + } + + #[cfg(feature = "state-transition-signing")] + async fn new_token_unshield_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + amount: TokenAmount, + recipient_id: Identifier, + bundle: OrchardBundleParams, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let resolved_options = options.unwrap_or_default(); + match resolved_options.batch_feature_version.unwrap_or( + platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .default_current_version, + ) { + 1 | 0 + if platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .max_version + >= 1 => + { + BatchTransitionV1::new_token_unshield_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + amount, + recipient_id, + bundle, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + options, + ) + .await + } + version => Err(ProtocolError::UnknownVersionMismatch { + method: "DocumentsBatchTransition::new_token_unshield_transition".to_string(), + known_versions: vec![1], + received: version, + }), + } + } + + #[cfg(feature = "state-transition-signing")] + async fn new_token_shielded_transfer_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + bundle: OrchardBundleParams, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let resolved_options = options.unwrap_or_default(); + match resolved_options.batch_feature_version.unwrap_or( + platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .default_current_version, + ) { + 1 | 0 + if platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .max_version + >= 1 => + { + BatchTransitionV1::new_token_shielded_transfer_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + bundle, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + options, + ) + .await + } + version => Err(ProtocolError::UnknownVersionMismatch { + method: "DocumentsBatchTransition::new_token_shielded_transfer_transition" + .to_string(), + known_versions: vec![1], + received: version, + }), + } + } + #[cfg(feature = "state-transition-signing")] async fn new_token_freeze_transition>( token_id: Identifier, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs index 0e3e0340ffd..25071ca34c8 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs @@ -14,6 +14,8 @@ use crate::identity::signer::Signer; use crate::identity::IdentityPublicKey; #[cfg(feature = "state-transition-signing")] use crate::prelude::{IdentityNonce, UserFeeIncrease}; +#[cfg(feature = "state-transition-signing")] +use crate::shielded::OrchardBundleParams; use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; #[cfg(feature = "state-transition-signing")] use crate::state_transition::batch_transition::methods::StateTransitionCreationOptions; @@ -146,6 +148,72 @@ pub trait DocumentsBatchTransitionMethodsV1: DocumentsBatchTransitionAccessorsV0 options: Option, ) -> Result; + /// Creates a `StateTransition` moving `amount` of a token from `owner_id`'s identity token + /// balance into the token's shielded pool. + /// + /// `bundle` is the outputs-only Orchard bundle (value balance `-amount`) the client proved for + /// the token pool; the identity signature over the batch binds it. + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + async fn new_token_shield_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + amount: TokenAmount, + bundle: OrchardBundleParams, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result; + + /// Creates a `StateTransition` moving `amount` of a token out of the token's shielded pool + /// into `recipient_id`'s identity token balance. `owner_id` signs and pays the credits fee. + /// + /// `bundle` is the Orchard spend bundle (value balance `+amount`) whose spend authorization + /// was signed over `token_unshield_extra_sighash_data(token_id, owner_id, recipient_id, amount)`. + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + async fn new_token_unshield_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + amount: TokenAmount, + recipient_id: Identifier, + bundle: OrchardBundleParams, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result; + + /// Creates a `StateTransition` transferring a token inside its shielded pool. `owner_id` + /// signs and pays the credits fee. + /// + /// `bundle` is the Orchard spend bundle (value balance `0`) whose spend authorization was + /// signed over `token_shielded_transfer_extra_sighash_data(token_id, owner_id)`. + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + async fn new_token_shielded_transfer_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + bundle: OrchardBundleParams, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result; + /// Creates a `StateTransition` to freeze tokens belonging to a specific identity. /// /// Frozen tokens cannot be transferred or used until explicitly unfrozen or destroyed. diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs index b3abe347bbb..ae88e85149c 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs @@ -29,8 +29,12 @@ pub use self::batched_transition::{ token_freeze_transition::TokenFreezeTransition, token_mint_transition, token_mint_transition::TokenMintTransition, token_set_price_for_direct_purchase_transition, token_set_price_for_direct_purchase_transition::TokenSetPriceForDirectPurchaseTransition, - token_transfer_transition, token_transfer_transition::TokenTransferTransition, - token_unfreeze_transition, token_unfreeze_transition::TokenUnfreezeTransition, + token_shield_transition, token_shield_transition::TokenShieldTransition, + token_shielded_transfer_transition, + token_shielded_transfer_transition::TokenShieldedTransferTransition, token_transfer_transition, + token_transfer_transition::TokenTransferTransition, token_unfreeze_transition, + token_unfreeze_transition::TokenUnfreezeTransition, token_unshield_transition, + token_unshield_transition::TokenUnshieldTransition, }; use platform_serialization_derive::{ diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/resolvers/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/resolvers/v0/mod.rs index bdad604186b..d830506aa75 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/resolvers/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/resolvers/v0/mod.rs @@ -7,7 +7,8 @@ use crate::state_transition::batch_transition::{ DocumentCreateTransition, DocumentDeleteTransition, DocumentReplaceTransition, TokenBurnTransition, TokenClaimTransition, TokenConfigUpdateTransition, TokenDestroyFrozenFundsTransition, TokenEmergencyActionTransition, TokenFreezeTransition, - TokenMintTransition, TokenSetPriceForDirectPurchaseTransition, TokenTransferTransition, + TokenMintTransition, TokenSetPriceForDirectPurchaseTransition, TokenShieldTransition, + TokenShieldedTransferTransition, TokenTransferTransition, TokenUnshieldTransition, }; pub trait BatchTransitionResolversV0 { @@ -33,4 +34,7 @@ pub trait BatchTransitionResolversV0 { fn as_transition_token_set_price_for_direct_purchase( &self, ) -> Option<&TokenSetPriceForDirectPurchaseTransition>; + fn as_transition_token_shield(&self) -> Option<&TokenShieldTransition>; + fn as_transition_token_unshield(&self) -> Option<&TokenUnshieldTransition>; + fn as_transition_token_shielded_transfer(&self) -> Option<&TokenShieldedTransferTransition>; } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v1_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v1_methods.rs index 1b5d848c9d6..35affa19994 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v1_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v1_methods.rs @@ -67,6 +67,16 @@ use crate::state_transition::batch_transition::token_set_price_for_direct_purcha #[cfg(feature = "state-transition-signing")] use crate::state_transition::batch_transition::token_transfer_transition::TokenTransferTransitionV0; #[cfg(feature = "state-transition-signing")] +use crate::shielded::OrchardBundleParams; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_shield_transition::TokenShieldTransitionV0; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_shielded_transfer_transition::TokenShieldedTransferTransitionV0; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_unshield_transition::TokenUnshieldTransitionV0; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::{TokenShieldTransition, TokenShieldedTransferTransition, TokenUnshieldTransition}; +#[cfg(feature = "state-transition-signing")] use crate::state_transition::batch_transition::token_unfreeze_transition::TokenUnfreezeTransitionV0; #[cfg(feature = "state-transition-signing")] use crate::state_transition::GetDataContractSecurityLevelRequirementFn; @@ -310,6 +320,219 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransitionV1 { Ok(state_transition) } + #[cfg(feature = "state-transition-signing")] + async fn new_token_shield_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + amount: TokenAmount, + bundle: OrchardBundleParams, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + _platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let OrchardBundleParams { + actions, + anchor, + proof, + binding_signature, + } = bundle; + + let base = TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce, + token_contract_position, + data_contract_id, + token_id, + using_group_info: None, + }); + + let transition = TokenShieldTransition::V0(TokenShieldTransitionV0 { + base, + amount, + actions, + anchor, + proof, + binding_signature, + }); + + let documents_batch_transition: BatchTransition = BatchTransitionV1 { + owner_id, + transitions: vec![BatchedTransition::Token(transition.into())], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + + let mut state_transition: StateTransition = documents_batch_transition.into(); + if let Some(options) = options { + state_transition + .sign_external_with_options( + identity_public_key, + signer, + None::, + options.signing_options, + ) + .await?; + } else { + state_transition + .sign_external( + identity_public_key, + signer, + None::, + ) + .await?; + } + + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + async fn new_token_unshield_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + amount: TokenAmount, + recipient_id: Identifier, + bundle: OrchardBundleParams, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + _platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let OrchardBundleParams { + actions, + anchor, + proof, + binding_signature, + } = bundle; + + let base = TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce, + token_contract_position, + data_contract_id, + token_id, + using_group_info: None, + }); + + let transition = TokenUnshieldTransition::V0(TokenUnshieldTransitionV0 { + base, + amount, + recipient_id, + actions, + anchor, + proof, + binding_signature, + }); + + let documents_batch_transition: BatchTransition = BatchTransitionV1 { + owner_id, + transitions: vec![BatchedTransition::Token(transition.into())], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + + let mut state_transition: StateTransition = documents_batch_transition.into(); + if let Some(options) = options { + state_transition + .sign_external_with_options( + identity_public_key, + signer, + None::, + options.signing_options, + ) + .await?; + } else { + state_transition + .sign_external( + identity_public_key, + signer, + None::, + ) + .await?; + } + + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + async fn new_token_shielded_transfer_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + bundle: OrchardBundleParams, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + _platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let OrchardBundleParams { + actions, + anchor, + proof, + binding_signature, + } = bundle; + + let base = TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce, + token_contract_position, + data_contract_id, + token_id, + using_group_info: None, + }); + + let transition = TokenShieldedTransferTransition::V0(TokenShieldedTransferTransitionV0 { + base, + actions, + anchor, + proof, + binding_signature, + }); + + let documents_batch_transition: BatchTransition = BatchTransitionV1 { + owner_id, + transitions: vec![BatchedTransition::Token(transition.into())], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + + let mut state_transition: StateTransition = documents_batch_transition.into(); + if let Some(options) = options { + state_transition + .sign_external_with_options( + identity_public_key, + signer, + None::, + options.signing_options, + ) + .await?; + } else { + state_transition + .sign_external( + identity_public_key, + signer, + None::, + ) + .await?; + } + + Ok(state_transition) + } + #[cfg(feature = "state-transition-signing")] async fn new_token_freeze_transition>( token_id: Identifier, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs index 83887155a60..05459a33574 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs @@ -31,6 +31,9 @@ use crate::state_transition::batch_transition::token_mint_transition::validate_s use crate::state_transition::batch_transition::token_claim_transition::validate_structure::TokenClaimTransitionStructureValidation; use crate::state_transition::batch_transition::token_direct_purchase_transition::validate_structure::TokenDirectPurchaseTransitionStructureValidation; use crate::state_transition::batch_transition::token_set_price_for_direct_purchase_transition::validate_structure::TokenSetPriceForDirectPurchaseTransitionStructureValidation; +use crate::state_transition::batch_transition::token_shield_transition::validate_structure::TokenShieldTransitionStructureValidation; +use crate::state_transition::batch_transition::token_shielded_transfer_transition::validate_structure::TokenShieldedTransferTransitionStructureValidation; +use crate::state_transition::batch_transition::token_unshield_transition::validate_structure::TokenUnshieldTransitionStructureValidation; use crate::state_transition::batch_transition::token_transfer_transition::validate_structure::TokenTransferTransitionStructureValidation; use crate::state_transition::batch_transition::token_unfreeze_transition::validate_structure::TokenUnfreezeTransitionStructureValidation; use crate::state_transition::state_transitions::document::batch_transition::batched_transition::document_transition::{DocumentTransition, DocumentTransitionV0Methods}; @@ -219,6 +222,15 @@ impl BatchTransition { ) => { set_price_for_direct_purchase_transition.validate_structure(platform_version)? } + TokenTransition::Shield(shield_transition) => { + shield_transition.validate_structure(platform_version)? + } + TokenTransition::Unshield(unshield_transition) => { + unshield_transition.validate_structure(platform_version)? + } + TokenTransition::ShieldedTransfer(shielded_transfer_transition) => { + shielded_transfer_transition.validate_structure(platform_version)? + } }; if !consensus_result.is_valid() { diff --git a/packages/rs-dpp/src/tokens/token_event.rs b/packages/rs-dpp/src/tokens/token_event.rs index 5c836775f12..ecc3e42fffb 100644 --- a/packages/rs-dpp/src/tokens/token_event.rs +++ b/packages/rs-dpp/src/tokens/token_event.rs @@ -166,6 +166,21 @@ pub enum TokenEvent { /// - `TokenAmount`: The amount of tokens purchased. /// - `Credits`: The number of credits paid. DirectPurchase(TokenAmount, Credits), + + /// Event representing tokens moving from an identity balance into the token's shielded pool. + /// + /// - `TokenAmount`: The amount shielded. + Shield(TokenAmount), + + /// Event representing tokens moving from the token's shielded pool to an identity balance. + /// + /// - `RecipientIdentifier`: The identity credited. + /// - `TokenAmount`: The amount unshielded. + Unshield(RecipientIdentifier, TokenAmount), + + /// Event representing a transfer inside the token's shielded pool. Nothing about the + /// transfer (parties, amount) is public. + ShieldedTransfer, } // Manual impl because TokenEvent is a flat enum with u64-alias tuple variants @@ -285,6 +300,24 @@ impl serde::Serialize for TokenEvent { m.serialize_entry("credits", &SafeU64(credits))?; m.end() } + TokenEvent::Shield(amount) => { + let mut m = serializer.serialize_map(Some(2))?; + m.serialize_entry("$type", "shield")?; + m.serialize_entry("amount", &SafeU64(amount))?; + m.end() + } + TokenEvent::Unshield(recipient, amount) => { + let mut m = serializer.serialize_map(Some(3))?; + m.serialize_entry("$type", "unshield")?; + m.serialize_entry("recipient", recipient)?; + m.serialize_entry("amount", &SafeU64(amount))?; + m.end() + } + TokenEvent::ShieldedTransfer => { + let mut m = serializer.serialize_map(Some(1))?; + m.serialize_entry("$type", "shieldedTransfer")?; + m.end() + } } } } @@ -416,6 +449,14 @@ impl<'de> serde::Deserialize<'de> for TokenEvent { amount.ok_or_else(|| A::Error::missing_field("amount"))?, credits.ok_or_else(|| A::Error::missing_field("credits"))?, )), + "shield" => Ok(TokenEvent::Shield( + amount.ok_or_else(|| A::Error::missing_field("amount"))?, + )), + "unshield" => Ok(TokenEvent::Unshield( + recipient.ok_or_else(|| A::Error::missing_field("recipient"))?, + amount.ok_or_else(|| A::Error::missing_field("amount"))?, + )), + "shieldedTransfer" => Ok(TokenEvent::ShieldedTransfer), other => Err(A::Error::unknown_variant( other, &[ @@ -430,6 +471,9 @@ impl<'de> serde::Deserialize<'de> for TokenEvent { "configUpdate", "changePriceForDirectPurchase", "directPurchase", + "shield", + "unshield", + "shieldedTransfer", ], )), } @@ -629,6 +673,11 @@ impl fmt::Display for TokenEvent { TokenEvent::DirectPurchase(amount, credits) => { write!(f, "Direct purchase of {} for {} credits", amount, credits) } + TokenEvent::Shield(amount) => write!(f, "Shield {} into the token pool", amount), + TokenEvent::Unshield(to, amount) => { + write!(f, "Unshield {} from the token pool to {}", amount, to) + } + TokenEvent::ShieldedTransfer => write!(f, "Shielded transfer inside the token pool"), } } } @@ -654,6 +703,9 @@ impl TokenEvent { TokenEvent::ConfigUpdate(..) => "configUpdate", TokenEvent::DirectPurchase(..) => "directPurchase", TokenEvent::ChangePriceForDirectPurchase(..) => "directPricing", + TokenEvent::Shield(..) => "shield", + TokenEvent::Unshield(..) => "unshield", + TokenEvent::ShieldedTransfer => "shieldedTransfer", } } @@ -872,6 +924,18 @@ impl TokenEvent { ("tokenAmount".to_string(), amount.into()), ("purchaseCost".to_string(), total_cost.into()), ]), + TokenEvent::Shield(amount) => BTreeMap::from([ + ("tokenId".to_string(), token_id.into()), + ("amount".to_string(), amount.into()), + ]), + TokenEvent::Unshield(recipient_id, amount) => BTreeMap::from([ + ("tokenId".to_string(), token_id.into()), + ("recipientId".to_string(), recipient_id.into()), + ("amount".to_string(), amount.into()), + ]), + TokenEvent::ShieldedTransfer => { + BTreeMap::from([("tokenId".to_string(), token_id.into())]) + } }; let document: Document = DocumentV0 { diff --git a/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs b/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs index da4dbce479c..9d19d0c43a3 100644 --- a/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs @@ -1,6 +1,7 @@ use crate::error::Error; use crate::execution::check_tx::{CheckTxLevel, CheckTxResult}; use crate::execution::validation::state_transition::check_tx_verification::state_transition_to_execution_event_for_check_tx; +use crate::execution::validation::state_transition::processor::traits::shielded_proof::ShieldedProofAdmissionKey; use crate::execution::validation::state_transition::processor::traits::shielded_proof::{ StateTransitionHasShieldedProofValidationV0, StateTransitionShieldedProofValidationV0, }; @@ -198,21 +199,31 @@ where // the execution-event fee check remains the authoritative check // against actual metered writes before expensive proof work. if errors.is_empty() && matches!(check_tx_level, CheckTxLevel::FirstTimeCheck) { - if let Some((identity_id, nonce)) = + if let Some(admission_key) = state_transition.shielded_proof_identity_nonce_admission_key() { - let committed_nonce = platform_ref.drive.fetch_identity_nonce( - identity_id, - true, - None, - platform_version, - )?; + let committed_nonce = match admission_key { + ShieldedProofAdmissionKey::Identity { identity_id, .. } => platform_ref + .drive + .fetch_identity_nonce(identity_id, true, None, platform_version)?, + ShieldedProofAdmissionKey::IdentityContract { + identity_id, + contract_id, + .. + } => platform_ref.drive.fetch_identity_contract_nonce( + identity_id, + contract_id, + true, + None, + platform_version, + )?, + }; let verification = self .check_tx_proof_verifier .try_acquire_identity_nonce( - identity_id, + admission_key.cache_key(), committed_nonce, - nonce, + admission_key.nonce(), hash_single(raw_tx), state_transition.shielded_proof_action_count(), platform_version.protocol_version, diff --git a/packages/rs-drive-abci/src/execution/engine/run_block_proposal/v0/mod.rs b/packages/rs-drive-abci/src/execution/engine/run_block_proposal/v0/mod.rs index 968882f94d9..6cf2f1c273e 100644 --- a/packages/rs-drive-abci/src/execution/engine/run_block_proposal/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/engine/run_block_proposal/v0/mod.rs @@ -413,6 +413,14 @@ where #[cfg(debug_assertions)] phases.end_phase("prune_shielded_pool_anchors"); + // Token shielded pools: record the new anchor of every pool this block wrote to and + // prune that pool's stale anchors (touch-driven, so untouched pools cost nothing). + self.record_token_shielded_pool_anchors( + state_transitions_result.token_shielded_pools_touched(), + block_proposal.height, + transaction, + platform_version, + )?; // Pool withdrawals into transactions queue diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/mod.rs index a0a96325351..651c02de8c8 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/mod.rs @@ -2,6 +2,7 @@ mod add_process_epoch_change_operations; pub mod process_block_fees_and_validate_sum_trees; mod prune_shielded_pool_anchors; mod record_shielded_pool_anchor; +mod record_token_shielded_pool_anchors; #[cfg(test)] mod tests; diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/record_token_shielded_pool_anchors/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/record_token_shielded_pool_anchors/mod.rs new file mode 100644 index 00000000000..46acffefd5e --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/record_token_shielded_pool_anchors/mod.rs @@ -0,0 +1,44 @@ +mod v0; + +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::platform_types::platform::Platform; +use crate::rpc::core::CoreRPCLike; +use dpp::version::PlatformVersion; +use drive::grovedb::Transaction; +use std::collections::BTreeSet; + +impl Platform +where + C: CoreRPCLike, +{ + /// Records the anchor of every token shielded pool the block touched, if it changed, and + /// prunes that pool's anchors older than the retention window. + pub(in crate::execution) fn record_token_shielded_pool_anchors( + &self, + token_ids: &BTreeSet<[u8; 32]>, + block_height: u64, + transaction: &Transaction, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + match platform_version + .drive_abci + .methods + .block_end + .record_token_shielded_pool_anchors + { + None => Ok(()), + Some(0) => self.record_token_shielded_pool_anchors_v0( + token_ids, + block_height, + transaction, + platform_version, + ), + Some(version) => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "record_token_shielded_pool_anchors".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/record_token_shielded_pool_anchors/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/record_token_shielded_pool_anchors/v0/mod.rs new file mode 100644 index 00000000000..1dcc94b8bad --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/record_token_shielded_pool_anchors/v0/mod.rs @@ -0,0 +1,57 @@ +use crate::error::Error; +use crate::platform_types::platform::Platform; +use crate::rpc::core::CoreRPCLike; +use dpp::version::PlatformVersion; +use drive::grovedb::Transaction; +use std::collections::BTreeSet; + +impl Platform +where + C: CoreRPCLike, +{ + /// For each touched pool: record the current anchor if the commitment tree changed this + /// block, then prune anchors older than `shielded_anchor_retention_blocks`. + /// + /// Pruning is touch-driven rather than interval-driven like the credit pool's: there can + /// be many token pools, so scanning all of them every N blocks is avoided; a pool that is + /// never touched again keeps at most the anchors it accumulated while active, and a pool + /// that is touched prunes as part of the same write. The same "always keep the newest" + /// rule applies, so an idle pool always has one valid anchor to spend against. + pub(super) fn record_token_shielded_pool_anchors_v0( + &self, + token_ids: &BTreeSet<[u8; 32]>, + block_height: u64, + transaction: &Transaction, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let retention_blocks = platform_version + .drive_abci + .validation_and_processing + .event_constants + .shielded_anchor_retention_blocks; + + for token_id in token_ids { + self.drive + .record_token_shielded_pool_anchor_if_changed( + *token_id, + block_height, + transaction, + platform_version, + ) + .map_err(Error::Drive)?; + + if block_height > retention_blocks { + self.drive + .prune_token_shielded_pool_anchors( + *token_id, + block_height - retention_blocks, + transaction, + platform_version, + ) + .map_err(Error::Drive)?; + } + } + + Ok(()) + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/test/tokens.rs b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/test/tokens.rs index 87887f2fae3..3f809f576f7 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/test/tokens.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/initialization/create_genesis_state/test/tokens.rs @@ -385,7 +385,7 @@ impl Platform { ); let mut token_configuration_2 = token_configuration.clone(); - let TokenConfiguration::V0(ref mut cfg) = token_configuration_2; + let cfg = token_configuration_2.as_v0_mut(); let TokenDistributionRules::V0(ref mut rules) = cfg.distribution_rules else { panic!("expected version 0 distribution rules"); }; diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index 5820f88c0c3..7c5badddf26 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -811,6 +811,12 @@ impl Platform { // exists, so both node populations build the same prefunded balances Merk. self.drive .insert_contract_fee_pot_trees(Some(transaction), platform_version)?; + // Token shielded pools root: the BigSumTree under the Tokens tree that holds one Orchard + // pool per token opting in (`TokenConfigurationV1::has_shielded_pool`). CONSENSUS-CRITICAL: + // the genesis-v14 path (`Drive::create_initial_state_structure_v4`) calls the same helper, + // so a chain born at v14 and one upgraded to it build a byte-identical `[Tokens]` subtree. + self.drive + .insert_token_shielded_pools_root_tree(Some(transaction), platform_version)?; Ok(()) } diff --git a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs index 41ee5c9a904..d7c3f2c9356 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs @@ -5,6 +5,12 @@ use crate::rpc::core::CoreRPCLike; use dpp::block::block_info::BlockInfo; use dpp::consensus::codes::ErrorWithCode; use dpp::fee::Credits; +use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; +use dpp::state_transition::batch_transition::batched_transition::token_transition::{ + TokenTransition, TokenTransitionV0Methods, +}; +use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRef; +use dpp::state_transition::StateTransition; use crate::execution::types::state_transition_container::v0::{ DecodedStateTransition, InvalidStateTransition, InvalidWithProtocolErrorStateTransition, @@ -188,6 +194,11 @@ where } let credit_mints_at_savepoint = block_credit_mints; + // Remembered before the transition is consumed: the pools an applied + // batch touched get their anchor recorded at block end. + let token_shielded_pools_touched = + token_shielded_pools_touched(&state_transition); + // Validate state transition and produce an execution event let execution_result = process_state_transition( &platform_ref, @@ -272,6 +283,19 @@ where } } + // An applied transition (successful, or a paid failure whose writes + // stay in the block) may have appended notes to a token pool. A paid + // failure that only bumped the nonce is recorded too: recording is + // "if changed", so an untouched pool costs one anchor read. + if matches!( + execution_result, + StateTransitionExecutionResult::SuccessfulExecution { .. } + | StateTransitionExecutionResult::PaidConsensusError { .. } + ) { + processing_result + .add_token_shielded_pools_touched(token_shielded_pools_touched); + } + // Store metrics let elapsed_time = start_time.elapsed() + decoding_elapsed_time; @@ -368,3 +392,22 @@ fn error_to_internal_error_execution_result( StateTransitionExecutionResult::InternalError(error_with_st.error.to_string()) } + +/// The token shielded pools a state transition writes to: the token ids of every +/// `TokenShield`, `TokenUnshield` and `TokenShieldedTransfer` in a batch. +fn token_shielded_pools_touched(state_transition: &StateTransition) -> Vec<[u8; 32]> { + match state_transition { + StateTransition::Batch(batch) => batch + .transitions_iter() + .filter_map(|transition| match transition { + BatchedTransitionRef::Token( + token_transition @ (TokenTransition::Shield(_) + | TokenTransition::Unshield(_) + | TokenTransition::ShieldedTransfer(_)), + ) => Some(token_transition.token_id().to_buffer()), + _ => None, + }) + .collect(), + _ => vec![], + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/tokens/validate_token_aggregated_balance/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/tokens/validate_token_aggregated_balance/v0/mod.rs index 0463df8551d..96da77bf13b 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/tokens/validate_token_aggregated_balance/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/tokens/validate_token_aggregated_balance/v0/mod.rs @@ -25,7 +25,7 @@ impl Platform { "tokens are not balanced after block execution {:?} off by {}", token_balance, token_balance - .total_identity_token_balances + .total_balances()? .abs_diff(token_balance.total_tokens_in_platform) )), )); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs index 3377772d2e0..d4e5c6f5a65 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs @@ -4,6 +4,9 @@ use crate::platform_types::platform::PlatformRef; use crate::rpc::core::CoreRPCLike; use dpp::consensus::basic::state_transition::StateTransitionNotActiveError; use dpp::prelude::ConsensusValidationResult; +use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; +use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransition; +use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRef; use dpp::state_transition::StateTransition; use dpp::version::feature_initial_protocol_versions::{ ADDRESS_FUNDS_INITIAL_PROTOCOL_VERSION, CONTRACT_FEE_CLAIM_INITIAL_PROTOCOL_VERSION, @@ -11,6 +14,7 @@ use dpp::version::feature_initial_protocol_versions::{ IDENTITY_KEY_LIMITS_UPDATE_INITIAL_PROTOCOL_VERSION, IDENTITY_TOP_UP_FROM_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION, SHIELDED_POOL_INITIAL_PROTOCOL_VERSION, SHIELD_FROM_IDENTITY_INITIAL_PROTOCOL_VERSION, + TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION, }; use dpp::version::PlatformVersion; @@ -64,7 +68,39 @@ impl StateTransitionIsAllowedValidationV0 for StateTransition { platform_version: &PlatformVersion, ) -> Result, Error> { match self { - StateTransition::Batch(st) => st.validate_is_allowed(platform, platform_version), + StateTransition::Batch(st) => { + // Token shielded pools (and the three batch transitions that use them) are a + // protocol-version feature, not a table-versioned validator, so the gate is + // applied to the batch as a whole before its own `is_allowed` runs. + if platform_version.protocol_version < TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION + { + if let Some(transition) = + st.transitions_iter() + .find_map(|transition| match transition { + BatchedTransitionRef::Token(TokenTransition::Shield(_)) => { + Some("TokenShield") + } + BatchedTransitionRef::Token(TokenTransition::Unshield(_)) => { + Some("TokenUnshield") + } + BatchedTransitionRef::Token(TokenTransition::ShieldedTransfer( + _, + )) => Some("TokenShieldedTransfer"), + _ => None, + }) + { + return Ok(ConsensusValidationResult::new_with_errors(vec![ + StateTransitionNotActiveError::new( + transition, + platform_version.protocol_version, + TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION, + ) + .into(), + ])); + } + } + st.validate_is_allowed(platform, platform_version) + } StateTransition::IdentityTopUpFromAddresses(_) | StateTransition::IdentityCreateFromAddresses(_) | StateTransition::AddressFundsTransfer(_) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs index 41c519609ea..c3c846f1b64 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs @@ -21,10 +21,69 @@ use dpp::state_transition::public_key_in_creation::IdentityPublicKeyInCreation; use dpp::state_transition::state_transitions::shielded::identity_create_from_shielded_pool_transition::IdentityCreateFromShieldedPoolTransition; use dpp::state_transition::identity_top_up_from_shielded_pool_transition::IdentityTopUpFromShieldedPoolTransition; use dpp::state_transition::shield_from_identity_transition::ShieldFromIdentityTransition; -use dpp::state_transition::StateTransition; +use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; +use dpp::state_transition::batch_transition::batched_transition::token_transition::{ + TokenTransition, TokenTransitionV0Methods, +}; +use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRef; +use dpp::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use dpp::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; +use dpp::state_transition::batch_transition::token_shield_transition::v0::v0_methods::TokenShieldTransitionV0Methods; +use dpp::state_transition::batch_transition::token_shielded_transfer_transition::v0::v0_methods::TokenShieldedTransferTransitionV0Methods; +use dpp::state_transition::batch_transition::token_unshield_transition::v0::v0_methods::TokenUnshieldTransitionV0Methods; +use dpp::state_transition::batch_transition::BatchTransition; +use dpp::state_transition::{StateTransition, StateTransitionOwned}; +use dpp::util::hash::hash_single; use dpp::validation::SimpleConsensusValidationResult; use dpp::version::PlatformVersion; +/// The (identity, nonce) pair CheckTx's `CheckTxProofVerifier` admits Orchard proof work under +/// for identity-signed shielded transitions, so an identity cannot start unbounded verification +/// attempts for one nonce. +/// +/// `ShieldFromIdentity` keys on the identity nonce. A batch carrying token shielded transitions +/// keys on its identity CONTRACT nonce: the batch is replay-protected by that nonce, so it is the +/// counter whose committed value bounds the attempts. The two nonce spaces are unrelated, so the +/// contract-keyed form derives its own cache identity from `(identity_id, contract_id)`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ShieldedProofAdmissionKey { + /// Keyed on the identity nonce (`ShieldFromIdentity`). + Identity { identity_id: [u8; 32], nonce: u64 }, + /// Keyed on the identity contract nonce (a batch with token shielded transitions). + IdentityContract { + identity_id: [u8; 32], + contract_id: [u8; 32], + nonce: u64, + }, +} + +impl ShieldedProofAdmissionKey { + /// The 32-byte key the verifier tracks attempts under. + pub(crate) fn cache_key(&self) -> [u8; 32] { + match self { + ShieldedProofAdmissionKey::Identity { identity_id, .. } => *identity_id, + ShieldedProofAdmissionKey::IdentityContract { + identity_id, + contract_id, + .. + } => { + let mut preimage = Vec::with_capacity(64); + preimage.extend_from_slice(identity_id); + preimage.extend_from_slice(contract_id); + hash_single(preimage) + } + } + } + + /// The nonce the transition attempts. + pub(crate) fn nonce(&self) -> u64 { + match self { + ShieldedProofAdmissionKey::Identity { nonce, .. } + | ShieldedProofAdmissionKey::IdentityContract { nonce, .. } => *nonce, + } + } +} + /// A trait for checking whether a state transition requires shielded ZK proof validation. pub(crate) trait StateTransitionHasShieldedProofValidationV0 { /// Returns true if this state transition has a ZK proof that must be verified @@ -34,11 +93,11 @@ pub(crate) trait StateTransitionHasShieldedProofValidationV0 { /// Returns the number of Orchard actions whose proof work must be admitted. fn shielded_proof_action_count(&self) -> usize; - /// Returns the identity and nonce that must not start repeated Orchard - /// verification attempts in CheckTx. Only ShieldFromIdentity uses this - /// admission key; shielded spends are already replay-protected by their - /// nullifiers. - fn shielded_proof_identity_nonce_admission_key(&self) -> Option<([u8; 32], u64)>; + /// Returns the admission key that must not start repeated Orchard verification attempts + /// in CheckTx: `ShieldFromIdentity` (identity nonce) and batches carrying token shielded + /// transitions (identity contract nonce). Pool-paid shielded spends are already + /// replay-protected by their nullifiers and return `None`. + fn shielded_proof_identity_nonce_admission_key(&self) -> Option; /// Returns true if this state transition pays fees from the shielded pool's /// value_balance and requires minimum fee validation. @@ -117,14 +176,45 @@ impl StateTransitionHasShieldedProofValidationV0 for StateTransition { v0.actions.len() } }, + StateTransition::Batch(batch) => batch + .transitions_iter() + .map(|transition| match transition { + BatchedTransitionRef::Token(TokenTransition::Shield(t)) => t.actions().len(), + BatchedTransitionRef::Token(TokenTransition::Unshield(t)) => t.actions().len(), + BatchedTransitionRef::Token(TokenTransition::ShieldedTransfer(t)) => { + t.actions().len() + } + _ => 0, + }) + .sum(), _ => 0, } } - fn shielded_proof_identity_nonce_admission_key(&self) -> Option<([u8; 32], u64)> { + fn shielded_proof_identity_nonce_admission_key(&self) -> Option { match self { StateTransition::ShieldFromIdentity(ShieldFromIdentityTransition::V0(v0)) => { - Some((v0.identity_id.to_buffer(), v0.nonce)) + Some(ShieldedProofAdmissionKey::Identity { + identity_id: v0.identity_id.to_buffer(), + nonce: v0.nonce, + }) + } + StateTransition::Batch(batch) => { + let owner_id = batch.owner_id().to_buffer(); + batch + .transitions_iter() + .find_map(|transition| match transition { + BatchedTransitionRef::Token( + token_transition @ (TokenTransition::Shield(_) + | TokenTransition::Unshield(_) + | TokenTransition::ShieldedTransfer(_)), + ) => Some(ShieldedProofAdmissionKey::IdentityContract { + identity_id: owner_id, + contract_id: token_transition.data_contract_id().to_buffer(), + nonce: token_transition.identity_contract_nonce(), + }), + _ => None, + }) } _ => None, } @@ -472,6 +562,9 @@ impl StateTransitionShieldedProofValidationV0 for StateTransition { { 0 => validate_shielded_proof_v0(self, platform_version), 1 => { + if let StateTransition::Batch(batch) = self { + return validate_batch_token_shielded_proofs(batch, platform_version); + } // v1 refuses, in `IdentityCreateFromShieldedPool`, the keys the v0 Orchard sighash // preimage cannot bind, before that preimage is built: a key bound to a contract // group (the layout predates group bounds, and an error out of the preimage @@ -1200,3 +1293,67 @@ mod tests { } } } + +/// Verifies every token shielded bundle a batch carries, statelessly. +fn validate_batch_token_shielded_proofs( + batch: &BatchTransition, + platform_version: &PlatformVersion, +) -> Result { + let owner_id = batch.owner_id().to_buffer(); + for transition in batch.transitions_iter() { + let result = match transition { + BatchedTransitionRef::Token(TokenTransition::Shield(t)) => { + reconstruct_and_verify_bundle( + t.actions(), + FLAGS_OUTPUTS_ONLY, + -(t.amount() as i64), + t.anchor(), + t.proof(), + t.binding_signature(), + &[], + ) + } + BatchedTransitionRef::Token(TokenTransition::Unshield(t)) => { + let extra_sighash_data = dpp::shielded::token_unshield_extra_sighash_data( + &t.base().token_id().to_buffer(), + &owner_id, + &t.recipient_id().to_buffer(), + t.amount(), + platform_version, + )?; + reconstruct_and_verify_bundle( + t.actions(), + FLAGS_SPENDS_AND_OUTPUTS, + t.amount() as i64, + t.anchor(), + t.proof(), + t.binding_signature(), + &extra_sighash_data, + ) + } + BatchedTransitionRef::Token(TokenTransition::ShieldedTransfer(t)) => { + let extra_sighash_data = dpp::shielded::token_shielded_transfer_extra_sighash_data( + &t.base().token_id().to_buffer(), + &owner_id, + platform_version, + )?; + reconstruct_and_verify_bundle( + t.actions(), + FLAGS_SPENDS_AND_OUTPUTS, + 0, + t.anchor(), + t.proof(), + t.binding_signature(), + &extra_sighash_data, + ) + } + _ => continue, + }; + if let Err(e) = result { + return Ok(SimpleConsensusValidationResult::new_with_error( + StateError::InvalidShieldedProofError(e).into(), + )); + } + } + Ok(SimpleConsensusValidationResult::new()) +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/mod.rs index de6bf27f70d..6cdc64363b0 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/mod.rs @@ -8,5 +8,9 @@ pub(crate) mod token_emergency_action_transition_action; pub(crate) mod token_freeze_transition_action; pub(crate) mod token_mint_transition_action; pub(crate) mod token_set_price_for_direct_purchase_transition_action; +pub(crate) mod token_shield_transition_action; +pub(crate) mod token_shielded_pool_common; +pub(crate) mod token_shielded_transfer_transition_action; pub(crate) mod token_transfer_transition_action; pub(crate) mod token_unfreeze_transition_action; +pub(crate) mod token_unshield_transition_action; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/mod.rs new file mode 100644 index 00000000000..d34d102487b --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/mod.rs @@ -0,0 +1,66 @@ +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::token::token_shield_transition_action::state_v0::TokenShieldTransitionActionStateValidationV0; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; +use dpp::identifier::Identifier; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::batch::batched_transition::token_transition::token_shield_transition_action::TokenShieldTransitionAction; + +mod state_v0; + +pub trait TokenShieldTransitionActionValidation { + /// Stateful validation. `validation_mode` decides whether the Orchard proof is verified + /// here (block processing) or left to CheckTx's admission-limited path. + #[allow(clippy::too_many_arguments)] + fn validate_state( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenShieldTransitionActionValidation for TokenShieldTransitionAction { + fn validate_state( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .token_shield_transition_state_validation + { + 0 => self.validate_state_v0( + platform, + owner_id, + block_info, + execution_context, + validation_mode, + transaction, + platform_version, + ), + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "TokenShieldTransitionAction::validate_state".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs new file mode 100644 index 00000000000..3cffb8a884f --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs @@ -0,0 +1,142 @@ +use crate::error::Error; +use crate::execution::types::execution_operation::ValidationOperation; +use crate::execution::types::state_transition_execution_context::{ + StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0, +}; +use crate::execution::validation::state_transition::batch::action_validation::token::token_base_transition_action::TokenBaseTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_pool_common::{ + validate_identity_token_account_not_frozen, validate_token_not_paused, + validate_token_shielded_pool_enabled, verify_token_pool_bundle, +}; +use crate::execution::validation::state_transition::state_transitions::shielded_common::FLAGS_OUTPUTS_ONLY; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; +use dpp::consensus::state::state_error::StateError; +use dpp::consensus::state::token::IdentityDoesNotHaveEnoughTokenBalanceError; +use dpp::consensus::ConsensusError; +use dpp::prelude::Identifier; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::query::TransactionArg; +use drive::state_transition_action::batch::batched_transition::token_transition::token_shield_transition_action::{ + TokenShieldTransitionAction, TokenShieldTransitionActionAccessorsV0, +}; + +pub(in crate::execution::validation::state_transition::state_transitions::batch::action_validation) trait TokenShieldTransitionActionStateValidationV0 { + #[allow(clippy::too_many_arguments)] + fn validate_state_v0( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenShieldTransitionActionStateValidationV0 for TokenShieldTransitionAction { + /// Shielding is a transfer from the owner's balance into the pool, so it runs the sender + /// side of the transfer checks (balance, frozen account, paused token) on top of the pool + /// opt-in, then verifies the outputs-only bundle. The bundle's anchor is not checked + /// against the pool: with spends disabled the anchor is not consumed. + fn validate_state_v0( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let validation_result = self.base().validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let validation_result = validate_token_shielded_pool_enabled(self.base())?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let token_id = self.token_id(); + + let balance = platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + owner_id.to_buffer(), + transaction, + platform_version, + )? + .unwrap_or_default(); + + execution_context.add_operation(ValidationOperation::RetrieveIdentityTokenBalance); + + if balance < self.amount() { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::IdentityDoesNotHaveEnoughTokenBalanceError( + IdentityDoesNotHaveEnoughTokenBalanceError::new( + token_id, + owner_id, + self.amount(), + balance, + "shield".to_string(), + ), + )), + )); + } + + let validation_result = validate_identity_token_account_not_frozen( + platform, + token_id, + owner_id, + "shield", + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let validation_result = validate_token_not_paused( + platform, + token_id, + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + // Outputs-only bundle entering the pool: value balance is `-amount`. The identity + // signature over the batch already binds the bundle to this token, owner and amount, so + // no extra sighash data is bound (exactly like `ShieldFromIdentity`). + verify_token_pool_bundle( + execution_context, + validation_mode, + self.actions(), + FLAGS_OUTPUTS_ONLY, + -(self.amount() as i64), + self.anchor(), + self.proof(), + self.binding_signature(), + &[], + platform_version, + ) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs new file mode 100644 index 00000000000..07847cd9bec --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs @@ -0,0 +1,293 @@ +//! Stateful checks shared by the three token shielded pool validators (`TokenShield`, +//! `TokenUnshield`, `TokenShieldedTransfer`). +//! +//! These mirror the credit pool helpers in `shielded_common`, re-rooted under the token's +//! pool, and meter every read into the execution context so the batch owner pays for them. +//! The Orchard proof is verified last and only in block processing (`ValidationMode::Validator`); +//! CheckTx runs it separately under the nonce-aware `CheckTxProofVerifier` admission, exactly +//! as `ShieldFromIdentity` does, so a mempool flood of bad proofs cannot burn validator CPU +//! for free while a bad proof in a block is still a paid failure (the identity contract nonce +//! is consumed and the verification fee charged). + +use crate::error::Error; +use crate::execution::types::execution_operation::ValidationOperation; +use crate::execution::types::state_transition_execution_context::{ + StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0, +}; +use crate::execution::validation::state_transition::state_transitions::shielded_common::reconstruct_and_verify_bundle; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; +use dpp::consensus::state::shielded::insufficient_pool_notes_error::InsufficientPoolNotesError; +use dpp::consensus::state::shielded::invalid_anchor_error::InvalidAnchorError; +use dpp::consensus::state::shielded::nullifier_already_spent_error::NullifierAlreadySpentError; +use dpp::consensus::state::state_error::StateError; +use dpp::consensus::state::token::{ + IdentityTokenAccountFrozenError, TokenIsPausedError, TokenShieldedPoolNotEnabledError, +}; +use dpp::consensus::ConsensusError; +use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters; +use dpp::fee::fee_result::FeeResult; +use dpp::prelude::Identifier; +use dpp::shielded::{compute_shielded_verification_fee, SerializedAction}; +use dpp::tokens::info::v0::IdentityTokenInfoV0Accessors; +use dpp::tokens::status::v0::TokenStatusV0Accessors; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::drive::Drive; +use drive::fees::op::LowLevelDriveOperation; +use drive::query::TransactionArg; +use drive::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{ + TokenBaseTransitionAction, TokenBaseTransitionActionAccessorsV0, +}; +use std::collections::HashSet; + +/// The token's configuration must opt into a shielded pool. +pub(super) fn validate_token_shielded_pool_enabled( + base: &TokenBaseTransitionAction, +) -> Result { + if !base.token_configuration()?.has_shielded_pool() { + return Ok(SimpleConsensusValidationResult::new_with_error( + TokenShieldedPoolNotEnabledError::new(base.token_id()).into(), + )); + } + Ok(SimpleConsensusValidationResult::new()) +} + +/// A paused token allows no shielded operation either: shielding, unshielding and pool +/// transfers all move the token. +pub(super) fn validate_token_not_paused( + platform: &PlatformStateRef, + token_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, +) -> Result { + let (token_status, fee_result) = platform.drive.fetch_token_status_with_costs( + token_id.to_buffer(), + block_info, + true, + transaction, + platform_version, + )?; + + execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + if let Some(status) = token_status { + if status.paused() { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::TokenIsPausedError( + TokenIsPausedError::new(token_id), + )), + )); + } + } + + Ok(SimpleConsensusValidationResult::new()) +} + +/// A frozen identity token account can neither shield out of nor (unless the token allows +/// transfers to frozen balances) receive an unshield into its balance. +#[allow(clippy::too_many_arguments)] +pub(super) fn validate_identity_token_account_not_frozen( + platform: &PlatformStateRef, + token_id: Identifier, + identity_id: Identifier, + action_name: &str, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, +) -> Result { + let (info, fee_result) = platform.drive.fetch_identity_token_info_with_costs( + token_id.to_buffer(), + identity_id.to_buffer(), + block_info, + true, + transaction, + platform_version, + )?; + + execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + if let Some(info) = info { + if info.frozen() { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::IdentityTokenAccountFrozenError( + IdentityTokenAccountFrozenError::new( + token_id, + identity_id, + action_name.to_string(), + ), + )), + )); + } + } + + Ok(SimpleConsensusValidationResult::new()) +} + +/// Meters the GroveDB reads collected in `drive_operations` into the execution context so +/// the batch owner pays for them, as every other identity-paid read does. +pub(super) fn charge_drive_operations( + drive: &Drive, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + drive_operations: Vec, + platform_version: &PlatformVersion, +) -> Result<(), Error> { + if drive_operations.is_empty() { + return Ok(()); + } + let fee_result = Drive::calculate_fee( + None, + Some(drive_operations), + &block_info.epoch, + drive.config.epochs_per_era, + platform_version, + None, + )?; + execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + Ok(()) +} + +/// The bundle's anchor must be one of the token pool's recorded anchors (O(1) lookup). +pub(super) fn validate_token_pool_anchor_exists( + drive: &Drive, + token_id: &[u8; 32], + anchor: &[u8; 32], + transaction: TransactionArg, + drive_operations: &mut Vec, + platform_version: &PlatformVersion, +) -> Result { + let found = drive.has_token_pool_anchor( + token_id, + anchor, + transaction, + drive_operations, + platform_version, + )?; + if !found { + return Ok(SimpleConsensusValidationResult::new_with_error( + StateError::InvalidAnchorError(InvalidAnchorError::new(*anchor)).into(), + )); + } + Ok(SimpleConsensusValidationResult::new()) +} + +/// No nullifier may repeat within the bundle or already be spent in the token pool. +pub(super) fn validate_token_pool_nullifiers( + drive: &Drive, + token_id: &[u8; 32], + nullifiers: &[[u8; 32]], + transaction: TransactionArg, + drive_operations: &mut Vec, + platform_version: &PlatformVersion, +) -> Result { + let mut seen = HashSet::new(); + for nullifier in nullifiers { + if !seen.insert(nullifier) { + return Ok(SimpleConsensusValidationResult::new_with_error( + StateError::NullifierAlreadySpentError(NullifierAlreadySpentError::new(*nullifier)) + .into(), + )); + } + } + for nullifier in nullifiers { + if drive.has_token_pool_nullifier( + token_id, + nullifier, + transaction, + drive_operations, + platform_version, + )? { + return Ok(SimpleConsensusValidationResult::new_with_error( + StateError::NullifierAlreadySpentError(NullifierAlreadySpentError::new(*nullifier)) + .into(), + )); + } + } + Ok(SimpleConsensusValidationResult::new()) +} + +/// The anonymity-set floor for outflows with an observable destination +/// (`minimum_token_pool_notes_for_outgoing`, 0 at introduction). +pub(super) fn validate_minimum_token_pool_notes( + drive: &Drive, + token_id: &[u8; 32], + transaction: TransactionArg, + drive_operations: &mut Vec, + platform_version: &PlatformVersion, +) -> Result { + let min_notes = platform_version + .drive_abci + .validation_and_processing + .event_constants + .minimum_token_pool_notes_for_outgoing; + if min_notes > 0 { + let notes_count = drive.token_shielded_pool_notes_count( + token_id, + transaction, + drive_operations, + platform_version, + )?; + if notes_count < min_notes { + return Ok(SimpleConsensusValidationResult::new_with_error( + StateError::InsufficientPoolNotesError(InsufficientPoolNotesError::new( + notes_count, + min_notes, + )) + .into(), + )); + } + } + Ok(SimpleConsensusValidationResult::new()) +} + +/// Verifies the Orchard bundle as a PAID step. +/// +/// The flat shielded compute fee (`compute_shielded_verification_fee`: one bundle verification +/// plus the per-action work) is charged BEFORE verifying, so an invalid proof in a block still +/// costs its verifier time and consumes the identity contract nonce. In CheckTx the proof is +/// skipped here: `check_tx` verifies it afterwards under the nonce-aware admission limiter, +/// once the fee estimate has shown the identity can pay for it. +#[allow(clippy::too_many_arguments)] +pub(super) fn verify_token_pool_bundle( + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + actions: &[SerializedAction], + flags: u8, + value_balance: i64, + anchor: &[u8; 32], + proof: &[u8], + binding_signature: &[u8; 64], + extra_sighash_data: &[u8], + platform_version: &PlatformVersion, +) -> Result { + let verification_fee = compute_shielded_verification_fee(actions.len(), platform_version)?; + execution_context.add_operation(ValidationOperation::PrecalculatedOperation(FeeResult { + processing_fee: verification_fee, + ..Default::default() + })); + + if !matches!(validation_mode, ValidationMode::Validator) { + return Ok(SimpleConsensusValidationResult::new()); + } + + if let Err(error) = reconstruct_and_verify_bundle( + actions, + flags, + value_balance, + anchor, + proof, + binding_signature, + extra_sighash_data, + ) { + return Ok(SimpleConsensusValidationResult::new_with_error( + StateError::InvalidShieldedProofError(error).into(), + )); + } + + Ok(SimpleConsensusValidationResult::new()) +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_transfer_transition_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_transfer_transition_action/mod.rs new file mode 100644 index 00000000000..692472c690b --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_transfer_transition_action/mod.rs @@ -0,0 +1,66 @@ +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_transfer_transition_action::state_v0::TokenShieldedTransferTransitionActionStateValidationV0; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; +use dpp::identifier::Identifier; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::batch::batched_transition::token_transition::token_shielded_transfer_transition_action::TokenShieldedTransferTransitionAction; + +mod state_v0; + +pub trait TokenShieldedTransferTransitionActionValidation { + /// Stateful validation. `validation_mode` decides whether the Orchard proof is verified + /// here (block processing) or left to CheckTx's admission-limited path. + #[allow(clippy::too_many_arguments)] + fn validate_state( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenShieldedTransferTransitionActionValidation for TokenShieldedTransferTransitionAction { + fn validate_state( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .token_shielded_transfer_transition_state_validation + { + 0 => self.validate_state_v0( + platform, + owner_id, + block_info, + execution_context, + validation_mode, + transaction, + platform_version, + ), + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "TokenShieldedTransferTransitionAction::validate_state".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_transfer_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_transfer_transition_action/state_v0/mod.rs new file mode 100644 index 00000000000..fe0e3fcb951 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_transfer_transition_action/state_v0/mod.rs @@ -0,0 +1,144 @@ +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::token::token_base_transition_action::TokenBaseTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_pool_common::{ + charge_drive_operations, validate_token_not_paused, validate_token_pool_anchor_exists, + validate_token_pool_nullifiers, validate_token_shielded_pool_enabled, + verify_token_pool_bundle, +}; +use crate::execution::validation::state_transition::state_transitions::shielded_common::FLAGS_SPENDS_AND_OUTPUTS; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; +use dpp::prelude::Identifier; +use dpp::shielded::token_shielded_transfer_extra_sighash_data; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::query::TransactionArg; +use drive::state_transition_action::batch::batched_transition::token_transition::token_shielded_transfer_transition_action::{ + TokenShieldedTransferTransitionAction, TokenShieldedTransferTransitionActionAccessorsV0, +}; + +pub(in crate::execution::validation::state_transition::state_transitions::batch::action_validation) trait TokenShieldedTransferTransitionActionStateValidationV0 { + #[allow(clippy::too_many_arguments)] + fn validate_state_v0( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenShieldedTransferTransitionActionStateValidationV0 + for TokenShieldedTransferTransitionAction +{ + /// A pool-internal transfer touches no identity balance: pool opt-in, paused token, anchor + /// and unspent nullifiers, then the zero-value-balance spend bundle is verified with the + /// token and batch owner bound into the sighash. No notes floor applies: nothing leaves + /// the pool, exactly as for the credit pool's `ShieldedTransfer`. + fn validate_state_v0( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let validation_result = self.base().validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let validation_result = validate_token_shielded_pool_enabled(self.base())?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let token_id = self.token_id(); + + let validation_result = validate_token_not_paused( + platform, + token_id, + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let token_id_bytes = token_id.to_buffer(); + let mut drive_operations = vec![]; + + let validation_result = validate_token_pool_anchor_exists( + platform.drive, + &token_id_bytes, + self.anchor(), + transaction, + &mut drive_operations, + platform_version, + )?; + if !validation_result.is_valid() { + charge_drive_operations( + platform.drive, + block_info, + execution_context, + drive_operations, + platform_version, + )?; + return Ok(validation_result); + } + + let validation_result = validate_token_pool_nullifiers( + platform.drive, + &token_id_bytes, + &self.nullifiers(), + transaction, + &mut drive_operations, + platform_version, + )?; + charge_drive_operations( + platform.drive, + block_info, + execution_context, + drive_operations, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let extra_sighash_data = token_shielded_transfer_extra_sighash_data( + &token_id_bytes, + &owner_id.to_buffer(), + platform_version, + )?; + + verify_token_pool_bundle( + execution_context, + validation_mode, + self.actions(), + FLAGS_SPENDS_AND_OUTPUTS, + 0, + self.anchor(), + self.proof(), + self.binding_signature(), + &extra_sighash_data, + platform_version, + ) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/mod.rs new file mode 100644 index 00000000000..c8f67daa886 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/mod.rs @@ -0,0 +1,66 @@ +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::token::token_unshield_transition_action::state_v0::TokenUnshieldTransitionActionStateValidationV0; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; +use dpp::identifier::Identifier; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::batch::batched_transition::token_transition::token_unshield_transition_action::TokenUnshieldTransitionAction; + +mod state_v0; + +pub trait TokenUnshieldTransitionActionValidation { + /// Stateful validation. `validation_mode` decides whether the Orchard proof is verified + /// here (block processing) or left to CheckTx's admission-limited path. + #[allow(clippy::too_many_arguments)] + fn validate_state( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenUnshieldTransitionActionValidation for TokenUnshieldTransitionAction { + fn validate_state( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .token_unshield_transition_state_validation + { + 0 => self.validate_state_v0( + platform, + owner_id, + block_info, + execution_context, + validation_mode, + transaction, + platform_version, + ), + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "TokenUnshieldTransitionAction::validate_state".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/state_v0/mod.rs new file mode 100644 index 00000000000..3f2d13d7247 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/state_v0/mod.rs @@ -0,0 +1,230 @@ +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::token::token_base_transition_action::TokenBaseTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_pool_common::{ + charge_drive_operations, validate_identity_token_account_not_frozen, + validate_minimum_token_pool_notes, validate_token_not_paused, validate_token_pool_anchor_exists, + validate_token_pool_nullifiers, validate_token_shielded_pool_enabled, + verify_token_pool_bundle, +}; +use crate::execution::validation::state_transition::common::validate_identity_exists::validate_identity_exists; +use crate::execution::validation::state_transition::state_transitions::shielded_common::FLAGS_SPENDS_AND_OUTPUTS; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; +use dpp::consensus::state::shielded::invalid_shielded_proof_error::InvalidShieldedProofError; +use dpp::consensus::state::state_error::StateError; +use dpp::consensus::state::token::TokenTransferRecipientIdentityNotExistError; +use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dpp::prelude::Identifier; +use dpp::shielded::token_unshield_extra_sighash_data; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::query::TransactionArg; +use drive::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::token_transition::token_unshield_transition_action::{ + TokenUnshieldTransitionAction, TokenUnshieldTransitionActionAccessorsV0, +}; + +pub(in crate::execution::validation::state_transition::state_transitions::batch::action_validation) trait TokenUnshieldTransitionActionStateValidationV0 { + #[allow(clippy::too_many_arguments)] + fn validate_state_v0( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenUnshieldTransitionActionStateValidationV0 for TokenUnshieldTransitionAction { + /// Unshielding is a transfer from the pool into the recipient's balance: the recipient + /// side of the transfer checks (exists, frozen account unless allowed, paused token) plus + /// the pool-side checks the credit pool's `Unshield` runs (notes floor, anchor, unspent + /// nullifiers, pool balance), then the spend bundle is verified with the token, batch + /// owner, recipient and amount bound into the sighash. + fn validate_state_v0( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let validation_result = self.base().validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let validation_result = validate_token_shielded_pool_enabled(self.base())?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let token_id = self.token_id(); + let recipient_id = self.recipient_id(); + + let validation_result = validate_token_not_paused( + platform, + token_id, + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let recipient_exists = validate_identity_exists( + platform.drive, + &recipient_id, + execution_context, + transaction, + platform_version, + )?; + if !recipient_exists { + return Ok(SimpleConsensusValidationResult::new_with_error( + TokenTransferRecipientIdentityNotExistError::new(recipient_id).into(), + )); + } + + if !self + .base() + .token_configuration()? + .is_allowed_transfer_to_frozen_balance() + { + let validation_result = validate_identity_token_account_not_frozen( + platform, + token_id, + recipient_id, + "unshield", + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + } + + let token_id_bytes = token_id.to_buffer(); + let mut drive_operations = vec![]; + + let validation_result = validate_minimum_token_pool_notes( + platform.drive, + &token_id_bytes, + transaction, + &mut drive_operations, + platform_version, + )?; + if !validation_result.is_valid() { + charge_drive_operations( + platform.drive, + block_info, + execution_context, + drive_operations, + platform_version, + )?; + return Ok(validation_result); + } + + let validation_result = validate_token_pool_anchor_exists( + platform.drive, + &token_id_bytes, + self.anchor(), + transaction, + &mut drive_operations, + platform_version, + )?; + if !validation_result.is_valid() { + charge_drive_operations( + platform.drive, + block_info, + execution_context, + drive_operations, + platform_version, + )?; + return Ok(validation_result); + } + + let validation_result = validate_token_pool_nullifiers( + platform.drive, + &token_id_bytes, + &self.nullifiers(), + transaction, + &mut drive_operations, + platform_version, + )?; + if !validation_result.is_valid() { + charge_drive_operations( + platform.drive, + block_info, + execution_context, + drive_operations, + platform_version, + )?; + return Ok(validation_result); + } + + let pool_balance = platform.drive.read_token_shielded_pool_total_balance( + &token_id_bytes, + transaction, + &mut drive_operations, + platform_version, + )?; + + charge_drive_operations( + platform.drive, + block_info, + execution_context, + drive_operations, + platform_version, + )?; + + if pool_balance < self.amount() { + return Ok(SimpleConsensusValidationResult::new_with_error( + StateError::InvalidShieldedProofError(InvalidShieldedProofError::new(format!( + "token shielded pool has insufficient balance: pool has {} but unshield requires {}", + pool_balance, + self.amount() + ))) + .into(), + )); + } + + let extra_sighash_data = token_unshield_extra_sighash_data( + &token_id_bytes, + &owner_id.to_buffer(), + &recipient_id.to_buffer(), + self.amount(), + platform_version, + )?; + + verify_token_pool_bundle( + execution_context, + validation_mode, + self.actions(), + FLAGS_SPENDS_AND_OUTPUTS, + self.amount() as i64, + self.anchor(), + self.proof(), + self.binding_signature(), + &extra_sighash_data, + platform_version, + ) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs index 5876fa86998..dd82a18c728 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs @@ -342,7 +342,7 @@ impl StateTransitionStateValidation for BatchTransition { &self, action: Option, platform: &PlatformRef, - _validation_mode: ValidationMode, + validation_mode: ValidationMode, block_info: &BlockInfo, execution_context: &mut StateTransitionExecutionContext, tx: TransactionArg, @@ -372,6 +372,7 @@ impl StateTransitionStateValidation for BatchTransition { &platform.into(), block_info, execution_context, + validation_mode, tx, platform_version, ) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs index b27a00d8ecf..27b61ee23d9 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs @@ -33,6 +33,9 @@ use crate::execution::validation::state_transition::batch::action_validation::to use crate::execution::validation::state_transition::batch::action_validation::token::token_set_price_for_direct_purchase_transition_action::TokenSetPriceForDirectPurchaseTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::token::token_transfer_transition_action::TokenTransferTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::token::token_unfreeze_transition_action::TokenUnfreezeTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_shield_transition_action::TokenShieldTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_transfer_transition_action::TokenShieldedTransferTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_unshield_transition_action::TokenUnshieldTransitionActionValidation; use crate::execution::validation::state_transition::batch::data_triggers::{data_trigger_bindings_list, DataTriggerExecutionContext, DataTriggerExecutor}; use crate::execution::validation::state_transition::batch::state::v0::index_only_batch_entries::IndexOnlyBatchEntries; use drive::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::DocumentCreateTransitionActionAccessorsV0; @@ -47,12 +50,14 @@ mod index_only_batch_entries; pub(in crate::execution::validation::state_transition::state_transitions::batch) trait DocumentsBatchStateTransitionStateValidationV0 { + #[allow(clippy::too_many_arguments)] fn validate_state_v0( &self, action: BatchTransitionAction, platform: &PlatformStateRef, block_info: &BlockInfo, execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, tx: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error>; @@ -73,6 +78,7 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { platform: &PlatformStateRef, block_info: &BlockInfo, execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, transaction: TransactionArg, platform_version: &PlatformVersion, ) -> Result, Error> { @@ -268,6 +274,37 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { transaction, platform_version, )?, + TokenTransitionAction::ShieldAction(shield_action) => shield_action + .validate_state( + platform, + owner_id, + block_info, + execution_context, + validation_mode, + transaction, + platform_version, + )?, + TokenTransitionAction::UnshieldAction(unshield_action) => unshield_action + .validate_state( + platform, + owner_id, + block_info, + execution_context, + validation_mode, + transaction, + platform_version, + )?, + TokenTransitionAction::ShieldedTransferAction(shielded_transfer_action) => { + shielded_transfer_action.validate_state( + platform, + owner_id, + block_info, + execution_context, + validation_mode, + transaction, + platform_version, + )? + } }, BatchedTransitionAction::BumpIdentityDataContractNonce(_) => { return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs index d6cc950a389..3e808264701 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs @@ -3175,7 +3175,7 @@ mod test_suite { token_configuration: &TokenConfiguration, contract_start_time: u64, ) -> Result<(), String> { - let TokenConfiguration::V0(token_config) = token_configuration; + let token_config = token_configuration.as_v0(); let dist_rules = token_config.distribution_rules(); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/mod.rs index 31901763afd..7d54d5655d2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/mod.rs @@ -8,6 +8,7 @@ mod emergency_action; mod freeze; mod mint; mod mint_many; +mod shielded; mod transfer; mod unfreeze; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs new file mode 100644 index 00000000000..117e3bd5baf --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -0,0 +1,1043 @@ +use super::*; + +/// Token shielded pool transitions: shield, unshield and shielded transfer inside a batch. +/// +/// The bundles are real Orchard bundles proven with the shared proving key, so the tests cover +/// the whole path: identity signature and fee, pool bookkeeping, sighash binding, anchor and +/// nullifier checks, and token conservation with the pool as a balance term. +mod token_shielded_pool_tests { + use super::*; + use crate::execution::validation::state_transition::state_transitions::test_helpers::{ + create_dummy_serialized_action, get_proving_key, serialize_authorized_bundle_i64, + }; + use crate::platform_types::state_transitions_processing_result::StateTransitionsProcessingResult; + use crate::rpc::core::MockCoreRPCLike; + use crate::test::helpers::setup::TempPlatform; + use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters; + use dpp::data_contract::DataContract; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::identity_nonce::IDENTITY_NONCE_VALUE_FILTER; + use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; + use dpp::shielded::{ + compute_platform_sighash, token_shielded_transfer_extra_sighash_data_v0, + token_unshield_extra_sighash_data_v0, OrchardBundleParams, + }; + use dpp::state_transition::data_contract_create_transition::methods::DataContractCreateTransitionMethodsV0; + use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; + use dpp::state_transition::StateTransition; + use dpp::tests::json_document::json_document_to_contract_with_ids; + use dpp::version::feature_initial_protocol_versions::TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION; + use drive::drive::shielded::paths::token_shielded_pool_anchors_path_vec; + use drive::grovedb::Element; + use grovedb_commitment_tree::{ + Anchor, Authorized, Builder, Bundle, BundleType, ClientMemoryCommitmentTree, DashMemo, + ExtractedNoteCommitment, Flags, FullViewingKey, MerklePath, Note, NoteValue, + PaymentAddress, Position, RandomSeed, Retention, Rho, Scope, SpendAuthorizingKey, + SpendingKey, + }; + use platform_version::version::PlatformVersion; + + /// The basic token fixture mints this much to the contract owner. + const OWNER_INITIAL_BALANCE: u64 = 100_000; + const SHIELD_AMOUNT: u64 = 10_000; + + fn enable_shielded_pool(configuration: &mut TokenConfiguration) { + configuration.set_has_shielded_pool(true); + } + + fn spend_keys() -> (FullViewingKey, SpendAuthorizingKey, PaymentAddress) { + let sk = SpendingKey::from_bytes([7u8; 32]).unwrap(); + let fvk = FullViewingKey::from(&sk); + let address = fvk.address_at(0u32, Scope::External); + (fvk, SpendAuthorizingKey::from(&sk), address) + } + + fn bundle_params(bundle: &Bundle) -> (OrchardBundleParams, i64) { + let (actions, value_balance, anchor, proof, binding_signature) = + serialize_authorized_bundle_i64(bundle); + ( + OrchardBundleParams { + actions, + anchor, + proof, + binding_signature, + }, + value_balance, + ) + } + + /// An outputs-only bundle paying `amount` to the test wallet: what a token shield carries. + /// The bundle has no spends, so no extra sighash data is bound. + fn build_shield_bundle(amount: u64, seed: u64) -> OrchardBundleParams { + let mut rng = StdRng::seed_from_u64(seed); + let (fvk, _, address) = spend_keys(); + let mut builder = Builder::::new( + BundleType::Transactional { + flags: Flags::SPENDS_DISABLED, + bundle_required: false, + }, + Anchor::empty_tree(), + ); + builder + .add_output( + Some(fvk.to_ovk(Scope::External)), + address, + NoteValue::from_raw(amount), + [0u8; 36], + ) + .unwrap(); + let (unauthorized, _) = builder.build::(&mut rng).unwrap().unwrap(); + let commitment: [u8; 32] = unauthorized.commitment().into(); + let sighash = compute_platform_sighash(&commitment, &[]); + let proven = unauthorized + .create_proof(get_proving_key(), &mut rng) + .unwrap(); + let bundle = proven.apply_signatures(rng, sighash, &[]).unwrap(); + let (params, value_balance) = bundle_params(&bundle); + assert_eq!(value_balance, -(amount as i64)); + params + } + + /// A note the test wallet controls, alone in a client-side commitment tree. The returned + /// anchor is the tree's root; a spend of the note is only accepted once that anchor is in + /// the pool's anchors. + fn spendable_note(value: u64, tag: u8) -> (Note, Anchor, MerklePath) { + let (_, _, address) = spend_keys(); + let mut rho_bytes = [0u8; 32]; + rho_bytes[0] = tag; + let rho = Rho::from_bytes(&rho_bytes).unwrap(); + let rseed = RandomSeed::from_bytes([42u8; 32], &rho).unwrap(); + let note = Note::from_parts(address, NoteValue::from_raw(value), rho, rseed).unwrap(); + let cmx = ExtractedNoteCommitment::from(note.commitment()); + let mut tree = ClientMemoryCommitmentTree::new(100); + tree.append(cmx.to_bytes(), Retention::Marked).unwrap(); + tree.checkpoint(0u32).unwrap(); + let anchor = tree.anchor().unwrap(); + let merkle_path = tree.witness(Position::from(0u64), 0).unwrap().unwrap(); + (note, anchor, merkle_path) + } + + /// Spends `note` against `anchor`, sending everything but `leaving_pool` back to the test + /// wallet as a change note, bound to `extra_sighash_data`. `leaving_pool` is the bundle's + /// value balance: the unshielded amount, or zero for a shielded transfer. + fn build_spend_bundle( + note: Note, + merkle_path: MerklePath, + anchor: Anchor, + leaving_pool: u64, + extra_sighash_data: &[u8], + seed: u64, + ) -> (OrchardBundleParams, i64) { + let mut rng = StdRng::seed_from_u64(seed); + let (fvk, ask, address) = spend_keys(); + let change = note.value().inner() - leaving_pool; + let mut builder = Builder::::new(BundleType::DEFAULT, anchor); + builder.add_spend(fvk.clone(), note, merkle_path).unwrap(); + builder + .add_output( + Some(fvk.to_ovk(Scope::External)), + address, + NoteValue::from_raw(change), + [0u8; 36], + ) + .unwrap(); + let (unauthorized, _) = builder.build::(&mut rng).unwrap().unwrap(); + let commitment: [u8; 32] = unauthorized.commitment().into(); + let sighash = compute_platform_sighash(&commitment, extra_sighash_data); + let proven = unauthorized + .create_proof(get_proving_key(), &mut rng) + .unwrap(); + let bundle = proven.apply_signatures(rng, sighash, &[ask]).unwrap(); + bundle_params(&bundle) + } + + /// Structurally valid but unprovable: for tests that fail before proof verification. + fn dummy_bundle() -> OrchardBundleParams { + OrchardBundleParams { + actions: vec![create_dummy_serialized_action()], + anchor: [42u8; 32], + proof: vec![0u8; 100], + binding_signature: [0u8; 64], + } + } + + fn insert_token_pool_anchor( + platform: &TempPlatform, + token_id: Identifier, + anchor: &Anchor, + ) { + let platform_version = PlatformVersion::latest(); + let transaction = platform.drive.grove.start_transaction(); + let anchors_path = token_shielded_pool_anchors_path_vec(token_id.to_buffer()); + platform + .drive + .grove + .insert( + anchors_path.as_slice(), + &anchor.to_bytes(), + Element::new_item(1u64.to_be_bytes().to_vec()), + None, + Some(&transaction), + &platform_version.drive.grove_version, + ) + .unwrap() + .expect("should insert token pool anchor"); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("should commit"); + } + + fn pool_balance(platform: &TempPlatform, token_id: Identifier) -> u64 { + platform + .drive + .read_token_shielded_pool_total_balance( + &token_id.to_buffer(), + None, + &mut vec![], + PlatformVersion::latest(), + ) + .expect("pool balance") + } + + fn pool_notes_count(platform: &TempPlatform, token_id: Identifier) -> u64 { + platform + .drive + .token_shielded_pool_notes_count( + &token_id.to_buffer(), + None, + &mut vec![], + PlatformVersion::latest(), + ) + .expect("pool notes count") + } + + fn nullifier_is_spent( + platform: &TempPlatform, + token_id: Identifier, + nullifier: &[u8; 32], + ) -> bool { + platform + .drive + .has_token_pool_nullifier( + &token_id.to_buffer(), + nullifier, + None, + &mut vec![], + PlatformVersion::latest(), + ) + .expect("nullifier lookup") + } + + fn identity_token_balance( + platform: &TempPlatform, + token_id: Identifier, + identity_id: Identifier, + ) -> Option { + platform + .drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity_id.to_buffer(), + None, + PlatformVersion::latest(), + ) + .expect("token balance") + } + + fn assert_tokens_conserved(platform: &TempPlatform) { + let total = platform + .drive + .calculate_total_tokens_balance(None, PlatformVersion::latest()) + .expect("total tokens balance"); + assert!( + total.ok().expect("conservation check"), + "identity balances plus pool balances must equal the total supply: {total:?}" + ); + } + + /// Processes one transition in its own transaction and commits it. + fn process( + platform: &TempPlatform, + transition: &StateTransition, + ) -> StateTransitionsProcessingResult { + let platform_version = PlatformVersion::latest(); + let platform_state = platform.state.load(); + let serialized = transition + .serialize_to_bytes() + .expect("serialize transition"); + let transaction = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("process state transition"); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("commit"); + result + } + + fn platform_with_latest_version() -> TempPlatform { + TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state() + } + + #[tokio::test] + async fn test_token_shield_unshield_and_shielded_transfer() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9001); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (recipient, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.1)); + + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + let token = token_id.to_buffer(); + + // Creating the contract created an empty pool. + assert_eq!(pool_balance(&platform, token_id), 0); + assert_eq!(pool_notes_count(&platform, token_id), 0); + assert_tokens_conserved(&platform); + + // Shield: identity balance to pool. + let shield_bundle = build_shield_bundle(SHIELD_AMOUNT, 11); + let shield = BatchTransition::new_token_shield_transition( + token_id, + identity.id(), + contract.id(), + 0, + SHIELD_AMOUNT, + shield_bundle.clone(), + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token shield transition"); + + let result = process(&platform, &shield); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert!( + result.token_shielded_pools_touched().contains(&token), + "the block end anchor hook must learn about the touched pool" + ); + assert_eq!( + identity_token_balance(&platform, token_id, identity.id()), + Some(OWNER_INITIAL_BALANCE - SHIELD_AMOUNT) + ); + assert_eq!(pool_balance(&platform, token_id), SHIELD_AMOUNT); + assert_eq!( + pool_notes_count(&platform, token_id), + shield_bundle.actions.len() as u64 + ); + assert_tokens_conserved(&platform); + + // The block end hook records the pool's anchor, and it must be the anchor a client + // computes from the shield's commitments in action order. + let transaction = platform.drive.grove.start_transaction(); + platform + .drive + .record_token_shielded_pool_anchor_if_changed(token, 1, &transaction, platform_version) + .expect("record anchor"); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("commit"); + let mut client_tree = ClientMemoryCommitmentTree::new(100); + for action in &shield_bundle.actions { + client_tree + .append(action.cmx, Retention::Marked) + .expect("append commitment"); + } + client_tree.checkpoint(1u32).unwrap(); + let client_anchor = client_tree.anchor().unwrap(); + let recorded = platform + .drive + .read_latest_recorded_token_shielded_pool_anchor(token, None, platform_version) + .expect("read recorded anchor"); + assert_eq!(recorded, Some(client_anchor.to_bytes())); + + // Unshield: pool to the recipient's balance. + let (note, anchor, merkle_path) = spendable_note(6_000, 1); + insert_token_pool_anchor(&platform, token_id, &anchor); + let unshield_amount = 4_000; + let extra = token_unshield_extra_sighash_data_v0( + &token, + &identity.id().to_buffer(), + &recipient.id().to_buffer(), + unshield_amount, + ); + let (unshield_bundle, value_balance) = + build_spend_bundle(note, merkle_path, anchor, unshield_amount, &extra, 12); + assert_eq!(value_balance, unshield_amount as i64); + + let unshield = BatchTransition::new_token_unshield_transition( + token_id, + identity.id(), + contract.id(), + 0, + unshield_amount, + recipient.id(), + unshield_bundle.clone(), + &key, + 3, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token unshield transition"); + + let result = process(&platform, &unshield); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!( + identity_token_balance(&platform, token_id, recipient.id()), + Some(unshield_amount) + ); + assert_eq!( + pool_balance(&platform, token_id), + SHIELD_AMOUNT - unshield_amount + ); + assert_eq!( + pool_notes_count(&platform, token_id), + (shield_bundle.actions.len() + unshield_bundle.actions.len()) as u64 + ); + for action in &unshield_bundle.actions { + assert!(nullifier_is_spent(&platform, token_id, &action.nullifier)); + } + assert_tokens_conserved(&platform); + + // Replaying the spent bundle under a fresh nonce is a paid failure. + let replay = BatchTransition::new_token_unshield_transition( + token_id, + identity.id(), + contract.id(), + 0, + unshield_amount, + recipient.id(), + unshield_bundle, + &key, + 4, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token unshield transition"); + let result = process(&platform, &replay); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::NullifierAlreadySpentError(_)), + .. + }] + ); + assert_eq!( + pool_balance(&platform, token_id), + SHIELD_AMOUNT - unshield_amount + ); + + // Shielded transfer: nothing leaves the pool. + let (note, anchor, merkle_path) = spendable_note(3_000, 2); + insert_token_pool_anchor(&platform, token_id, &anchor); + let extra = + token_shielded_transfer_extra_sighash_data_v0(&token, &identity.id().to_buffer()); + let (transfer_bundle, value_balance) = + build_spend_bundle(note, merkle_path, anchor, 0, &extra, 13); + assert_eq!(value_balance, 0); + + let transfer = BatchTransition::new_token_shielded_transfer_transition( + token_id, + identity.id(), + contract.id(), + 0, + transfer_bundle.clone(), + &key, + 5, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token shielded transfer transition"); + + let result = process(&platform, &transfer); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!( + pool_balance(&platform, token_id), + SHIELD_AMOUNT - unshield_amount + ); + assert_eq!( + pool_notes_count(&platform, token_id), + (shield_bundle.actions.len() + 2 * transfer_bundle.actions.len()) as u64 + ); + for action in &transfer_bundle.actions { + assert!(nullifier_is_spent(&platform, token_id, &action.nullifier)); + } + assert_eq!( + identity_token_balance(&platform, token_id, identity.id()), + Some(OWNER_INITIAL_BALANCE - SHIELD_AMOUNT) + ); + assert_tokens_conserved(&platform); + } + + #[tokio::test] + async fn test_token_shield_rejected_when_pool_not_enabled() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9002); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + None::, + None, + None, + None, + platform_version, + ); + + let shield = BatchTransition::new_token_shield_transition( + token_id, + identity.id(), + contract.id(), + 0, + SHIELD_AMOUNT, + dummy_bundle(), + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token shield transition"); + + let result = process(&platform, &shield); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::TokenShieldedPoolNotEnabledError(_)), + .. + }] + ); + assert_eq!( + identity_token_balance(&platform, token_id, identity.id()), + Some(OWNER_INITIAL_BALANCE) + ); + } + + #[tokio::test] + async fn test_token_shield_rejected_when_balance_too_low() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9003); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + + let shield = BatchTransition::new_token_shield_transition( + token_id, + identity.id(), + contract.id(), + 0, + OWNER_INITIAL_BALANCE + 1, + dummy_bundle(), + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token shield transition"); + + let result = process(&platform, &shield); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError( + StateError::IdentityDoesNotHaveEnoughTokenBalanceError(_) + ), + .. + }] + ); + assert_eq!(pool_balance(&platform, token_id), 0); + } + + #[tokio::test] + async fn test_token_shield_rejected_when_token_paused() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9004); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|configuration: &mut TokenConfiguration| { + configuration.set_has_shielded_pool(true); + configuration.set_start_as_paused(true); + }), + None, + None, + None, + platform_version, + ); + + let shield = BatchTransition::new_token_shield_transition( + token_id, + identity.id(), + contract.id(), + 0, + SHIELD_AMOUNT, + dummy_bundle(), + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token shield transition"); + + let result = process(&platform, &shield); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::TokenIsPausedError(_)), + .. + }] + ); + assert_eq!(pool_balance(&platform, token_id), 0); + } + + #[tokio::test] + async fn test_token_shield_rejected_when_identity_frozen() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9005); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + + platform + .drive + .token_freeze( + token_id, + identity.id(), + &BlockInfo::default(), + true, + None, + platform_version, + ) + .expect("freeze identity token account"); + + let shield = BatchTransition::new_token_shield_transition( + token_id, + identity.id(), + contract.id(), + 0, + SHIELD_AMOUNT, + dummy_bundle(), + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token shield transition"); + + let result = process(&platform, &shield); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::IdentityTokenAccountFrozenError(_)), + .. + }] + ); + assert_eq!(pool_balance(&platform, token_id), 0); + } + + #[tokio::test] + async fn test_token_shield_with_invalid_proof_is_a_paid_failure() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9006); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + + let credits_before = platform + .drive + .fetch_identity_balance(identity.id().to_buffer(), None, platform_version) + .expect("identity balance") + .expect("identity exists"); + + let shield = BatchTransition::new_token_shield_transition( + token_id, + identity.id(), + contract.id(), + 0, + SHIELD_AMOUNT, + dummy_bundle(), + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token shield transition"); + + let result = process(&platform, &shield); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::InvalidShieldedProofError(_)), + .. + }] + ); + + // Nothing moved, but the identity paid for the verification attempt and its nonce + // advanced, so the same transition cannot be resubmitted for free. + assert_eq!( + identity_token_balance(&platform, token_id, identity.id()), + Some(OWNER_INITIAL_BALANCE) + ); + assert_eq!(pool_balance(&platform, token_id), 0); + let credits_after = platform + .drive + .fetch_identity_balance(identity.id().to_buffer(), None, platform_version) + .expect("identity balance") + .expect("identity exists"); + assert!(credits_after < credits_before); + let nonce = platform + .drive + .fetch_identity_contract_nonce( + identity.id().to_buffer(), + contract.id().to_buffer(), + true, + None, + platform_version, + ) + .expect("identity contract nonce") + .expect("nonce was set"); + // The stored value also carries the recent-nonce bitmap above the counter. + assert_eq!(nonce & IDENTITY_NONCE_VALUE_FILTER, 2); + } + + #[tokio::test] + async fn test_token_unshield_rejected_for_unknown_anchor() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9007); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (recipient, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.1)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + + let unshield = BatchTransition::new_token_unshield_transition( + token_id, + identity.id(), + contract.id(), + 0, + 1_000, + recipient.id(), + dummy_bundle(), + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token unshield transition"); + + let result = process(&platform, &unshield); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::InvalidAnchorError(_)), + .. + }] + ); + assert_eq!( + identity_token_balance(&platform, token_id, recipient.id()), + None + ); + } + + #[tokio::test] + async fn test_token_shield_rejected_before_protocol_version_14() { + let platform_version = + PlatformVersion::get(TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION - 1) + .expect("previous protocol version"); + let mut platform = TestPlatformBuilder::new() + .with_initial_protocol_version(platform_version.protocol_version) + .build_with_mock_rpc() + .set_genesis_state(); + let mut rng = StdRng::seed_from_u64(9008); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + None::, + None, + None, + None, + platform_version, + ); + + let shield = BatchTransition::new_token_shield_transition( + token_id, + identity.id(), + contract.id(), + 0, + SHIELD_AMOUNT, + dummy_bundle(), + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token shield transition"); + + let platform_state = platform.state.load(); + let serialized = shield.serialize_to_bytes().expect("serialize"); + let transaction = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("process state transition"); + + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::BasicError(BasicError::StateTransitionNotActiveError(_)) + )] + ); + } + + /// A contract carrying a token configuration with a shielded pool. + fn shielded_token_contract( + owner_id: Identifier, + platform_version: &PlatformVersion, + ) -> DataContract { + let data_contract_id = DataContract::generate_data_contract_id_v0(owner_id, 1); + let mut contract = json_document_to_contract_with_ids( + "tests/supporting_files/contract/basic-token/basic-token.json", + Some(data_contract_id), + Some(owner_id), + false, + platform_version, + ) + .expect("basic token contract"); + contract + .token_configuration_mut(0) + .expect("token configuration") + .set_has_shielded_pool(true); + contract + } + + #[tokio::test] + async fn test_contract_create_with_shielded_pool_token_creates_the_pool() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9009); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let contract = shielded_token_contract(identity.id(), platform_version); + let token_id = contract.token_id(0).expect("token id"); + + let create = DataContractCreateTransition::new_from_data_contract( + contract, + 1, + &identity.clone().into_partial_identity_info(), + key.id(), + &signer, + platform_version, + None, + ) + .await + .expect("contract create transition"); + + let result = process(&platform, &create); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(&platform, token_id), 0); + assert_eq!(pool_notes_count(&platform, token_id), 0); + assert_tokens_conserved(&platform); + } + + #[tokio::test] + async fn test_contract_create_with_shielded_pool_token_rejected_before_protocol_version_14() { + let platform_version = + PlatformVersion::get(TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION - 1) + .expect("previous protocol version"); + let mut platform = TestPlatformBuilder::new() + .with_initial_protocol_version(platform_version.protocol_version) + .build_with_mock_rpc() + .set_genesis_state(); + let mut rng = StdRng::seed_from_u64(9010); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let contract = shielded_token_contract(identity.id(), platform_version); + + let create = DataContractCreateTransition::new_from_data_contract( + contract, + 1, + &identity.clone().into_partial_identity_info(), + key.id(), + &signer, + platform_version, + None, + ) + .await + .expect("contract create transition"); + + let platform_state = platform.state.load(); + let serialized = create.serialize_to_bytes().expect("serialize"); + let transaction = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("process state transition"); + + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::BasicError(BasicError::UnsupportedVersionError(_)) + )] + ); + } + + /// `hasShieldedPool` is a format-version-1 field, so clients see it in the contract JSON + /// and it survives the round trip. + #[test] + fn test_token_configuration_shielded_pool_flag_round_trips_through_json() { + let platform_version = PlatformVersion::latest(); + let contract = shielded_token_contract(Identifier::from([9u8; 32]), platform_version); + let configuration = contract + .expected_token_configuration(0) + .expect("token configuration"); + let json = serde_json::to_value(configuration).expect("configuration json"); + assert_eq!(json.get("$formatVersion"), Some(&serde_json::json!("1"))); + assert_eq!(json.get("hasShieldedPool"), Some(&serde_json::json!(true))); + let decoded: TokenConfiguration = + serde_json::from_value(json).expect("configuration from json"); + assert_eq!(&decoded, configuration); + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs index cf5458e9034..8a1b56af705 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs @@ -92,6 +92,9 @@ use drive::state_transition_action::batch::batched_transition::token_transition: use drive::state_transition_action::batch::batched_transition::token_transition::token_mint_transition_action::TokenMintTransitionAction; use drive::state_transition_action::batch::batched_transition::token_transition::token_claim_transition_action::TokenClaimTransitionAction; use drive::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_transition_action::TokenDirectPurchaseTransitionAction; +use drive::state_transition_action::batch::batched_transition::token_transition::token_shield_transition_action::TokenShieldTransitionAction; +use drive::state_transition_action::batch::batched_transition::token_transition::token_shielded_transfer_transition_action::TokenShieldedTransferTransitionAction; +use drive::state_transition_action::batch::batched_transition::token_transition::token_unshield_transition_action::TokenUnshieldTransitionAction; use drive::state_transition_action::batch::batched_transition::token_transition::token_set_price_for_direct_purchase_transition_action::TokenSetPriceForDirectPurchaseTransitionAction; use drive::state_transition_action::batch::batched_transition::token_transition::token_transfer_transition_action::TokenTransferTransitionAction; use drive::state_transition_action::batch::batched_transition::token_transition::token_unfreeze_transition_action::TokenUnfreezeTransitionAction; @@ -737,6 +740,36 @@ impl BatchTransitionInternalTransformerV0 for BatchTransition { Ok(data_contract_fetch_info.clone()) }, platform_version)?; + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action) + } + TokenTransition::Shield(token_shield) => { + let (batched_action, fee_result) = TokenShieldTransitionAction::try_from_borrowed_token_shield_transition_with_contract_lookup(drive, owner_id, token_shield, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + }, platform_version)?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action) + } + TokenTransition::Unshield(token_unshield) => { + let (batched_action, fee_result) = TokenUnshieldTransitionAction::try_from_borrowed_token_unshield_transition_with_contract_lookup(drive, owner_id, token_unshield, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + }, platform_version)?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action) + } + TokenTransition::ShieldedTransfer(token_shielded_transfer) => { + let (batched_action, fee_result) = TokenShieldedTransferTransitionAction::try_from_borrowed_token_shielded_transfer_transition_with_contract_lookup(drive, owner_id, token_shielded_transfer, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + }, platform_version)?; + execution_context .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs index 8cee70813d9..f1a60ac113d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs @@ -73,6 +73,18 @@ impl DataContractCreateStateTransitionBasicStructureValidationV0 for DataContrac )); } + // A newer configuration format than this protocol version admits is a consensus + + // error so that nodes already running newer software agree with the rest of the + + // network until the version that introduces the format activates. + + let validation_result = token_configuration.validate_format_version(platform_version); + + if !validation_result.is_valid() { + return Ok(validation_result); + } + let validation_result = token_configuration .conventions() .validate_localizations(platform_version)?; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v0/mod.rs index 0f95b20d133..69afeb1c5a2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v0/mod.rs @@ -58,6 +58,18 @@ impl DataContractUpdateStateTransitionBasicStructureValidationV0 for DataContrac )); } + // A newer configuration format than this protocol version admits is a consensus + + // error so that nodes already running newer software agree with the rest of the + + // network until the version that introduces the format activates. + + let validation_result = token_configuration.validate_format_version(platform_version); + + if !validation_result.is_valid() { + return Ok(validation_result); + } + let validation_result = token_configuration .conventions() .validate_localizations(platform_version)?; diff --git a/packages/rs-drive-abci/src/platform_types/state_transitions_processing_result/mod.rs b/packages/rs-drive-abci/src/platform_types/state_transitions_processing_result/mod.rs index 1c26adde5f2..fe3e1283981 100644 --- a/packages/rs-drive-abci/src/platform_types/state_transitions_processing_result/mod.rs +++ b/packages/rs-drive-abci/src/platform_types/state_transitions_processing_result/mod.rs @@ -2,7 +2,7 @@ use dpp::address_funds::PlatformAddress; use dpp::balances::credits::CreditOperation; use dpp::consensus::ConsensusError; use dpp::fee::Credits; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use crate::error::Error; use crate::platform_types::event_execution_result::EstimatedFeeResult; @@ -64,6 +64,9 @@ pub enum StateTransitionExecutionResult { pub struct StateTransitionsProcessingResult { execution_results: Vec, pub(crate) address_balances_updated: BTreeMap, + /// The token shielded pools the block's applied transitions touched, so the block end can + /// record each pool's new anchor (and prune its old ones) without scanning every pool. + pub(crate) token_shielded_pools_touched: BTreeSet<[u8; 32]>, invalid_paid_count: usize, invalid_unpaid_count: usize, valid_count: usize, @@ -111,6 +114,19 @@ impl StateTransitionsProcessingResult { .or_insert(new_op); } } + /// Records the token shielded pools an applied state transition touched. + pub fn add_token_shielded_pools_touched( + &mut self, + token_ids: impl IntoIterator, + ) { + self.token_shielded_pools_touched.extend(token_ids); + } + + /// The token shielded pools the block's applied state transitions touched. + pub fn token_shielded_pools_touched(&self) -> &BTreeSet<[u8; 32]> { + &self.token_shielded_pools_touched + } + /// Add a new execution result pub fn add(&mut self, execution_result: StateTransitionExecutionResult) -> Result<(), Error> { match &execution_result { diff --git a/packages/rs-drive-abci/src/query/group_queries/group_actions/v0/mod.rs b/packages/rs-drive-abci/src/query/group_queries/group_actions/v0/mod.rs index 7af6bfa13a2..5835e315571 100644 --- a/packages/rs-drive-abci/src/query/group_queries/group_actions/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/group_queries/group_actions/v0/mod.rs @@ -224,7 +224,12 @@ impl Platform { })), }) } - TokenEvent::Transfer(..) | TokenEvent::DirectPurchase(..) | TokenEvent::Claim(..) => { + TokenEvent::Transfer(..) + | TokenEvent::DirectPurchase(..) + | TokenEvent::Claim(..) + | TokenEvent::Shield(..) + | TokenEvent::Unshield(..) + | TokenEvent::ShieldedTransfer => { return None; }, }, diff --git a/packages/rs-drive-abci/src/query/shielded/anchors/mod.rs b/packages/rs-drive-abci/src/query/shielded/anchors/mod.rs index 19f0fa4ee30..683427f80a8 100644 --- a/packages/rs-drive-abci/src/query/shielded/anchors/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/anchors/mod.rs @@ -84,6 +84,7 @@ mod tests { let request = GetShieldedAnchorsRequest { version: Some(RequestVersion::V0(GetShieldedAnchorsRequestV0 { prove: false, + token_id: None, })), }; @@ -116,6 +117,7 @@ mod tests { let request = GetShieldedAnchorsRequest { version: Some(RequestVersion::V0(GetShieldedAnchorsRequestV0 { prove: true, + token_id: None, })), }; diff --git a/packages/rs-drive-abci/src/query/shielded/anchors/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/anchors/v0/mod.rs index 10bd3b2ff80..2e0077b8bf0 100644 --- a/packages/rs-drive-abci/src/query/shielded/anchors/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/anchors/v0/mod.rs @@ -3,6 +3,7 @@ use crate::error::Error; use crate::platform_types::platform::Platform; use crate::platform_types::platform_state::PlatformState; use crate::query::response_metadata::CheckpointUsed; +use crate::query::shielded::ShieldedPoolSelector; use crate::query::QueryValidationResult; use dapi_grpc::platform::v0::get_shielded_anchors_request::GetShieldedAnchorsRequestV0; use dapi_grpc::platform::v0::get_shielded_anchors_response::get_shielded_anchors_response_v0::Anchors; @@ -12,7 +13,6 @@ use dapi_grpc::platform::v0::get_shielded_anchors_response::{ use dpp::check_validation_result_with_data; use dpp::validation::ValidationResult; use dpp::version::PlatformVersion; -use drive::drive::shielded::paths::shielded_credit_pool_anchors_path_vec; use drive::grovedb::query_result_type::QueryResultType; use drive::grovedb::{PathQuery, Query, SizedQuery}; use drive::util::grove_operations::GroveDBToUse; @@ -47,10 +47,14 @@ fn v0_shielded_anchor_limits(platform_version: &PlatformVersion) -> Option<(u16, impl Platform { pub(super) fn query_shielded_anchors_v0( &self, - GetShieldedAnchorsRequestV0 { prove }: GetShieldedAnchorsRequestV0, + GetShieldedAnchorsRequestV0 { prove, token_id }: GetShieldedAnchorsRequestV0, platform_state: &PlatformState, platform_version: &PlatformVersion, ) -> Result, Error> { + let pool = match ShieldedPoolSelector::from_request(token_id, platform_version) { + Ok(pool) => pool, + Err(error) => return Ok(QueryValidationResult::new_with_error(error)), + }; let Some((safety_limit, preflight_limit)) = v0_shielded_anchor_limits(platform_version) else { return Ok(QueryValidationResult::new_with_error( @@ -66,7 +70,7 @@ impl Platform { // while failing closed if configuration or state ever exceeds the // amount that this unpaginated response can safely return. let bounded_path_query = PathQuery { - path: shielded_credit_pool_anchors_path_vec(), + path: pool.anchors_path_vec(), query: SizedQuery { query: Query::new_range_full(), limit: Some(preflight_limit), @@ -91,7 +95,7 @@ impl Platform { } let path_query = PathQuery { - path: shielded_credit_pool_anchors_path_vec(), + path: pool.anchors_path_vec(), query: SizedQuery { query: Query::new_range_full(), limit: None, @@ -138,6 +142,7 @@ mod tests { use super::*; use crate::query::tests::setup_platform; use dpp::dashcore::Network; + use drive::drive::shielded::paths::shielded_credit_pool_anchors_path_vec; use drive::drive::Drive; use drive::grovedb::batch::QualifiedGroveDbOp; use drive::grovedb::Element; @@ -174,7 +179,10 @@ mod tests { let at_limit = platform .query_shielded_anchors_v0( - GetShieldedAnchorsRequestV0 { prove: false }, + GetShieldedAnchorsRequestV0 { + prove: false, + token_id: None, + }, &state, version, ) @@ -205,7 +213,14 @@ mod tests { for prove in [false, true] { let result = platform - .query_shielded_anchors_v0(GetShieldedAnchorsRequestV0 { prove }, &state, version) + .query_shielded_anchors_v0( + GetShieldedAnchorsRequestV0 { + prove, + token_id: None, + }, + &state, + version, + ) .expect("expected bounded query to complete"); assert!(matches!( @@ -255,7 +270,10 @@ mod tests { let plain = platform .query_shielded_anchors_v0( - GetShieldedAnchorsRequestV0 { prove: false }, + GetShieldedAnchorsRequestV0 { + prove: false, + token_id: None, + }, &state, version, ) @@ -271,7 +289,14 @@ mod tests { assert_eq!(plain_anchors, expected_sorted); let proved = platform - .query_shielded_anchors_v0(GetShieldedAnchorsRequestV0 { prove: true }, &state, version) + .query_shielded_anchors_v0( + GetShieldedAnchorsRequestV0 { + prove: true, + token_id: None, + }, + &state, + version, + ) .expect("proved small-state query"); let proof = match proved.data.and_then(|response| response.result) { Some(get_shielded_anchors_response_v0::Result::Proof(proof)) => proof.grovedb_proof, diff --git a/packages/rs-drive-abci/src/query/shielded/encrypted_notes/mod.rs b/packages/rs-drive-abci/src/query/shielded/encrypted_notes/mod.rs index 4438da2280d..fa9ee049c64 100644 --- a/packages/rs-drive-abci/src/query/shielded/encrypted_notes/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/encrypted_notes/mod.rs @@ -99,6 +99,7 @@ mod tests { start_index: 1, count: 16, prove: false, + token_id: None, })), }; @@ -124,6 +125,7 @@ mod tests { start_index: 0, count: 8, prove: false, + token_id: None, })), }; @@ -162,6 +164,7 @@ mod tests { start_index: 0, count: 0, prove: false, + token_id: None, })), }; @@ -197,6 +200,7 @@ mod tests { start_index: 0, count: max + 100, prove: false, + token_id: None, })), }; diff --git a/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs index d3c8f7b3dcc..0199fa17406 100644 --- a/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs @@ -14,10 +14,8 @@ use dapi_grpc::platform::v0::get_shielded_encrypted_notes_response::{ use dpp::check_validation_result_with_data; use dpp::validation::ValidationResult; use dpp::version::PlatformVersion; -use drive::drive::shielded::paths::{ - shielded_credit_pool_path, shielded_credit_pool_path_vec, SHIELDED_NOTES_CHUNK_POWER, - SHIELDED_NOTES_KEY, -}; +use crate::query::shielded::ShieldedPoolSelector; +use drive::drive::shielded::paths::{SHIELDED_NOTES_CHUNK_POWER, SHIELDED_NOTES_KEY}; use drive::grovedb::{PathQuery, Query, QueryItem, SizedQuery, SubqueryBranch}; use drive::grovedb_path::SubtreePath; use drive::util::grove_operations::GroveDBToUse; @@ -29,10 +27,15 @@ impl Platform { start_index, count, prove, + token_id, }: GetShieldedEncryptedNotesRequestV0, platform_state: &PlatformState, platform_version: &PlatformVersion, ) -> Result, Error> { + let pool = match ShieldedPoolSelector::from_request(token_id, platform_version) { + Ok(pool) => pool, + Err(error) => return Ok(QueryValidationResult::new_with_error(error)), + }; // Two distinct quantities: // * `mmr_chunk_size` — the on-chain MMR chunk size // (`1 << SHIELDED_NOTES_CHUNK_POWER` = 2048 today). This is the @@ -78,7 +81,7 @@ impl Platform { ); let path_query = PathQuery { - path: shielded_credit_pool_path_vec(), + path: pool.pool_path_vec(), query: SizedQuery { query: Query { read_mode: None, @@ -115,8 +118,8 @@ impl Platform { } } else { // Non-proved: loop over commitment_tree_get_value for each position - let pool_path = shielded_credit_pool_path(); - let pool_subtree: SubtreePath<&[u8]> = (&pool_path).into(); + let pool_path = pool.pool_path_vec(); + let pool_subtree: SubtreePath> = pool_path.as_slice().into(); let notes_key: &[u8] = &[SHIELDED_NOTES_KEY]; let mut entries = Vec::with_capacity(limit as usize); @@ -197,6 +200,7 @@ mod tests { start_index: chunk - 1, // not aligned to chunk size count: 10, prove: false, + token_id: None, }; let result = platform @@ -219,6 +223,7 @@ mod tests { start_index: chunk + 1, count: 10, prove: false, + token_id: None, }; let result = platform @@ -242,6 +247,7 @@ mod tests { start_index: chunk, count: 1, prove: false, + token_id: None, }; let result = platform @@ -268,6 +274,7 @@ mod tests { start_index: chunk * 2, count: 1, prove: false, + token_id: None, }; let result = platform @@ -286,6 +293,7 @@ mod tests { start_index: 0, count: 1, prove: false, + token_id: None, }; let result = platform @@ -313,6 +321,7 @@ mod tests { start_index: 0, count: 16, prove: true, + token_id: None, }; let result = platform @@ -339,6 +348,7 @@ mod tests { start_index: 3, count: 4, prove: true, + token_id: None, }; let result = platform @@ -362,6 +372,7 @@ mod tests { start_index: 0, count: max, prove: false, + token_id: None, }; let result = platform @@ -381,6 +392,7 @@ mod tests { start_index: 0, count: 8, prove: false, + token_id: None, }; let result = platform diff --git a/packages/rs-drive-abci/src/query/shielded/mod.rs b/packages/rs-drive-abci/src/query/shielded/mod.rs index 4ef6094db5a..ab01fb9ff4b 100644 --- a/packages/rs-drive-abci/src/query/shielded/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/mod.rs @@ -4,3 +4,138 @@ mod most_recent_anchor; mod notes_count; mod nullifiers; mod pool_state; + +use crate::error::query::QueryError; +use dpp::version::feature_initial_protocol_versions::TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION; +use dpp::version::PlatformVersion; +use drive::drive::shielded::paths::{ + shielded_credit_pool_anchors_path_vec, shielded_credit_pool_nullifiers_path_vec, + shielded_credit_pool_path_vec, shielded_latest_recorded_anchor_path_query, + token_shielded_pool_anchors_path_vec, token_shielded_pool_latest_recorded_anchor_path_query, + token_shielded_pool_nullifiers_path_vec, token_shielded_pool_path_vec, +}; +use drive::grovedb::PathQuery; + +/// Which shielded pool a shielded query targets: the credit pool (no `token_id` in the request) +/// or one token's pool. Every pool has the same subtree layout, so the handlers only differ in +/// the paths the selector hands them. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum ShieldedPoolSelector { + /// The main credit shielded pool. + Credit, + /// The shielded pool of the token with this id. + Token([u8; 32]), +} + +impl ShieldedPoolSelector { + /// Resolves the optional `token_id` a request carries. A token pool is only addressable + /// once token shielded pools exist (protocol version 14), and the id must be 32 bytes. + pub(super) fn from_request( + token_id: Option>, + platform_version: &PlatformVersion, + ) -> Result { + match token_id { + None => Ok(ShieldedPoolSelector::Credit), + Some(token_id) => { + if platform_version.protocol_version < TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION + { + return Err(QueryError::InvalidArgument(format!( + "token shielded pools are not active before protocol version {}", + TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION + ))); + } + let token_id: [u8; 32] = token_id.try_into().map_err(|token_id: Vec| { + QueryError::InvalidArgument(format!( + "token_id must be 32 bytes, got {}", + token_id.len() + )) + })?; + Ok(ShieldedPoolSelector::Token(token_id)) + } + } + } + + /// The pool subtree path. + pub(super) fn pool_path_vec(&self) -> Vec> { + match self { + ShieldedPoolSelector::Credit => shielded_credit_pool_path_vec(), + ShieldedPoolSelector::Token(token_id) => token_shielded_pool_path_vec(*token_id), + } + } + + /// The pool's nullifiers tree path. + pub(super) fn nullifiers_path_vec(&self) -> Vec> { + match self { + ShieldedPoolSelector::Credit => shielded_credit_pool_nullifiers_path_vec(), + ShieldedPoolSelector::Token(token_id) => { + token_shielded_pool_nullifiers_path_vec(*token_id) + } + } + } + + /// The pool's anchors tree path. + pub(super) fn anchors_path_vec(&self) -> Vec> { + match self { + ShieldedPoolSelector::Credit => shielded_credit_pool_anchors_path_vec(), + ShieldedPoolSelector::Token(token_id) => { + token_shielded_pool_anchors_path_vec(*token_id) + } + } + } + + /// The pool's canonical most-recent-anchor query. + pub(super) fn latest_recorded_anchor_path_query(&self) -> PathQuery { + match self { + ShieldedPoolSelector::Credit => shielded_latest_recorded_anchor_path_query(), + ShieldedPoolSelector::Token(token_id) => { + token_shielded_pool_latest_recorded_anchor_path_query(*token_id) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn selector_resolves_credit_pool_without_token_id() { + let platform_version = PlatformVersion::latest(); + assert_eq!( + ShieldedPoolSelector::from_request(None, platform_version).expect("credit"), + ShieldedPoolSelector::Credit + ); + } + + #[test] + fn selector_resolves_token_pool_with_32_byte_token_id() { + let platform_version = PlatformVersion::latest(); + let selector = ShieldedPoolSelector::from_request(Some(vec![7u8; 32]), platform_version) + .expect("token pool"); + assert_eq!(selector, ShieldedPoolSelector::Token([7u8; 32])); + assert_eq!( + selector.nullifiers_path_vec(), + token_shielded_pool_nullifiers_path_vec([7u8; 32]) + ); + } + + #[test] + fn selector_rejects_wrong_length_token_id() { + let platform_version = PlatformVersion::latest(); + assert!(matches!( + ShieldedPoolSelector::from_request(Some(vec![7u8; 20]), platform_version), + Err(QueryError::InvalidArgument(_)) + )); + } + + #[test] + fn selector_rejects_token_pools_before_activation() { + let platform_version = + PlatformVersion::get(TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION - 1) + .expect("previous version"); + assert!(matches!( + ShieldedPoolSelector::from_request(Some(vec![7u8; 32]), platform_version), + Err(QueryError::InvalidArgument(_)) + )); + } +} diff --git a/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/mod.rs b/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/mod.rs index bbaf361f587..8004b6716f6 100644 --- a/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/mod.rs @@ -97,6 +97,7 @@ mod tests { let request = GetMostRecentShieldedAnchorRequest { version: Some(RequestVersion::V0(GetMostRecentShieldedAnchorRequestV0 { prove: false, + token_id: None, })), }; @@ -132,6 +133,7 @@ mod tests { let request = GetMostRecentShieldedAnchorRequest { version: Some(RequestVersion::V0(GetMostRecentShieldedAnchorRequestV0 { prove: true, + token_id: None, })), }; diff --git a/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/v0/mod.rs index f59b4050209..fbd300a3a56 100644 --- a/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/v0/mod.rs @@ -2,6 +2,7 @@ use crate::error::Error; use crate::platform_types::platform::Platform; use crate::platform_types::platform_state::PlatformState; use crate::query::response_metadata::CheckpointUsed; +use crate::query::shielded::ShieldedPoolSelector; use crate::query::QueryValidationResult; use dapi_grpc::platform::v0::get_most_recent_shielded_anchor_request::GetMostRecentShieldedAnchorRequestV0; use dapi_grpc::platform::v0::get_most_recent_shielded_anchor_response::{ @@ -10,7 +11,6 @@ use dapi_grpc::platform::v0::get_most_recent_shielded_anchor_response::{ use dpp::check_validation_result_with_data; use dpp::validation::ValidationResult; use dpp::version::PlatformVersion; -use drive::drive::shielded::paths::shielded_latest_recorded_anchor_path_query; use drive::error::drive::DriveError; use drive::grovedb::query_result_type::QueryResultType; use drive::grovedb::Element; @@ -27,11 +27,16 @@ impl Platform { /// recorded an anchor yet on this chain. pub(super) fn query_most_recent_shielded_anchor_v0( &self, - GetMostRecentShieldedAnchorRequestV0 { prove }: GetMostRecentShieldedAnchorRequestV0, + GetMostRecentShieldedAnchorRequestV0 { prove, token_id }: GetMostRecentShieldedAnchorRequestV0, platform_state: &PlatformState, platform_version: &PlatformVersion, ) -> Result, Error> { - let path_query = shielded_latest_recorded_anchor_path_query(); + let pool = match ShieldedPoolSelector::from_request(token_id, platform_version) { + Ok(pool) => pool, + Err(error) => return Ok(QueryValidationResult::new_with_error(error)), + }; + + let path_query = pool.latest_recorded_anchor_path_query(); let response = if prove { let proof = check_validation_result_with_data!(self.drive.grove_get_proved_path_query( diff --git a/packages/rs-drive-abci/src/query/shielded/notes_count/mod.rs b/packages/rs-drive-abci/src/query/shielded/notes_count/mod.rs index de0fc215f14..678580fe92e 100644 --- a/packages/rs-drive-abci/src/query/shielded/notes_count/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/notes_count/mod.rs @@ -105,6 +105,7 @@ mod tests { let request = GetShieldedNotesCountRequest { version: Some(RequestVersion::V0(GetShieldedNotesCountRequestV0 { prove: false, + token_id: None, })), }; @@ -134,6 +135,7 @@ mod tests { let request = GetShieldedNotesCountRequest { version: Some(RequestVersion::V0(GetShieldedNotesCountRequestV0 { prove: true, + token_id: None, })), }; diff --git a/packages/rs-drive-abci/src/query/shielded/notes_count/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/notes_count/v0/mod.rs index fc7f275df5d..6eaed5d2541 100644 --- a/packages/rs-drive-abci/src/query/shielded/notes_count/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/notes_count/v0/mod.rs @@ -2,6 +2,7 @@ use crate::error::Error; use crate::platform_types::platform::Platform; use crate::platform_types::platform_state::PlatformState; use crate::query::response_metadata::CheckpointUsed; +use crate::query::shielded::ShieldedPoolSelector; use crate::query::QueryValidationResult; use dapi_grpc::platform::v0::get_shielded_notes_count_request::GetShieldedNotesCountRequestV0; use dapi_grpc::platform::v0::get_shielded_notes_count_response::{ @@ -10,7 +11,7 @@ use dapi_grpc::platform::v0::get_shielded_notes_count_response::{ use dpp::check_validation_result_with_data; use dpp::validation::ValidationResult; use dpp::version::PlatformVersion; -use drive::drive::shielded::paths::{shielded_credit_pool_path_vec, SHIELDED_NOTES_KEY}; +use drive::drive::shielded::paths::SHIELDED_NOTES_KEY; use drive::grovedb::{PathQuery, Query, SizedQuery}; use drive::util::grove_operations::GroveDBToUse; @@ -28,16 +29,20 @@ impl Platform { /// `query_shielded_pool_state_v0`. pub(super) fn query_shielded_notes_count_v0( &self, - GetShieldedNotesCountRequestV0 { prove }: GetShieldedNotesCountRequestV0, + GetShieldedNotesCountRequestV0 { prove, token_id }: GetShieldedNotesCountRequestV0, platform_state: &PlatformState, platform_version: &PlatformVersion, ) -> Result, Error> { + let pool = match ShieldedPoolSelector::from_request(token_id, platform_version) { + Ok(pool) => pool, + Err(error) => return Ok(QueryValidationResult::new_with_error(error)), + }; let response = if prove { // Single-key query lands on the notes CommitmentTree element // (no subquery), so the proof carries the serialized element // whose `total_count` is hashed into the root. let path_query = PathQuery { - path: shielded_credit_pool_path_vec(), + path: pool.pool_path_vec(), query: SizedQuery { query: Query::new_single_key(vec![SHIELDED_NOTES_KEY]), limit: Some(1), @@ -60,9 +65,20 @@ impl Platform { metadata: Some(self.response_metadata_v0(platform_state, grovedb_used)), } } else { - let total_notes_count = - self.drive - .shielded_pool_notes_count(None, &mut vec![], platform_version)?; + let total_notes_count = match pool { + ShieldedPoolSelector::Credit => { + self.drive + .shielded_pool_notes_count(None, &mut vec![], platform_version)? + } + ShieldedPoolSelector::Token(token_id) => { + self.drive.token_shielded_pool_notes_count( + &token_id, + None, + &mut vec![], + platform_version, + )? + } + }; GetShieldedNotesCountResponseV0 { result: Some( diff --git a/packages/rs-drive-abci/src/query/shielded/nullifiers/mod.rs b/packages/rs-drive-abci/src/query/shielded/nullifiers/mod.rs index ecdf07a6c51..b0656dd53d6 100644 --- a/packages/rs-drive-abci/src/query/shielded/nullifiers/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/nullifiers/mod.rs @@ -93,6 +93,7 @@ mod tests { version: Some(RequestVersion::V0(GetShieldedNullifiersRequestV0 { nullifiers: vec![vec![0x11u8; 32]], prove: false, + token_id: None, })), }; diff --git a/packages/rs-drive-abci/src/query/shielded/nullifiers/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/nullifiers/v0/mod.rs index cb9f12bff57..bec4d767c4b 100644 --- a/packages/rs-drive-abci/src/query/shielded/nullifiers/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/nullifiers/v0/mod.rs @@ -14,9 +14,7 @@ use dapi_grpc::platform::v0::get_shielded_nullifiers_response::{ use dpp::check_validation_result_with_data; use dpp::validation::ValidationResult; use dpp::version::PlatformVersion; -use drive::drive::shielded::paths::{ - shielded_credit_pool_nullifiers_path, shielded_credit_pool_nullifiers_path_vec, -}; +use crate::query::shielded::ShieldedPoolSelector; use drive::error::query::QuerySyntaxError; use drive::grovedb::{PathQuery, Query, SizedQuery}; use drive::util::grove_operations::{DirectQueryType, GroveDBToUse}; @@ -24,10 +22,18 @@ use drive::util::grove_operations::{DirectQueryType, GroveDBToUse}; impl Platform { pub(super) fn query_shielded_nullifiers_v0( &self, - GetShieldedNullifiersRequestV0 { nullifiers, prove }: GetShieldedNullifiersRequestV0, + GetShieldedNullifiersRequestV0 { + nullifiers, + prove, + token_id, + }: GetShieldedNullifiersRequestV0, platform_state: &PlatformState, platform_version: &PlatformVersion, ) -> Result, Error> { + let pool = match ShieldedPoolSelector::from_request(token_id, platform_version) { + Ok(pool) => pool, + Err(error) => return Ok(QueryValidationResult::new_with_error(error)), + }; let max_elements = platform_version.drive_abci.query.max_returned_elements as usize; if nullifiers.len() > max_elements { return Ok(QueryValidationResult::new_with_error(QueryError::Query( @@ -60,7 +66,7 @@ impl Platform { let response = if prove { let path_query = PathQuery { - path: shielded_credit_pool_nullifiers_path_vec(), + path: pool.nullifiers_path_vec(), query: SizedQuery { query: { let mut q = Query::new(); @@ -87,13 +93,13 @@ impl Platform { metadata: Some(self.response_metadata_v0(platform_state, grovedb_used)), } } else { - let nullifiers_path = shielded_credit_pool_nullifiers_path(); + let nullifiers_path = pool.nullifiers_path_vec(); let entries: Vec = nullifiers .into_iter() .map(|nullifier| { let is_spent = self.drive.grove_has_raw( - (&nullifiers_path).into(), + nullifiers_path.as_slice().into(), &nullifier, DirectQueryType::StatefulDirectQuery, None, @@ -139,6 +145,7 @@ mod tests { let request = GetShieldedNullifiersRequestV0 { nullifiers: vec![], prove: false, + token_id: None, }; let result = platform @@ -159,6 +166,7 @@ mod tests { let request = GetShieldedNullifiersRequestV0 { nullifiers: vec![nullifier(1), vec![0u8; 16]], prove: false, + token_id: None, }; let result = platform @@ -182,6 +190,7 @@ mod tests { let request = GetShieldedNullifiersRequestV0 { nullifiers, prove: false, + token_id: None, }; let result = platform @@ -205,6 +214,7 @@ mod tests { let request = GetShieldedNullifiersRequestV0 { nullifiers: queried.clone(), prove: false, + token_id: None, }; let result = platform @@ -240,6 +250,7 @@ mod tests { let request = GetShieldedNullifiersRequestV0 { nullifiers: vec![nullifier(0x01)], prove: true, + token_id: None, }; let result = platform diff --git a/packages/rs-drive-abci/src/query/shielded/pool_state/mod.rs b/packages/rs-drive-abci/src/query/shielded/pool_state/mod.rs index 9853f4a3daa..ab42db90aaa 100644 --- a/packages/rs-drive-abci/src/query/shielded/pool_state/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/pool_state/mod.rs @@ -91,6 +91,7 @@ mod tests { let request = GetShieldedPoolStateRequest { version: Some(RequestVersion::V0(GetShieldedPoolStateRequestV0 { prove: false, + token_id: None, })), }; @@ -121,6 +122,7 @@ mod tests { let request = GetShieldedPoolStateRequest { version: Some(RequestVersion::V0(GetShieldedPoolStateRequestV0 { prove: true, + token_id: None, })), }; diff --git a/packages/rs-drive-abci/src/query/shielded/pool_state/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/pool_state/v0/mod.rs index c71ef44fd2c..4f2df152bcf 100644 --- a/packages/rs-drive-abci/src/query/shielded/pool_state/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/pool_state/v0/mod.rs @@ -2,6 +2,7 @@ use crate::error::Error; use crate::platform_types::platform::Platform; use crate::platform_types::platform_state::PlatformState; use crate::query::response_metadata::CheckpointUsed; +use crate::query::shielded::ShieldedPoolSelector; use crate::query::QueryValidationResult; use dapi_grpc::platform::v0::get_shielded_pool_state_request::GetShieldedPoolStateRequestV0; use dapi_grpc::platform::v0::get_shielded_pool_state_response::{ @@ -10,22 +11,24 @@ use dapi_grpc::platform::v0::get_shielded_pool_state_response::{ use dpp::check_validation_result_with_data; use dpp::validation::ValidationResult; use dpp::version::PlatformVersion; -use drive::drive::shielded::paths::{ - shielded_credit_pool_path, shielded_credit_pool_path_vec, SHIELDED_TOTAL_BALANCE_KEY, -}; +use drive::drive::shielded::paths::SHIELDED_TOTAL_BALANCE_KEY; use drive::grovedb::{PathQuery, Query, SizedQuery}; use drive::util::grove_operations::{DirectQueryType, GroveDBToUse}; impl Platform { pub(super) fn query_shielded_pool_state_v0( &self, - GetShieldedPoolStateRequestV0 { prove }: GetShieldedPoolStateRequestV0, + GetShieldedPoolStateRequestV0 { prove, token_id }: GetShieldedPoolStateRequestV0, platform_state: &PlatformState, platform_version: &PlatformVersion, ) -> Result, Error> { + let pool = match ShieldedPoolSelector::from_request(token_id, platform_version) { + Ok(pool) => pool, + Err(error) => return Ok(QueryValidationResult::new_with_error(error)), + }; let response = if prove { let path_query = PathQuery { - path: shielded_credit_pool_path_vec(), + path: pool.pool_path_vec(), query: SizedQuery { query: Query::new_single_key(vec![SHIELDED_TOTAL_BALANCE_KEY]), limit: Some(1), @@ -48,12 +51,12 @@ impl Platform { metadata: Some(self.response_metadata_v0(platform_state, grovedb_used)), } } else { - let pool_path = shielded_credit_pool_path(); + let pool_path = pool.pool_path_vec(); let total_balance = self .drive .grove_get_raw_value_u64_from_encoded_var_vec( - (&pool_path).into(), + pool_path.as_slice().into(), &[SHIELDED_TOTAL_BALANCE_KEY], DirectQueryType::StatefulDirectQuery, None, diff --git a/packages/rs-drive-proof-verifier/src/proof.rs b/packages/rs-drive-proof-verifier/src/proof.rs index 2aba1197a43..7dcfd8374ab 100644 --- a/packages/rs-drive-proof-verifier/src/proof.rs +++ b/packages/rs-drive-proof-verifier/src/proof.rs @@ -2519,12 +2519,27 @@ fn u32_to_u16_opt(i: u32) -> Result, Error> { // --- Shielded Pool Query Proof Verification --- +/// The optional `token_id` every shielded pool request carries: `None` targets the credit +/// shielded pool, `Some` the pool of that token. The verifier must prove against the same +/// pool the request named, so a malformed id is a request error rather than a silent fallback. +fn shielded_pool_token_id(token_id: Option>) -> Result, Error> { + token_id + .map(|token_id| { + token_id + .try_into() + .map_err(|token_id: Vec| Error::RequestError { + error: format!("token_id must be 32 bytes, got {}", token_id.len()), + }) + }) + .transpose() +} + impl FromProof for ShieldedPoolState { type Request = platform::GetShieldedPoolStateRequest; type Response = platform::GetShieldedPoolStateResponse; fn maybe_from_proof_with_metadata<'a, I: Into, O: Into>( - _request: I, + request: I, response: O, _network: Network, platform_version: &PlatformVersion, @@ -2533,15 +2548,29 @@ impl FromProof for ShieldedPoolState { where Self: Sized + 'a, { + use dapi_grpc::platform::v0::get_shielded_pool_state_request; + + let request: Self::Request = request.into(); let response: Self::Response = response.into(); let proof = response.proof().or(Err(Error::NoProofInResult))?; let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?; - let (root_hash, maybe_balance) = Drive::verify_shielded_pool_state( - supported_grovedb_proof_bytes(proof, platform_version)?, - false, - platform_version, - ) + let token_id = match request.version.ok_or(Error::EmptyVersion)? { + get_shielded_pool_state_request::Version::V0(v0) => { + shielded_pool_token_id(v0.token_id)? + } + }; + + let proof_bytes = supported_grovedb_proof_bytes(proof, platform_version)?; + let (root_hash, maybe_balance) = match token_id { + None => Drive::verify_shielded_pool_state(proof_bytes, false, platform_version), + Some(token_id) => Drive::verify_token_shielded_pool_state( + proof_bytes, + token_id, + false, + platform_version, + ), + } .map_drive_error(proof, mtd)?; verify_tenderdash_proof(proof, mtd, &root_hash, provider, platform_version)?; @@ -2559,7 +2588,7 @@ impl FromProof for ShieldedNotesCount { type Response = platform::GetShieldedNotesCountResponse; fn maybe_from_proof_with_metadata<'a, I: Into, O: Into>( - _request: I, + request: I, response: O, _network: Network, platform_version: &PlatformVersion, @@ -2568,19 +2597,33 @@ impl FromProof for ShieldedNotesCount { where Self: Sized + 'a, { + use dapi_grpc::platform::v0::get_shielded_notes_count_request; + + let request: Self::Request = request.into(); let response: Self::Response = response.into(); let proof = response.proof().or(Err(Error::NoProofInResult))?; let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?; + let token_id = match request.version.ok_or(Error::EmptyVersion)? { + get_shielded_notes_count_request::Version::V0(v0) => { + shielded_pool_token_id(v0.token_id)? + } + }; + // Mirrors `ShieldedPoolState` above; the only difference is the // proved element type — `verify_shielded_notes_count` decodes // `total_count` out of the `CommitmentTree` element rather than a // `SumItem` balance. - let (root_hash, maybe_count) = Drive::verify_shielded_notes_count( - supported_grovedb_proof_bytes(proof, platform_version)?, - false, - platform_version, - ) + let proof_bytes = supported_grovedb_proof_bytes(proof, platform_version)?; + let (root_hash, maybe_count) = match token_id { + None => Drive::verify_shielded_notes_count(proof_bytes, false, platform_version), + Some(token_id) => Drive::verify_token_shielded_pool_notes_count( + proof_bytes, + token_id, + false, + platform_version, + ), + } .map_drive_error(proof, mtd)?; verify_tenderdash_proof(proof, mtd, &root_hash, provider, platform_version)?; @@ -2598,7 +2641,7 @@ impl FromProof for ShieldedAnchors { type Response = platform::GetShieldedAnchorsResponse; fn maybe_from_proof_with_metadata<'a, I: Into, O: Into>( - _request: I, + request: I, response: O, _network: Network, platform_version: &PlatformVersion, @@ -2607,15 +2650,27 @@ impl FromProof for ShieldedAnchors { where Self: Sized + 'a, { + use dapi_grpc::platform::v0::get_shielded_anchors_request; + + let request: Self::Request = request.into(); let response: Self::Response = response.into(); let proof = response.proof().or(Err(Error::NoProofInResult))?; let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?; - let (root_hash, anchors) = Drive::verify_shielded_anchors( - supported_grovedb_proof_bytes(proof, platform_version)?, - false, - platform_version, - ) + let token_id = match request.version.ok_or(Error::EmptyVersion)? { + get_shielded_anchors_request::Version::V0(v0) => shielded_pool_token_id(v0.token_id)?, + }; + + let proof_bytes = supported_grovedb_proof_bytes(proof, platform_version)?; + let (root_hash, anchors) = match token_id { + None => Drive::verify_shielded_anchors(proof_bytes, false, platform_version), + Some(token_id) => Drive::verify_token_shielded_pool_anchors( + proof_bytes, + token_id, + false, + platform_version, + ), + } .map_drive_error(proof, mtd)?; verify_tenderdash_proof(proof, mtd, &root_hash, provider, platform_version)?; @@ -2635,7 +2690,7 @@ impl FromProof for MostRecentShiel type Response = platform::GetMostRecentShieldedAnchorResponse; fn maybe_from_proof_with_metadata<'a, I: Into, O: Into>( - _request: I, + request: I, response: O, _network: Network, platform_version: &PlatformVersion, @@ -2644,15 +2699,29 @@ impl FromProof for MostRecentShiel where Self: Sized + 'a, { + use dapi_grpc::platform::v0::get_most_recent_shielded_anchor_request; + + let request: Self::Request = request.into(); let response: Self::Response = response.into(); let proof = response.proof().or(Err(Error::NoProofInResult))?; let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?; - let (root_hash, maybe_anchor) = Drive::verify_most_recent_shielded_anchor( - supported_grovedb_proof_bytes(proof, platform_version)?, - false, - platform_version, - ) + let token_id = match request.version.ok_or(Error::EmptyVersion)? { + get_most_recent_shielded_anchor_request::Version::V0(v0) => { + shielded_pool_token_id(v0.token_id)? + } + }; + + let proof_bytes = supported_grovedb_proof_bytes(proof, platform_version)?; + let (root_hash, maybe_anchor) = match token_id { + None => Drive::verify_most_recent_shielded_anchor(proof_bytes, false, platform_version), + Some(token_id) => Drive::verify_most_recent_token_shielded_pool_anchor( + proof_bytes, + token_id, + false, + platform_version, + ), + } .map_drive_error(proof, mtd)?; verify_tenderdash_proof(proof, mtd, &root_hash, provider, platform_version)?; @@ -2686,8 +2755,12 @@ impl FromProof for ShieldedEncrypted let proof = response.proof().or(Err(Error::NoProofInResult))?; let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?; - let (start_index, count) = match request.version.ok_or(Error::EmptyVersion)? { - get_shielded_encrypted_notes_request::Version::V0(v0) => (v0.start_index, v0.count), + let (start_index, count, token_id) = match request.version.ok_or(Error::EmptyVersion)? { + get_shielded_encrypted_notes_request::Version::V0(v0) => ( + v0.start_index, + v0.count, + shielded_pool_token_id(v0.token_id)?, + ), }; let max_elements = platform_version @@ -2697,14 +2770,26 @@ impl FromProof for ShieldedEncrypted .max_query_chunks as u32 * (1u32 << drive::drive::shielded::paths::SHIELDED_NOTES_CHUNK_POWER); - let (root_hash, notes, total_count) = Drive::verify_shielded_encrypted_notes( - supported_grovedb_proof_bytes(proof, platform_version)?, - start_index, - count, - max_elements, - false, - platform_version, - ) + let proof_bytes = supported_grovedb_proof_bytes(proof, platform_version)?; + let (root_hash, notes, total_count) = match token_id { + None => Drive::verify_shielded_encrypted_notes( + proof_bytes, + start_index, + count, + max_elements, + false, + platform_version, + ), + Some(token_id) => Drive::verify_token_shielded_pool_encrypted_notes( + proof_bytes, + token_id, + start_index, + count, + max_elements, + false, + platform_version, + ), + } .map_drive_error(proof, mtd)?; verify_tenderdash_proof(proof, mtd, &root_hash, provider, platform_version)?; @@ -2752,16 +2837,25 @@ impl FromProof for ShieldedNullifierStat let proof = response.proof().or(Err(Error::NoProofInResult))?; let mtd = response.metadata().or(Err(Error::EmptyResponseMetadata))?; - let nullifiers = match request.version.ok_or(Error::EmptyVersion)? { - get_shielded_nullifiers_request::Version::V0(v0) => v0.nullifiers, + let (nullifiers, token_id) = match request.version.ok_or(Error::EmptyVersion)? { + get_shielded_nullifiers_request::Version::V0(v0) => { + (v0.nullifiers, shielded_pool_token_id(v0.token_id)?) + } }; - let (root_hash, statuses) = Drive::verify_shielded_nullifiers( - supported_grovedb_proof_bytes(proof, platform_version)?, - &nullifiers, - false, - platform_version, - ) + let proof_bytes = supported_grovedb_proof_bytes(proof, platform_version)?; + let (root_hash, statuses) = match token_id { + None => { + Drive::verify_shielded_nullifiers(proof_bytes, &nullifiers, false, platform_version) + } + Some(token_id) => Drive::verify_token_shielded_pool_nullifiers( + proof_bytes, + token_id, + &nullifiers, + false, + platform_version, + ), + } .map_drive_error(proof, mtd)?; verify_tenderdash_proof(proof, mtd, &root_hash, provider, platform_version)?; @@ -6889,7 +6983,10 @@ mod tests { request!( get_shielded_pool_state_request, GetShieldedPoolStateRequestV0, - GetShieldedPoolStateRequest { prove: true } + GetShieldedPoolStateRequest { + prove: true, + token_id: None + } ), legacy_response!( get_shielded_pool_state_response, @@ -6904,7 +7001,10 @@ mod tests { request!( get_shielded_notes_count_request, GetShieldedNotesCountRequestV0, - GetShieldedNotesCountRequest { prove: true } + GetShieldedNotesCountRequest { + prove: true, + token_id: None + } ), legacy_response!( get_shielded_notes_count_response, @@ -6919,7 +7019,10 @@ mod tests { request!( get_shielded_anchors_request, GetShieldedAnchorsRequestV0, - GetShieldedAnchorsRequest { prove: true } + GetShieldedAnchorsRequest { + prove: true, + token_id: None + } ), legacy_response!( get_shielded_anchors_response, @@ -6934,7 +7037,10 @@ mod tests { request!( get_most_recent_shielded_anchor_request, GetMostRecentShieldedAnchorRequestV0, - GetMostRecentShieldedAnchorRequest { prove: true } + GetMostRecentShieldedAnchorRequest { + prove: true, + token_id: None + } ), legacy_response!( get_most_recent_shielded_anchor_response, @@ -6951,7 +7057,8 @@ mod tests { GetShieldedEncryptedNotesRequestV0, GetShieldedEncryptedNotesRequest { count: 1, - prove: true + prove: true, + token_id: None } ), legacy_response!( @@ -6969,7 +7076,8 @@ mod tests { GetShieldedNullifiersRequestV0, GetShieldedNullifiersRequest { nullifiers: vec![id(1)], - prove: true + prove: true, + token_id: None } ), legacy_response!( diff --git a/packages/rs-drive-proof-verifier/src/types.rs b/packages/rs-drive-proof-verifier/src/types.rs index 05cdf8a1a5e..165accd54f7 100644 --- a/packages/rs-drive-proof-verifier/src/types.rs +++ b/packages/rs-drive-proof-verifier/src/types.rs @@ -938,3 +938,46 @@ pub struct ShieldedEncryptedNotesQuery { platform_serialize(unversioned) )] pub struct ShieldedNullifiersQuery(pub Vec<[u8; 32]>); + +/// Targets one token's shielded pool (protocol version 14+) for the parameterless shielded +/// queries: pool state, notes count, anchors and most recent anchor. +#[derive(Debug, Clone)] +#[cfg_attr( + feature = "mocks", + derive(Encode, Decode, PlatformSerialize, PlatformDeserializeTrusted), + platform_serialize(unversioned) +)] +pub struct TokenShieldedPoolQuery { + /// The token whose shielded pool is queried + pub token_id: Identifier, +} + +/// Encrypted notes page of one token's shielded pool +#[derive(Debug, Clone)] +#[cfg_attr( + feature = "mocks", + derive(Encode, Decode, PlatformSerialize, PlatformDeserializeTrusted), + platform_serialize(unversioned) +)] +pub struct TokenShieldedEncryptedNotesQuery { + /// The token whose shielded pool is queried + pub token_id: Identifier, + /// Starting index in the encrypted notes count tree (inclusive, 0 = from beginning) + pub start_index: u64, + /// Max number of notes to return + pub count: u32, +} + +/// Nullifier status check inside one token's shielded pool +#[derive(Debug, Clone)] +#[cfg_attr( + feature = "mocks", + derive(Encode, Decode, PlatformSerialize, PlatformDeserializeTrusted), + platform_serialize(unversioned) +)] +pub struct TokenShieldedNullifiersQuery { + /// The token whose shielded pool is queried + pub token_id: Identifier, + /// The nullifiers to check + pub nullifiers: Vec<[u8; 32]>, +} diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v1/mod.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v1/mod.rs index afbc243b51f..c729cccd9aa 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/v1/mod.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v1/mod.rs @@ -19,6 +19,7 @@ use crate::util::object_size_info::PathKeyElementInfo::PathKeyElement; use crate::util::object_size_info::{DriveKeyInfo, PathKeyElementInfo}; use dpp::data_contract::accessors::v1::DataContractV1Getters; use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters; use dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; use dpp::data_contract::associated_token::token_distribution_rules::accessors::v1::TokenDistributionRulesV1Getters; use dpp::serialization::{PlatformSerializable, PlatformSerializableWithPlatformVersion}; @@ -262,6 +263,16 @@ impl Drive { )?; } + if token_config.has_shielded_pool() { + batch_operations.extend(self.create_token_shielded_pool_trees_operations( + token_id_bytes, + false, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?); + } + if token_config.start_as_paused() { // no status also means active. let starting_status = TokenStatus::new(true, platform_version)?; diff --git a/packages/rs-drive/src/drive/contract/update/update_contract/v1/mod.rs b/packages/rs-drive/src/drive/contract/update/update_contract/v1/mod.rs index c19c1d5ce3f..dc6a9187bb5 100644 --- a/packages/rs-drive/src/drive/contract/update/update_contract/v1/mod.rs +++ b/packages/rs-drive/src/drive/contract/update/update_contract/v1/mod.rs @@ -14,6 +14,7 @@ use dpp::serialization::PlatformSerializableWithPlatformVersion; use crate::error::contract::DataContractError; use dpp::data_contract::accessors::v1::DataContractV1Getters; use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters; use dpp::fee::default_costs::CachedEpochIndexFeeVersions; use dpp::version::PlatformVersion; use grovedb::batch::KeyInfoPath; @@ -241,6 +242,16 @@ impl Drive { transaction, platform_version, )?); + + if configuration.has_shielded_pool() { + batch_operations.extend(self.create_token_shielded_pool_trees_operations( + token_id.to_buffer(), + true, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?); + } } if !contract.groups().is_empty() { diff --git a/packages/rs-drive/src/drive/initialization/v4/mod.rs b/packages/rs-drive/src/drive/initialization/v4/mod.rs index c89df4de467..4faef58650e 100644 --- a/packages/rs-drive/src/drive/initialization/v4/mod.rs +++ b/packages/rs-drive/src/drive/initialization/v4/mod.rs @@ -103,6 +103,10 @@ impl Drive { // through the same helper: the prefunded balances Merk is then built by the same // sequence of inserts on both node populations. self.insert_contract_fee_pot_trees(transaction, platform_version)?; + // Token shielded pools root (v4, protocol version 14): an empty BigSumTree under the + // Tokens tree. Inserted after the batch so `[Tokens]` already exists, through the + // helper the in-place upgrade (`Platform::transition_to_version_14`) also calls. + self.insert_token_shielded_pools_root_tree(transaction, platform_version)?; Ok(()) } diff --git a/packages/rs-drive/src/drive/shielded/has_anchor/mod.rs b/packages/rs-drive/src/drive/shielded/has_anchor/mod.rs index 60d70072a89..16171fae3b5 100644 --- a/packages/rs-drive/src/drive/shielded/has_anchor/mod.rs +++ b/packages/rs-drive/src/drive/shielded/has_anchor/mod.rs @@ -1,5 +1,6 @@ mod v0; +use crate::drive::shielded::paths::token_shielded_pool_anchors_path; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; @@ -40,3 +41,34 @@ impl Drive { } } } + +impl Drive { + /// Checks whether an anchor exists in a TOKEN shielded pool's anchors tree. Same versioning + /// as [`Drive::has_shielded_anchor`]. + pub fn has_token_pool_anchor( + &self, + token_id: &[u8; 32], + anchor: &[u8; 32], + transaction: TransactionArg, + drive_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version.drive.methods.shielded.has_anchor { + 0 => { + let anchors_path = token_shielded_pool_anchors_path(token_id); + self.has_anchor_in_pool_v0( + &anchors_path, + anchor, + transaction, + drive_operations, + platform_version, + ) + } + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "has_token_pool_anchor".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/shielded/has_anchor/v0/mod.rs b/packages/rs-drive/src/drive/shielded/has_anchor/v0/mod.rs index d9da382780a..b4d9a08e714 100644 --- a/packages/rs-drive/src/drive/shielded/has_anchor/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/has_anchor/v0/mod.rs @@ -19,9 +19,27 @@ impl Drive { platform_version: &PlatformVersion, ) -> Result { let anchors_path = shielded_credit_pool_anchors_path(); + self.has_anchor_in_pool_v0( + &anchors_path, + anchor, + transaction, + drive_operations, + platform_version, + ) + } + /// O(1) anchor membership lookup in the anchors tree at `anchors_path`, whichever shielded + /// pool (credit or token) it belongs to. + pub(in crate::drive) fn has_anchor_in_pool_v0( + &self, + anchors_path: &[&[u8]], + anchor: &[u8; 32], + transaction: TransactionArg, + drive_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result { self.grove_has_raw( - (&anchors_path).into(), + anchors_path.into(), anchor, DirectQueryType::StatefulDirectQuery, transaction, diff --git a/packages/rs-drive/src/drive/shielded/has_nullifier/mod.rs b/packages/rs-drive/src/drive/shielded/has_nullifier/mod.rs index b89085bc288..42d5181b467 100644 --- a/packages/rs-drive/src/drive/shielded/has_nullifier/mod.rs +++ b/packages/rs-drive/src/drive/shielded/has_nullifier/mod.rs @@ -1,5 +1,6 @@ mod v0; +use crate::drive::shielded::paths::token_shielded_pool_nullifiers_path; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; @@ -38,3 +39,34 @@ impl Drive { } } } + +impl Drive { + /// Checks whether a nullifier has already been spent in a TOKEN shielded pool. Same + /// versioning as [`Drive::has_nullifier`]. + pub fn has_token_pool_nullifier( + &self, + token_id: &[u8; 32], + nullifier: &[u8; 32], + transaction: TransactionArg, + drive_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version.drive.methods.shielded.has_nullifier { + 0 => { + let nullifiers_path = token_shielded_pool_nullifiers_path(token_id); + self.has_nullifier_in_pool_v0( + &nullifiers_path, + nullifier, + transaction, + drive_operations, + platform_version, + ) + } + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "has_token_pool_nullifier".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/shielded/has_nullifier/v0/mod.rs b/packages/rs-drive/src/drive/shielded/has_nullifier/v0/mod.rs index 067ce9be418..09e1132baae 100644 --- a/packages/rs-drive/src/drive/shielded/has_nullifier/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/has_nullifier/v0/mod.rs @@ -19,9 +19,27 @@ impl Drive { platform_version: &PlatformVersion, ) -> Result { let nullifiers_path = shielded_credit_pool_nullifiers_path(); + self.has_nullifier_in_pool_v0( + &nullifiers_path, + nullifier, + transaction, + drive_operations, + platform_version, + ) + } + /// O(1) nullifier membership lookup in the nullifiers tree at `nullifiers_path`, whichever + /// shielded pool (credit or token) it belongs to. + pub(in crate::drive) fn has_nullifier_in_pool_v0( + &self, + nullifiers_path: &[&[u8]], + nullifier: &[u8; 32], + transaction: TransactionArg, + drive_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result { self.grove_has_raw( - (&nullifiers_path).into(), + nullifiers_path.into(), nullifier, DirectQueryType::StatefulDirectQuery, transaction, diff --git a/packages/rs-drive/src/drive/shielded/insert_note/mod.rs b/packages/rs-drive/src/drive/shielded/insert_note/mod.rs index 8b649cecf19..321f8956ddd 100644 --- a/packages/rs-drive/src/drive/shielded/insert_note/mod.rs +++ b/packages/rs-drive/src/drive/shielded/insert_note/mod.rs @@ -1,5 +1,6 @@ mod v0; +use crate::drive::shielded::paths::token_shielded_pool_notes_path_vec; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; @@ -36,3 +37,32 @@ impl Drive { } } } + +impl Drive { + /// Constructs the low-level drive operations to insert a note into a TOKEN shielded pool's + /// commitment tree. Same layout and versioning as [`Drive::insert_note_op`], re-rooted under + /// the token's pool. + pub fn insert_token_pool_note_op( + token_id: [u8; 32], + nullifier: [u8; 32], + cmx: [u8; 32], + cv_net: [u8; 32], + encrypted_note: Vec, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match platform_version.drive.methods.shielded.insert_note { + 0 => Ok(Self::insert_note_op_in_pool_v0( + token_shielded_pool_notes_path_vec(token_id), + nullifier, + cmx, + cv_net, + encrypted_note, + )), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "insert_token_pool_note_op".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/shielded/insert_note/v0/mod.rs b/packages/rs-drive/src/drive/shielded/insert_note/v0/mod.rs index f0e59aa5bda..59f152cfe9f 100644 --- a/packages/rs-drive/src/drive/shielded/insert_note/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/insert_note/v0/mod.rs @@ -17,8 +17,25 @@ impl Drive { cv_net: [u8; 32], encrypted_note: Vec, ) -> Result, Error> { - let notes_path = shielded_credit_pool_notes_path_vec(); - Ok(vec![GroveOperation( + Ok(Self::insert_note_op_in_pool_v0( + shielded_credit_pool_notes_path_vec(), + nullifier, + cmx, + cv_net, + encrypted_note, + )) + } + + /// Builds the note insertion operation for the notes tree at `notes_path`, whichever + /// shielded pool (credit or token) it belongs to. + pub(in crate::drive) fn insert_note_op_in_pool_v0( + notes_path: Vec>, + nullifier: [u8; 32], + cmx: [u8; 32], + cv_net: [u8; 32], + encrypted_note: Vec, + ) -> Vec { + vec![GroveOperation( QualifiedGroveDbOp::commitment_tree_insert_op( notes_path, cmx, @@ -26,7 +43,7 @@ impl Drive { cv_net, encrypted_note, ), - )]) + )] } } diff --git a/packages/rs-drive/src/drive/shielded/insert_nullifiers/mod.rs b/packages/rs-drive/src/drive/shielded/insert_nullifiers/mod.rs index 56f5cb45c7b..e076c2d36f2 100644 --- a/packages/rs-drive/src/drive/shielded/insert_nullifiers/mod.rs +++ b/packages/rs-drive/src/drive/shielded/insert_nullifiers/mod.rs @@ -1,5 +1,6 @@ mod v0; +use crate::drive::shielded::paths::token_shielded_pool_nullifiers_path_vec; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; @@ -28,3 +29,25 @@ impl Drive { } } } + +impl Drive { + /// Inserts nullifiers into a TOKEN shielded pool's permanent nullifiers tree (double-spend + /// prevention). Same versioning as [`Drive::insert_nullifiers`]. + pub fn insert_token_pool_nullifiers( + token_id: [u8; 32], + nullifiers: &[[u8; 32]], + platform_version: &PlatformVersion, + ) -> Result, Error> { + match platform_version.drive.methods.shielded.insert_nullifiers { + 0 => Ok(Self::insert_nullifiers_in_pool_v0( + token_shielded_pool_nullifiers_path_vec(token_id), + nullifiers, + )), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "insert_token_pool_nullifiers".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/shielded/insert_nullifiers/v0/mod.rs b/packages/rs-drive/src/drive/shielded/insert_nullifiers/v0/mod.rs index 6a59b446881..d4e7837144a 100644 --- a/packages/rs-drive/src/drive/shielded/insert_nullifiers/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/insert_nullifiers/v0/mod.rs @@ -15,10 +15,20 @@ impl Drive { &self, nullifiers: &[[u8; 32]], ) -> Result, Error> { - let nullifiers_path = shielded_credit_pool_nullifiers_path_vec(); + Ok(Self::insert_nullifiers_in_pool_v0( + shielded_credit_pool_nullifiers_path_vec(), + nullifiers, + )) + } + /// Builds the nullifier insertion operations for the nullifiers tree at `nullifiers_path`, + /// whichever shielded pool (credit or token) it belongs to. + pub(in crate::drive) fn insert_nullifiers_in_pool_v0( + nullifiers_path: Vec>, + nullifiers: &[[u8; 32]], + ) -> Vec { // Build permanent nullifier tree ops - let ops = nullifiers + nullifiers .iter() .map(|nullifier| { GroveOperation( @@ -29,9 +39,7 @@ impl Drive { ), ) }) - .collect(); - - Ok(ops) + .collect() } } diff --git a/packages/rs-drive/src/drive/shielded/notes_count/mod.rs b/packages/rs-drive/src/drive/shielded/notes_count/mod.rs index a05317c2558..d193775c01b 100644 --- a/packages/rs-drive/src/drive/shielded/notes_count/mod.rs +++ b/packages/rs-drive/src/drive/shielded/notes_count/mod.rs @@ -1,5 +1,6 @@ mod v0; +use crate::drive::shielded::paths::token_shielded_pool_path; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; @@ -33,3 +34,32 @@ impl Drive { } } } + +impl Drive { + /// Returns the number of notes in a TOKEN shielded pool's commitment tree. Same versioning + /// as [`Drive::shielded_pool_notes_count`]. + pub fn token_shielded_pool_notes_count( + &self, + token_id: &[u8; 32], + transaction: TransactionArg, + drive_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version.drive.methods.shielded.notes_count { + 0 => { + let pool_path = token_shielded_pool_path(token_id); + self.pool_notes_count_v0( + &pool_path, + transaction, + drive_operations, + platform_version, + ) + } + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "token_shielded_pool_notes_count".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/shielded/notes_count/v0/mod.rs b/packages/rs-drive/src/drive/shielded/notes_count/v0/mod.rs index b8c26ddaff4..d78194ccfa6 100644 --- a/packages/rs-drive/src/drive/shielded/notes_count/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/notes_count/v0/mod.rs @@ -17,8 +17,20 @@ impl Drive { platform_version: &PlatformVersion, ) -> Result { let pool_path = shielded_credit_pool_path(); + self.pool_notes_count_v0(&pool_path, transaction, drive_operations, platform_version) + } + + /// Counts the notes in the commitment tree of the pool at `pool_path`, whichever shielded + /// pool (credit or token) it is. + pub(in crate::drive) fn pool_notes_count_v0( + &self, + pool_path: &[&[u8]], + transaction: TransactionArg, + drive_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result { self.grove_commitment_tree_count( - (&pool_path).into(), + pool_path.into(), &[SHIELDED_NOTES_KEY], transaction, drive_operations, diff --git a/packages/rs-drive/src/drive/shielded/paths.rs b/packages/rs-drive/src/drive/shielded/paths.rs index fc7ad7d6707..79219c8a931 100644 --- a/packages/rs-drive/src/drive/shielded/paths.rs +++ b/packages/rs-drive/src/drive/shielded/paths.rs @@ -1,3 +1,4 @@ +use crate::drive::tokens::paths::TOKEN_SHIELDED_POOLS_KEY; use crate::drive::RootTree; use grovedb::{PathQuery, Query, SizedQuery}; @@ -187,24 +188,188 @@ pub fn shielded_latest_recorded_anchor_path_query() -> PathQuery { } } +// --------------------------------------------------------------------------- +// Token shielded pools +// +// A token pool lives at `[Tokens, TOKEN_SHIELDED_POOLS_KEY, token_id]` and has the same five +// children, under the same keys, as the credit pool above. Everything that reads or writes a +// pool takes the pool's path, so the credit pool and every token pool share one implementation. +// --------------------------------------------------------------------------- + +/// Path to a token's shielded pool: `[Tokens, 224, token_id]` +pub fn token_shielded_pool_path(token_id: &[u8; 32]) -> [&[u8]; 3] { + [ + Into::<&[u8; 1]>::into(RootTree::Tokens), + &[TOKEN_SHIELDED_POOLS_KEY], + token_id, + ] +} + +/// Path to a token's shielded pool as a vec +pub fn token_shielded_pool_path_vec(token_id: [u8; 32]) -> Vec> { + vec![ + vec![RootTree::Tokens as u8], + vec![TOKEN_SHIELDED_POOLS_KEY], + token_id.to_vec(), + ] +} + +/// Path to a token pool's notes tree: `[Tokens, 224, token_id, [128]]` +pub fn token_shielded_pool_notes_path(token_id: &[u8; 32]) -> [&[u8]; 4] { + [ + Into::<&[u8; 1]>::into(RootTree::Tokens), + &[TOKEN_SHIELDED_POOLS_KEY], + token_id, + &[SHIELDED_NOTES_KEY], + ] +} + +/// Path to a token pool's notes tree as a vec +pub fn token_shielded_pool_notes_path_vec(token_id: [u8; 32]) -> Vec> { + vec![ + vec![RootTree::Tokens as u8], + vec![TOKEN_SHIELDED_POOLS_KEY], + token_id.to_vec(), + vec![SHIELDED_NOTES_KEY], + ] +} + +/// Path to a token pool's nullifiers tree: `[Tokens, 224, token_id, [64]]` +pub fn token_shielded_pool_nullifiers_path(token_id: &[u8; 32]) -> [&[u8]; 4] { + [ + Into::<&[u8; 1]>::into(RootTree::Tokens), + &[TOKEN_SHIELDED_POOLS_KEY], + token_id, + &[SHIELDED_NULLIFIERS_KEY], + ] +} + +/// Path to a token pool's nullifiers tree as a vec +pub fn token_shielded_pool_nullifiers_path_vec(token_id: [u8; 32]) -> Vec> { + vec![ + vec![RootTree::Tokens as u8], + vec![TOKEN_SHIELDED_POOLS_KEY], + token_id.to_vec(), + vec![SHIELDED_NULLIFIERS_KEY], + ] +} + +/// Path to a token pool's anchors tree: `[Tokens, 224, token_id, [192]]` +pub fn token_shielded_pool_anchors_path(token_id: &[u8; 32]) -> [&[u8]; 4] { + [ + Into::<&[u8; 1]>::into(RootTree::Tokens), + &[TOKEN_SHIELDED_POOLS_KEY], + token_id, + &[SHIELDED_ANCHORS_IN_POOL_KEY], + ] +} + +/// Path to a token pool's anchors tree as a vec +pub fn token_shielded_pool_anchors_path_vec(token_id: [u8; 32]) -> Vec> { + vec![ + vec![RootTree::Tokens as u8], + vec![TOKEN_SHIELDED_POOLS_KEY], + token_id.to_vec(), + vec![SHIELDED_ANCHORS_IN_POOL_KEY], + ] +} + +/// Path to a token pool's anchors-by-height tree: `[Tokens, 224, token_id, [96]]` +pub fn token_shielded_pool_anchors_by_height_path(token_id: &[u8; 32]) -> [&[u8]; 4] { + [ + Into::<&[u8; 1]>::into(RootTree::Tokens), + &[TOKEN_SHIELDED_POOLS_KEY], + token_id, + &[SHIELDED_ANCHORS_BY_HEIGHT_KEY], + ] +} + +/// Path to a token pool's anchors-by-height tree as a vec +pub fn token_shielded_pool_anchors_by_height_path_vec(token_id: [u8; 32]) -> Vec> { + vec![ + vec![RootTree::Tokens as u8], + vec![TOKEN_SHIELDED_POOLS_KEY], + token_id.to_vec(), + vec![SHIELDED_ANCHORS_BY_HEIGHT_KEY], + ] +} + +/// The token-pool twin of [`shielded_latest_recorded_anchor_path_query`]: a `limit 1` reverse +/// scan over the pool's anchors-by-height tree. Shared by the anchor recorder, the proven RPC +/// handler and the SDK-side verifier for the same byte-for-byte reason. +pub fn token_shielded_pool_latest_recorded_anchor_path_query(token_id: [u8; 32]) -> PathQuery { + let mut query = Query::new(); + query.insert_all(); + query.left_to_right = false; + PathQuery { + path: token_shielded_pool_anchors_by_height_path_vec(token_id), + query: SizedQuery { + query, + limit: Some(1), + offset: None, + }, + } +} + +/// The `PathQuery` proving the spent status of `nullifiers` in a token pool: one key per +/// nullifier under the pool's nullifiers tree, limited to the number asked. Shared by the +/// state transition proof (`TokenShieldedTransfer`) and `Drive::verify_token_shielded_pool_nullifiers` +/// so prover and verifier agree byte-for-byte. +pub fn token_shielded_pool_nullifiers_path_query( + token_id: [u8; 32], + nullifiers: &[[u8; 32]], +) -> PathQuery { + let mut query = Query::new(); + query.insert_keys( + nullifiers + .iter() + .map(|nullifier| nullifier.to_vec()) + .collect(), + ); + PathQuery { + path: token_shielded_pool_nullifiers_path_vec(token_id), + query: SizedQuery { + query, + limit: Some(nullifiers.len().min(u16::MAX as usize) as u16), + offset: None, + }, + } +} + /// Resolves the nullifiers path based on pool type. /// /// Pool types: /// - 0: Main credit shielded pool → `[ShieldedBalances, "M", [64]]` -/// - 1: Main token shielded pool (not yet implemented) -/// - 2: Individual token shielded pool (not yet implemented, requires pool_identifier) +/// - 1: Main token shielded pool (not supported: the Orchard note carries no asset identifier, +/// so tokens cannot share one pool) +/// - 2: Individual token shielded pool → `[Tokens, 224, token_id, [64]]`; `pool_identifier` is +/// the 32-byte token id pub fn nullifiers_path_for_pool( pool_type: u32, - _pool_identifier: Option<&[u8]>, + pool_identifier: Option<&[u8]>, ) -> Result>, crate::error::Error> { use crate::error::drive::DriveError; use crate::error::Error; match pool_type { 0 => Ok(shielded_credit_pool_nullifiers_path_vec()), - 1 | 2 => Err(Error::Drive(DriveError::NotSupported( - "Token shielded pools not yet implemented", + 1 => Err(Error::Drive(DriveError::NotSupported( + "a shared token shielded pool is not supported: each token has its own pool", ))), + 2 => { + let token_id: [u8; 32] = pool_identifier + .ok_or(Error::Drive(DriveError::InvalidInput( + "an individual token shielded pool requires the token id as pool identifier" + .to_string(), + )))? + .try_into() + .map_err(|_| { + Error::Drive(DriveError::InvalidInput( + "token shielded pool identifier must be a 32-byte token id".to_string(), + )) + })?; + Ok(token_shielded_pool_nullifiers_path_vec(token_id)) + } _ => Err(Error::Drive(DriveError::InvalidInput(format!( "Unknown pool type: {}", pool_type @@ -229,15 +394,77 @@ mod tests { } #[test] - fn pool_type_1_and_2_return_not_supported() { - // Pool types 1 and 2 hit the NotSupported error branch. + fn pool_type_1_returns_not_supported() { + // A shared token pool does not exist: the Orchard note has no asset identifier. let err1 = nullifiers_path_for_pool(1, None).expect_err("pool type 1 should return NotSupported"); assert!(matches!(err1, Error::Drive(DriveError::NotSupported(_)))); + } + + #[test] + fn pool_type_2_resolves_the_token_pool_nullifiers_path() { + let token_id = [0xFFu8; 32]; + let path = nullifiers_path_for_pool(2, Some(&token_id)).expect("token pool path"); + assert_eq!(path, token_shielded_pool_nullifiers_path_vec(token_id)); + assert_eq!( + path, + vec![ + vec![RootTree::Tokens as u8], + vec![TOKEN_SHIELDED_POOLS_KEY], + token_id.to_vec(), + vec![SHIELDED_NULLIFIERS_KEY], + ] + ); + // The identifier is mandatory and must be a token id. + assert!(matches!( + nullifiers_path_for_pool(2, None), + Err(Error::Drive(DriveError::InvalidInput(_))) + )); + assert!(matches!( + nullifiers_path_for_pool(2, Some(&[1u8; 20])), + Err(Error::Drive(DriveError::InvalidInput(_))) + )); + } - let err2 = nullifiers_path_for_pool(2, Some(&[0xFFu8; 32])) - .expect_err("pool type 2 should return NotSupported"); - assert!(matches!(err2, Error::Drive(DriveError::NotSupported(_)))); + #[test] + fn token_pool_paths_share_the_credit_pool_child_keys() { + // The token pool is the credit pool layout re-rooted under the token: every child key + // (notes, nullifiers, anchors, anchors-by-height) is identical, so one implementation + // serves both. + let token_id = [7u8; 32]; + let pool = token_shielded_pool_path_vec(token_id); + assert_eq!(pool.len(), 3); + for (path, key) in [ + ( + token_shielded_pool_notes_path_vec(token_id), + SHIELDED_NOTES_KEY, + ), + ( + token_shielded_pool_nullifiers_path_vec(token_id), + SHIELDED_NULLIFIERS_KEY, + ), + ( + token_shielded_pool_anchors_path_vec(token_id), + SHIELDED_ANCHORS_IN_POOL_KEY, + ), + ( + token_shielded_pool_anchors_by_height_path_vec(token_id), + SHIELDED_ANCHORS_BY_HEIGHT_KEY, + ), + ] { + assert_eq!(&path[..3], &pool[..]); + assert_eq!(path[3], vec![key]); + } + // Static and vec forms agree. + let arr = token_shielded_pool_notes_path(&token_id); + let v = token_shielded_pool_notes_path_vec(token_id); + for (a, b) in arr.iter().zip(v.iter()) { + assert_eq!(*a, b.as_slice()); + } + assert_eq!( + token_shielded_pool_latest_recorded_anchor_path_query(token_id).path, + token_shielded_pool_anchors_by_height_path_vec(token_id) + ); } #[test] diff --git a/packages/rs-drive/src/drive/shielded/prune_anchors/mod.rs b/packages/rs-drive/src/drive/shielded/prune_anchors/mod.rs index 8f43416d190..7d533a9234a 100644 --- a/packages/rs-drive/src/drive/shielded/prune_anchors/mod.rs +++ b/packages/rs-drive/src/drive/shielded/prune_anchors/mod.rs @@ -1,5 +1,9 @@ mod v0; +use crate::drive::shielded::paths::{ + token_shielded_pool_anchors_by_height_path, token_shielded_pool_anchors_by_height_path_vec, + token_shielded_pool_anchors_path, +}; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; @@ -37,3 +41,35 @@ impl Drive { } } } + +impl Drive { + /// Prunes a TOKEN shielded pool's anchors older than `cutoff_height`, always keeping the + /// most recent one. Same versioning as [`Drive::prune_shielded_pool_anchors`]. + pub fn prune_token_shielded_pool_anchors( + &self, + token_id: [u8; 32], + cutoff_height: u64, + transaction: &Transaction, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + match platform_version.drive.methods.shielded.prune_anchors { + 0 => { + let anchors_path = token_shielded_pool_anchors_path(&token_id); + let by_height_path = token_shielded_pool_anchors_by_height_path(&token_id); + self.prune_pool_anchors_v0( + &anchors_path, + &by_height_path, + token_shielded_pool_anchors_by_height_path_vec(token_id), + cutoff_height, + transaction, + platform_version, + ) + } + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "prune_token_shielded_pool_anchors".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/shielded/prune_anchors/v0/mod.rs b/packages/rs-drive/src/drive/shielded/prune_anchors/v0/mod.rs index 65c4013a208..7f269af1b4f 100644 --- a/packages/rs-drive/src/drive/shielded/prune_anchors/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/prune_anchors/v0/mod.rs @@ -35,9 +35,31 @@ impl Drive { transaction: &Transaction, platform_version: &PlatformVersion, ) -> Result<(), Error> { - let grove_version = &platform_version.drive.grove_version; + let anchors_path = shielded_credit_pool_anchors_path(); let by_height_path = shielded_credit_pool_anchors_by_height_path(); - let by_height_path_vec = shielded_credit_pool_anchors_by_height_path_vec(); + self.prune_pool_anchors_v0( + &anchors_path, + &by_height_path, + shielded_credit_pool_anchors_by_height_path_vec(), + cutoff_height, + transaction, + platform_version, + ) + } + + /// Prunes the anchors of the pool whose anchors tree and anchors-by-height tree are at the + /// given paths, whichever shielded pool (credit or token) it is. Same "always keep the + /// highest entry" rule as the credit pool. + pub(in crate::drive) fn prune_pool_anchors_v0( + &self, + anchors_path: &[&[u8]], + by_height_path: &[&[u8]], + by_height_path_vec: Vec>, + cutoff_height: u64, + transaction: &Transaction, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let grove_version = &platform_version.drive.grove_version; // 1. Query for entries strictly below cutoff (`RangeTo` is // exclusive). Anything in this set is a candidate for @@ -114,7 +136,6 @@ impl Drive { // 3. Delete from both trees. Order doesn't matter for // correctness — both writes occur atomically as part of // the block transaction. - let anchors_path = shielded_credit_pool_anchors_path(); for (height_key, element) in to_delete { if let Element::Item(anchor_bytes, _) = element { // NOTE: `.unwrap()` is `CostContext::unwrap()`, NOT @@ -122,7 +143,7 @@ impl Drive { // never panics — standard pattern across Drive. self.grove .delete( - &anchors_path, + anchors_path, &anchor_bytes, None, Some(transaction), @@ -133,7 +154,7 @@ impl Drive { } self.grove .delete( - &by_height_path, + by_height_path, &height_key, None, Some(transaction), diff --git a/packages/rs-drive/src/drive/shielded/read_total_balance/mod.rs b/packages/rs-drive/src/drive/shielded/read_total_balance/mod.rs index 915baea2f2e..3a83d43681e 100644 --- a/packages/rs-drive/src/drive/shielded/read_total_balance/mod.rs +++ b/packages/rs-drive/src/drive/shielded/read_total_balance/mod.rs @@ -1,9 +1,11 @@ mod v0; +use crate::drive::shielded::paths::token_shielded_pool_path; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; +use dpp::balances::credits::TokenAmount; use dpp::fee::Credits; use dpp::version::PlatformVersion; use grovedb::TransactionArg; @@ -40,3 +42,33 @@ impl Drive { } } } + +impl Drive { + /// Reads a TOKEN shielded pool's total balance (the amount of the token currently + /// shielded). Returns 0 if the pool has no balance item yet. Same versioning as + /// [`Drive::read_shielded_pool_total_balance`]. + pub fn read_token_shielded_pool_total_balance( + &self, + token_id: &[u8; 32], + transaction: TransactionArg, + drive_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version.drive.methods.shielded.read_total_balance { + 0 => { + let pool_path = token_shielded_pool_path(token_id); + self.read_pool_total_balance_v0( + &pool_path, + transaction, + drive_operations, + platform_version, + ) + } + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "read_token_shielded_pool_total_balance".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/shielded/read_total_balance/v0/mod.rs b/packages/rs-drive/src/drive/shielded/read_total_balance/v0/mod.rs index 51506755068..899e4eb0372 100644 --- a/packages/rs-drive/src/drive/shielded/read_total_balance/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/read_total_balance/v0/mod.rs @@ -19,9 +19,21 @@ impl Drive { platform_version: &PlatformVersion, ) -> Result { let pool_path = shielded_credit_pool_path(); + self.read_pool_total_balance_v0(&pool_path, transaction, drive_operations, platform_version) + } + + /// Reads the total balance SumItem of the pool at `pool_path`, whichever shielded pool + /// (credit or token) it is. Returns 0 if the key does not exist yet. + pub(in crate::drive) fn read_pool_total_balance_v0( + &self, + pool_path: &[&[u8]], + transaction: TransactionArg, + drive_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result { Ok(self .grove_get_raw_value_u64_from_encoded_var_vec( - (&pool_path).into(), + pool_path.into(), &[SHIELDED_TOTAL_BALANCE_KEY], DirectQueryType::StatefulDirectQuery, transaction, diff --git a/packages/rs-drive/src/drive/shielded/record_anchor_if_changed/mod.rs b/packages/rs-drive/src/drive/shielded/record_anchor_if_changed/mod.rs index 77d5b803d5a..4e7293f8303 100644 --- a/packages/rs-drive/src/drive/shielded/record_anchor_if_changed/mod.rs +++ b/packages/rs-drive/src/drive/shielded/record_anchor_if_changed/mod.rs @@ -1,10 +1,14 @@ mod v0; +use crate::drive::shielded::paths::{ + token_shielded_pool_anchors_by_height_path, token_shielded_pool_anchors_path, + token_shielded_pool_latest_recorded_anchor_path_query, token_shielded_pool_path, +}; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use dpp::version::PlatformVersion; -use grovedb::Transaction; +use grovedb::{Transaction, TransactionArg}; impl Drive { /// Records the current shielded pool anchor if the commitment tree changed @@ -44,3 +48,69 @@ impl Drive { } } } + +impl Drive { + /// Records a TOKEN shielded pool's anchor if its commitment tree changed this block. Same + /// versioning as [`Drive::record_shielded_pool_anchor_if_changed`]. + pub fn record_token_shielded_pool_anchor_if_changed( + &self, + token_id: [u8; 32], + block_height: u64, + transaction: &Transaction, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + match platform_version + .drive + .methods + .shielded + .record_anchor_if_changed + { + 0 => { + let pool_path = token_shielded_pool_path(&token_id); + let anchors_path = token_shielded_pool_anchors_path(&token_id); + let anchors_by_height_path = token_shielded_pool_anchors_by_height_path(&token_id); + self.record_pool_anchor_if_changed_v0( + &pool_path, + &anchors_path, + &anchors_by_height_path, + &token_shielded_pool_latest_recorded_anchor_path_query(token_id), + block_height, + transaction, + platform_version, + ) + } + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "record_token_shielded_pool_anchor_if_changed".to_string(), + known_versions: vec![0], + received: version, + })), + } + } + + /// Reads the latest anchor recorded for a TOKEN shielded pool (`None` before the first + /// block-end record). Same versioning as [`Drive::record_shielded_pool_anchor_if_changed`]. + pub fn read_latest_recorded_token_shielded_pool_anchor( + &self, + token_id: [u8; 32], + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match platform_version + .drive + .methods + .shielded + .record_anchor_if_changed + { + 0 => self.read_latest_recorded_pool_anchor_v0( + &token_shielded_pool_latest_recorded_anchor_path_query(token_id), + transaction, + &platform_version.drive, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "read_latest_recorded_token_shielded_pool_anchor".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/shielded/record_anchor_if_changed/v0/mod.rs b/packages/rs-drive/src/drive/shielded/record_anchor_if_changed/v0/mod.rs index 8fd5d631020..900ffd20131 100644 --- a/packages/rs-drive/src/drive/shielded/record_anchor_if_changed/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/record_anchor_if_changed/v0/mod.rs @@ -7,7 +7,7 @@ use crate::error::drive::DriveError; use crate::error::Error; use dpp::version::PlatformVersion; use grovedb::query_result_type::QueryResultType; -use grovedb::{Element, Transaction}; +use grovedb::{Element, PathQuery, Transaction}; impl Drive { /// Version 0 implementation of recording the shielded pool anchor. @@ -30,10 +30,37 @@ impl Drive { block_height: u64, transaction: &Transaction, platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let pool_path = shielded_credit_pool_path(); + let anchors_path = shielded_credit_pool_anchors_path(); + let anchors_by_height_path = shielded_credit_pool_anchors_by_height_path(); + self.record_pool_anchor_if_changed_v0( + &pool_path, + &anchors_path, + &anchors_by_height_path, + &shielded_latest_recorded_anchor_path_query(), + block_height, + transaction, + platform_version, + ) + } + + /// Records the anchor of the pool at `pool_path` if its commitment tree changed, whichever + /// shielded pool (credit or token) it is. `latest_recorded_anchor_query` must be the pool's + /// own `limit 1` reverse scan over `anchors_by_height_path`. + #[allow(clippy::too_many_arguments)] + pub(in crate::drive) fn record_pool_anchor_if_changed_v0( + &self, + pool_path: &[&[u8]], + anchors_path: &[&[u8]], + anchors_by_height_path: &[&[u8]], + latest_recorded_anchor_query: &PathQuery, + block_height: u64, + transaction: &Transaction, + platform_version: &PlatformVersion, ) -> Result<(), Error> { let drive_version = &platform_version.drive; let grove_version = &drive_version.grove_version; - let pool_path = shielded_credit_pool_path(); // 1. Read current anchor from the CommitmentTree. // @@ -45,7 +72,7 @@ impl Drive { let current_anchor = self .grove .commitment_tree_anchor( - &pool_path, + pool_path, &[SHIELDED_NOTES_KEY], Some(transaction), grove_version, @@ -55,20 +82,21 @@ impl Drive { let current_anchor_bytes: [u8; 32] = current_anchor.to_bytes(); - // 2. Read the latest recorded anchor from `[8]` via a - // `limit 1` reverse query. This is the post-removal - // replacement for the old `most_recent_anchor` slot — same - // value, but derived from the canonical log so it cannot - // drift out of sync with the anchors tree under prune. + // 2. Read the latest recorded anchor via a `limit 1` reverse query over the + // anchors-by-height index, the canonical log that cannot drift out of sync + // with the anchors tree under prune. // // NOTE: there is intentionally no "skip when current is the // Sinsemilla empty root" guard. The empty root is a // well-defined value, recording it is harmless (it can't - // be spent against — no notes), and it ensures `[6]` is + // be spent against, no notes), and it ensures the index is // populated from the very first block-end event onward // rather than only after the first shield op. - let latest_recorded = - self.read_latest_recorded_shielded_anchor_v0(Some(transaction), drive_version)?; + let latest_recorded = self.read_latest_recorded_pool_anchor_v0( + latest_recorded_anchor_query, + Some(transaction), + drive_version, + )?; // 3. Only insert if the anchor actually changed. Orchard's // commitment tree only changes when a new note is @@ -80,10 +108,9 @@ impl Drive { // 4. Anchor changed — insert into both trees atomically with // the rest of the block transaction. - let anchors_path = shielded_credit_pool_anchors_path(); self.grove .insert( - &anchors_path, + anchors_path, ¤t_anchor_bytes, Element::new_item(block_height.to_be_bytes().to_vec()), None, @@ -93,10 +120,9 @@ impl Drive { .unwrap() .map_err(Error::from)?; - let anchors_by_height_path = shielded_credit_pool_anchors_by_height_path(); self.grove .insert( - &anchors_by_height_path, + anchors_by_height_path, &block_height.to_be_bytes(), Element::new_item(current_anchor_bytes.to_vec()), None, @@ -110,7 +136,7 @@ impl Drive { } /// Read the latest recorded shielded-pool anchor from - /// `SHIELDED_ANCHORS_BY_HEIGHT_KEY` (`[..., "s", [96]]`) via a + /// `SHIELDED_ANCHORS_BY_HEIGHT_KEY` (`[..., "M", [96]]`) via a /// `limit 1` reverse query. Returns `None` if the index is empty /// (pool has never recorded an anchor — chain is at genesis or /// no shielded ops yet). @@ -122,15 +148,29 @@ impl Drive { /// share the same `PathQuery` shape via /// `shielded_latest_recorded_anchor_path_query`, but operate on /// proofs rather than this raw helper. + #[cfg(test)] pub(in crate::drive) fn read_latest_recorded_shielded_anchor_v0( &self, transaction: grovedb::TransactionArg, drive_version: &dpp::version::drive_versions::DriveVersion, ) -> Result, Error> { - let path_query = shielded_latest_recorded_anchor_path_query(); + self.read_latest_recorded_pool_anchor_v0( + &shielded_latest_recorded_anchor_path_query(), + transaction, + drive_version, + ) + } + /// The pool-agnostic form of [`Drive::read_latest_recorded_shielded_anchor_v0`]: reads the + /// highest-height entry the given `limit 1` reverse scan returns. + pub(in crate::drive) fn read_latest_recorded_pool_anchor_v0( + &self, + latest_recorded_anchor_query: &PathQuery, + transaction: grovedb::TransactionArg, + drive_version: &dpp::version::drive_versions::DriveVersion, + ) -> Result, Error> { let (results, _) = self.grove_get_raw_path_query( - &path_query, + latest_recorded_anchor_query, transaction, QueryResultType::QueryKeyElementPairResultType, &mut vec![], diff --git a/packages/rs-drive/src/drive/shielded/update_total_balance/mod.rs b/packages/rs-drive/src/drive/shielded/update_total_balance/mod.rs index a6083ae4e96..b82569f47bc 100644 --- a/packages/rs-drive/src/drive/shielded/update_total_balance/mod.rs +++ b/packages/rs-drive/src/drive/shielded/update_total_balance/mod.rs @@ -1,5 +1,6 @@ mod v0; +use crate::drive::shielded::paths::token_shielded_pool_path_vec; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; @@ -27,3 +28,25 @@ impl Drive { } } } + +impl Drive { + /// Constructs the low-level drive operations to update a TOKEN shielded pool's total + /// balance. Same versioning as [`Drive::update_total_balance_op`]. + pub fn update_token_pool_total_balance_op( + token_id: [u8; 32], + new_total_balance: u64, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match platform_version.drive.methods.shielded.update_total_balance { + 0 => Self::update_total_balance_op_in_pool_v0( + token_shielded_pool_path_vec(token_id), + new_total_balance, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "update_token_pool_total_balance_op".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/shielded/update_total_balance/v0/mod.rs b/packages/rs-drive/src/drive/shielded/update_total_balance/v0/mod.rs index 278447e5991..6c18a57122a 100644 --- a/packages/rs-drive/src/drive/shielded/update_total_balance/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/update_total_balance/v0/mod.rs @@ -15,7 +15,15 @@ impl Drive { pub(in crate::drive) fn update_total_balance_op_v0( new_total_balance: u64, ) -> Result, Error> { - let pool_path = shielded_credit_pool_path_vec(); + Self::update_total_balance_op_in_pool_v0(shielded_credit_pool_path_vec(), new_total_balance) + } + + /// Builds the total balance update for the pool at `pool_path`, whichever shielded pool + /// (credit or token) it is. + pub(in crate::drive) fn update_total_balance_op_in_pool_v0( + pool_path: Vec>, + new_total_balance: u64, + ) -> Result, Error> { let balance_i64 = i64::try_from(new_total_balance).map_err(|_| { Error::Drive(DriveError::CorruptedDriveState( "shielded pool total balance exceeds i64::MAX".to_string(), diff --git a/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/mod.rs b/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/mod.rs index 8472cfb8aa2..7818b611a8d 100644 --- a/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/mod.rs +++ b/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/mod.rs @@ -1,4 +1,5 @@ mod v0; +mod v1; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -37,9 +38,10 @@ impl Drive { .calculate_total_tokens_balance { 0 => self.calculate_total_tokens_balance_v0(transaction, platform_version), + 1 => self.calculate_total_tokens_balance_v1(transaction, platform_version), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "calculate_total_tokens_balance".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v0/mod.rs b/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v0/mod.rs index 6d4acc61691..6e723e81c4e 100644 --- a/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v0/mod.rs @@ -42,6 +42,8 @@ impl Drive { Ok(TotalTokensBalance { total_tokens_in_platform, total_identity_token_balances, + // Token shielded pools do not exist before protocol version 14. + total_token_shielded_pool_balances: 0, }) } } diff --git a/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v1/mod.rs b/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v1/mod.rs new file mode 100644 index 00000000000..ddd56681587 --- /dev/null +++ b/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v1/mod.rs @@ -0,0 +1,58 @@ +use crate::drive::balances::TOTAL_TOKEN_SUPPLIES_STORAGE_KEY; +use crate::drive::system::misc_path; +use crate::drive::tokens::paths::{tokens_root_path, TOKEN_BALANCES_KEY, TOKEN_SHIELDED_POOLS_KEY}; +use crate::drive::Drive; +use crate::error::Error; +use crate::util::grove_operations::DirectQueryType; +use dpp::balances::total_tokens_balance::TotalTokensBalance; +use grovedb::TransactionArg; +use platform_version::version::PlatformVersion; + +impl Drive { + /// Version 1 (protocol version 14): the token shielded pools BigSumTree joins the identity + /// balances on the balance side, so `identity balances + shielded pool balances` must equal + /// the total supplies. Shielding moves tokens between the two terms without touching supply. + #[inline(always)] + pub(super) fn calculate_total_tokens_balance_v1( + &self, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let mut drive_operations = vec![]; + let path_holding_total_credits = misc_path(); + let total_tokens_in_platform = self.grove_get_big_sum_tree_total_value( + (&path_holding_total_credits).into(), + TOTAL_TOKEN_SUPPLIES_STORAGE_KEY, + DirectQueryType::StatefulDirectQuery, + transaction, + &mut drive_operations, + &platform_version.drive, + )?; + + let tokens_root_path = tokens_root_path(); + + let total_identity_token_balances = self.grove_get_big_sum_tree_total_value( + (&tokens_root_path).into(), + &[TOKEN_BALANCES_KEY], + DirectQueryType::StatefulDirectQuery, + transaction, + &mut drive_operations, + &platform_version.drive, + )?; + + let total_token_shielded_pool_balances = self.grove_get_big_sum_tree_total_value( + (&tokens_root_path).into(), + &[TOKEN_SHIELDED_POOLS_KEY], + DirectQueryType::StatefulDirectQuery, + transaction, + &mut drive_operations, + &platform_version.drive, + )?; + + Ok(TotalTokensBalance { + total_tokens_in_platform, + total_identity_token_balances, + total_token_shielded_pool_balances, + }) + } +} diff --git a/packages/rs-drive/src/drive/tokens/mod.rs b/packages/rs-drive/src/drive/tokens/mod.rs index eda24268ae9..72e9be5897b 100644 --- a/packages/rs-drive/src/drive/tokens/mod.rs +++ b/packages/rs-drive/src/drive/tokens/mod.rs @@ -34,6 +34,11 @@ pub mod mint; #[cfg(feature = "server")] pub mod mint_many; +/// Per-token shielded pools: creation, cost estimation and the shield / unshield / +/// shielded-transfer operations. +#[cfg(feature = "server")] +pub mod shielded_pool; + /// Manages system-level operations and utilities. #[cfg(feature = "server")] pub mod system; diff --git a/packages/rs-drive/src/drive/tokens/paths.rs b/packages/rs-drive/src/drive/tokens/paths.rs index 920a1ccd004..69fc5945300 100644 --- a/packages/rs-drive/src/drive/tokens/paths.rs +++ b/packages/rs-drive/src/drive/tokens/paths.rs @@ -22,6 +22,14 @@ pub const TOKEN_DIRECT_SELL_PRICE_KEY: u8 = 92; /// Key for token distributions sub level pub const TOKEN_DISTRIBUTIONS_KEY: u8 = 32; +/// Key for the token shielded pools tree (a BigSumTree of per-token pool SumTrees). +/// +/// Introduced in protocol version 14. Each token whose configuration has `has_shielded_pool` +/// owns one child SumTree keyed by its token id, laid out exactly like the credit shielded pool +/// (`crate::drive::shielded::paths`): notes, nullifiers, anchors, anchors-by-height and a total +/// balance SumItem. The BigSumTree total is the amount of every token currently shielded and +/// joins the identity balances on the balance side of the token conservation check. +pub const TOKEN_SHIELDED_POOLS_KEY: u8 = 224; // The Token Merk tree looks like // TOKEN_BALANCES_KEY @@ -674,6 +682,20 @@ pub fn token_once_per_identity_distributions_path_vec(token_id: [u8; 32]) -> Vec ] } +/// The root path of the token shielded pools tree: `[Tokens, 224]`, a BigSumTree whose children +/// are the per-token pool SumTrees. +pub fn token_shielded_pools_root_path() -> [&'static [u8]; 2] { + [ + Into::<&[u8; 1]>::into(RootTree::Tokens), + &[TOKEN_SHIELDED_POOLS_KEY], + ] +} + +/// The root path of the token shielded pools tree as a vec +pub fn token_shielded_pools_root_path_vec() -> Vec> { + vec![vec![RootTree::Tokens as u8], vec![TOKEN_SHIELDED_POOLS_KEY]] +} + #[cfg(feature = "server")] #[cfg(test)] mod tests { diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/create_pool_trees/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/create_pool_trees/mod.rs new file mode 100644 index 00000000000..3f5b7cf990e --- /dev/null +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/create_pool_trees/mod.rs @@ -0,0 +1,50 @@ +mod v0; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::{EstimatedLayerInformation, TransactionArg}; +use std::collections::HashMap; + +impl Drive { + /// Gathers the operations that create a token's shielded pool subtree: the pool SumTree + /// under the token shielded pools root and its five children (notes, nullifiers, anchors, + /// anchors-by-height, total balance). + /// + /// With `allow_already_exists` the call is idempotent (a contract update re-registering the + /// token); without it, an existing pool is a `CorruptedDriveState` error. + pub fn create_token_shielded_pool_trees_operations( + &self, + token_id: [u8; 32], + allow_already_exists: bool, + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match platform_version + .drive + .methods + .token + .update + .create_token_shielded_pool_trees + { + 0 => self.create_token_shielded_pool_trees_operations_v0( + token_id, + allow_already_exists, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "create_token_shielded_pool_trees_operations".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/create_pool_trees/v0/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/create_pool_trees/v0/mod.rs new file mode 100644 index 00000000000..8a3fd5a42ed --- /dev/null +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/create_pool_trees/v0/mod.rs @@ -0,0 +1,114 @@ +use crate::drive::shielded::paths::{ + token_shielded_pool_path_vec, SHIELDED_ANCHORS_BY_HEIGHT_KEY, SHIELDED_ANCHORS_IN_POOL_KEY, + SHIELDED_NOTES_CHUNK_POWER, SHIELDED_NOTES_KEY, SHIELDED_NULLIFIERS_KEY, + SHIELDED_TOTAL_BALANCE_KEY, +}; +use crate::drive::tokens::paths::token_shielded_pools_root_path; +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::util::grove_operations::BatchInsertTreeApplyType; +use crate::util::object_size_info::PathKeyInfo::PathFixedSizeKeyRef; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::{Element, EstimatedLayerInformation, TransactionArg, TreeType}; +use std::collections::HashMap; + +impl Drive { + /// Version 0: the pool SumTree is inserted if it does not exist, and the five children are + /// inserted only when the pool was created here (an existing pool already has them). + /// + /// The children mirror `Drive::insert_shielded_pool_structure` for the credit pool. They + /// are batch operations, so the parent Merk shape is the batch's sorted-key shape rather + /// than the credit pool's hand-ordered one; every node builds it the same way, which is all + /// consensus needs. + pub(super) fn create_token_shielded_pool_trees_operations_v0( + &self, + token_id: [u8; 32], + allow_already_exists: bool, + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let mut batch_operations: Vec = vec![]; + + let pool_apply_type = match estimated_costs_only_with_layer_info { + None => BatchInsertTreeApplyType::StatefulBatchInsertTree, + Some(estimated_costs_only_with_layer_info) => { + Self::add_estimation_costs_for_token_shielded_pool_operations( + token_id, + estimated_costs_only_with_layer_info, + ); + BatchInsertTreeApplyType::StatelessBatchInsertTree { + in_tree_type: TreeType::BigSumTree, + tree_type: TreeType::SumTree, + flags_len: 0, + } + } + }; + + // The pool itself: [Tokens, 224, token_id], a SumTree so its balance item propagates + // into the pools BigSumTree. + let inserted = self.batch_insert_empty_tree_if_not_exists( + PathFixedSizeKeyRef::<2>((token_shielded_pools_root_path(), token_id.as_slice())), + TreeType::SumTree, + None, + pool_apply_type, + transaction, + &mut None, + &mut batch_operations, + &platform_version.drive, + )?; + + if !inserted { + if allow_already_exists { + return Ok(batch_operations); + } + return Err(Error::Drive(DriveError::CorruptedDriveState( + "token shielded pool already exists".to_string(), + ))); + } + + let pool_path = token_shielded_pool_path_vec(token_id); + + // Notes: the Orchard note commitment tree (Sinsemilla frontier + bulk-append items). + batch_operations.push(LowLevelDriveOperation::insert_for_known_path_key_element( + pool_path.clone(), + vec![SHIELDED_NOTES_KEY], + Element::empty_commitment_tree(SHIELDED_NOTES_CHUNK_POWER)?, + )); + + // Nullifiers: spent-note set, provable count for the notes floor. + batch_operations.push(LowLevelDriveOperation::insert_for_known_path_key_element( + pool_path.clone(), + vec![SHIELDED_NULLIFIERS_KEY], + Element::empty_provable_count_tree(), + )); + + // Anchors: anchor_bytes -> block_height_be. + batch_operations.push(LowLevelDriveOperation::insert_for_known_path_key_element( + pool_path.clone(), + vec![SHIELDED_ANCHORS_IN_POOL_KEY], + Element::empty_tree(), + )); + + // Anchors by height: block_height_be -> anchor_bytes. + batch_operations.push(LowLevelDriveOperation::insert_for_known_path_key_element( + pool_path.clone(), + vec![SHIELDED_ANCHORS_BY_HEIGHT_KEY], + Element::empty_tree(), + )); + + // Total balance: the amount of the token currently in the pool. + batch_operations.push(LowLevelDriveOperation::insert_for_known_path_key_element( + pool_path, + vec![SHIELDED_TOTAL_BALANCE_KEY], + Element::new_sum_item(0), + )); + + Ok(batch_operations) + } +} diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/estimated_costs.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/estimated_costs.rs new file mode 100644 index 00000000000..273fa31a1f6 --- /dev/null +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/estimated_costs.rs @@ -0,0 +1,145 @@ +use crate::drive::shielded::paths::{ + token_shielded_pool_anchors_by_height_path_vec, token_shielded_pool_anchors_path_vec, + token_shielded_pool_notes_path_vec, token_shielded_pool_nullifiers_path_vec, + token_shielded_pool_path_vec, SHIELDED_NOTES_CHUNK_POWER, +}; +use crate::drive::tokens::paths::{token_shielded_pools_root_path, tokens_root_path}; +use crate::drive::Drive; +use crate::util::type_constants::DEFAULT_HASH_SIZE_U8; +use grovedb::batch::KeyInfoPath; +use grovedb::EstimatedLayerCount::EstimatedLevel; +use grovedb::EstimatedLayerSizes::{AllItems, AllSubtrees, Mix}; +use grovedb::EstimatedSumTrees::{AllBigSumTrees, AllSumTrees, NoSumTrees, SomeSumTrees}; +use grovedb::{EstimatedLayerInformation, TreeType}; +use std::collections::HashMap; + +/// Average size of a note value: 32 cmx + 32 rho + 32 cv_net + 216 encrypted note = 312 bytes +/// (the same item the credit pool stores; see `crate::drive::shielded::estimated_costs`). +const AVERAGE_NOTE_VALUE_SIZE: u32 = 312; + +/// Size of a nullifier key (32 bytes) +const NULLIFIER_KEY_SIZE: u8 = 32; + +/// Size of an anchor key (32 bytes) +const ANCHOR_KEY_SIZE: u8 = 32; + +/// Size of an anchor value (u64 big-endian block height = 8 bytes) +const ANCHOR_VALUE_SIZE: u32 = 8; + +impl Drive { + /// Registers the layer information for every tree a token shielded pool write can touch, + /// from the root down to the pool's five children, so a batch touching the pool can be + /// priced without state. + pub(crate) fn add_estimation_costs_for_token_shielded_pool_operations( + token_id: [u8; 32], + estimated_costs_only_with_layer_info: &mut HashMap, + ) { + // Root level: [] — the Tokens tree sits on layer 2 like the balances estimation assumes. + estimated_costs_only_with_layer_info.insert( + KeyInfoPath::from_known_path([]), + EstimatedLayerInformation { + tree_type: TreeType::NormalTree, + estimated_layer_count: EstimatedLevel(2, false), + estimated_layer_sizes: AllSubtrees(1, NoSumTrees, None), + }, + ); + + // Tokens root: [16] + estimated_costs_only_with_layer_info.insert( + KeyInfoPath::from_known_path(tokens_root_path()), + EstimatedLayerInformation { + tree_type: TreeType::NormalTree, + estimated_layer_count: EstimatedLevel(0, false), + estimated_layer_sizes: AllSubtrees(1, AllBigSumTrees, None), + }, + ); + + // Token shielded pools root: [16, 224] — a BigSumTree of per-token pool SumTrees. + estimated_costs_only_with_layer_info.insert( + KeyInfoPath::from_known_path(token_shielded_pools_root_path()), + EstimatedLayerInformation { + tree_type: TreeType::BigSumTree, + estimated_layer_count: EstimatedLevel(10, false), + estimated_layer_sizes: AllSubtrees(DEFAULT_HASH_SIZE_U8, AllSumTrees, None), + }, + ); + + // The token's pool: [16, 224, token_id] — SumTree of 4 subtrees + 1 SumItem, like the + // credit pool (balanced Merk depth ceil(log2(5)) = 3). + estimated_costs_only_with_layer_info.insert( + KeyInfoPath::from_known_owned_path(token_shielded_pool_path_vec(token_id)), + EstimatedLayerInformation { + tree_type: TreeType::SumTree, + estimated_layer_count: EstimatedLevel(3, false), + estimated_layer_sizes: Mix { + subtrees_size: Some(( + 1, + SomeSumTrees { + sum_trees_weight: 0, + big_sum_trees_weight: 0, + count_trees_weight: 1, // nullifiers (ProvableCountTree) + count_sum_trees_weight: 0, + non_sum_trees_weight: 3, // notes, anchors, anchors-by-height + provable_sum_trees_weight: 0, + provable_count_trees_weight: 0, + provable_count_sum_trees_weight: 0, + provable_count_provable_sum_trees_weight: 0, + }, + None, + 4, + )), + items_size: Some((1, 8, None, 1)), // total balance SumItem + references_size: None, + items_with_sum_item_size: None, + references_with_sum_item_size: None, + }, + }, + ); + + // Notes tree: [16, 224, token_id, 128] + estimated_costs_only_with_layer_info.insert( + KeyInfoPath::from_known_owned_path(token_shielded_pool_notes_path_vec(token_id)), + EstimatedLayerInformation { + tree_type: TreeType::CommitmentTree(SHIELDED_NOTES_CHUNK_POWER), + estimated_layer_count: EstimatedLevel(16, false), + estimated_layer_sizes: AllItems(8, AVERAGE_NOTE_VALUE_SIZE, None), + }, + ); + + // Nullifiers tree: [16, 224, token_id, 64] + estimated_costs_only_with_layer_info.insert( + KeyInfoPath::from_known_owned_path(token_shielded_pool_nullifiers_path_vec(token_id)), + EstimatedLayerInformation { + tree_type: TreeType::ProvableCountTree, + estimated_layer_count: EstimatedLevel(16, false), + estimated_layer_sizes: AllItems(NULLIFIER_KEY_SIZE, 0, None), + }, + ); + + // Anchors tree: [16, 224, token_id, 192] + estimated_costs_only_with_layer_info.insert( + KeyInfoPath::from_known_owned_path(token_shielded_pool_anchors_path_vec(token_id)), + EstimatedLayerInformation { + tree_type: TreeType::NormalTree, + estimated_layer_count: EstimatedLevel(7, false), + estimated_layer_sizes: AllItems(ANCHOR_KEY_SIZE, ANCHOR_VALUE_SIZE, None), + }, + ); + + // Anchors-by-height tree: [16, 224, token_id, 96] + estimated_costs_only_with_layer_info.insert( + KeyInfoPath::from_known_owned_path(token_shielded_pool_anchors_by_height_path_vec( + token_id, + )), + EstimatedLayerInformation { + tree_type: TreeType::NormalTree, + estimated_layer_count: EstimatedLevel(7, false), + estimated_layer_sizes: AllItems( + ANCHOR_VALUE_SIZE as u8, + ANCHOR_KEY_SIZE as u32, + None, + ), + }, + ); + } +} diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/insert_root_tree.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/insert_root_tree.rs new file mode 100644 index 00000000000..0e50083c219 --- /dev/null +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/insert_root_tree.rs @@ -0,0 +1,32 @@ +use crate::drive::tokens::paths::{tokens_root_path, TOKEN_SHIELDED_POOLS_KEY}; +use crate::drive::Drive; +use crate::error::Error; +use dpp::version::PlatformVersion; +use grovedb::{Element, TransactionArg}; +use grovedb_path::SubtreePath; + +impl Drive { + /// Inserts the token shielded pools root tree `[Tokens, TOKEN_SHIELDED_POOLS_KEY]` (an empty + /// BigSumTree) if it does not exist yet. + /// + /// CONSENSUS-CRITICAL: both the genesis path (`Drive::create_initial_state_structure_v4`) + /// and the in-place upgrade path (`Platform::transition_to_version_14`) call this one helper + /// so a chain born at protocol version 14 and a chain upgraded to it build a byte-identical + /// `[Tokens]` subtree. The tree stays empty until a token with a shielded pool is registered. + pub fn insert_token_shielded_pools_root_tree( + &self, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let tokens_root = tokens_root_path(); + self.grove_insert_if_not_exists( + SubtreePath::from(&tokens_root), + &[TOKEN_SHIELDED_POOLS_KEY], + Element::empty_big_sum_tree(), + transaction, + None, + &platform_version.drive, + )?; + Ok(()) + } +} diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs new file mode 100644 index 00000000000..a74d4c4cb9b --- /dev/null +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs @@ -0,0 +1,638 @@ +//! Per-token shielded pools. +//! +//! A token whose configuration has `has_shielded_pool` owns an Orchard pool at +//! `[Tokens, TOKEN_SHIELDED_POOLS_KEY, token_id]`, laid out exactly like the credit shielded +//! pool: a note commitment tree, a nullifier set, the recorded anchors (by anchor and by height) +//! and a total balance SumItem. The pool SumTrees hang under one BigSumTree so the amount of every +//! token currently shielded is one sum, which the token conservation check adds to the identity +//! balances. +//! +//! The primitives (note / nullifier insertion, balance update, anchor bookkeeping, membership +//! reads) are the credit pool's, re-rooted under the token: see `crate::drive::shielded`. This +//! module holds what is token-specific: pool creation, cost estimation, and the three composite +//! operations (`shield`, `unshield`, `shielded_transfer`) the batch token transitions lower to. + +mod create_pool_trees; +mod estimated_costs; +mod insert_root_tree; +mod shield; +mod shielded_transfer; +mod unshield; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::state_transition_action::shielded::ShieldedActionNote; +use dpp::balances::credits::TokenAmount; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::{EstimatedLayerInformation, TransactionArg}; +use std::collections::HashMap; + +/// How a pool operation moves the pool's total balance. +#[derive(Debug, Clone, Copy)] +pub(in crate::drive::tokens) enum TokenPoolBalanceChange { + /// Tokens enter the pool (shield). + Add(TokenAmount), + /// Tokens leave the pool (unshield). + Remove(TokenAmount), + /// The balance is unchanged (shielded transfer). + Unchanged, +} + +impl Drive { + /// The operations every token pool write shares: nullifier insertion, note appends and the + /// total balance update, with the pool's cost estimation registered when estimating. + /// + /// The balance is read from state only when applying; an estimate assumes a fresh pool, the + /// worst case for the write. + #[allow(clippy::too_many_arguments)] + pub(in crate::drive::tokens) fn token_shielded_pool_update_operations( + &self, + token_id: [u8; 32], + balance_change: TokenPoolBalanceChange, + nullifiers: &[[u8; 32]], + notes: &[ShieldedActionNote], + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let mut drive_operations = vec![]; + + if let Some(estimated_costs_only_with_layer_info) = estimated_costs_only_with_layer_info { + Self::add_estimation_costs_for_token_shielded_pool_operations( + token_id, + estimated_costs_only_with_layer_info, + ); + } + + if !nullifiers.is_empty() { + drive_operations.extend(Self::insert_token_pool_nullifiers( + token_id, + nullifiers, + platform_version, + )?); + } + + for note in notes { + drive_operations.extend(Self::insert_token_pool_note_op( + token_id, + note.nullifier, + note.cmx, + note.cv_net, + note.encrypted_note.clone(), + platform_version, + )?); + } + + let apply = estimated_costs_only_with_layer_info.is_none(); + + let new_total_balance = match balance_change { + TokenPoolBalanceChange::Unchanged => None, + TokenPoolBalanceChange::Add(amount) => { + let current = if apply { + self.read_token_shielded_pool_total_balance( + &token_id, + transaction, + &mut drive_operations, + platform_version, + )? + } else { + 0 + }; + Some(current.checked_add(amount).ok_or_else(|| { + Error::Drive(DriveError::CorruptedDriveState( + "token shielded pool total balance overflow when shielding".to_string(), + )) + })?) + } + TokenPoolBalanceChange::Remove(amount) => { + let current = if apply { + self.read_token_shielded_pool_total_balance( + &token_id, + transaction, + &mut drive_operations, + platform_version, + )? + } else { + amount + }; + Some(current.checked_sub(amount).ok_or_else(|| { + Error::Drive(DriveError::CorruptedDriveState( + "token shielded pool total balance would go negative when unshielding" + .to_string(), + )) + })?) + } + }; + + if let Some(new_total_balance) = new_total_balance { + drive_operations.extend(Self::update_token_pool_total_balance_op( + token_id, + new_total_balance, + platform_version, + )?); + } + + Ok(drive_operations) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use dpp::block::block_info::BlockInfo; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::Identity; + use dpp::prelude::Identifier; + + const TOKEN_ID: [u8; 32] = [7u8; 32]; + + fn note(tag: u8) -> ShieldedActionNote { + ShieldedActionNote { + nullifier: [tag; 32], + cmx: [tag.wrapping_add(1); 32], + cv_net: [tag.wrapping_add(2); 32], + encrypted_note: vec![tag; 216], + } + } + + /// A drive with an identity holding `balance` of the token and an empty pool for it. + fn setup(balance: TokenAmount) -> (Drive, [u8; 32]) { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + let block_info = BlockInfo::default(); + + let identity = + Identity::random_identity(3, Some(1), platform_version).expect("random identity"); + drive + .add_new_identity( + identity.clone(), + false, + &block_info, + true, + None, + platform_version, + ) + .expect("insert identity"); + drive + .create_token_trees( + Identifier::from([5u8; 32]), + 0, + TOKEN_ID, + false, + false, + &block_info, + true, + None, + platform_version, + ) + .expect("create token trees"); + drive + .add_to_identity_token_balance( + TOKEN_ID, + identity.id().to_buffer(), + balance, + &block_info, + true, + None, + platform_version, + None, + ) + .expect("add token balance"); + + let operations = drive + .create_token_shielded_pool_trees_operations( + TOKEN_ID, + false, + &mut None, + None, + platform_version, + ) + .expect("pool tree operations"); + drive + .apply_batch_low_level_drive_operations( + None, + None, + operations, + &mut vec![], + &platform_version.drive, + ) + .expect("create pool trees"); + + (drive, identity.id().to_buffer()) + } + + fn apply(drive: &Drive, operations: Vec) { + drive + .apply_batch_low_level_drive_operations( + None, + None, + operations, + &mut vec![], + &PlatformVersion::latest().drive, + ) + .expect("apply operations"); + } + + fn pool_balance(drive: &Drive) -> TokenAmount { + drive + .read_token_shielded_pool_total_balance( + &TOKEN_ID, + None, + &mut vec![], + PlatformVersion::latest(), + ) + .expect("pool balance") + } + + fn notes_count(drive: &Drive) -> u64 { + drive + .token_shielded_pool_notes_count( + &TOKEN_ID, + None, + &mut vec![], + PlatformVersion::latest(), + ) + .expect("notes count") + } + + fn token_balance(drive: &Drive, identity_id: [u8; 32]) -> Option { + drive + .fetch_identity_token_balance(TOKEN_ID, identity_id, None, PlatformVersion::latest()) + .expect("token balance") + } + + fn nullifier_is_spent(drive: &Drive, nullifier: &[u8; 32]) -> bool { + drive + .has_token_pool_nullifier( + &TOKEN_ID, + nullifier, + None, + &mut vec![], + PlatformVersion::latest(), + ) + .expect("nullifier lookup") + } + + #[test] + fn should_create_an_empty_pool_once() { + let (drive, _) = setup(1_000); + let platform_version = PlatformVersion::latest(); + + assert_eq!(pool_balance(&drive), 0); + assert_eq!(notes_count(&drive), 0); + + // Creating the same pool again is only allowed when the caller opts into it. + let operations = drive + .create_token_shielded_pool_trees_operations( + TOKEN_ID, + true, + &mut None, + None, + platform_version, + ) + .expect("idempotent pool tree operations"); + apply(&drive, operations); + assert_eq!(pool_balance(&drive), 0); + + // Without the opt-in, an existing pool is a corrupted-state error at operation time. + assert!( + matches!( + drive.create_token_shielded_pool_trees_operations( + TOKEN_ID, + false, + &mut None, + None, + platform_version, + ), + Err(Error::Drive(DriveError::CorruptedDriveState(_))) + ), + "recreating an existing pool must fail" + ); + } + + #[test] + fn should_shield_from_the_identity_into_the_pool() { + let (drive, identity_id) = setup(1_000); + let platform_version = PlatformVersion::latest(); + let notes = [note(1), note(2)]; + + let operations = drive + .token_shield_operations( + TOKEN_ID, + identity_id, + 400, + ¬es, + &mut None, + None, + platform_version, + ) + .expect("shield operations"); + apply(&drive, operations); + + assert_eq!(token_balance(&drive, identity_id), Some(600)); + assert_eq!(pool_balance(&drive), 400); + assert_eq!(notes_count(&drive), 2); + // Outputs-only bundles never spend: their dummy nullifiers are not recorded. + assert!(!nullifier_is_spent(&drive, ¬es[0].nullifier)); + } + + #[test] + fn should_reject_shielding_more_than_the_identity_holds() { + let (drive, identity_id) = setup(100); + let platform_version = PlatformVersion::latest(); + + let result = drive.token_shield_operations( + TOKEN_ID, + identity_id, + 101, + &[note(1)], + &mut None, + None, + platform_version, + ); + assert!(result.is_err()); + assert_eq!(pool_balance(&drive), 0); + } + + #[test] + fn should_unshield_from_the_pool_to_the_recipient() { + let (drive, identity_id) = setup(1_000); + let platform_version = PlatformVersion::latest(); + let recipient_id = [9u8; 32]; + + let operations = drive + .token_shield_operations( + TOKEN_ID, + identity_id, + 400, + &[note(1), note(2)], + &mut None, + None, + platform_version, + ) + .expect("shield operations"); + apply(&drive, operations); + + let nullifiers = [[11u8; 32], [12u8; 32]]; + let change_notes = [note(3), note(4)]; + let operations = drive + .token_unshield_operations( + TOKEN_ID, + recipient_id, + 150, + &nullifiers, + &change_notes, + &mut None, + None, + platform_version, + ) + .expect("unshield operations"); + apply(&drive, operations); + + assert_eq!(token_balance(&drive, recipient_id), Some(150)); + assert_eq!(token_balance(&drive, identity_id), Some(600)); + assert_eq!(pool_balance(&drive), 250); + assert_eq!(notes_count(&drive), 4); + assert!(nullifier_is_spent(&drive, &nullifiers[0])); + assert!(nullifier_is_spent(&drive, &nullifiers[1])); + assert!(!nullifier_is_spent(&drive, &[13u8; 32])); + } + + #[test] + fn should_reject_unshielding_more_than_the_pool_holds() { + let (drive, identity_id) = setup(1_000); + let platform_version = PlatformVersion::latest(); + + let operations = drive + .token_shield_operations( + TOKEN_ID, + identity_id, + 100, + &[note(1)], + &mut None, + None, + platform_version, + ) + .expect("shield operations"); + apply(&drive, operations); + + let result = drive.token_unshield_operations( + TOKEN_ID, + [9u8; 32], + 101, + &[[11u8; 32]], + &[note(2)], + &mut None, + None, + platform_version, + ); + assert!(matches!( + result, + Err(Error::Drive(DriveError::CorruptedDriveState(_))) + )); + assert_eq!(pool_balance(&drive), 100); + } + + #[test] + fn should_keep_the_pool_balance_on_a_shielded_transfer() { + let (drive, identity_id) = setup(1_000); + let platform_version = PlatformVersion::latest(); + + let operations = drive + .token_shield_operations( + TOKEN_ID, + identity_id, + 400, + &[note(1), note(2)], + &mut None, + None, + platform_version, + ) + .expect("shield operations"); + apply(&drive, operations); + + let nullifiers = [[21u8; 32], [22u8; 32]]; + let operations = drive + .token_shielded_transfer_operations( + TOKEN_ID, + &nullifiers, + &[note(5), note(6)], + &mut None, + None, + platform_version, + ) + .expect("shielded transfer operations"); + apply(&drive, operations); + + assert_eq!(pool_balance(&drive), 400); + assert_eq!(notes_count(&drive), 4); + assert_eq!(token_balance(&drive, identity_id), Some(600)); + assert!(nullifier_is_spent(&drive, &nullifiers[0])); + assert!(nullifier_is_spent(&drive, &nullifiers[1])); + } + + #[test] + fn should_estimate_without_touching_state() { + let (drive, identity_id) = setup(1_000); + let platform_version = PlatformVersion::latest(); + + let mut layer_info = Some(HashMap::new()); + let operations = drive + .token_unshield_operations( + TOKEN_ID, + [9u8; 32], + 5_000, + &[[11u8; 32]], + &[note(2)], + &mut layer_info, + None, + platform_version, + ) + .expect("estimated unshield operations"); + assert!(!operations.is_empty()); + assert!( + !layer_info.expect("layer info").is_empty(), + "estimation must register the pool layers" + ); + assert_eq!(pool_balance(&drive), 0); + assert_eq!(token_balance(&drive, identity_id), Some(1_000)); + } + + #[test] + fn should_record_and_prune_pool_anchors() { + let (drive, identity_id) = setup(1_000); + let platform_version = PlatformVersion::latest(); + + let operations = drive + .token_shield_operations( + TOKEN_ID, + identity_id, + 100, + &[note(1), note(2)], + &mut None, + None, + platform_version, + ) + .expect("shield operations"); + apply(&drive, operations); + + let transaction = drive.grove.start_transaction(); + drive + .record_token_shielded_pool_anchor_if_changed( + TOKEN_ID, + 1, + &transaction, + platform_version, + ) + .expect("record anchor"); + drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("commit"); + + let first_anchor = drive + .read_latest_recorded_token_shielded_pool_anchor(TOKEN_ID, None, platform_version) + .expect("read anchor") + .expect("an anchor is recorded after the first write"); + assert!(drive + .has_token_pool_anchor( + &TOKEN_ID, + &first_anchor, + None, + &mut vec![], + platform_version + ) + .expect("anchor lookup")); + + // Another write changes the tree and therefore the anchor. + let operations = drive + .token_shielded_transfer_operations( + TOKEN_ID, + &[[21u8; 32]], + &[note(5)], + &mut None, + None, + platform_version, + ) + .expect("shielded transfer operations"); + apply(&drive, operations); + + let transaction = drive.grove.start_transaction(); + drive + .record_token_shielded_pool_anchor_if_changed( + TOKEN_ID, + 2, + &transaction, + platform_version, + ) + .expect("record anchor"); + drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("commit"); + + let second_anchor = drive + .read_latest_recorded_token_shielded_pool_anchor(TOKEN_ID, None, platform_version) + .expect("read anchor") + .expect("an anchor is recorded"); + assert_ne!(first_anchor, second_anchor); + + // Pruning below height 2 drops the first anchor and keeps the newest one. + let transaction = drive.grove.start_transaction(); + drive + .prune_token_shielded_pool_anchors(TOKEN_ID, 2, &transaction, platform_version) + .expect("prune anchors"); + drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("commit"); + + assert!(!drive + .has_token_pool_anchor( + &TOKEN_ID, + &first_anchor, + None, + &mut vec![], + platform_version + ) + .expect("anchor lookup")); + assert!(drive + .has_token_pool_anchor( + &TOKEN_ID, + &second_anchor, + None, + &mut vec![], + platform_version + ) + .expect("anchor lookup")); + + // The newest anchor survives even when the cutoff is past its height. + let transaction = drive.grove.start_transaction(); + drive + .prune_token_shielded_pool_anchors(TOKEN_ID, 100, &transaction, platform_version) + .expect("prune anchors"); + drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("commit"); + assert!(drive + .has_token_pool_anchor( + &TOKEN_ID, + &second_anchor, + None, + &mut vec![], + platform_version + ) + .expect("anchor lookup")); + } +} diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/shield/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/shield/mod.rs new file mode 100644 index 00000000000..0c8b08acf43 --- /dev/null +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/shield/mod.rs @@ -0,0 +1,46 @@ +mod v0; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::state_transition_action::shielded::ShieldedActionNote; +use dpp::balances::credits::TokenAmount; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::{EstimatedLayerInformation, TransactionArg}; +use std::collections::HashMap; + +impl Drive { + /// Moves `amount` of the token from `from_identity_id`'s balance into the token's shielded pool, appending the bundle's output notes. + #[allow(clippy::too_many_arguments)] + pub fn token_shield_operations( + &self, + token_id: [u8; 32], + from_identity_id: [u8; 32], + amount: TokenAmount, + notes: &[ShieldedActionNote], + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match platform_version.drive.methods.token.update.shield { + 0 => self.token_shield_operations_v0( + token_id, + from_identity_id, + amount, + notes, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "token_shield_operations".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/shield/v0/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/shield/v0/mod.rs new file mode 100644 index 00000000000..5e77312f960 --- /dev/null +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/shield/v0/mod.rs @@ -0,0 +1,50 @@ +use crate::drive::tokens::shielded_pool::TokenPoolBalanceChange; +use crate::drive::Drive; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::state_transition_action::shielded::ShieldedActionNote; +use dpp::balances::credits::TokenAmount; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::{EstimatedLayerInformation, TransactionArg}; +use std::collections::HashMap; + +impl Drive { + /// Version 0: debit the identity, then append the notes and credit the pool balance. + /// The token's total supply is unchanged: the pool balance is a conservation-side term. + #[allow(clippy::too_many_arguments)] + pub(super) fn token_shield_operations_v0( + &self, + token_id: [u8; 32], + from_identity_id: [u8; 32], + amount: TokenAmount, + notes: &[ShieldedActionNote], + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let mut drive_operations = self.remove_from_identity_token_balance_operations( + token_id, + from_identity_id, + amount, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?; + + // An outputs-only bundle spends nothing: its dummy nullifiers are never recorded. + drive_operations.extend(self.token_shielded_pool_update_operations( + token_id, + TokenPoolBalanceChange::Add(amount), + &[], + notes, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?); + + Ok(drive_operations) + } +} diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/shielded_transfer/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/shielded_transfer/mod.rs new file mode 100644 index 00000000000..fa2be6e31b1 --- /dev/null +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/shielded_transfer/mod.rs @@ -0,0 +1,49 @@ +mod v0; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::state_transition_action::shielded::ShieldedActionNote; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::{EstimatedLayerInformation, TransactionArg}; +use std::collections::HashMap; + +impl Drive { + /// Applies a transfer inside the token's shielded pool: the spent nullifiers are recorded and the bundle's output notes appended; the pool balance is unchanged. + #[allow(clippy::too_many_arguments)] + pub fn token_shielded_transfer_operations( + &self, + token_id: [u8; 32], + nullifiers: &[[u8; 32]], + notes: &[ShieldedActionNote], + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match platform_version + .drive + .methods + .token + .update + .shielded_transfer + { + 0 => self.token_shielded_transfer_operations_v0( + token_id, + nullifiers, + notes, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "token_shielded_transfer_operations".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/shielded_transfer/v0/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/shielded_transfer/v0/mod.rs new file mode 100644 index 00000000000..313540b7c28 --- /dev/null +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/shielded_transfer/v0/mod.rs @@ -0,0 +1,35 @@ +use crate::drive::tokens::shielded_pool::TokenPoolBalanceChange; +use crate::drive::Drive; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::state_transition_action::shielded::ShieldedActionNote; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::{EstimatedLayerInformation, TransactionArg}; +use std::collections::HashMap; + +impl Drive { + /// Version 0: record the spent nullifiers and append the output notes; nothing leaves or + /// enters the pool, so the balance is untouched. + pub(super) fn token_shielded_transfer_operations_v0( + &self, + token_id: [u8; 32], + nullifiers: &[[u8; 32]], + notes: &[ShieldedActionNote], + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + self.token_shielded_pool_update_operations( + token_id, + TokenPoolBalanceChange::Unchanged, + nullifiers, + notes, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ) + } +} diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/unshield/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/unshield/mod.rs new file mode 100644 index 00000000000..3f807730c00 --- /dev/null +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/unshield/mod.rs @@ -0,0 +1,48 @@ +mod v0; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::state_transition_action::shielded::ShieldedActionNote; +use dpp::balances::credits::TokenAmount; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::{EstimatedLayerInformation, TransactionArg}; +use std::collections::HashMap; + +impl Drive { + /// Moves `amount` of the token out of the shielded pool into `recipient_id`'s balance, recording the spent nullifiers and appending the bundle's output (change) notes. + #[allow(clippy::too_many_arguments)] + pub fn token_unshield_operations( + &self, + token_id: [u8; 32], + recipient_id: [u8; 32], + amount: TokenAmount, + nullifiers: &[[u8; 32]], + notes: &[ShieldedActionNote], + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match platform_version.drive.methods.token.update.unshield { + 0 => self.token_unshield_operations_v0( + token_id, + recipient_id, + amount, + nullifiers, + notes, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "token_unshield_operations".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/unshield/v0/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/unshield/v0/mod.rs new file mode 100644 index 00000000000..867fad1a2ae --- /dev/null +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/unshield/v0/mod.rs @@ -0,0 +1,50 @@ +use crate::drive::tokens::shielded_pool::TokenPoolBalanceChange; +use crate::drive::Drive; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::state_transition_action::shielded::ShieldedActionNote; +use dpp::balances::credits::TokenAmount; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::{EstimatedLayerInformation, TransactionArg}; +use std::collections::HashMap; + +impl Drive { + /// Version 0: record the spent nullifiers, append the change notes, debit the pool balance, + /// then credit the recipient. The token's total supply is unchanged. + #[allow(clippy::too_many_arguments)] + pub(super) fn token_unshield_operations_v0( + &self, + token_id: [u8; 32], + recipient_id: [u8; 32], + amount: TokenAmount, + nullifiers: &[[u8; 32]], + notes: &[ShieldedActionNote], + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let mut drive_operations = self.token_shielded_pool_update_operations( + token_id, + TokenPoolBalanceChange::Remove(amount), + nullifiers, + notes, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?; + + drive_operations.extend(self.add_to_identity_token_balance_operations( + token_id, + recipient_id, + amount, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?); + + Ok(drive_operations) + } +} diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/mod.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/mod.rs index 4638abc332d..9ebd49a08d0 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/mod.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/mod.rs @@ -7,6 +7,9 @@ mod token_emergency_action_transition; mod token_freeze_transition; mod token_mint_transition; mod token_set_price_for_direct_purchase_transition; +mod token_shield_transition; +mod token_shielded_transfer_transition; mod token_transfer_transition; mod token_transition; mod token_unfreeze_transition; +mod token_unshield_transition; diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_shield_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_shield_transition.rs new file mode 100644 index 00000000000..2c9bebed05a --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_shield_transition.rs @@ -0,0 +1,59 @@ +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::state_transition_action::action_convert_to_operations::batch::DriveHighLevelBatchOperationConverter; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::token_transition::token_shield_transition_action::{TokenShieldTransitionAction, TokenShieldTransitionActionAccessorsV0}; +use crate::util::batch::drive_op_batch::TokenOperationType; +use crate::util::batch::DriveOperation::{IdentityOperation, TokenOperation}; +use crate::util::batch::{DriveOperation, IdentityOperationType}; +use dpp::block::epoch::Epoch; +use dpp::identifier::Identifier; +use platform_version::version::PlatformVersion; + +impl DriveHighLevelBatchOperationConverter for TokenShieldTransitionAction { + /// The identity contract nonce bump plus the pool operation. No history document is + /// written for shielded token operations. + fn into_high_level_batch_drive_operations<'b>( + self, + _epoch: &Epoch, + owner_id: Identifier, + platform_version: &PlatformVersion, + ) -> Result>, Error> { + match platform_version + .drive + .methods + .state_transitions + .convert_to_high_level_operations + .token_shield_transition + { + 0 => { + let data_contract_id = self.base().data_contract_id(); + let identity_contract_nonce = self.base().identity_contract_nonce(); + let token_id = self.token_id(); + + let mut ops = vec![IdentityOperation( + IdentityOperationType::UpdateIdentityContractNonce { + identity_id: owner_id.into_buffer(), + contract_id: data_contract_id.into_buffer(), + nonce: identity_contract_nonce, + }, + )]; + + ops.push(TokenOperation(TokenOperationType::TokenShield { + token_id, + identity_id: owner_id, + amount: self.amount(), + notes: self.notes(), + })); + + Ok(ops) + } + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "TokenShieldTransitionAction::into_high_level_batch_drive_operations" + .to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_shielded_transfer_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_shielded_transfer_transition.rs new file mode 100644 index 00000000000..18a1a4ee7ff --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_shielded_transfer_transition.rs @@ -0,0 +1,59 @@ +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::state_transition_action::action_convert_to_operations::batch::DriveHighLevelBatchOperationConverter; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::token_transition::token_shielded_transfer_transition_action::{TokenShieldedTransferTransitionAction, TokenShieldedTransferTransitionActionAccessorsV0}; +use crate::util::batch::drive_op_batch::TokenOperationType; +use crate::util::batch::DriveOperation::{IdentityOperation, TokenOperation}; +use crate::util::batch::{DriveOperation, IdentityOperationType}; +use dpp::block::epoch::Epoch; +use dpp::identifier::Identifier; +use platform_version::version::PlatformVersion; + +impl DriveHighLevelBatchOperationConverter for TokenShieldedTransferTransitionAction { + /// The identity contract nonce bump plus the pool operation. No history document is + /// written for shielded token operations. + fn into_high_level_batch_drive_operations<'b>( + self, + _epoch: &Epoch, + owner_id: Identifier, + platform_version: &PlatformVersion, + ) -> Result>, Error> { + match platform_version + .drive + .methods + .state_transitions + .convert_to_high_level_operations + .token_shielded_transfer_transition + { + 0 => { + let data_contract_id = self.base().data_contract_id(); + let identity_contract_nonce = self.base().identity_contract_nonce(); + let token_id = self.token_id(); + + let mut ops = vec![IdentityOperation( + IdentityOperationType::UpdateIdentityContractNonce { + identity_id: owner_id.into_buffer(), + contract_id: data_contract_id.into_buffer(), + nonce: identity_contract_nonce, + }, + )]; + + ops.push(TokenOperation(TokenOperationType::TokenShieldedTransfer { + token_id, + nullifiers: self.nullifiers(), + notes: self.notes(), + })); + + Ok(ops) + } + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: + "TokenShieldedTransferTransitionAction::into_high_level_batch_drive_operations" + .to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_transition.rs index 54c8009d197..9bbc300bdae 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_transition.rs @@ -17,6 +17,8 @@ use crate::state_transition_action::batch::batched_transition::token_transition: use crate::state_transition_action::batch::batched_transition::token_transition::token_set_price_for_direct_purchase_transition_action::TokenSetPriceForDirectPurchaseTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::token_transition::token_transfer_transition_action::TokenTransferTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::token_transition::token_unfreeze_transition_action::TokenUnfreezeTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::token_transition::token_shield_transition_action::TokenShieldTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::token_transition::token_unshield_transition_action::TokenUnshieldTransitionActionAccessorsV0; impl DriveHighLevelBatchOperationConverter for TokenTransitionAction { fn into_high_level_batch_drive_operations<'b>( @@ -64,6 +66,14 @@ impl DriveHighLevelBatchOperationConverter for TokenTransitionAction { TokenTransitionAction::SetPriceForDirectPurchaseAction(set_price) => { set_price.into_high_level_batch_drive_operations(epoch, owner_id, platform_version) } + TokenTransitionAction::ShieldAction(shield) => { + shield.into_high_level_batch_drive_operations(epoch, owner_id, platform_version) + } + TokenTransitionAction::UnshieldAction(unshield) => { + unshield.into_high_level_batch_drive_operations(epoch, owner_id, platform_version) + } + TokenTransitionAction::ShieldedTransferAction(shielded_transfer) => shielded_transfer + .into_high_level_batch_drive_operations(epoch, owner_id, platform_version), } } } @@ -135,6 +145,13 @@ impl TokenTransitionAction { set_price_action.public_note().cloned(), ) } + TokenTransitionAction::ShieldAction(shield_action) => { + TokenEvent::Shield(shield_action.amount()) + } + TokenTransitionAction::UnshieldAction(unshield_action) => { + TokenEvent::Unshield(unshield_action.recipient_id(), unshield_action.amount()) + } + TokenTransitionAction::ShieldedTransferAction(_) => TokenEvent::ShieldedTransfer, } } } diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_unshield_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_unshield_transition.rs new file mode 100644 index 00000000000..031b3de8107 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_unshield_transition.rs @@ -0,0 +1,60 @@ +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::state_transition_action::action_convert_to_operations::batch::DriveHighLevelBatchOperationConverter; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::token_transition::token_unshield_transition_action::{TokenUnshieldTransitionAction, TokenUnshieldTransitionActionAccessorsV0}; +use crate::util::batch::drive_op_batch::TokenOperationType; +use crate::util::batch::DriveOperation::{IdentityOperation, TokenOperation}; +use crate::util::batch::{DriveOperation, IdentityOperationType}; +use dpp::block::epoch::Epoch; +use dpp::identifier::Identifier; +use platform_version::version::PlatformVersion; + +impl DriveHighLevelBatchOperationConverter for TokenUnshieldTransitionAction { + /// The identity contract nonce bump plus the pool operation. No history document is + /// written for shielded token operations. + fn into_high_level_batch_drive_operations<'b>( + self, + _epoch: &Epoch, + owner_id: Identifier, + platform_version: &PlatformVersion, + ) -> Result>, Error> { + match platform_version + .drive + .methods + .state_transitions + .convert_to_high_level_operations + .token_unshield_transition + { + 0 => { + let data_contract_id = self.base().data_contract_id(); + let identity_contract_nonce = self.base().identity_contract_nonce(); + let token_id = self.token_id(); + + let mut ops = vec![IdentityOperation( + IdentityOperationType::UpdateIdentityContractNonce { + identity_id: owner_id.into_buffer(), + contract_id: data_contract_id.into_buffer(), + nonce: identity_contract_nonce, + }, + )]; + + ops.push(TokenOperation(TokenOperationType::TokenUnshield { + token_id, + recipient_id: self.recipient_id(), + amount: self.amount(), + nullifiers: self.nullifiers(), + notes: self.notes(), + })); + + Ok(ops) + } + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "TokenUnshieldTransitionAction::into_high_level_batch_drive_operations" + .to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/mod.rs index ff3a9df2fe6..7b0ce0466d2 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/mod.rs @@ -29,6 +29,15 @@ pub mod token_direct_purchase_transition_action; /// token_set_price_for_direct_purchase_transition_action pub mod token_set_price_for_direct_purchase_transition_action; +/// token_shield_transition_action +pub mod token_shield_transition_action; + +/// token_shielded_transfer_transition_action +pub mod token_shielded_transfer_transition_action; + +/// token_unshield_transition_action +pub mod token_unshield_transition_action; + use derive_more::From; use dpp::block::block_info::BlockInfo; use dpp::data_contract::accessors::v0::DataContractV0Getters; @@ -56,6 +65,9 @@ use crate::state_transition_action::batch::batched_transition::token_transition: use crate::state_transition_action::batch::batched_transition::token_transition::token_claim_transition_action::{TokenClaimTransitionAction, TokenClaimTransitionActionAccessorsV0}; use crate::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_transition_action::{TokenDirectPurchaseTransitionAction, TokenDirectPurchaseTransitionActionAccessorsV0}; use crate::state_transition_action::batch::batched_transition::token_transition::token_set_price_for_direct_purchase_transition_action::{TokenSetPriceForDirectPurchaseTransitionAction, TokenSetPriceForDirectPurchaseTransitionActionAccessorsV0}; +use crate::state_transition_action::batch::batched_transition::token_transition::token_shield_transition_action::{TokenShieldTransitionAction, TokenShieldTransitionActionAccessorsV0}; +use crate::state_transition_action::batch::batched_transition::token_transition::token_shielded_transfer_transition_action::{TokenShieldedTransferTransitionAction, TokenShieldedTransferTransitionActionAccessorsV0}; +use crate::state_transition_action::batch::batched_transition::token_transition::token_unshield_transition_action::{TokenUnshieldTransitionAction, TokenUnshieldTransitionActionAccessorsV0}; /// token action #[derive(Debug, Clone, From)] @@ -82,6 +94,12 @@ pub enum TokenTransitionAction { DirectPurchaseAction(TokenDirectPurchaseTransitionAction), /// sets the price for direct purchase of the token SetPriceForDirectPurchaseAction(TokenSetPriceForDirectPurchaseTransitionAction), + /// identity token balance -> token shielded pool + ShieldAction(TokenShieldTransitionAction), + /// token shielded pool -> identity token balance + UnshieldAction(TokenUnshieldTransitionAction), + /// transfer inside the token shielded pool + ShieldedTransferAction(TokenShieldedTransferTransitionAction), } impl TokenTransitionAction { @@ -99,6 +117,9 @@ impl TokenTransitionAction { TokenTransitionAction::ConfigUpdateAction(action) => action.base(), TokenTransitionAction::DirectPurchaseAction(action) => action.base(), TokenTransitionAction::SetPriceForDirectPurchaseAction(action) => action.base(), + TokenTransitionAction::ShieldAction(action) => action.base(), + TokenTransitionAction::UnshieldAction(action) => action.base(), + TokenTransitionAction::ShieldedTransferAction(action) => action.base(), } } @@ -116,6 +137,9 @@ impl TokenTransitionAction { TokenTransitionAction::ConfigUpdateAction(action) => action.base_owned(), TokenTransitionAction::DirectPurchaseAction(action) => action.base_owned(), TokenTransitionAction::SetPriceForDirectPurchaseAction(action) => action.base_owned(), + TokenTransitionAction::ShieldAction(action) => action.base_owned(), + TokenTransitionAction::UnshieldAction(action) => action.base_owned(), + TokenTransitionAction::ShieldedTransferAction(action) => action.base_owned(), } } @@ -133,6 +157,9 @@ impl TokenTransitionAction { TokenTransitionAction::ConfigUpdateAction(_) => "configUpdate", TokenTransitionAction::DirectPurchaseAction(_) => "directPurchase", TokenTransitionAction::SetPriceForDirectPurchaseAction(_) => "directPricing", + TokenTransitionAction::ShieldAction(_) => "shield", + TokenTransitionAction::UnshieldAction(_) => "unshield", + TokenTransitionAction::ShieldedTransferAction(_) => "shieldedTransfer", } } @@ -196,6 +223,11 @@ impl TokenTransitionAction { TokenTransitionAction::SetPriceForDirectPurchaseAction(_) => { Ok(keeps_history.keeps_direct_pricing_history()) } + // Shielded token operations never write history: the token history contract has + // no document types for them and a shielded transfer has nothing public to record. + TokenTransitionAction::ShieldAction(_) + | TokenTransitionAction::UnshieldAction(_) + | TokenTransitionAction::ShieldedTransferAction(_) => Ok(false), } } } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/mod.rs new file mode 100644 index 00000000000..b2aff3c264f --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/mod.rs @@ -0,0 +1,62 @@ +use derive_more::From; +use dpp::balances::credits::TokenAmount; +use dpp::shielded::SerializedAction; + +/// transformer module +pub mod transformer; +mod v0; + +pub use v0::*; + +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; + +/// Identity token balance -> token shielded pool. Carries the outputs-only Orchard bundle so state validation can verify the proof and lower it to note appends. +#[derive(Debug, Clone, From)] +pub enum TokenShieldTransitionAction { + /// v0 + V0(TokenShieldTransitionActionV0), +} + +impl TokenShieldTransitionActionAccessorsV0 for TokenShieldTransitionAction { + fn base(&self) -> &TokenBaseTransitionAction { + match self { + TokenShieldTransitionAction::V0(v0) => &v0.base, + } + } + + fn base_owned(self) -> TokenBaseTransitionAction { + match self { + TokenShieldTransitionAction::V0(v0) => v0.base, + } + } + + fn amount(&self) -> TokenAmount { + match self { + TokenShieldTransitionAction::V0(v0) => v0.amount(), + } + } + + fn actions(&self) -> &[SerializedAction] { + match self { + TokenShieldTransitionAction::V0(v0) => v0.actions(), + } + } + + fn anchor(&self) -> &[u8; 32] { + match self { + TokenShieldTransitionAction::V0(v0) => v0.anchor(), + } + } + + fn proof(&self) -> &[u8] { + match self { + TokenShieldTransitionAction::V0(v0) => v0.proof(), + } + } + + fn binding_signature(&self) -> &[u8; 64] { + match self { + TokenShieldTransitionAction::V0(v0) => v0.binding_signature(), + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/transformer.rs new file mode 100644 index 00000000000..1d264f5a6ae --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/transformer.rs @@ -0,0 +1,53 @@ +use crate::drive::contract::DataContractFetchInfo; +use crate::drive::Drive; +use crate::error::Error; +use crate::state_transition_action::batch::batched_transition::token_transition::token_shield_transition_action::{TokenShieldTransitionAction, TokenShieldTransitionActionV0}; +use crate::state_transition_action::batch::BatchedTransitionAction; +use dpp::block::block_info::BlockInfo; +use dpp::fee::fee_result::FeeResult; +use dpp::platform_value::Identifier; +use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; +use dpp::state_transition::batch_transition::token_shield_transition::TokenShieldTransition; +use dpp::ProtocolError; +use grovedb::TransactionArg; +use platform_version::version::PlatformVersion; +use std::sync::Arc; + +impl TokenShieldTransitionAction { + /// Converts a borrowed `TokenShieldTransition` into a `TokenShieldTransitionAction`, resolving the token's contract + /// and metering the lookup. A failed base resolution becomes a paid nonce bump. + #[allow(clippy::too_many_arguments)] + pub fn try_from_borrowed_token_shield_transition_with_contract_lookup( + drive: &Drive, + owner_id: Identifier, + value: &TokenShieldTransition, + approximate_without_state_for_costs: bool, + transaction: TransactionArg, + block_info: &BlockInfo, + user_fee_increase: UserFeeIncrease, + get_data_contract: impl Fn(Identifier) -> Result, ProtocolError>, + platform_version: &PlatformVersion, + ) -> Result< + ( + ConsensusValidationResult, + FeeResult, + ), + Error, + > { + match value { + TokenShieldTransition::V0(v0) => { + TokenShieldTransitionActionV0::try_from_borrowed_token_shield_transition_with_contract_lookup( + drive, + owner_id, + v0, + approximate_without_state_for_costs, + transaction, + block_info, + user_fee_increase, + get_data_contract, + platform_version, + ) + } + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/v0/mod.rs new file mode 100644 index 00000000000..2fb36b33780 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/v0/mod.rs @@ -0,0 +1,130 @@ +mod transformer; + +use std::sync::Arc; + +use dpp::balances::credits::TokenAmount; +use dpp::identifier::Identifier; +use dpp::prelude::IdentityNonce; +use dpp::shielded::SerializedAction; +use crate::drive::contract::DataContractFetchInfo; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{ + TokenBaseTransitionAction, TokenBaseTransitionActionAccessorsV0, +}; +use crate::state_transition_action::shielded::ShieldedActionNote; + +/// Identity token balance -> token shielded pool. Carries the outputs-only Orchard bundle so state validation can verify the proof and lower it to note appends. +#[derive(Debug, Clone)] +pub struct TokenShieldTransitionActionV0 { + /// Base token transition action + pub base: TokenBaseTransitionAction, + /// Tokens leaving the owner's identity balance and entering the token's shielded pool + pub amount: TokenAmount, + /// Orchard actions (spend-output pairs) + pub actions: Vec, + /// Sinsemilla root of the token pool's note commitment tree the bundle was built against + pub anchor: [u8; 32], + /// Halo 2 proof bytes + pub proof: Vec, + /// RedPallas binding signature + pub binding_signature: [u8; 64], +} + +/// Accessors for `TokenShieldTransitionActionV0` +pub trait TokenShieldTransitionActionAccessorsV0 { + /// Returns the base token transition action + fn base(&self) -> &TokenBaseTransitionAction; + + /// Returns the base owned token transition action + fn base_owned(self) -> TokenBaseTransitionAction; + + /// Tokens entering the shielded pool + fn amount(&self) -> TokenAmount; + + /// The Orchard actions + fn actions(&self) -> &[SerializedAction]; + + /// The Orchard anchor the bundle was built against + fn anchor(&self) -> &[u8; 32]; + + /// The Halo 2 proof bytes + fn proof(&self) -> &[u8]; + + /// The RedPallas binding signature + fn binding_signature(&self) -> &[u8; 64]; + + /// The output notes to append to the pool's commitment tree, one per action + fn notes(&self) -> Vec { + self.actions() + .iter() + .map(ShieldedActionNote::from) + .collect() + } + + /// The nullifiers revealed by the actions + fn nullifiers(&self) -> Vec<[u8; 32]> { + self.actions() + .iter() + .map(|action| action.nullifier) + .collect() + } + + /// Returns the token position in the contract + fn token_position(&self) -> u16 { + self.base().token_position() + } + + /// Returns the token ID + fn token_id(&self) -> Identifier { + self.base().token_id() + } + + /// Returns the data contract ID from the base action + fn data_contract_id(&self) -> Identifier { + self.base().data_contract_id() + } + + /// Returns a reference to the data contract fetch info from the base action + fn data_contract_fetch_info_ref(&self) -> &Arc { + self.base().data_contract_fetch_info_ref() + } + + /// Returns the data contract fetch info + fn data_contract_fetch_info(&self) -> Arc { + self.base().data_contract_fetch_info() + } + + /// Returns the identity contract nonce from the base action + fn identity_contract_nonce(&self) -> IdentityNonce { + self.base().identity_contract_nonce() + } +} + +impl TokenShieldTransitionActionAccessorsV0 for TokenShieldTransitionActionV0 { + fn base(&self) -> &TokenBaseTransitionAction { + &self.base + } + + fn base_owned(self) -> TokenBaseTransitionAction { + self.base + } + + fn amount(&self) -> TokenAmount { + self.amount + } + + fn actions(&self) -> &[SerializedAction] { + &self.actions + } + + fn anchor(&self) -> &[u8; 32] { + &self.anchor + } + + fn proof(&self) -> &[u8] { + &self.proof + } + + fn binding_signature(&self) -> &[u8; 64] { + &self.binding_signature + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/v0/transformer.rs new file mode 100644 index 00000000000..848761db1f5 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/v0/transformer.rs @@ -0,0 +1,111 @@ +use crate::drive::contract::DataContractFetchInfo; +use crate::drive::Drive; +use crate::error::Error; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; +use crate::state_transition_action::batch::batched_transition::token_transition::token_shield_transition_action::{TokenShieldTransitionAction, TokenShieldTransitionActionV0}; +use crate::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; +use crate::state_transition_action::batch::BatchedTransitionAction; +use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; +use dpp::block::block_info::BlockInfo; +use dpp::fee::fee_result::FeeResult; +use dpp::identifier::Identifier; +use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; +use dpp::state_transition::batch_transition::token_shield_transition::v0::TokenShieldTransitionV0; +use dpp::ProtocolError; +use grovedb::TransactionArg; +use platform_version::version::PlatformVersion; +use std::sync::Arc; + +impl TokenShieldTransitionActionV0 { + /// Resolves the base transition (contract lookup, group handling) and lifts the bundle + /// fields into the action. Stateful pool checks and proof verification happen in the + /// state validator, where their cost is charged to the owner. + #[allow(clippy::too_many_arguments)] + pub fn try_from_borrowed_token_shield_transition_with_contract_lookup( + drive: &Drive, + owner_id: Identifier, + value: &TokenShieldTransitionV0, + approximate_without_state_for_costs: bool, + transaction: TransactionArg, + block_info: &BlockInfo, + user_fee_increase: UserFeeIncrease, + get_data_contract: impl Fn(Identifier) -> Result, ProtocolError>, + platform_version: &PlatformVersion, + ) -> Result< + ( + ConsensusValidationResult, + FeeResult, + ), + Error, + > { + let TokenShieldTransitionV0 { + base, + amount, + actions, + anchor, + proof, + binding_signature, + } = value; + + let mut drive_operations = vec![]; + + let base_action_validation_result = + TokenBaseTransitionAction::try_from_borrowed_base_transition_with_contract_lookup( + drive, + owner_id, + base, + approximate_without_state_for_costs, + transaction, + &mut drive_operations, + get_data_contract, + platform_version, + )?; + + let fee_result = Drive::calculate_fee( + None, + Some(drive_operations), + &block_info.epoch, + drive.config.epochs_per_era, + platform_version, + None, + )?; + + // Shielded token transitions carry no public note to change. + let (base_action, _change_note) = match base_action_validation_result.is_valid() { + true => base_action_validation_result.into_data()?, + false => { + let bump_action = + BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( + base, + owner_id, + user_fee_increase, + ); + let batched_action = + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); + + return Ok(( + ConsensusValidationResult::new_with_data_and_errors( + batched_action, + base_action_validation_result.errors, + ), + fee_result, + )); + } + }; + + Ok(( + BatchedTransitionAction::TokenAction(TokenTransitionAction::from( + TokenShieldTransitionAction::V0(TokenShieldTransitionActionV0 { + base: base_action, + amount: *amount, + actions: actions.clone(), + anchor: *anchor, + proof: proof.clone(), + binding_signature: *binding_signature, + }), + )) + .into(), + fee_result, + )) + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/mod.rs new file mode 100644 index 00000000000..82644ea9c5f --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/mod.rs @@ -0,0 +1,55 @@ +use derive_more::From; +use dpp::shielded::SerializedAction; + +/// transformer module +pub mod transformer; +mod v0; + +pub use v0::*; + +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; + +/// A transfer inside the token's shielded pool. Carries the Orchard spend bundle (value balance zero) so state validation can verify the proof and lower it to nullifier inserts and note appends. +#[derive(Debug, Clone, From)] +pub enum TokenShieldedTransferTransitionAction { + /// v0 + V0(TokenShieldedTransferTransitionActionV0), +} + +impl TokenShieldedTransferTransitionActionAccessorsV0 for TokenShieldedTransferTransitionAction { + fn base(&self) -> &TokenBaseTransitionAction { + match self { + TokenShieldedTransferTransitionAction::V0(v0) => &v0.base, + } + } + + fn base_owned(self) -> TokenBaseTransitionAction { + match self { + TokenShieldedTransferTransitionAction::V0(v0) => v0.base, + } + } + + fn actions(&self) -> &[SerializedAction] { + match self { + TokenShieldedTransferTransitionAction::V0(v0) => v0.actions(), + } + } + + fn anchor(&self) -> &[u8; 32] { + match self { + TokenShieldedTransferTransitionAction::V0(v0) => v0.anchor(), + } + } + + fn proof(&self) -> &[u8] { + match self { + TokenShieldedTransferTransitionAction::V0(v0) => v0.proof(), + } + } + + fn binding_signature(&self) -> &[u8; 64] { + match self { + TokenShieldedTransferTransitionAction::V0(v0) => v0.binding_signature(), + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/transformer.rs new file mode 100644 index 00000000000..b05cfd7eca8 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/transformer.rs @@ -0,0 +1,53 @@ +use crate::drive::contract::DataContractFetchInfo; +use crate::drive::Drive; +use crate::error::Error; +use crate::state_transition_action::batch::batched_transition::token_transition::token_shielded_transfer_transition_action::{TokenShieldedTransferTransitionAction, TokenShieldedTransferTransitionActionV0}; +use crate::state_transition_action::batch::BatchedTransitionAction; +use dpp::block::block_info::BlockInfo; +use dpp::fee::fee_result::FeeResult; +use dpp::platform_value::Identifier; +use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; +use dpp::state_transition::batch_transition::token_shielded_transfer_transition::TokenShieldedTransferTransition; +use dpp::ProtocolError; +use grovedb::TransactionArg; +use platform_version::version::PlatformVersion; +use std::sync::Arc; + +impl TokenShieldedTransferTransitionAction { + /// Converts a borrowed `TokenShieldedTransferTransition` into a `TokenShieldedTransferTransitionAction`, resolving the token's contract + /// and metering the lookup. A failed base resolution becomes a paid nonce bump. + #[allow(clippy::too_many_arguments)] + pub fn try_from_borrowed_token_shielded_transfer_transition_with_contract_lookup( + drive: &Drive, + owner_id: Identifier, + value: &TokenShieldedTransferTransition, + approximate_without_state_for_costs: bool, + transaction: TransactionArg, + block_info: &BlockInfo, + user_fee_increase: UserFeeIncrease, + get_data_contract: impl Fn(Identifier) -> Result, ProtocolError>, + platform_version: &PlatformVersion, + ) -> Result< + ( + ConsensusValidationResult, + FeeResult, + ), + Error, + > { + match value { + TokenShieldedTransferTransition::V0(v0) => { + TokenShieldedTransferTransitionActionV0::try_from_borrowed_token_shielded_transfer_transition_with_contract_lookup( + drive, + owner_id, + v0, + approximate_without_state_for_costs, + transaction, + block_info, + user_fee_increase, + get_data_contract, + platform_version, + ) + } + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/v0/mod.rs new file mode 100644 index 00000000000..52d27c39930 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/v0/mod.rs @@ -0,0 +1,120 @@ +mod transformer; + +use std::sync::Arc; + +use dpp::identifier::Identifier; +use dpp::prelude::IdentityNonce; +use dpp::shielded::SerializedAction; +use crate::drive::contract::DataContractFetchInfo; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{ + TokenBaseTransitionAction, TokenBaseTransitionActionAccessorsV0, +}; +use crate::state_transition_action::shielded::ShieldedActionNote; + +/// A transfer inside the token's shielded pool. Carries the Orchard spend bundle (value balance zero) so state validation can verify the proof and lower it to nullifier inserts and note appends. +#[derive(Debug, Clone)] +pub struct TokenShieldedTransferTransitionActionV0 { + /// Base token transition action + pub base: TokenBaseTransitionAction, + /// Orchard actions (spend-output pairs) + pub actions: Vec, + /// Sinsemilla root of the token pool's note commitment tree the bundle was built against + pub anchor: [u8; 32], + /// Halo 2 proof bytes + pub proof: Vec, + /// RedPallas binding signature + pub binding_signature: [u8; 64], +} + +/// Accessors for `TokenShieldedTransferTransitionActionV0` +pub trait TokenShieldedTransferTransitionActionAccessorsV0 { + /// Returns the base token transition action + fn base(&self) -> &TokenBaseTransitionAction; + + /// Returns the base owned token transition action + fn base_owned(self) -> TokenBaseTransitionAction; + + /// The Orchard actions + fn actions(&self) -> &[SerializedAction]; + + /// The Orchard anchor the bundle was built against + fn anchor(&self) -> &[u8; 32]; + + /// The Halo 2 proof bytes + fn proof(&self) -> &[u8]; + + /// The RedPallas binding signature + fn binding_signature(&self) -> &[u8; 64]; + + /// The output notes to append to the pool's commitment tree, one per action + fn notes(&self) -> Vec { + self.actions() + .iter() + .map(ShieldedActionNote::from) + .collect() + } + + /// The nullifiers revealed by the actions + fn nullifiers(&self) -> Vec<[u8; 32]> { + self.actions() + .iter() + .map(|action| action.nullifier) + .collect() + } + + /// Returns the token position in the contract + fn token_position(&self) -> u16 { + self.base().token_position() + } + + /// Returns the token ID + fn token_id(&self) -> Identifier { + self.base().token_id() + } + + /// Returns the data contract ID from the base action + fn data_contract_id(&self) -> Identifier { + self.base().data_contract_id() + } + + /// Returns a reference to the data contract fetch info from the base action + fn data_contract_fetch_info_ref(&self) -> &Arc { + self.base().data_contract_fetch_info_ref() + } + + /// Returns the data contract fetch info + fn data_contract_fetch_info(&self) -> Arc { + self.base().data_contract_fetch_info() + } + + /// Returns the identity contract nonce from the base action + fn identity_contract_nonce(&self) -> IdentityNonce { + self.base().identity_contract_nonce() + } +} + +impl TokenShieldedTransferTransitionActionAccessorsV0 for TokenShieldedTransferTransitionActionV0 { + fn base(&self) -> &TokenBaseTransitionAction { + &self.base + } + + fn base_owned(self) -> TokenBaseTransitionAction { + self.base + } + + fn actions(&self) -> &[SerializedAction] { + &self.actions + } + + fn anchor(&self) -> &[u8; 32] { + &self.anchor + } + + fn proof(&self) -> &[u8] { + &self.proof + } + + fn binding_signature(&self) -> &[u8; 64] { + &self.binding_signature + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/v0/transformer.rs new file mode 100644 index 00000000000..bb4f1af95a7 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/v0/transformer.rs @@ -0,0 +1,111 @@ +use crate::drive::contract::DataContractFetchInfo; +use crate::drive::Drive; +use crate::error::Error; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; +use crate::state_transition_action::batch::batched_transition::token_transition::token_shielded_transfer_transition_action::{TokenShieldedTransferTransitionAction, TokenShieldedTransferTransitionActionV0}; +use crate::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; +use crate::state_transition_action::batch::BatchedTransitionAction; +use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; +use dpp::block::block_info::BlockInfo; +use dpp::fee::fee_result::FeeResult; +use dpp::identifier::Identifier; +use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; +use dpp::state_transition::batch_transition::token_shielded_transfer_transition::v0::TokenShieldedTransferTransitionV0; +use dpp::ProtocolError; +use grovedb::TransactionArg; +use platform_version::version::PlatformVersion; +use std::sync::Arc; + +impl TokenShieldedTransferTransitionActionV0 { + /// Resolves the base transition (contract lookup, group handling) and lifts the bundle + /// fields into the action. Stateful pool checks and proof verification happen in the + /// state validator, where their cost is charged to the owner. + #[allow(clippy::too_many_arguments)] + pub fn try_from_borrowed_token_shielded_transfer_transition_with_contract_lookup( + drive: &Drive, + owner_id: Identifier, + value: &TokenShieldedTransferTransitionV0, + approximate_without_state_for_costs: bool, + transaction: TransactionArg, + block_info: &BlockInfo, + user_fee_increase: UserFeeIncrease, + get_data_contract: impl Fn(Identifier) -> Result, ProtocolError>, + platform_version: &PlatformVersion, + ) -> Result< + ( + ConsensusValidationResult, + FeeResult, + ), + Error, + > { + let TokenShieldedTransferTransitionV0 { + base, + actions, + anchor, + proof, + binding_signature, + } = value; + + let mut drive_operations = vec![]; + + let base_action_validation_result = + TokenBaseTransitionAction::try_from_borrowed_base_transition_with_contract_lookup( + drive, + owner_id, + base, + approximate_without_state_for_costs, + transaction, + &mut drive_operations, + get_data_contract, + platform_version, + )?; + + let fee_result = Drive::calculate_fee( + None, + Some(drive_operations), + &block_info.epoch, + drive.config.epochs_per_era, + platform_version, + None, + )?; + + // Shielded token transitions carry no public note to change. + let (base_action, _change_note) = match base_action_validation_result.is_valid() { + true => base_action_validation_result.into_data()?, + false => { + let bump_action = + BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( + base, + owner_id, + user_fee_increase, + ); + let batched_action = + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); + + return Ok(( + ConsensusValidationResult::new_with_data_and_errors( + batched_action, + base_action_validation_result.errors, + ), + fee_result, + )); + } + }; + + Ok(( + BatchedTransitionAction::TokenAction(TokenTransitionAction::from( + TokenShieldedTransferTransitionAction::V0( + TokenShieldedTransferTransitionActionV0 { + base: base_action, + actions: actions.clone(), + anchor: *anchor, + proof: proof.clone(), + binding_signature: *binding_signature, + }, + ), + )) + .into(), + fee_result, + )) + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_transition_action_type.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_transition_action_type.rs index dfa1ee4dfb4..0bad8ea4c43 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_transition_action_type.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_transition_action_type.rs @@ -25,6 +25,11 @@ impl TokenTransitionActionTypeGetter for TokenTransitionAction { TokenTransitionAction::SetPriceForDirectPurchaseAction(_) => { TokenTransitionActionType::SetPriceForDirectPurchase } + TokenTransitionAction::ShieldAction(_) => TokenTransitionActionType::Shield, + TokenTransitionAction::UnshieldAction(_) => TokenTransitionActionType::Unshield, + TokenTransitionAction::ShieldedTransferAction(_) => { + TokenTransitionActionType::ShieldedTransfer + } } } } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/mod.rs new file mode 100644 index 00000000000..7ed381e0b6f --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/mod.rs @@ -0,0 +1,69 @@ +use derive_more::From; +use dpp::balances::credits::TokenAmount; +use dpp::identifier::Identifier; +use dpp::shielded::SerializedAction; + +/// transformer module +pub mod transformer; +mod v0; + +pub use v0::*; + +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; + +/// Token shielded pool -> identity token balance. Carries the Orchard spend bundle so state validation can verify the proof and lower it to nullifier inserts, change-note appends and the recipient credit. +#[derive(Debug, Clone, From)] +pub enum TokenUnshieldTransitionAction { + /// v0 + V0(TokenUnshieldTransitionActionV0), +} + +impl TokenUnshieldTransitionActionAccessorsV0 for TokenUnshieldTransitionAction { + fn base(&self) -> &TokenBaseTransitionAction { + match self { + TokenUnshieldTransitionAction::V0(v0) => &v0.base, + } + } + + fn base_owned(self) -> TokenBaseTransitionAction { + match self { + TokenUnshieldTransitionAction::V0(v0) => v0.base, + } + } + + fn amount(&self) -> TokenAmount { + match self { + TokenUnshieldTransitionAction::V0(v0) => v0.amount(), + } + } + + fn recipient_id(&self) -> Identifier { + match self { + TokenUnshieldTransitionAction::V0(v0) => v0.recipient_id(), + } + } + + fn actions(&self) -> &[SerializedAction] { + match self { + TokenUnshieldTransitionAction::V0(v0) => v0.actions(), + } + } + + fn anchor(&self) -> &[u8; 32] { + match self { + TokenUnshieldTransitionAction::V0(v0) => v0.anchor(), + } + } + + fn proof(&self) -> &[u8] { + match self { + TokenUnshieldTransitionAction::V0(v0) => v0.proof(), + } + } + + fn binding_signature(&self) -> &[u8; 64] { + match self { + TokenUnshieldTransitionAction::V0(v0) => v0.binding_signature(), + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/transformer.rs new file mode 100644 index 00000000000..add9b3afa06 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/transformer.rs @@ -0,0 +1,53 @@ +use crate::drive::contract::DataContractFetchInfo; +use crate::drive::Drive; +use crate::error::Error; +use crate::state_transition_action::batch::batched_transition::token_transition::token_unshield_transition_action::{TokenUnshieldTransitionAction, TokenUnshieldTransitionActionV0}; +use crate::state_transition_action::batch::BatchedTransitionAction; +use dpp::block::block_info::BlockInfo; +use dpp::fee::fee_result::FeeResult; +use dpp::platform_value::Identifier; +use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; +use dpp::state_transition::batch_transition::token_unshield_transition::TokenUnshieldTransition; +use dpp::ProtocolError; +use grovedb::TransactionArg; +use platform_version::version::PlatformVersion; +use std::sync::Arc; + +impl TokenUnshieldTransitionAction { + /// Converts a borrowed `TokenUnshieldTransition` into a `TokenUnshieldTransitionAction`, resolving the token's contract + /// and metering the lookup. A failed base resolution becomes a paid nonce bump. + #[allow(clippy::too_many_arguments)] + pub fn try_from_borrowed_token_unshield_transition_with_contract_lookup( + drive: &Drive, + owner_id: Identifier, + value: &TokenUnshieldTransition, + approximate_without_state_for_costs: bool, + transaction: TransactionArg, + block_info: &BlockInfo, + user_fee_increase: UserFeeIncrease, + get_data_contract: impl Fn(Identifier) -> Result, ProtocolError>, + platform_version: &PlatformVersion, + ) -> Result< + ( + ConsensusValidationResult, + FeeResult, + ), + Error, + > { + match value { + TokenUnshieldTransition::V0(v0) => { + TokenUnshieldTransitionActionV0::try_from_borrowed_token_unshield_transition_with_contract_lookup( + drive, + owner_id, + v0, + approximate_without_state_for_costs, + transaction, + block_info, + user_fee_increase, + get_data_contract, + platform_version, + ) + } + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/v0/mod.rs new file mode 100644 index 00000000000..f50fc6de0cd --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/v0/mod.rs @@ -0,0 +1,139 @@ +mod transformer; + +use std::sync::Arc; + +use dpp::balances::credits::TokenAmount; +use dpp::identifier::Identifier; +use dpp::prelude::IdentityNonce; +use dpp::shielded::SerializedAction; +use crate::drive::contract::DataContractFetchInfo; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{ + TokenBaseTransitionAction, TokenBaseTransitionActionAccessorsV0, +}; +use crate::state_transition_action::shielded::ShieldedActionNote; + +/// Token shielded pool -> identity token balance. Carries the Orchard spend bundle so state validation can verify the proof and lower it to nullifier inserts, change-note appends and the recipient credit. +#[derive(Debug, Clone)] +pub struct TokenUnshieldTransitionActionV0 { + /// Base token transition action + pub base: TokenBaseTransitionAction, + /// Tokens leaving the shielded pool and credited to `recipient_id` + pub amount: TokenAmount, + /// The identity whose token balance receives the unshielded amount + pub recipient_id: Identifier, + /// Orchard actions (spend-output pairs) + pub actions: Vec, + /// Sinsemilla root of the token pool's note commitment tree the bundle was built against + pub anchor: [u8; 32], + /// Halo 2 proof bytes + pub proof: Vec, + /// RedPallas binding signature + pub binding_signature: [u8; 64], +} + +/// Accessors for `TokenUnshieldTransitionActionV0` +pub trait TokenUnshieldTransitionActionAccessorsV0 { + /// Returns the base token transition action + fn base(&self) -> &TokenBaseTransitionAction; + + /// Returns the base owned token transition action + fn base_owned(self) -> TokenBaseTransitionAction; + + /// Tokens leaving the shielded pool + fn amount(&self) -> TokenAmount; + + /// The identity credited with the unshielded amount + fn recipient_id(&self) -> Identifier; + + /// The Orchard actions + fn actions(&self) -> &[SerializedAction]; + + /// The Orchard anchor the bundle was built against + fn anchor(&self) -> &[u8; 32]; + + /// The Halo 2 proof bytes + fn proof(&self) -> &[u8]; + + /// The RedPallas binding signature + fn binding_signature(&self) -> &[u8; 64]; + + /// The output notes to append to the pool's commitment tree, one per action + fn notes(&self) -> Vec { + self.actions() + .iter() + .map(ShieldedActionNote::from) + .collect() + } + + /// The nullifiers revealed by the actions + fn nullifiers(&self) -> Vec<[u8; 32]> { + self.actions() + .iter() + .map(|action| action.nullifier) + .collect() + } + + /// Returns the token position in the contract + fn token_position(&self) -> u16 { + self.base().token_position() + } + + /// Returns the token ID + fn token_id(&self) -> Identifier { + self.base().token_id() + } + + /// Returns the data contract ID from the base action + fn data_contract_id(&self) -> Identifier { + self.base().data_contract_id() + } + + /// Returns a reference to the data contract fetch info from the base action + fn data_contract_fetch_info_ref(&self) -> &Arc { + self.base().data_contract_fetch_info_ref() + } + + /// Returns the data contract fetch info + fn data_contract_fetch_info(&self) -> Arc { + self.base().data_contract_fetch_info() + } + + /// Returns the identity contract nonce from the base action + fn identity_contract_nonce(&self) -> IdentityNonce { + self.base().identity_contract_nonce() + } +} + +impl TokenUnshieldTransitionActionAccessorsV0 for TokenUnshieldTransitionActionV0 { + fn base(&self) -> &TokenBaseTransitionAction { + &self.base + } + + fn base_owned(self) -> TokenBaseTransitionAction { + self.base + } + + fn amount(&self) -> TokenAmount { + self.amount + } + + fn recipient_id(&self) -> Identifier { + self.recipient_id + } + + fn actions(&self) -> &[SerializedAction] { + &self.actions + } + + fn anchor(&self) -> &[u8; 32] { + &self.anchor + } + + fn proof(&self) -> &[u8] { + &self.proof + } + + fn binding_signature(&self) -> &[u8; 64] { + &self.binding_signature + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/v0/transformer.rs new file mode 100644 index 00000000000..72aa62814b1 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/v0/transformer.rs @@ -0,0 +1,113 @@ +use crate::drive::contract::DataContractFetchInfo; +use crate::drive::Drive; +use crate::error::Error; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; +use crate::state_transition_action::batch::batched_transition::token_transition::token_unshield_transition_action::{TokenUnshieldTransitionAction, TokenUnshieldTransitionActionV0}; +use crate::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; +use crate::state_transition_action::batch::BatchedTransitionAction; +use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; +use dpp::block::block_info::BlockInfo; +use dpp::fee::fee_result::FeeResult; +use dpp::identifier::Identifier; +use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; +use dpp::state_transition::batch_transition::token_unshield_transition::v0::TokenUnshieldTransitionV0; +use dpp::ProtocolError; +use grovedb::TransactionArg; +use platform_version::version::PlatformVersion; +use std::sync::Arc; + +impl TokenUnshieldTransitionActionV0 { + /// Resolves the base transition (contract lookup, group handling) and lifts the bundle + /// fields into the action. Stateful pool checks and proof verification happen in the + /// state validator, where their cost is charged to the owner. + #[allow(clippy::too_many_arguments)] + pub fn try_from_borrowed_token_unshield_transition_with_contract_lookup( + drive: &Drive, + owner_id: Identifier, + value: &TokenUnshieldTransitionV0, + approximate_without_state_for_costs: bool, + transaction: TransactionArg, + block_info: &BlockInfo, + user_fee_increase: UserFeeIncrease, + get_data_contract: impl Fn(Identifier) -> Result, ProtocolError>, + platform_version: &PlatformVersion, + ) -> Result< + ( + ConsensusValidationResult, + FeeResult, + ), + Error, + > { + let TokenUnshieldTransitionV0 { + base, + amount, + recipient_id, + actions, + anchor, + proof, + binding_signature, + } = value; + + let mut drive_operations = vec![]; + + let base_action_validation_result = + TokenBaseTransitionAction::try_from_borrowed_base_transition_with_contract_lookup( + drive, + owner_id, + base, + approximate_without_state_for_costs, + transaction, + &mut drive_operations, + get_data_contract, + platform_version, + )?; + + let fee_result = Drive::calculate_fee( + None, + Some(drive_operations), + &block_info.epoch, + drive.config.epochs_per_era, + platform_version, + None, + )?; + + // Shielded token transitions carry no public note to change. + let (base_action, _change_note) = match base_action_validation_result.is_valid() { + true => base_action_validation_result.into_data()?, + false => { + let bump_action = + BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( + base, + owner_id, + user_fee_increase, + ); + let batched_action = + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); + + return Ok(( + ConsensusValidationResult::new_with_data_and_errors( + batched_action, + base_action_validation_result.errors, + ), + fee_result, + )); + } + }; + + Ok(( + BatchedTransitionAction::TokenAction(TokenTransitionAction::from( + TokenUnshieldTransitionAction::V0(TokenUnshieldTransitionActionV0 { + base: base_action, + amount: *amount, + recipient_id: *recipient_id, + actions: actions.clone(), + anchor: *anchor, + proof: proof.clone(), + binding_signature: *binding_signature, + }), + )) + .into(), + fee_result, + )) + } +} diff --git a/packages/rs-drive/src/util/batch/drive_op_batch/token.rs b/packages/rs-drive/src/util/batch/drive_op_batch/token.rs index ff83722f108..fc77899e300 100644 --- a/packages/rs-drive/src/util/batch/drive_op_batch/token.rs +++ b/packages/rs-drive/src/util/batch/drive_op_batch/token.rs @@ -1,6 +1,7 @@ use crate::drive::Drive; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; +use crate::state_transition_action::shielded::ShieldedActionNote; use crate::util::batch::drive_op_batch::DriveLowLevelOperationConverter; use dpp::balances::credits::TokenAmount; use dpp::block::block_info::BlockInfo; @@ -133,6 +134,39 @@ pub enum TokenOperationType { /// None means it's not currently for sale price: Option, }, + /// Moves tokens from an identity balance into the token's shielded pool. + TokenShield { + /// The token id + token_id: Identifier, + /// The identity whose balance is debited + identity_id: Identifier, + /// The amount entering the pool + amount: TokenAmount, + /// The bundle's output notes, appended to the pool's commitment tree + notes: Vec, + }, + /// Moves tokens from the token's shielded pool into an identity balance. + TokenUnshield { + /// The token id + token_id: Identifier, + /// The identity whose balance is credited + recipient_id: Identifier, + /// The amount leaving the pool + amount: TokenAmount, + /// The spent nullifiers, recorded in the pool's nullifier set + nullifiers: Vec<[u8; 32]>, + /// The bundle's output (change) notes, appended to the pool's commitment tree + notes: Vec, + }, + /// A transfer inside the token's shielded pool (the pool balance is unchanged). + TokenShieldedTransfer { + /// The token id + token_id: Identifier, + /// The spent nullifiers, recorded in the pool's nullifier set + nullifiers: Vec<[u8; 32]>, + /// The bundle's output notes, appended to the pool's commitment tree + notes: Vec, + }, } impl DriveLowLevelOperationConverter for TokenOperationType { @@ -332,6 +366,48 @@ impl DriveLowLevelOperationConverter for TokenOperationType { )?; Ok(batch_operations) } + TokenOperationType::TokenShield { + token_id, + identity_id, + amount, + notes, + } => drive.token_shield_operations( + token_id.to_buffer(), + identity_id.to_buffer(), + amount, + ¬es, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ), + TokenOperationType::TokenUnshield { + token_id, + recipient_id, + amount, + nullifiers, + notes, + } => drive.token_unshield_operations( + token_id.to_buffer(), + recipient_id.to_buffer(), + amount, + &nullifiers, + ¬es, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ), + TokenOperationType::TokenShieldedTransfer { + token_id, + nullifiers, + notes, + } => drive.token_shielded_transfer_operations( + token_id.to_buffer(), + &nullifiers, + ¬es, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ), } } } diff --git a/packages/rs-drive/src/verify/shielded/verify_most_recent_shielded_anchor/mod.rs b/packages/rs-drive/src/verify/shielded/verify_most_recent_shielded_anchor/mod.rs index 7b1472dae8d..8870073d6de 100644 --- a/packages/rs-drive/src/verify/shielded/verify_most_recent_shielded_anchor/mod.rs +++ b/packages/rs-drive/src/verify/shielded/verify_most_recent_shielded_anchor/mod.rs @@ -1,5 +1,6 @@ mod v0; +use crate::drive::shielded::paths::token_shielded_pool_latest_recorded_anchor_path_query; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; @@ -34,6 +35,37 @@ impl Drive { } } +impl Drive { + /// Verifies a proof of a TOKEN shielded pool's most recently recorded anchor. Same + /// versioning as [`Drive::verify_most_recent_shielded_anchor`]. + pub fn verify_most_recent_token_shielded_pool_anchor( + proof: &[u8], + token_id: [u8; 32], + verify_subset_of_proof: bool, + platform_version: &PlatformVersion, + ) -> Result<(RootHash, Option<[u8; 32]>), Error> { + match platform_version + .drive + .methods + .verify + .shielded + .verify_most_recent_shielded_anchor + { + 0 => Self::verify_pool_most_recent_anchor_v0( + proof, + token_shielded_pool_latest_recorded_anchor_path_query(token_id), + verify_subset_of_proof, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "verify_most_recent_token_shielded_pool_anchor".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/rs-drive/src/verify/shielded/verify_most_recent_shielded_anchor/v0/mod.rs b/packages/rs-drive/src/verify/shielded/verify_most_recent_shielded_anchor/v0/mod.rs index 8889de59908..1847b2a607d 100644 --- a/packages/rs-drive/src/verify/shielded/verify_most_recent_shielded_anchor/v0/mod.rs +++ b/packages/rs-drive/src/verify/shielded/verify_most_recent_shielded_anchor/v0/mod.rs @@ -4,7 +4,7 @@ use crate::error::drive::DriveError; use crate::error::proof::ProofError; use crate::error::Error; use crate::verify::RootHash; -use grovedb::{Element, GroveDb}; +use grovedb::{Element, GroveDb, PathQuery}; use platform_version::version::PlatformVersion; impl Drive { @@ -23,8 +23,22 @@ impl Drive { verify_subset_of_proof: bool, platform_version: &PlatformVersion, ) -> Result<(RootHash, Option<[u8; 32]>), Error> { - let path_query = shielded_latest_recorded_anchor_path_query(); + Self::verify_pool_most_recent_anchor_v0( + proof, + shielded_latest_recorded_anchor_path_query(), + verify_subset_of_proof, + platform_version, + ) + } + /// Verifies the most recent anchor of a pool given that pool's own `limit 1` reverse + /// scan, whichever shielded pool (credit or token) it is. + pub(super) fn verify_pool_most_recent_anchor_v0( + proof: &[u8], + path_query: PathQuery, + verify_subset_of_proof: bool, + platform_version: &PlatformVersion, + ) -> Result<(RootHash, Option<[u8; 32]>), Error> { let (root_hash, mut proved_key_values) = if verify_subset_of_proof { GroveDb::verify_subset_query(proof, &path_query, &platform_version.drive.grove_version)? } else { diff --git a/packages/rs-drive/src/verify/shielded/verify_shielded_anchors/mod.rs b/packages/rs-drive/src/verify/shielded/verify_shielded_anchors/mod.rs index 0b679abb5d1..ae58e3315ee 100644 --- a/packages/rs-drive/src/verify/shielded/verify_shielded_anchors/mod.rs +++ b/packages/rs-drive/src/verify/shielded/verify_shielded_anchors/mod.rs @@ -1,5 +1,6 @@ mod v0; +use crate::drive::shielded::paths::token_shielded_pool_anchors_path_vec; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; @@ -30,6 +31,37 @@ impl Drive { } } +impl Drive { + /// Verifies a proof of a TOKEN shielded pool's recorded anchors. Same versioning as + /// [`Drive::verify_shielded_anchors`]. + pub fn verify_token_shielded_pool_anchors( + proof: &[u8], + token_id: [u8; 32], + verify_subset_of_proof: bool, + platform_version: &PlatformVersion, + ) -> Result<(RootHash, Vec<[u8; 32]>), Error> { + match platform_version + .drive + .methods + .verify + .shielded + .verify_shielded_anchors + { + 0 => Self::verify_pool_anchors_v0( + proof, + token_shielded_pool_anchors_path_vec(token_id), + verify_subset_of_proof, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "verify_token_shielded_pool_anchors".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/rs-drive/src/verify/shielded/verify_shielded_anchors/v0/mod.rs b/packages/rs-drive/src/verify/shielded/verify_shielded_anchors/v0/mod.rs index 77ee33304fa..455a52e123e 100644 --- a/packages/rs-drive/src/verify/shielded/verify_shielded_anchors/v0/mod.rs +++ b/packages/rs-drive/src/verify/shielded/verify_shielded_anchors/v0/mod.rs @@ -11,9 +11,25 @@ impl Drive { proof: &[u8], verify_subset_of_proof: bool, platform_version: &PlatformVersion, + ) -> Result<(RootHash, Vec<[u8; 32]>), Error> { + Self::verify_pool_anchors_v0( + proof, + shielded_credit_pool_anchors_path_vec(), + verify_subset_of_proof, + platform_version, + ) + } + + /// Verifies the anchors tree at `anchors_path`, whichever shielded pool (credit or token) + /// it belongs to. + pub(super) fn verify_pool_anchors_v0( + proof: &[u8], + anchors_path: Vec>, + verify_subset_of_proof: bool, + platform_version: &PlatformVersion, ) -> Result<(RootHash, Vec<[u8; 32]>), Error> { let path_query = PathQuery { - path: shielded_credit_pool_anchors_path_vec(), + path: anchors_path, query: SizedQuery { query: Query::new_range_full(), limit: None, diff --git a/packages/rs-drive/src/verify/shielded/verify_shielded_encrypted_notes/mod.rs b/packages/rs-drive/src/verify/shielded/verify_shielded_encrypted_notes/mod.rs index e5e8103f141..1c63fb3029d 100644 --- a/packages/rs-drive/src/verify/shielded/verify_shielded_encrypted_notes/mod.rs +++ b/packages/rs-drive/src/verify/shielded/verify_shielded_encrypted_notes/mod.rs @@ -1,5 +1,6 @@ mod v0; +use crate::drive::shielded::paths::token_shielded_pool_path_vec; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; @@ -69,6 +70,44 @@ impl Drive { } } +impl Drive { + /// Verifies a `GetShieldedEncryptedNotes` proof for a TOKEN shielded pool. Same versioning + /// as [`Drive::verify_shielded_encrypted_notes`]. + #[allow(clippy::type_complexity)] + pub fn verify_token_shielded_pool_encrypted_notes( + proof: &[u8], + token_id: [u8; 32], + start_index: u64, + count: u32, + max_elements: u32, + verify_subset_of_proof: bool, + platform_version: &PlatformVersion, + ) -> Result<(RootHash, Vec, u64), Error> { + match platform_version + .drive + .methods + .verify + .shielded + .verify_shielded_encrypted_notes + { + 0 => Self::verify_pool_encrypted_notes_v0( + proof, + token_shielded_pool_path_vec(token_id), + start_index, + count, + max_elements, + verify_subset_of_proof, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "verify_token_shielded_pool_encrypted_notes".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/rs-drive/src/verify/shielded/verify_shielded_encrypted_notes/v0/mod.rs b/packages/rs-drive/src/verify/shielded/verify_shielded_encrypted_notes/v0/mod.rs index a94a5825d1e..1b761a17715 100644 --- a/packages/rs-drive/src/verify/shielded/verify_shielded_encrypted_notes/v0/mod.rs +++ b/packages/rs-drive/src/verify/shielded/verify_shielded_encrypted_notes/v0/mod.rs @@ -37,6 +37,28 @@ impl Drive { max_elements: u32, verify_subset_of_proof: bool, platform_version: &PlatformVersion, + ) -> Result<(RootHash, Vec, u64), Error> { + Self::verify_pool_encrypted_notes_v0( + proof, + shielded_credit_pool_path_vec(), + start_index, + count, + max_elements, + verify_subset_of_proof, + platform_version, + ) + } + + /// Verifies an encrypted-notes proof against the notes tree of the pool at `pool_path`, + /// whichever shielded pool (credit or token) it is. + pub(super) fn verify_pool_encrypted_notes_v0( + proof: &[u8], + pool_path: Vec>, + start_index: u64, + count: u32, + max_elements: u32, + verify_subset_of_proof: bool, + platform_version: &PlatformVersion, ) -> Result<(RootHash, Vec, u64), Error> { if max_elements == 0 { return Err(Error::Drive(DriveError::CorruptedElementType( @@ -78,7 +100,7 @@ impl Drive { ); let path_query = PathQuery { - path: shielded_credit_pool_path_vec(), + path: pool_path.clone(), query: SizedQuery { query: Query { read_mode: None, @@ -163,7 +185,7 @@ impl Drive { // `verify_subset_of_proof`) because this element query is, by // construction, a sub-portion of the larger proof. let count_path_query = PathQuery { - path: shielded_credit_pool_path_vec(), + path: pool_path.clone(), query: SizedQuery { query: Query::new_single_key(vec![SHIELDED_NOTES_KEY]), limit: Some(1), diff --git a/packages/rs-drive/src/verify/shielded/verify_shielded_notes_count/mod.rs b/packages/rs-drive/src/verify/shielded/verify_shielded_notes_count/mod.rs index fe3cf480b28..c5018c8e6c8 100644 --- a/packages/rs-drive/src/verify/shielded/verify_shielded_notes_count/mod.rs +++ b/packages/rs-drive/src/verify/shielded/verify_shielded_notes_count/mod.rs @@ -1,5 +1,6 @@ mod v0; +use crate::drive::shielded::paths::token_shielded_pool_path_vec; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; @@ -39,6 +40,37 @@ impl Drive { } } +impl Drive { + /// Verifies a proof of a TOKEN shielded pool's notes count. Same versioning as + /// [`Drive::verify_shielded_notes_count`]. + pub fn verify_token_shielded_pool_notes_count( + proof: &[u8], + token_id: [u8; 32], + verify_subset_of_proof: bool, + platform_version: &PlatformVersion, + ) -> Result<(RootHash, Option), Error> { + match platform_version + .drive + .methods + .verify + .shielded + .verify_shielded_notes_count + { + 0 => Self::verify_pool_notes_count_v0( + proof, + token_shielded_pool_path_vec(token_id), + verify_subset_of_proof, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "verify_token_shielded_pool_notes_count".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/rs-drive/src/verify/shielded/verify_shielded_notes_count/v0/mod.rs b/packages/rs-drive/src/verify/shielded/verify_shielded_notes_count/v0/mod.rs index e62d7de4fe9..50776dce93e 100644 --- a/packages/rs-drive/src/verify/shielded/verify_shielded_notes_count/v0/mod.rs +++ b/packages/rs-drive/src/verify/shielded/verify_shielded_notes_count/v0/mod.rs @@ -11,6 +11,22 @@ impl Drive { proof: &[u8], verify_subset_of_proof: bool, platform_version: &PlatformVersion, + ) -> Result<(RootHash, Option), Error> { + Self::verify_pool_notes_count_v0( + proof, + shielded_credit_pool_path_vec(), + verify_subset_of_proof, + platform_version, + ) + } + + /// Verifies the notes count of the pool at `pool_path`, whichever shielded pool (credit or + /// token) it is. + pub(super) fn verify_pool_notes_count_v0( + proof: &[u8], + pool_path: Vec>, + verify_subset_of_proof: bool, + platform_version: &PlatformVersion, ) -> Result<(RootHash, Option), Error> { // Single-key query that lands on the notes `CommitmentTree` // element itself (no subquery into its contents). Because the @@ -22,7 +38,7 @@ impl Drive { // except the proved element is a `CommitmentTree` (decode field // 0, `total_count`) rather than a `SumItem`. let path_query = PathQuery { - path: shielded_credit_pool_path_vec(), + path: pool_path, query: SizedQuery { query: Query::new_single_key(vec![SHIELDED_NOTES_KEY]), limit: Some(1), diff --git a/packages/rs-drive/src/verify/shielded/verify_shielded_nullifiers/mod.rs b/packages/rs-drive/src/verify/shielded/verify_shielded_nullifiers/mod.rs index 70b0f2234b3..53bbf97a8f2 100644 --- a/packages/rs-drive/src/verify/shielded/verify_shielded_nullifiers/mod.rs +++ b/packages/rs-drive/src/verify/shielded/verify_shielded_nullifiers/mod.rs @@ -1,5 +1,6 @@ mod v0; +use crate::drive::shielded::paths::token_shielded_pool_nullifiers_path_vec; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; @@ -37,6 +38,40 @@ impl Drive { } } +impl Drive { + /// Verifies a proof of nullifier spent statuses in a TOKEN shielded pool. Same versioning + /// as [`Drive::verify_shielded_nullifiers`]. + #[allow(clippy::type_complexity)] + pub fn verify_token_shielded_pool_nullifiers( + proof: &[u8], + token_id: [u8; 32], + nullifiers: &[Vec], + verify_subset_of_proof: bool, + platform_version: &PlatformVersion, + ) -> Result<(RootHash, Vec<(Vec, bool)>), Error> { + match platform_version + .drive + .methods + .verify + .shielded + .verify_shielded_nullifiers + { + 0 => Self::verify_pool_nullifiers_v0( + proof, + token_shielded_pool_nullifiers_path_vec(token_id), + nullifiers, + verify_subset_of_proof, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "verify_token_shielded_pool_nullifiers".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/rs-drive/src/verify/shielded/verify_shielded_nullifiers/v0/mod.rs b/packages/rs-drive/src/verify/shielded/verify_shielded_nullifiers/v0/mod.rs index c2a2e60564c..5e922d945a3 100644 --- a/packages/rs-drive/src/verify/shielded/verify_shielded_nullifiers/v0/mod.rs +++ b/packages/rs-drive/src/verify/shielded/verify_shielded_nullifiers/v0/mod.rs @@ -12,12 +12,31 @@ impl Drive { nullifiers: &[Vec], verify_subset_of_proof: bool, platform_version: &PlatformVersion, + ) -> Result<(RootHash, Vec<(Vec, bool)>), Error> { + Self::verify_pool_nullifiers_v0( + proof, + shielded_credit_pool_nullifiers_path_vec(), + nullifiers, + verify_subset_of_proof, + platform_version, + ) + } + + /// Verifies nullifier spent statuses against the nullifiers tree at `nullifiers_path`, + /// whichever shielded pool (credit or token) it belongs to. + #[allow(clippy::type_complexity)] + pub(super) fn verify_pool_nullifiers_v0( + proof: &[u8], + nullifiers_path: Vec>, + nullifiers: &[Vec], + verify_subset_of_proof: bool, + platform_version: &PlatformVersion, ) -> Result<(RootHash, Vec<(Vec, bool)>), Error> { let mut query = Query::new(); query.insert_keys(nullifiers.to_vec()); let path_query = PathQuery { - path: shielded_credit_pool_nullifiers_path_vec(), + path: nullifiers_path, query: SizedQuery { query, limit: Some(u16::try_from(nullifiers.len()).map_err(|_| { diff --git a/packages/rs-drive/src/verify/shielded/verify_shielded_pool_state/mod.rs b/packages/rs-drive/src/verify/shielded/verify_shielded_pool_state/mod.rs index b4455772438..f05fc839e8f 100644 --- a/packages/rs-drive/src/verify/shielded/verify_shielded_pool_state/mod.rs +++ b/packages/rs-drive/src/verify/shielded/verify_shielded_pool_state/mod.rs @@ -1,5 +1,6 @@ mod v0; +use crate::drive::shielded::paths::token_shielded_pool_path_vec; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; @@ -32,6 +33,37 @@ impl Drive { } } +impl Drive { + /// Verifies a proof for a TOKEN shielded pool's total balance (the amount of the token + /// currently shielded). Same versioning as [`Drive::verify_shielded_pool_state`]. + pub fn verify_token_shielded_pool_state( + proof: &[u8], + token_id: [u8; 32], + verify_subset_of_proof: bool, + platform_version: &PlatformVersion, + ) -> Result<(RootHash, Option), Error> { + match platform_version + .drive + .methods + .verify + .shielded + .verify_shielded_pool_state + { + 0 => Self::verify_pool_state_v0( + proof, + token_shielded_pool_path_vec(token_id), + verify_subset_of_proof, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "verify_token_shielded_pool_state".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/rs-drive/src/verify/shielded/verify_shielded_pool_state/v0/mod.rs b/packages/rs-drive/src/verify/shielded/verify_shielded_pool_state/v0/mod.rs index 0a1f202f7f7..10dc1d171b8 100644 --- a/packages/rs-drive/src/verify/shielded/verify_shielded_pool_state/v0/mod.rs +++ b/packages/rs-drive/src/verify/shielded/verify_shielded_pool_state/v0/mod.rs @@ -11,9 +11,25 @@ impl Drive { proof: &[u8], verify_subset_of_proof: bool, platform_version: &PlatformVersion, + ) -> Result<(RootHash, Option), Error> { + Self::verify_pool_state_v0( + proof, + shielded_credit_pool_path_vec(), + verify_subset_of_proof, + platform_version, + ) + } + + /// Verifies the total balance item of the pool at `pool_path`, whichever shielded pool + /// (credit or token) it is. + pub(super) fn verify_pool_state_v0( + proof: &[u8], + pool_path: Vec>, + verify_subset_of_proof: bool, + platform_version: &PlatformVersion, ) -> Result<(RootHash, Option), Error> { let path_query = PathQuery { - path: shielded_credit_pool_path_vec(), + path: pool_path, query: SizedQuery { query: Query::new_single_key(vec![SHIELDED_TOTAL_BALANCE_KEY]), limit: Some(1), diff --git a/packages/rs-drive/src/verify/state_transition/state_transition_execution_path_queries/token_transition.rs b/packages/rs-drive/src/verify/state_transition/state_transition_execution_path_queries/token_transition.rs index af6665127cd..9494cee0397 100644 --- a/packages/rs-drive/src/verify/state_transition/state_transition_execution_path_queries/token_transition.rs +++ b/packages/rs-drive/src/verify/state_transition/state_transition_execution_path_queries/token_transition.rs @@ -1,3 +1,4 @@ +use crate::drive::shielded::paths::token_shielded_pool_nullifiers_path_query; use crate::drive::Drive; use crate::error::Error; use crate::query::{SingleDocumentDriveQuery, SingleDocumentDriveQueryContestedStatus}; @@ -16,8 +17,10 @@ use dpp::state_transition::batch_transition::batched_transition::token_transitio use dpp::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; use dpp::state_transition::batch_transition::token_freeze_transition::v0::v0_methods::TokenFreezeTransitionV0Methods; use dpp::state_transition::batch_transition::token_mint_transition::v0::v0_methods::TokenMintTransitionV0Methods; +use dpp::state_transition::batch_transition::token_shielded_transfer_transition::v0::v0_methods::TokenShieldedTransferTransitionV0Methods; use dpp::state_transition::batch_transition::token_transfer_transition::v0::v0_methods::TokenTransferTransitionV0Methods; use dpp::state_transition::batch_transition::token_unfreeze_transition::v0::v0_methods::TokenUnfreezeTransitionV0Methods; +use dpp::state_transition::batch_transition::token_unshield_transition::v0::v0_methods::TokenUnshieldTransitionV0Methods; use dpp::system_data_contracts::load_system_data_contract; use grovedb::PathQuery; use platform_version::version::PlatformVersion; @@ -158,6 +161,27 @@ impl TryTransitionIntoPathQuery for TokenTransition { | TokenTransition::Claim(_) => { create_token_historical_document_query(self, owner_id, platform_version)? } + // Shielded token operations keep no history. A shield or unshield is attested by + // the identity balance it moved; a shielded transfer by the nullifiers it spent, + // which now exist in the token pool. + TokenTransition::Shield(_) => Drive::token_balance_for_identity_id_query( + token_id.to_buffer(), + owner_id.to_buffer(), + ), + TokenTransition::Unshield(token_unshield_transition) => { + Drive::token_balance_for_identity_id_query( + token_id.to_buffer(), + token_unshield_transition.recipient_id().to_buffer(), + ) + } + TokenTransition::ShieldedTransfer(token_shielded_transfer_transition) => { + let nullifiers: Vec<[u8; 32]> = token_shielded_transfer_transition + .actions() + .iter() + .map(|action| action.nullifier) + .collect(); + token_shielded_pool_nullifiers_path_query(token_id.to_buffer(), &nullifiers) + } }; if let Some(group_state_transition_info) = self.base().using_group_info() { diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs index a3958140802..55f075e6735 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs @@ -55,6 +55,8 @@ use dpp::state_transition::batch_transition::token_base_transition::v0::v0_metho use dpp::state_transition::batch_transition::token_freeze_transition::v0::v0_methods::TokenFreezeTransitionV0Methods; use dpp::state_transition::batch_transition::token_mint_transition::v0::v0_methods::TokenMintTransitionV0Methods; use dpp::state_transition::batch_transition::token_transfer_transition::v0::v0_methods::TokenTransferTransitionV0Methods; +use dpp::state_transition::batch_transition::token_shielded_transfer_transition::v0::v0_methods::TokenShieldedTransferTransitionV0Methods; +use dpp::state_transition::batch_transition::token_unshield_transition::v0::v0_methods::TokenUnshieldTransitionV0Methods; use dpp::state_transition::batch_transition::token_unfreeze_transition::v0::v0_methods::TokenUnfreezeTransitionV0Methods; use dpp::state_transition::address_credit_withdrawal_transition::accessors::AddressCreditWithdrawalTransitionAccessorsV0; use dpp::state_transition::identity_credit_transfer_transition::accessors::IdentityCreditTransferTransitionAccessorsV0; @@ -1023,6 +1025,65 @@ impl Drive { | TokenTransition::EmergencyAction(_) | TokenTransition::ConfigUpdate(_) | TokenTransition::Claim(_) => historical_query(), + TokenTransition::Shield(_) => { + let (root_hash, Some(balance)) = + Drive::verify_token_balance_for_identity_id( + proof, + token_id.into_buffer(), + owner_id.into_buffer(), + false, + platform_version, + )? + else { + return Err(Error::Proof(ProofError::IncorrectProof( + format!("proof did not contain token balance for identity {} expected to exist because of state transition (token shield)", owner_id)))); + }; + Ok((root_hash, VerifiedTokenBalance(owner_id, balance))) + } + TokenTransition::Unshield(token_unshield_transition) => { + let recipient_id = token_unshield_transition.recipient_id(); + let (root_hash, Some(balance)) = + Drive::verify_token_balance_for_identity_id( + proof, + token_id.into_buffer(), + recipient_id.into_buffer(), + false, + platform_version, + )? + else { + return Err(Error::Proof(ProofError::IncorrectProof( + format!("proof did not contain token balance for identity {} expected to exist because of state transition (token unshield)", recipient_id)))); + }; + Ok((root_hash, VerifiedTokenBalance(recipient_id, balance))) + } + TokenTransition::ShieldedTransfer( + token_shielded_transfer_transition, + ) => { + let nullifiers: Vec> = token_shielded_transfer_transition + .actions() + .iter() + .map(|action| action.nullifier.to_vec()) + .collect(); + let (root_hash, statuses) = + Drive::verify_token_shielded_pool_nullifiers( + proof, + token_id.into_buffer(), + &nullifiers, + false, + platform_version, + )?; + if statuses.len() != nullifiers.len() + || statuses.iter().any(|(_, spent)| !spent) + { + return Err(Error::Proof(ProofError::IncorrectProof( + "proof did not show every nullifier of the token shielded transfer as spent".to_string(), + ))); + } + Ok(( + root_hash, + dpp::state_transition::proof_result::StateTransitionProofResult::VerifiedShieldedNullifiers(statuses), + )) + } } } } @@ -2631,6 +2692,11 @@ impl Drive { | TokenTransition::EmergencyAction(_) | TokenTransition::ConfigUpdate(_) | TokenTransition::Claim(_) => true, + // Balance snapshots: the moved amount cannot be tied to one shield or + // unshield. A shielded transfer's spent nullifiers exist only if it + // executed. + TokenTransition::Shield(_) | TokenTransition::Unshield(_) => false, + TokenTransition::ShieldedTransfer(_) => true, } } }, diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs index ce45a9b8f5e..14fc0e7e10e 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs @@ -62,6 +62,10 @@ pub struct TokenVersions { /// `TokenPreProgrammedDistribution::validate_amounts`. Called from protocol version 14 on /// (data contract create `basic_structure` v2 and `DataContract::validate_update` v1). pub validate_pre_programmed_distribution_amounts: FeatureVersion, + /// Accepted `TokenConfiguration` format versions. `max_version` 0 admits only `V0`; 1 also + /// admits `V1`, which adds the per-token shielded pool flag. Contract creates and updates + /// carrying a format above the bound are rejected with `UnsupportedVersionError`. + pub token_configuration_format: FeatureVersionBounds, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v1.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v1.rs index 5d8268e5c02..9144fa1bfb3 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v1.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v1.rs @@ -67,5 +67,10 @@ pub const CONTRACT_VERSIONS_V1: DPPContractVersions = DPPContractVersions { token_versions: TokenVersions { validate_structure_interval: 0, validate_pre_programmed_distribution_amounts: 0, + token_configuration_format: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, }, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v2.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v2.rs index 0adcf1900fa..20c5bc6b842 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v2.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v2.rs @@ -67,5 +67,10 @@ pub const CONTRACT_VERSIONS_V2: DPPContractVersions = DPPContractVersions { token_versions: TokenVersions { validate_structure_interval: 0, validate_pre_programmed_distribution_amounts: 0, + token_configuration_format: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, }, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v3.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v3.rs index 8aa7bcfd4ff..51ab0259484 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v3.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v3.rs @@ -69,5 +69,10 @@ pub const CONTRACT_VERSIONS_V3: DPPContractVersions = DPPContractVersions { token_versions: TokenVersions { validate_structure_interval: 0, validate_pre_programmed_distribution_amounts: 0, + token_configuration_format: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, }, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs index c7b75d7bd50..6e95d24ad34 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v4.rs @@ -69,5 +69,10 @@ pub const CONTRACT_VERSIONS_V4: DPPContractVersions = DPPContractVersions { token_versions: TokenVersions { validate_structure_interval: 0, validate_pre_programmed_distribution_amounts: 0, + token_configuration_format: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, }, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v5.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v5.rs index e192b402bdc..bd934d8d7c3 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v5.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v5.rs @@ -71,5 +71,10 @@ pub const CONTRACT_VERSIONS_V5: DPPContractVersions = DPPContractVersions { token_versions: TokenVersions { validate_structure_interval: 0, validate_pre_programmed_distribution_amounts: 0, + token_configuration_format: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, }, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v6.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v6.rs index a5ec478500c..38972bb758e 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v6.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v6.rs @@ -116,5 +116,12 @@ pub const CONTRACT_VERSIONS_V6: DPPContractVersions = DPPContractVersions { // 1: an epoch-based perpetual distribution needs an interval of at least one epoch. validate_structure_interval: 1, validate_pre_programmed_distribution_amounts: 0, + // changed: `TokenConfigurationV1` (the per-token shielded pool flag) is admitted from + // protocol version 14; v5 keeps max 0 so a pre-activation contract cannot carry it. + token_configuration_format: FeatureVersionBounds { + min_version: 0, + max_version: 1, + default_current_version: 0, + }, }, }; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/mod.rs index 3b1499e29cd..c69dd339fe1 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/mod.rs @@ -139,6 +139,9 @@ pub struct DriveAbciBlockEndMethodVersions { pub update_checkpoints: OptionalFeatureVersion, pub record_shielded_pool_anchor: OptionalFeatureVersion, pub prune_shielded_pool_anchors: OptionalFeatureVersion, + /// Records the anchor of every token shielded pool a block touched and prunes that + /// pool's anchors older than the retention window. `None` before token pools exist. + pub record_token_shielded_pool_anchors: OptionalFeatureVersion, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v1.rs index 1356dbdaac1..a29349dc2a4 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v1.rs @@ -128,6 +128,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V1: DriveAbciMethodVersions = DriveAbciMeth update_checkpoints: None, record_shielded_pool_anchor: None, prune_shielded_pool_anchors: None, + record_token_shielded_pool_anchors: None, }, platform_state_storage: DriveAbciPlatformStateStorageMethodVersions { fetch_platform_state: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs index 49de2a0083f..f8580e95fcf 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs @@ -139,6 +139,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V10: DriveAbciMethodVersions = DriveAbciMet update_checkpoints: Some(0), record_shielded_pool_anchor: Some(0), prune_shielded_pool_anchors: Some(0), + record_token_shielded_pool_anchors: Some(0), // changed: per-token shielded pool anchors }, platform_state_storage: DriveAbciPlatformStateStorageMethodVersions { fetch_platform_state: 1, // changed: reads structure 1 records together with their masternode and validator set entries diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v2.rs index a648c5553d5..904a330cf64 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v2.rs @@ -129,6 +129,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V2: DriveAbciMethodVersions = DriveAbciMeth update_checkpoints: None, record_shielded_pool_anchor: None, prune_shielded_pool_anchors: None, + record_token_shielded_pool_anchors: None, }, platform_state_storage: DriveAbciPlatformStateStorageMethodVersions { fetch_platform_state: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v3.rs index dc4873efd51..e18de471b2a 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v3.rs @@ -128,6 +128,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V3: DriveAbciMethodVersions = DriveAbciMeth update_checkpoints: None, record_shielded_pool_anchor: None, prune_shielded_pool_anchors: None, + record_token_shielded_pool_anchors: None, }, platform_state_storage: DriveAbciPlatformStateStorageMethodVersions { fetch_platform_state: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v4.rs index 6aa6c34ac4c..64952baf683 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v4.rs @@ -128,6 +128,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V4: DriveAbciMethodVersions = DriveAbciMeth update_checkpoints: None, record_shielded_pool_anchor: None, prune_shielded_pool_anchors: None, + record_token_shielded_pool_anchors: None, }, platform_state_storage: DriveAbciPlatformStateStorageMethodVersions { fetch_platform_state: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v5.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v5.rs index 8772809b224..531ba350c3f 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v5.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v5.rs @@ -132,6 +132,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V5: DriveAbciMethodVersions = DriveAbciMeth update_checkpoints: None, record_shielded_pool_anchor: None, prune_shielded_pool_anchors: None, + record_token_shielded_pool_anchors: None, }, platform_state_storage: DriveAbciPlatformStateStorageMethodVersions { fetch_platform_state: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v6.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v6.rs index 71f03c433f5..df1539d587d 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v6.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v6.rs @@ -130,6 +130,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V6: DriveAbciMethodVersions = DriveAbciMeth update_checkpoints: None, record_shielded_pool_anchor: None, prune_shielded_pool_anchors: None, + record_token_shielded_pool_anchors: None, }, platform_state_storage: DriveAbciPlatformStateStorageMethodVersions { fetch_platform_state: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v7.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v7.rs index a03a16bb9eb..5d5189cdb10 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v7.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v7.rs @@ -139,6 +139,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V7: DriveAbciMethodVersions = DriveAbciMeth // the `[52, "M"]` subtree they read does not exist on v11. record_shielded_pool_anchor: None, prune_shielded_pool_anchors: None, + record_token_shielded_pool_anchors: None, }, platform_state_storage: DriveAbciPlatformStateStorageMethodVersions { fetch_platform_state: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v8.rs index a89543c3f84..9f9a6d5c87e 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v8.rs @@ -139,6 +139,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V8: DriveAbciMethodVersions = DriveAbciMeth update_checkpoints: Some(0), record_shielded_pool_anchor: Some(0), prune_shielded_pool_anchors: Some(0), + record_token_shielded_pool_anchors: None, }, platform_state_storage: DriveAbciPlatformStateStorageMethodVersions { fetch_platform_state: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v9.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v9.rs index 47e32f39da8..5f2f4125fe3 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v9.rs @@ -157,6 +157,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V9: DriveAbciMethodVersions = DriveAbciMeth update_checkpoints: Some(0), record_shielded_pool_anchor: Some(0), prune_shielded_pool_anchors: Some(0), + record_token_shielded_pool_anchors: None, }, platform_state_storage: DriveAbciPlatformStateStorageMethodVersions { fetch_platform_state: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs index 06b46921a71..5ca2e84ce8f 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs @@ -33,6 +33,12 @@ pub struct DriveAbciValidationConstants { /// transitions (Unshield, ShieldedWithdrawal) are allowed. This ensures a /// sufficient anonymity set before funds can leave the pool. pub minimum_pool_notes_for_outgoing: u64, + /// Minimum number of notes a TOKEN shielded pool must hold before a `TokenUnshield` + /// (the outflow with an observable destination) is allowed. Token pools start empty + /// and are small, so this is 0 at introduction: a floor would trap the first + /// depositors until enough other holders shielded. The knob exists so a later version + /// can raise it once per-token anonymity sets are meaningful. + pub minimum_token_pool_notes_for_outgoing: u64, /// Number of blocks of anchors to retain. Anchors older than this are /// pruned at the end of each block. Clients must use an anchor no older /// than this many blocks when building shielded transactions. @@ -288,6 +294,12 @@ pub struct DriveAbciDocumentsStateTransitionValidationVersions { pub token_direct_purchase_transition_state_validation: FeatureVersion, pub token_set_price_for_direct_purchase_transition_structure_validation: FeatureVersion, pub token_set_price_for_direct_purchase_transition_state_validation: FeatureVersion, + pub token_shield_transition_structure_validation: FeatureVersion, + pub token_shield_transition_state_validation: FeatureVersion, + pub token_unshield_transition_structure_validation: FeatureVersion, + pub token_unshield_transition_state_validation: FeatureVersion, + pub token_shielded_transfer_transition_structure_validation: FeatureVersion, + pub token_shielded_transfer_transition_state_validation: FeatureVersion, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs index 5c4a32fe8f1..fa8d2a10818 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs @@ -193,6 +193,12 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V1: DriveAbciValidationVersions = token_direct_purchase_transition_state_validation: 0, token_set_price_for_direct_purchase_transition_structure_validation: 0, token_set_price_for_direct_purchase_transition_state_validation: 0, + token_shield_transition_structure_validation: 0, + token_shield_transition_state_validation: 0, + token_unshield_transition_structure_validation: 0, + token_unshield_transition_state_validation: 0, + token_shielded_transfer_transition_structure_validation: 0, + token_shielded_transfer_transition_state_validation: 0, }, identity_create_from_addresses_state_transition: DriveAbciStateTransitionValidationVersion { @@ -322,6 +328,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V1: DriveAbciValidationVersions = maximum_vote_polls_to_process: 2, maximum_contenders_to_consider: 100, minimum_pool_notes_for_outgoing: 250, + minimum_token_pool_notes_for_outgoing: 0, shielded_anchor_retention_blocks: 1000, shielded_anchor_pruning_interval: 100, shielded_proof_verification_fee: 100_000_000, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs index bf3417cbf7b..ff32be56541 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs @@ -262,6 +262,12 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = token_direct_purchase_transition_state_validation: 0, token_set_price_for_direct_purchase_transition_structure_validation: 0, token_set_price_for_direct_purchase_transition_state_validation: 0, + token_shield_transition_structure_validation: 0, + token_shield_transition_state_validation: 0, + token_unshield_transition_structure_validation: 0, + token_unshield_transition_state_validation: 0, + token_shielded_transfer_transition_structure_validation: 0, + token_shielded_transfer_transition_state_validation: 0, }, identity_create_from_addresses_state_transition: DriveAbciStateTransitionValidationVersion { @@ -391,6 +397,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = maximum_vote_polls_to_process: 2, maximum_contenders_to_consider: 100, minimum_pool_notes_for_outgoing: 250, + minimum_token_pool_notes_for_outgoing: 0, shielded_anchor_retention_blocks: 1000, shielded_anchor_pruning_interval: 100, // Rebalanced for protocol 14: one Halo 2 bundle verification is diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs index 0155575b4b8..c20376a1649 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs @@ -193,6 +193,12 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V2: DriveAbciValidationVersions = token_direct_purchase_transition_state_validation: 0, token_set_price_for_direct_purchase_transition_structure_validation: 0, token_set_price_for_direct_purchase_transition_state_validation: 0, + token_shield_transition_structure_validation: 0, + token_shield_transition_state_validation: 0, + token_unshield_transition_structure_validation: 0, + token_unshield_transition_state_validation: 0, + token_shielded_transfer_transition_structure_validation: 0, + token_shielded_transfer_transition_state_validation: 0, }, identity_create_from_addresses_state_transition: DriveAbciStateTransitionValidationVersion { @@ -322,6 +328,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V2: DriveAbciValidationVersions = maximum_vote_polls_to_process: 2, maximum_contenders_to_consider: 100, minimum_pool_notes_for_outgoing: 250, + minimum_token_pool_notes_for_outgoing: 0, shielded_anchor_retention_blocks: 1000, shielded_anchor_pruning_interval: 100, shielded_proof_verification_fee: 100_000_000, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs index 1bad8364568..53f1a1be613 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs @@ -193,6 +193,12 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V3: DriveAbciValidationVersions = token_direct_purchase_transition_state_validation: 0, token_set_price_for_direct_purchase_transition_structure_validation: 0, token_set_price_for_direct_purchase_transition_state_validation: 0, + token_shield_transition_structure_validation: 0, + token_shield_transition_state_validation: 0, + token_unshield_transition_structure_validation: 0, + token_unshield_transition_state_validation: 0, + token_shielded_transfer_transition_structure_validation: 0, + token_shielded_transfer_transition_state_validation: 0, }, identity_create_from_addresses_state_transition: DriveAbciStateTransitionValidationVersion { @@ -322,6 +328,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V3: DriveAbciValidationVersions = maximum_vote_polls_to_process: 2, maximum_contenders_to_consider: 100, minimum_pool_notes_for_outgoing: 250, + minimum_token_pool_notes_for_outgoing: 0, shielded_anchor_retention_blocks: 1000, shielded_anchor_pruning_interval: 100, shielded_proof_verification_fee: 100_000_000, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs index 1ec242e2dcb..2f84c258196 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs @@ -196,6 +196,12 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V4: DriveAbciValidationVersions = token_direct_purchase_transition_state_validation: 0, token_set_price_for_direct_purchase_transition_structure_validation: 0, token_set_price_for_direct_purchase_transition_state_validation: 0, + token_shield_transition_structure_validation: 0, + token_shield_transition_state_validation: 0, + token_unshield_transition_structure_validation: 0, + token_unshield_transition_state_validation: 0, + token_shielded_transfer_transition_structure_validation: 0, + token_shielded_transfer_transition_state_validation: 0, }, identity_create_from_addresses_state_transition: DriveAbciStateTransitionValidationVersion { @@ -325,6 +331,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V4: DriveAbciValidationVersions = maximum_vote_polls_to_process: 2, maximum_contenders_to_consider: 100, minimum_pool_notes_for_outgoing: 250, + minimum_token_pool_notes_for_outgoing: 0, shielded_anchor_retention_blocks: 1000, shielded_anchor_pruning_interval: 100, shielded_proof_verification_fee: 100_000_000, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs index 09e0ada5244..23ffdfd9822 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs @@ -197,6 +197,12 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V5: DriveAbciValidationVersions = token_direct_purchase_transition_state_validation: 0, token_set_price_for_direct_purchase_transition_structure_validation: 0, token_set_price_for_direct_purchase_transition_state_validation: 0, + token_shield_transition_structure_validation: 0, + token_shield_transition_state_validation: 0, + token_unshield_transition_structure_validation: 0, + token_unshield_transition_state_validation: 0, + token_shielded_transfer_transition_structure_validation: 0, + token_shielded_transfer_transition_state_validation: 0, }, identity_create_from_addresses_state_transition: DriveAbciStateTransitionValidationVersion { @@ -326,6 +332,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V5: DriveAbciValidationVersions = maximum_vote_polls_to_process: 2, maximum_contenders_to_consider: 100, minimum_pool_notes_for_outgoing: 250, + minimum_token_pool_notes_for_outgoing: 0, shielded_anchor_retention_blocks: 1000, shielded_anchor_pruning_interval: 100, shielded_proof_verification_fee: 100_000_000, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs index cbf5c7e6e41..c043c033a05 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs @@ -200,6 +200,12 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V6: DriveAbciValidationVersions = token_direct_purchase_transition_state_validation: 0, token_set_price_for_direct_purchase_transition_structure_validation: 0, token_set_price_for_direct_purchase_transition_state_validation: 0, + token_shield_transition_structure_validation: 0, + token_shield_transition_state_validation: 0, + token_unshield_transition_structure_validation: 0, + token_unshield_transition_state_validation: 0, + token_shielded_transfer_transition_structure_validation: 0, + token_shielded_transfer_transition_state_validation: 0, }, identity_create_from_addresses_state_transition: DriveAbciStateTransitionValidationVersion { @@ -329,6 +335,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V6: DriveAbciValidationVersions = maximum_vote_polls_to_process: 2, maximum_contenders_to_consider: 100, minimum_pool_notes_for_outgoing: 250, + minimum_token_pool_notes_for_outgoing: 0, shielded_anchor_retention_blocks: 1000, shielded_anchor_pruning_interval: 100, shielded_proof_verification_fee: 100_000_000, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs index 2282a6065cb..a0c532c31e6 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs @@ -194,6 +194,12 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V7: DriveAbciValidationVersions = token_direct_purchase_transition_state_validation: 0, token_set_price_for_direct_purchase_transition_structure_validation: 0, token_set_price_for_direct_purchase_transition_state_validation: 0, + token_shield_transition_structure_validation: 0, + token_shield_transition_state_validation: 0, + token_unshield_transition_structure_validation: 0, + token_unshield_transition_state_validation: 0, + token_shielded_transfer_transition_structure_validation: 0, + token_shielded_transfer_transition_state_validation: 0, }, identity_create_from_addresses_state_transition: DriveAbciStateTransitionValidationVersion { @@ -323,6 +329,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V7: DriveAbciValidationVersions = maximum_vote_polls_to_process: 2, maximum_contenders_to_consider: 100, minimum_pool_notes_for_outgoing: 250, + minimum_token_pool_notes_for_outgoing: 0, shielded_anchor_retention_blocks: 1000, shielded_anchor_pruning_interval: 100, shielded_proof_verification_fee: 100_000_000, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs index b972fae1cbe..547e49230bb 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs @@ -248,6 +248,12 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = token_direct_purchase_transition_state_validation: 0, token_set_price_for_direct_purchase_transition_structure_validation: 0, token_set_price_for_direct_purchase_transition_state_validation: 0, + token_shield_transition_structure_validation: 0, + token_shield_transition_state_validation: 0, + token_unshield_transition_structure_validation: 0, + token_unshield_transition_state_validation: 0, + token_shielded_transfer_transition_structure_validation: 0, + token_shielded_transfer_transition_state_validation: 0, }, identity_create_from_addresses_state_transition: DriveAbciStateTransitionValidationVersion { @@ -377,6 +383,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = maximum_vote_polls_to_process: 2, maximum_contenders_to_consider: 100, minimum_pool_notes_for_outgoing: 250, + minimum_token_pool_notes_for_outgoing: 0, shielded_anchor_retention_blocks: 1000, shielded_anchor_pruning_interval: 100, shielded_proof_verification_fee: 100_000_000, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs index 2951f42e14f..c3968675fea 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs @@ -244,6 +244,12 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V9: DriveAbciValidationVersions = token_direct_purchase_transition_state_validation: 0, token_set_price_for_direct_purchase_transition_structure_validation: 0, token_set_price_for_direct_purchase_transition_state_validation: 0, + token_shield_transition_structure_validation: 0, + token_shield_transition_state_validation: 0, + token_unshield_transition_structure_validation: 0, + token_unshield_transition_state_validation: 0, + token_shielded_transfer_transition_structure_validation: 0, + token_shielded_transfer_transition_state_validation: 0, }, identity_create_from_addresses_state_transition: DriveAbciStateTransitionValidationVersion { @@ -373,6 +379,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V9: DriveAbciValidationVersions = maximum_vote_polls_to_process: 2, maximum_contenders_to_consider: 100, minimum_pool_notes_for_outgoing: 250, + minimum_token_pool_notes_for_outgoing: 0, shielded_anchor_retention_blocks: 1000, shielded_anchor_pruning_interval: 100, shielded_proof_verification_fee: 100_000_000, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs index 886410121d3..f94a5b5ea14 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs @@ -62,6 +62,9 @@ pub struct DriveStateTransitionActionConvertToHighLevelOperationsMethodVersions pub token_claim_transition: FeatureVersion, pub token_direct_purchase_transition: FeatureVersion, pub token_set_price_for_direct_purchase_transition: FeatureVersion, + pub token_shield_transition: FeatureVersion, + pub token_unshield_transition: FeatureVersion, + pub token_shielded_transfer_transition: FeatureVersion, pub identity_credit_transfer_to_addresses_transition: FeatureVersion, pub address_funds_transfer_transition: FeatureVersion, pub address_credit_withdrawal_transition: FeatureVersion, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v1.rs index 7ab6be15f10..56835bb83ef 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v1.rs @@ -50,6 +50,9 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V1: DriveStateTransitionMethodV token_claim_transition: 0, token_direct_purchase_transition: 0, token_set_price_for_direct_purchase_transition: 0, + token_shield_transition: 0, + token_unshield_transition: 0, + token_shielded_transfer_transition: 0, identity_credit_transfer_to_addresses_transition: 0, address_funds_transfer_transition: 0, address_credit_withdrawal_transition: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v2.rs index 5fb33d6d22c..8ea5c9e8cac 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v2.rs @@ -51,6 +51,9 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V2: DriveStateTransitionMethodV token_claim_transition: 0, token_direct_purchase_transition: 0, token_set_price_for_direct_purchase_transition: 0, + token_shield_transition: 0, + token_unshield_transition: 0, + token_shielded_transfer_transition: 0, identity_credit_transfer_to_addresses_transition: 0, address_funds_transfer_transition: 0, address_credit_withdrawal_transition: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v3.rs index 817cdc582c9..ecb2f07d6ec 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v3.rs @@ -55,6 +55,9 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V3: DriveStateTransitionMethodV token_claim_transition: 0, token_direct_purchase_transition: 0, token_set_price_for_direct_purchase_transition: 0, + token_shield_transition: 0, + token_unshield_transition: 0, + token_shielded_transfer_transition: 0, identity_credit_transfer_to_addresses_transition: 0, address_funds_transfer_transition: 0, address_credit_withdrawal_transition: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs index b4e6636725c..520abac3ddd 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs @@ -55,6 +55,9 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4: DriveStateTransitionMethodV token_claim_transition: 0, token_direct_purchase_transition: 0, token_set_price_for_direct_purchase_transition: 0, + token_shield_transition: 0, + token_unshield_transition: 0, + token_shielded_transfer_transition: 0, identity_credit_transfer_to_addresses_transition: 0, address_funds_transfer_transition: 0, address_credit_withdrawal_transition: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/mod.rs index 7e222e183b0..65df6fe5236 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/mod.rs @@ -75,4 +75,13 @@ pub struct DriveTokenUpdateMethodVersions { pub unfreeze: FeatureVersion, pub apply_status: FeatureVersion, pub perpetual_distribution_next_event_for_identity_id: FeatureVersion, + /// Creates the per-token shielded pool subtree when a token with `has_shielded_pool` is + /// registered. + pub create_token_shielded_pool_trees: FeatureVersion, + /// Identity token balance -> token shielded pool. + pub shield: FeatureVersion, + /// Token shielded pool -> identity token balance. + pub unshield: FeatureVersion, + /// Pool-internal token transfer. + pub shielded_transfer: FeatureVersion, } diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v1.rs index bd52288fd26..87c21b0774a 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v1.rs @@ -52,6 +52,10 @@ pub const DRIVE_TOKEN_METHOD_VERSIONS_V1: DriveTokenMethodVersions = DriveTokenM unfreeze: 0, apply_status: 0, perpetual_distribution_next_event_for_identity_id: 0, + create_token_shielded_pool_trees: 0, + shield: 0, + unshield: 0, + shielded_transfer: 0, }, calculate_total_tokens_balance: 0, distribution: DriveTokenDistributionMethodVersions { diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v2.rs index 89a564d343d..22156b66455 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v2.rs @@ -19,6 +19,7 @@ use crate::version::drive_versions::drive_token_method_versions::{ /// default node; the processing fee drops by the existence read the later /// tokens no longer make. pub const DRIVE_TOKEN_METHOD_VERSIONS_V2: DriveTokenMethodVersions = DriveTokenMethodVersions { + calculate_total_tokens_balance: 1, distribution: DriveTokenDistributionMethodVersions { add_pre_programmed_distributions: 1, ..DRIVE_TOKEN_METHOD_VERSIONS_V1.distribution diff --git a/packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs b/packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs index c283d55151e..17da6c6f124 100644 --- a/packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs +++ b/packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs @@ -22,3 +22,7 @@ pub const CONTRACT_FEE_CLAIM_INITIAL_PROTOCOL_VERSION: ProtocolVersion = 14; /// version 14 and registered at genesis from that version on; below it the contract does /// not exist and lookups must report it absent. pub const APP_CONNECT_CONTRACT_INITIAL_PROTOCOL_VERSION: ProtocolVersion = 14; +/// Token shielded pools (per-token Orchard pools behind `TokenConfigurationV1::has_shielded_pool`, +/// with the `TokenShield`, `TokenUnshield` and `TokenShieldedTransfer` batch transitions) activate +/// with protocol version 14. +pub const TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION: ProtocolVersion = 14; diff --git a/packages/rs-platform-version/src/version/mocks/v3_test.rs b/packages/rs-platform-version/src/version/mocks/v3_test.rs index 12d62c79dc3..691daead273 100644 --- a/packages/rs-platform-version/src/version/mocks/v3_test.rs +++ b/packages/rs-platform-version/src/version/mocks/v3_test.rs @@ -163,6 +163,7 @@ pub const TEST_PLATFORM_V3: PlatformVersion = PlatformVersion { update_checkpoints: None, record_shielded_pool_anchor: None, prune_shielded_pool_anchors: None, + record_token_shielded_pool_anchors: None, }, platform_state_storage: DriveAbciPlatformStateStorageMethodVersions { fetch_platform_state: 0, diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index 2dcdc60b4e3..6cf76a00872 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -30,7 +30,7 @@ use crate::version::ProtocolVersion; pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; -/// v14 hosts six consensus changes: +/// v14 hosts seven consensus changes: /// /// 1. **Contract-level ranked aggregates**: an index can /// declare that its groups are rankable by an aggregate, so a query like @@ -145,6 +145,23 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// is determinism, not correct rounding: on a boundary tuple the host /// libm (glibc, macOS) can still be 1 ulp away, so anything predicting /// rewards with host math may differ from consensus by one unit. +/// 7. **Token shielded pools**: a token configuration in format version 1 +/// (`TokenConfiguration::V1`, admitted by `CONTRACT_VERSIONS_V6`'s +/// `token_configuration_format` bounds) can set `hasShieldedPool`, which +/// gives the token its own Orchard pool under +/// `[Tokens, TOKEN_SHIELDED_POOLS_KEY, token_id]` laid out like the credit +/// pool. Three batch token transitions (`TokenShield`, `TokenUnshield`, +/// `TokenShieldedTransfer`, `DRIVE_ABCI_VALIDATION_V10` / +/// `DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4`) move tokens between an +/// identity balance and the pool or inside it; the identity signs and pays +/// the fee in credits, the token id and owner id (and recipient and amount +/// for an unshield) are bound into the Orchard sighash, and the pool +/// balances are a term of the token conservation check +/// (`calculate_total_tokens_balance` v1 in `DRIVE_TOKEN_METHOD_VERSIONS_V2`). +/// `record_token_shielded_pool_anchors` records and prunes the anchors of +/// the pools a block touched. The pools root tree is inserted by the +/// upgrade transition and by `create_initial_state_structure` v4; the six +/// shielded queries accept an optional `token_id` to target a token pool. /// /// The first two are orthogonal by construction: the ranked upgrade decides the /// *property-name* tree type, the demotion decides the *value* tree type diff --git a/packages/rs-sdk/src/platform/query.rs b/packages/rs-sdk/src/platform/query.rs index 8854cc7d934..f64b8dfa488 100644 --- a/packages/rs-sdk/src/platform/query.rs +++ b/packages/rs-sdk/src/platform/query.rs @@ -51,6 +51,7 @@ use drive::query::{DriveDocumentQuery, VotePollsByEndDateDriveQuery}; use drive_proof_verifier::from_request::TryFromRequest; use drive_proof_verifier::types::{ KeysInPath, NoParamQuery, ShieldedEncryptedNotesQuery, ShieldedNullifiersQuery, + TokenShieldedEncryptedNotesQuery, TokenShieldedNullifiersQuery, TokenShieldedPoolQuery, }; use rs_dapi_client::transport::TransportRequest; use std::collections::BTreeSet; @@ -1312,7 +1313,10 @@ impl Query for NoParamQuery { Ok(GetShieldedPoolStateRequest { version: Some(get_shielded_pool_state_request::Version::V0( - get_shielded_pool_state_request::GetShieldedPoolStateRequestV0 { prove }, + get_shielded_pool_state_request::GetShieldedPoolStateRequestV0 { + prove, + token_id: None, + }, )), }) } @@ -1338,7 +1342,10 @@ impl Query for NoParamQuery { Ok(GetShieldedNotesCountRequest { version: Some(get_shielded_notes_count_request::Version::V0( - get_shielded_notes_count_request::GetShieldedNotesCountRequestV0 { prove }, + get_shielded_notes_count_request::GetShieldedNotesCountRequestV0 { + prove, + token_id: None, + }, )), }) } @@ -1356,7 +1363,10 @@ impl Query for NoParamQuery { Ok(GetShieldedAnchorsRequest { version: Some(get_shielded_anchors_request::Version::V0( - get_shielded_anchors_request::GetShieldedAnchorsRequestV0 { prove }, + get_shielded_anchors_request::GetShieldedAnchorsRequestV0 { + prove, + token_id: None, + }, )), }) } @@ -1376,6 +1386,7 @@ impl Query for NoParamQuery { version: Some(get_most_recent_shielded_anchor_request::Version::V0( get_most_recent_shielded_anchor_request::GetMostRecentShieldedAnchorRequestV0 { prove, + token_id: None, }, )), }) @@ -1398,6 +1409,7 @@ impl Query for ShieldedEncryptedNotesQuery { start_index: self.start_index, count: self.count, prove, + token_id: None, }, )), }) @@ -1419,6 +1431,141 @@ impl Query for ShieldedNullifiersQuery { get_shielded_nullifiers_request::GetShieldedNullifiersRequestV0 { nullifiers: self.0.iter().map(|n| n.to_vec()).collect(), prove, + token_id: None, + }, + )), + }) + } +} + +// --- Token Shielded Pool Queries (protocol version 14+) --- + +impl Query for TokenShieldedPoolQuery { + fn query( + &self, + settings: &crate::platform::QuerySettings<'_>, + ) -> Result { + let prove = settings.prove; + if !prove { + unimplemented!("queries without proofs are not supported yet"); + } + + Ok(GetShieldedPoolStateRequest { + version: Some(get_shielded_pool_state_request::Version::V0( + get_shielded_pool_state_request::GetShieldedPoolStateRequestV0 { + prove, + token_id: Some(self.token_id.to_vec()), + }, + )), + }) + } +} + +impl Query for TokenShieldedPoolQuery { + fn query( + &self, + settings: &crate::platform::QuerySettings<'_>, + ) -> Result { + let prove = settings.prove; + if !prove { + return Err(Error::Generic( + "GetShieldedNotesCount requires proofs; unproved queries are not supported" + .to_string(), + )); + } + + Ok(GetShieldedNotesCountRequest { + version: Some(get_shielded_notes_count_request::Version::V0( + get_shielded_notes_count_request::GetShieldedNotesCountRequestV0 { + prove, + token_id: Some(self.token_id.to_vec()), + }, + )), + }) + } +} + +impl Query for TokenShieldedPoolQuery { + fn query( + &self, + settings: &crate::platform::QuerySettings<'_>, + ) -> Result { + let prove = settings.prove; + if !prove { + unimplemented!("queries without proofs are not supported yet"); + } + + Ok(GetShieldedAnchorsRequest { + version: Some(get_shielded_anchors_request::Version::V0( + get_shielded_anchors_request::GetShieldedAnchorsRequestV0 { + prove, + token_id: Some(self.token_id.to_vec()), + }, + )), + }) + } +} + +impl Query for TokenShieldedPoolQuery { + fn query( + &self, + settings: &crate::platform::QuerySettings<'_>, + ) -> Result { + let prove = settings.prove; + if !prove { + unimplemented!("queries without proofs are not supported yet"); + } + + Ok(GetMostRecentShieldedAnchorRequest { + version: Some(get_most_recent_shielded_anchor_request::Version::V0( + get_most_recent_shielded_anchor_request::GetMostRecentShieldedAnchorRequestV0 { + prove, + token_id: Some(self.token_id.to_vec()), + }, + )), + }) + } +} + +impl Query for TokenShieldedEncryptedNotesQuery { + fn query( + &self, + settings: &crate::platform::QuerySettings<'_>, + ) -> Result { + let prove = settings.prove; + if !prove { + unimplemented!("queries without proofs are not supported yet"); + } + + Ok(GetShieldedEncryptedNotesRequest { + version: Some(get_shielded_encrypted_notes_request::Version::V0( + get_shielded_encrypted_notes_request::GetShieldedEncryptedNotesRequestV0 { + start_index: self.start_index, + count: self.count, + prove, + token_id: Some(self.token_id.to_vec()), + }, + )), + }) + } +} + +impl Query for TokenShieldedNullifiersQuery { + fn query( + &self, + settings: &crate::platform::QuerySettings<'_>, + ) -> Result { + let prove = settings.prove; + if !prove { + unimplemented!("queries without proofs are not supported yet"); + } + + Ok(GetShieldedNullifiersRequest { + version: Some(get_shielded_nullifiers_request::Version::V0( + get_shielded_nullifiers_request::GetShieldedNullifiersRequestV0 { + nullifiers: self.nullifiers.iter().map(|n| n.to_vec()).collect(), + prove, + token_id: Some(self.token_id.to_vec()), }, )), }) diff --git a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/mod.rs b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/mod.rs index 16659a63dde..c8d8cae5f2e 100644 --- a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/mod.rs +++ b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/mod.rs @@ -7,8 +7,11 @@ pub mod emergency_action; pub mod freeze; pub mod mint; pub mod set_price_for_direct_purchase; +pub mod shield; +pub mod shielded_transfer; pub mod transfer; pub mod unfreeze; +pub mod unshield; use crate::batch_transition::token_transition::burn::TokenBurnTransitionWasm; use crate::batch_transition::token_transition::claim::TokenClaimTransitionWasm; @@ -19,8 +22,11 @@ use crate::batch_transition::token_transition::emergency_action::TokenEmergencyA use crate::batch_transition::token_transition::freeze::TokenFreezeTransitionWasm; use crate::batch_transition::token_transition::mint::TokenMintTransitionWasm; use crate::batch_transition::token_transition::set_price_for_direct_purchase::TokenSetPriceForDirectPurchaseTransitionWasm; +use crate::batch_transition::token_transition::shield::TokenShieldTransitionWasm; +use crate::batch_transition::token_transition::shielded_transfer::TokenShieldedTransferTransitionWasm; use crate::batch_transition::token_transition::transfer::TokenTransferTransitionWasm; use crate::batch_transition::token_transition::unfreeze::TokenUnfreezeTransitionWasm; +use crate::batch_transition::token_transition::unshield::TokenUnshieldTransitionWasm; use crate::identifier::IdentifierWrapper; use dpp::prelude::IdentityNonce; use dpp::state_transition::batch_transition::batched_transition::token_transition::{ @@ -44,6 +50,9 @@ pub enum TokenTransitionType { ConfigUpdate, DirectPurchase, SetPriceForDirectPurchase, + Shield, + Unshield, + ShieldedTransfer, } impl From<&TokenTransition> for TokenTransitionType { @@ -62,6 +71,9 @@ impl From<&TokenTransition> for TokenTransitionType { TokenTransition::SetPriceForDirectPurchase(_) => { TokenTransitionType::SetPriceForDirectPurchase } + TokenTransition::Shield(_) => TokenTransitionType::Shield, + TokenTransition::Unshield(_) => TokenTransitionType::Unshield, + TokenTransition::ShieldedTransfer(_) => TokenTransitionType::ShieldedTransfer, } } } @@ -149,6 +161,15 @@ impl TokenTransitionWasm { TokenTransition::SetPriceForDirectPurchase(set_price) => { TokenSetPriceForDirectPurchaseTransitionWasm::from(set_price.clone()).into() } + TokenTransition::Shield(shield) => { + TokenShieldTransitionWasm::from(shield.clone()).into() + } + TokenTransition::Unshield(unshield) => { + TokenUnshieldTransitionWasm::from(unshield.clone()).into() + } + TokenTransition::ShieldedTransfer(shielded_transfer) => { + TokenShieldedTransferTransitionWasm::from(shielded_transfer.clone()).into() + } } } } diff --git a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/shield.rs b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/shield.rs new file mode 100644 index 00000000000..2f138c4bc41 --- /dev/null +++ b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/shield.rs @@ -0,0 +1,41 @@ +use dpp::state_transition::batch_transition::token_shield_transition::v0::v0_methods::TokenShieldTransitionV0Methods; +use dpp::state_transition::batch_transition::TokenShieldTransition; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(js_name=TokenShieldTransition)] +#[derive(Debug, Clone)] +pub struct TokenShieldTransitionWasm(TokenShieldTransition); + +impl From for TokenShieldTransitionWasm { + fn from(value: TokenShieldTransition) -> Self { + Self(value) + } +} + +#[wasm_bindgen(js_class = TokenShieldTransition)] +impl TokenShieldTransitionWasm { + #[wasm_bindgen(js_name=getAmount)] + pub fn amount(&self) -> u64 { + self.0.amount() + } + + #[wasm_bindgen(js_name=getActionsCount)] + pub fn actions_count(&self) -> u32 { + self.0.actions().len() as u32 + } + + #[wasm_bindgen(js_name=getAnchor)] + pub fn anchor(&self) -> Vec { + self.0.anchor().to_vec() + } + + #[wasm_bindgen(js_name=getProof)] + pub fn proof(&self) -> Vec { + self.0.proof().to_vec() + } + + #[wasm_bindgen(js_name=getBindingSignature)] + pub fn binding_signature(&self) -> Vec { + self.0.binding_signature().to_vec() + } +} diff --git a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/shielded_transfer.rs b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/shielded_transfer.rs new file mode 100644 index 00000000000..ff52a8a1f69 --- /dev/null +++ b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/shielded_transfer.rs @@ -0,0 +1,36 @@ +use dpp::state_transition::batch_transition::token_shielded_transfer_transition::v0::v0_methods::TokenShieldedTransferTransitionV0Methods; +use dpp::state_transition::batch_transition::TokenShieldedTransferTransition; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(js_name=TokenShieldedTransferTransition)] +#[derive(Debug, Clone)] +pub struct TokenShieldedTransferTransitionWasm(TokenShieldedTransferTransition); + +impl From for TokenShieldedTransferTransitionWasm { + fn from(value: TokenShieldedTransferTransition) -> Self { + Self(value) + } +} + +#[wasm_bindgen(js_class = TokenShieldedTransferTransition)] +impl TokenShieldedTransferTransitionWasm { + #[wasm_bindgen(js_name=getActionsCount)] + pub fn actions_count(&self) -> u32 { + self.0.actions().len() as u32 + } + + #[wasm_bindgen(js_name=getAnchor)] + pub fn anchor(&self) -> Vec { + self.0.anchor().to_vec() + } + + #[wasm_bindgen(js_name=getProof)] + pub fn proof(&self) -> Vec { + self.0.proof().to_vec() + } + + #[wasm_bindgen(js_name=getBindingSignature)] + pub fn binding_signature(&self) -> Vec { + self.0.binding_signature().to_vec() + } +} diff --git a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/unshield.rs b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/unshield.rs new file mode 100644 index 00000000000..5cb5ee7527d --- /dev/null +++ b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/unshield.rs @@ -0,0 +1,47 @@ +use crate::identifier::IdentifierWrapper; +use dpp::state_transition::batch_transition::token_unshield_transition::v0::v0_methods::TokenUnshieldTransitionV0Methods; +use dpp::state_transition::batch_transition::TokenUnshieldTransition; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(js_name=TokenUnshieldTransition)] +#[derive(Debug, Clone)] +pub struct TokenUnshieldTransitionWasm(TokenUnshieldTransition); + +impl From for TokenUnshieldTransitionWasm { + fn from(value: TokenUnshieldTransition) -> Self { + Self(value) + } +} + +#[wasm_bindgen(js_class = TokenUnshieldTransition)] +impl TokenUnshieldTransitionWasm { + #[wasm_bindgen(js_name=getAmount)] + pub fn amount(&self) -> u64 { + self.0.amount() + } + + #[wasm_bindgen(js_name=getRecipientId)] + pub fn recipient_id(&self) -> IdentifierWrapper { + self.0.recipient_id().into() + } + + #[wasm_bindgen(js_name=getActionsCount)] + pub fn actions_count(&self) -> u32 { + self.0.actions().len() as u32 + } + + #[wasm_bindgen(js_name=getAnchor)] + pub fn anchor(&self) -> Vec { + self.0.anchor().to_vec() + } + + #[wasm_bindgen(js_name=getProof)] + pub fn proof(&self) -> Vec { + self.0.proof().to_vec() + } + + #[wasm_bindgen(js_name=getBindingSignature)] + pub fn binding_signature(&self) -> Vec { + self.0.binding_signature().to_vec() + } +} diff --git a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs index 06fa5f6d474..9eb801e884b 100644 --- a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs +++ b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs @@ -136,7 +136,7 @@ use dpp::consensus::state::identity::no_transfer_key_for_core_withdrawal_availab use dpp::consensus::state::identity::RecipientIdentityDoesNotExistError; use dpp::consensus::state::prefunded_specialized_balances::prefunded_specialized_balance_insufficient_error::PrefundedSpecializedBalanceInsufficientError; use dpp::consensus::state::prefunded_specialized_balances::prefunded_specialized_balance_not_found_error::PrefundedSpecializedBalanceNotFoundError; -use dpp::consensus::state::token::{IdentityDoesNotHaveEnoughTokenBalanceError, IdentityTokenAccountNotFrozenError, IdentityTokenAccountFrozenError, TokenIsPausedError, IdentityTokenAccountAlreadyFrozenError, UnauthorizedTokenActionError, TokenSettingMaxSupplyToLessThanCurrentSupplyError, TokenMintPastMaxSupplyError, NewTokensDestinationIdentityDoesNotExistError, NewAuthorizedActionTakerIdentityDoesNotExistError, NewAuthorizedActionTakerGroupDoesNotExistError, NewAuthorizedActionTakerMainGroupNotSetError, InvalidGroupPositionError, TokenAlreadyPausedError, TokenNotPausedError, InvalidTokenClaimPropertyMismatch, InvalidTokenClaimNoCurrentRewards, InvalidTokenClaimWrongClaimant, TokenTransferRecipientIdentityNotExistError, PreProgrammedDistributionTimestampInPastError, IdentityHasNotAgreedToPayRequiredTokenAmountError, RequiredTokenPaymentInfoNotSetError, IdentityTryingToPayWithWrongTokenError, TokenDirectPurchaseUserPriceTooLow, TokenAmountUnderMinimumSaleAmount, TokenNotForDirectSale, InvalidTokenPositionStateError, TokenOncePerIdentityDistributionAlreadyClaimedError}; +use dpp::consensus::state::token::{IdentityDoesNotHaveEnoughTokenBalanceError, IdentityTokenAccountNotFrozenError, IdentityTokenAccountFrozenError, TokenIsPausedError, IdentityTokenAccountAlreadyFrozenError, UnauthorizedTokenActionError, TokenSettingMaxSupplyToLessThanCurrentSupplyError, TokenMintPastMaxSupplyError, NewTokensDestinationIdentityDoesNotExistError, NewAuthorizedActionTakerIdentityDoesNotExistError, NewAuthorizedActionTakerGroupDoesNotExistError, NewAuthorizedActionTakerMainGroupNotSetError, InvalidGroupPositionError, TokenAlreadyPausedError, TokenNotPausedError, InvalidTokenClaimPropertyMismatch, InvalidTokenClaimNoCurrentRewards, InvalidTokenClaimWrongClaimant, TokenTransferRecipientIdentityNotExistError, PreProgrammedDistributionTimestampInPastError, IdentityHasNotAgreedToPayRequiredTokenAmountError, RequiredTokenPaymentInfoNotSetError, IdentityTryingToPayWithWrongTokenError, TokenDirectPurchaseUserPriceTooLow, TokenAmountUnderMinimumSaleAmount, TokenNotForDirectSale, InvalidTokenPositionStateError, TokenOncePerIdentityDistributionAlreadyClaimedError, TokenShieldedPoolNotEnabledError}; use dpp::consensus::state::address_funds::{AddressDoesNotExistError, AddressInvalidNonceError, AddressNotEnoughFundsError, AddressesNotEnoughFundsError}; use dpp::consensus::state::document::referenced_document_type_deletable_error::ReferencedDocumentTypeDeletableError; use dpp::consensus::state::document::referenced_document_type_not_deletable_error::ReferencedDocumentTypeNotDeletableError; @@ -490,6 +490,9 @@ pub fn from_state_error(state_error: &StateError) -> JsValue { StateError::TokenNotForDirectSale(e) => { generic_consensus_error!(TokenNotForDirectSale, e).into() } + StateError::TokenShieldedPoolNotEnabledError(e) => { + generic_consensus_error!(TokenShieldedPoolNotEnabledError, e).into() + } StateError::IdentityInTokenConfigurationNotFoundError(e) => { generic_consensus_error!(IdentityInTokenConfigurationNotFoundError, e).into() } diff --git a/packages/wasm-dpp2/src/group/token_event.rs b/packages/wasm-dpp2/src/group/token_event.rs index a080db06bd4..1c91f3e74ee 100644 --- a/packages/wasm-dpp2/src/group/token_event.rs +++ b/packages/wasm-dpp2/src/group/token_event.rs @@ -25,6 +25,9 @@ const TS_TYPES: &str = r#" * - ConfigUpdate: { $type, configChange, publicNote } * - ChangePriceForDirectPurchase: { $type, pricingSchedule, publicNote } * - DirectPurchase: { $type, amount, credits } + * - Shield: { $type: "shield", amount } + * - Unshield:{ $type: "unshield", recipientId, amount } + * - ShieldedTransfer: { $type: "shieldedTransfer" } * * `amount`/`credits` are routed through json_safe_u64 — small numbers, JS * BigInt-safe stringification above 2^53. Identifier fields use base58 in @@ -69,6 +72,9 @@ pub enum TokenEventVariant { ConfigUpdate = 8, ChangePriceForDirectPurchase = 9, DirectPurchase = 10, + Shield = 11, + Unshield = 12, + ShieldedTransfer = 13, } #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] @@ -106,6 +112,9 @@ impl TokenEventWasm { TokenEventVariant::ChangePriceForDirectPurchase } TokenEvent::DirectPurchase(..) => TokenEventVariant::DirectPurchase, + TokenEvent::Shield(..) => TokenEventVariant::Shield, + TokenEvent::Unshield(..) => TokenEventVariant::Unshield, + TokenEvent::ShieldedTransfer => TokenEventVariant::ShieldedTransfer, } } } diff --git a/packages/wasm-dpp2/src/state_transitions/batch/token_transition.rs b/packages/wasm-dpp2/src/state_transitions/batch/token_transition.rs index 18529a7ca09..9f609413cf7 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/token_transition.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/token_transition.rs @@ -12,8 +12,11 @@ use crate::state_transitions::batch::token_transitions::token_destroy_frozen_fun use crate::state_transitions::batch::token_transitions::token_emergency_action::TokenEmergencyActionTransitionWasm; use crate::state_transitions::batch::token_transitions::token_freeze::TokenFreezeTransitionWasm; use crate::state_transitions::batch::token_transitions::token_mint::TokenMintTransitionWasm; +use crate::state_transitions::batch::token_transitions::token_shield::TokenShieldTransitionWasm; +use crate::state_transitions::batch::token_transitions::token_shielded_transfer::TokenShieldedTransferTransitionWasm; use crate::state_transitions::batch::token_transitions::token_transfer::TokenTransferTransitionWasm; use crate::state_transitions::batch::token_transitions::token_unfreeze::TokenUnFreezeTransitionWasm; +use crate::state_transitions::batch::token_transitions::token_unshield::TokenUnshieldTransitionWasm; use crate::utils::get_class_type; use dpp::prelude::{Identifier, IdentityNonce}; use dpp::state_transition::batch_transition::batched_transition::token_transition::{ @@ -23,14 +26,16 @@ use dpp::state_transition::batch_transition::{ TokenBurnTransition, TokenClaimTransition, TokenConfigUpdateTransition, TokenDestroyFrozenFundsTransition, TokenDirectPurchaseTransition, TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintTransition, - TokenSetPriceForDirectPurchaseTransition, TokenTransferTransition, TokenUnfreezeTransition, + TokenSetPriceForDirectPurchaseTransition, TokenShieldTransition, + TokenShieldedTransferTransition, TokenTransferTransition, TokenUnfreezeTransition, + TokenUnshieldTransition, }; use wasm_bindgen::JsValue; use wasm_bindgen::prelude::wasm_bindgen; #[wasm_bindgen(typescript_custom_section)] const TOKEN_TRANSITION_TYPES_TS: &str = r#" -export type TokenTransitionLike = TokenMintTransition | TokenBurnTransition | TokenTransferTransition | TokenFreezeTransition | TokenUnFreezeTransition | TokenDestroyFrozenFundsTransition | TokenClaimTransition | TokenEmergencyActionTransition | TokenConfigUpdateTransition | TokenDirectPurchaseTransition | TokenSetPriceForDirectPurchaseTransition; +export type TokenTransitionLike = TokenMintTransition | TokenBurnTransition | TokenTransferTransition | TokenFreezeTransition | TokenUnFreezeTransition | TokenDestroyFrozenFundsTransition | TokenClaimTransition | TokenEmergencyActionTransition | TokenConfigUpdateTransition | TokenDirectPurchaseTransition | TokenSetPriceForDirectPurchaseTransition | TokenShieldTransition | TokenUnshieldTransition | TokenShieldedTransferTransition; "#; /// Extern type for flexible TokenTransition input @@ -53,6 +58,9 @@ impl_from_for_extern_type!( TokenConfigUpdateTransitionWasm, TokenDirectPurchaseTransitionWasm, TokenSetPriceForDirectPurchaseTransitionWasm, + TokenShieldTransitionWasm, + TokenUnshieldTransitionWasm, + TokenShieldedTransferTransitionWasm, ); #[derive(Debug, Clone, PartialEq)] @@ -124,6 +132,17 @@ impl TokenTransitionWasm { TokenEmergencyActionTransitionWasm::try_from(&transition)?, )) } + "TokenShieldTransition" => TokenTransition::from(TokenShieldTransition::from( + TokenShieldTransitionWasm::try_from(&transition)?, + )), + "TokenUnshieldTransition" => TokenTransition::from(TokenUnshieldTransition::from( + TokenUnshieldTransitionWasm::try_from(&transition)?, + )), + "TokenShieldedTransferTransition" => { + TokenTransition::from(TokenShieldedTransferTransition::from( + TokenShieldedTransferTransitionWasm::try_from(&transition)?, + )) + } _ => { return Err(WasmDppError::invalid_argument("Bad token transition input")); } @@ -168,6 +187,15 @@ impl TokenTransitionWasm { TokenTransition::SetPriceForDirectPurchase(token_transition) => { TokenSetPriceForDirectPurchaseTransitionWasm::from(token_transition).into() } + TokenTransition::Shield(token_transition) => { + TokenShieldTransitionWasm::from(token_transition).into() + } + TokenTransition::Unshield(token_transition) => { + TokenUnshieldTransitionWasm::from(token_transition).into() + } + TokenTransition::ShieldedTransfer(token_transition) => { + TokenShieldedTransferTransitionWasm::from(token_transition).into() + } } } @@ -185,6 +213,9 @@ impl TokenTransitionWasm { TokenTransition::ConfigUpdate(_) => 8, TokenTransition::DirectPurchase(_) => 9, TokenTransition::SetPriceForDirectPurchase(_) => 10, + TokenTransition::Shield(_) => 11, + TokenTransition::Unshield(_) => 12, + TokenTransition::ShieldedTransfer(_) => 13, } } @@ -204,6 +235,9 @@ impl TokenTransitionWasm { TokenTransition::SetPriceForDirectPurchase(_) => { "SetPriceForDirectPurchase".to_string() } + TokenTransition::Shield(_) => "Shield".to_string(), + TokenTransition::Unshield(_) => "Unshield".to_string(), + TokenTransition::ShieldedTransfer(_) => "ShieldedTransfer".to_string(), } } diff --git a/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/mod.rs b/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/mod.rs index 97fe1ee522b..482fa4064f7 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/mod.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/mod.rs @@ -7,5 +7,8 @@ pub mod token_destroy_frozen_funds; pub mod token_emergency_action; pub mod token_freeze; pub mod token_mint; +pub mod token_shield; +pub mod token_shielded_transfer; pub mod token_transfer; pub mod token_unfreeze; +pub mod token_unshield; diff --git a/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_shield.rs b/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_shield.rs new file mode 100644 index 00000000000..cdd50f117a5 --- /dev/null +++ b/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_shield.rs @@ -0,0 +1,146 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::impl_try_from_js_value; +use crate::impl_wasm_type_info; +use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; +use crate::state_transitions::batch::token_base_transition::TokenBaseTransitionWasm; +use crate::utils::{try_from_options, try_to_u64, try_vec_to_fixed_bytes}; +use dpp::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use dpp::state_transition::batch_transition::token_shield_transition::v0::v0_methods::TokenShieldTransitionV0Methods; +use dpp::state_transition::batch_transition::token_shield_transition::TokenShieldTransitionV0; +use dpp::state_transition::batch_transition::TokenShieldTransition; +use serde::Deserialize; +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(typescript_custom_section)] +const TOKEN_SHIELD_OPTIONS_TS: &str = r#" +/** + * Options for constructing a TokenShieldTransition. Moves `amount` tokens from the signer's identity balance into the token's shielded pool. + */ +export interface TokenShieldTransitionOptions { + base: TokenBaseTransition; + amount: bigint; + actions: SerializedOrchardAction[]; + anchor: Uint8Array; + proof: Uint8Array; + bindingSignature: Uint8Array; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "TokenShieldTransitionOptions")] + pub type TokenShieldTransitionOptionsJs; +} + +/// Non-WASM-instance fields extracted from the constructor options via serde. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct TokenShieldTransitionSimpleFields { + amount: u64, + anchor: Vec, + proof: Vec, + binding_signature: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +#[wasm_bindgen(js_name = "TokenShieldTransition")] +pub struct TokenShieldTransitionWasm(TokenShieldTransition); + +impl From for TokenShieldTransitionWasm { + fn from(transition: TokenShieldTransition) -> Self { + Self(transition) + } +} + +impl From for TokenShieldTransition { + fn from(transition: TokenShieldTransitionWasm) -> Self { + transition.0 + } +} + +#[wasm_bindgen(js_class = TokenShieldTransition)] +impl TokenShieldTransitionWasm { + #[wasm_bindgen(constructor)] + pub fn constructor( + options: TokenShieldTransitionOptionsJs, + ) -> WasmDppResult { + let js_opts: &JsValue = options.as_ref(); + + let base: TokenBaseTransitionWasm = try_from_options(js_opts, "base")?; + let actions = actions_from_js_options(js_opts, "actions")?; + + let fields: TokenShieldTransitionSimpleFields = + serde_wasm_bindgen::from_value(options.into()) + .map_err(|e| WasmDppError::invalid_argument(e.to_string()))?; + + let anchor: [u8; 32] = try_vec_to_fixed_bytes(fields.anchor, "anchor")?; + let binding_signature: [u8; 64] = + try_vec_to_fixed_bytes(fields.binding_signature, "bindingSignature")?; + + Ok(TokenShieldTransitionWasm(TokenShieldTransition::V0( + TokenShieldTransitionV0 { + base: base.into(), + amount: fields.amount, + actions: actions.into_iter().map(Into::into).collect(), + anchor, + proof: fields.proof, + binding_signature, + }, + ))) + } + + #[wasm_bindgen(getter = "base")] + pub fn base(&self) -> TokenBaseTransitionWasm { + self.0.base().clone().into() + } + + #[wasm_bindgen(setter = "base")] + pub fn set_base(&mut self, base: TokenBaseTransitionWasm) { + self.0.set_base(base.into()) + } + + /// Tokens crossing the pool boundary. + #[wasm_bindgen(getter = "amount")] + pub fn amount(&self) -> u64 { + self.0.amount() + } + + #[wasm_bindgen(setter = "amount")] + pub fn set_amount(&mut self, amount: &js_sys::BigInt) -> WasmDppResult<()> { + self.0.set_amount(try_to_u64(amount, "amount")?); + Ok(()) + } + + /// The serialized Orchard actions. + #[wasm_bindgen(getter = "actions")] + pub fn actions(&self) -> Vec { + self.0 + .actions() + .iter() + .cloned() + .map(SerializedOrchardActionWasm::from) + .collect() + } + + /// The Orchard anchor (32-byte note commitment tree root) the bundle was built against. + #[wasm_bindgen(getter = "anchor")] + pub fn anchor(&self) -> Vec { + self.0.anchor().to_vec() + } + + /// The Halo 2 proof bytes. + #[wasm_bindgen(getter = "proof")] + pub fn proof(&self) -> Vec { + self.0.proof().to_vec() + } + + /// The RedPallas binding signature (64 bytes). + #[wasm_bindgen(getter = "bindingSignature")] + pub fn binding_signature(&self) -> Vec { + self.0.binding_signature().to_vec() + } +} + +impl_try_from_js_value!(TokenShieldTransitionWasm, "TokenShieldTransition"); +impl_wasm_type_info!(TokenShieldTransitionWasm, TokenShieldTransition); diff --git a/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_shielded_transfer.rs b/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_shielded_transfer.rs new file mode 100644 index 00000000000..7bc37f024bc --- /dev/null +++ b/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_shielded_transfer.rs @@ -0,0 +1,137 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::impl_try_from_js_value; +use crate::impl_wasm_type_info; +use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; +use crate::state_transitions::batch::token_base_transition::TokenBaseTransitionWasm; +use crate::utils::{try_from_options, try_vec_to_fixed_bytes}; +use dpp::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use dpp::state_transition::batch_transition::token_shielded_transfer_transition::v0::v0_methods::TokenShieldedTransferTransitionV0Methods; +use dpp::state_transition::batch_transition::token_shielded_transfer_transition::TokenShieldedTransferTransitionV0; +use dpp::state_transition::batch_transition::TokenShieldedTransferTransition; +use serde::Deserialize; +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(typescript_custom_section)] +const TOKEN_SHIELDED_TRANSFER_OPTIONS_TS: &str = r#" +/** + * Options for constructing a TokenShieldedTransferTransition. Spends and re-creates notes inside the token's shielded pool; the identity only pays the fee. + */ +export interface TokenShieldedTransferTransitionOptions { + base: TokenBaseTransition; + actions: SerializedOrchardAction[]; + anchor: Uint8Array; + proof: Uint8Array; + bindingSignature: Uint8Array; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "TokenShieldedTransferTransitionOptions")] + pub type TokenShieldedTransferTransitionOptionsJs; +} + +/// Non-WASM-instance fields extracted from the constructor options via serde. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct TokenShieldedTransferTransitionSimpleFields { + anchor: Vec, + proof: Vec, + binding_signature: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +#[wasm_bindgen(js_name = "TokenShieldedTransferTransition")] +pub struct TokenShieldedTransferTransitionWasm(TokenShieldedTransferTransition); + +impl From for TokenShieldedTransferTransitionWasm { + fn from(transition: TokenShieldedTransferTransition) -> Self { + Self(transition) + } +} + +impl From for TokenShieldedTransferTransition { + fn from(transition: TokenShieldedTransferTransitionWasm) -> Self { + transition.0 + } +} + +#[wasm_bindgen(js_class = TokenShieldedTransferTransition)] +impl TokenShieldedTransferTransitionWasm { + #[wasm_bindgen(constructor)] + pub fn constructor( + options: TokenShieldedTransferTransitionOptionsJs, + ) -> WasmDppResult { + let js_opts: &JsValue = options.as_ref(); + + let base: TokenBaseTransitionWasm = try_from_options(js_opts, "base")?; + let actions = actions_from_js_options(js_opts, "actions")?; + + let fields: TokenShieldedTransferTransitionSimpleFields = + serde_wasm_bindgen::from_value(options.into()) + .map_err(|e| WasmDppError::invalid_argument(e.to_string()))?; + + let anchor: [u8; 32] = try_vec_to_fixed_bytes(fields.anchor, "anchor")?; + let binding_signature: [u8; 64] = + try_vec_to_fixed_bytes(fields.binding_signature, "bindingSignature")?; + + Ok(TokenShieldedTransferTransitionWasm( + TokenShieldedTransferTransition::V0(TokenShieldedTransferTransitionV0 { + base: base.into(), + actions: actions.into_iter().map(Into::into).collect(), + anchor, + proof: fields.proof, + binding_signature, + }), + )) + } + + #[wasm_bindgen(getter = "base")] + pub fn base(&self) -> TokenBaseTransitionWasm { + self.0.base().clone().into() + } + + #[wasm_bindgen(setter = "base")] + pub fn set_base(&mut self, base: TokenBaseTransitionWasm) { + self.0.set_base(base.into()) + } + + /// The serialized Orchard actions. + #[wasm_bindgen(getter = "actions")] + pub fn actions(&self) -> Vec { + self.0 + .actions() + .iter() + .cloned() + .map(SerializedOrchardActionWasm::from) + .collect() + } + + /// The Orchard anchor (32-byte note commitment tree root) the bundle was built against. + #[wasm_bindgen(getter = "anchor")] + pub fn anchor(&self) -> Vec { + self.0.anchor().to_vec() + } + + /// The Halo 2 proof bytes. + #[wasm_bindgen(getter = "proof")] + pub fn proof(&self) -> Vec { + self.0.proof().to_vec() + } + + /// The RedPallas binding signature (64 bytes). + #[wasm_bindgen(getter = "bindingSignature")] + pub fn binding_signature(&self) -> Vec { + self.0.binding_signature().to_vec() + } +} + +impl_try_from_js_value!( + TokenShieldedTransferTransitionWasm, + "TokenShieldedTransferTransition" +); +impl_wasm_type_info!( + TokenShieldedTransferTransitionWasm, + TokenShieldedTransferTransition +); diff --git a/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_unshield.rs b/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_unshield.rs new file mode 100644 index 00000000000..7cad56ee906 --- /dev/null +++ b/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_unshield.rs @@ -0,0 +1,164 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::identifier::{IdentifierLikeJs, IdentifierWasm}; +use crate::impl_try_from_js_value; +use crate::impl_wasm_type_info; +use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; +use crate::state_transitions::batch::token_base_transition::TokenBaseTransitionWasm; +use crate::utils::{try_from_options, try_to_u64, try_vec_to_fixed_bytes}; +use dpp::prelude::Identifier; +use dpp::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use dpp::state_transition::batch_transition::token_unshield_transition::v0::v0_methods::TokenUnshieldTransitionV0Methods; +use dpp::state_transition::batch_transition::token_unshield_transition::TokenUnshieldTransitionV0; +use dpp::state_transition::batch_transition::TokenUnshieldTransition; +use serde::Deserialize; +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(typescript_custom_section)] +const TOKEN_UNSHIELD_OPTIONS_TS: &str = r#" +/** + * Options for constructing a TokenUnshieldTransition. Spends shielded notes and credits `amount` tokens to `recipientId`. + */ +export interface TokenUnshieldTransitionOptions { + base: TokenBaseTransition; + amount: bigint; + recipientId: IdentifierLike; + actions: SerializedOrchardAction[]; + anchor: Uint8Array; + proof: Uint8Array; + bindingSignature: Uint8Array; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "TokenUnshieldTransitionOptions")] + pub type TokenUnshieldTransitionOptionsJs; +} + +/// Non-WASM-instance fields extracted from the constructor options via serde. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct TokenUnshieldTransitionSimpleFields { + amount: u64, + anchor: Vec, + proof: Vec, + binding_signature: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +#[wasm_bindgen(js_name = "TokenUnshieldTransition")] +pub struct TokenUnshieldTransitionWasm(TokenUnshieldTransition); + +impl From for TokenUnshieldTransitionWasm { + fn from(transition: TokenUnshieldTransition) -> Self { + Self(transition) + } +} + +impl From for TokenUnshieldTransition { + fn from(transition: TokenUnshieldTransitionWasm) -> Self { + transition.0 + } +} + +#[wasm_bindgen(js_class = TokenUnshieldTransition)] +impl TokenUnshieldTransitionWasm { + #[wasm_bindgen(constructor)] + pub fn constructor( + options: TokenUnshieldTransitionOptionsJs, + ) -> WasmDppResult { + let js_opts: &JsValue = options.as_ref(); + + let base: TokenBaseTransitionWasm = try_from_options(js_opts, "base")?; + let recipient_id: IdentifierWasm = try_from_options(js_opts, "recipientId")?; + let actions = actions_from_js_options(js_opts, "actions")?; + + let fields: TokenUnshieldTransitionSimpleFields = + serde_wasm_bindgen::from_value(options.into()) + .map_err(|e| WasmDppError::invalid_argument(e.to_string()))?; + + let anchor: [u8; 32] = try_vec_to_fixed_bytes(fields.anchor, "anchor")?; + let binding_signature: [u8; 64] = + try_vec_to_fixed_bytes(fields.binding_signature, "bindingSignature")?; + + Ok(TokenUnshieldTransitionWasm(TokenUnshieldTransition::V0( + TokenUnshieldTransitionV0 { + base: base.into(), + amount: fields.amount, + recipient_id: recipient_id.into(), + actions: actions.into_iter().map(Into::into).collect(), + anchor, + proof: fields.proof, + binding_signature, + }, + ))) + } + + #[wasm_bindgen(getter = "base")] + pub fn base(&self) -> TokenBaseTransitionWasm { + self.0.base().clone().into() + } + + #[wasm_bindgen(setter = "base")] + pub fn set_base(&mut self, base: TokenBaseTransitionWasm) { + self.0.set_base(base.into()) + } + + /// Tokens crossing the pool boundary. + #[wasm_bindgen(getter = "amount")] + pub fn amount(&self) -> u64 { + self.0.amount() + } + + #[wasm_bindgen(setter = "amount")] + pub fn set_amount(&mut self, amount: &js_sys::BigInt) -> WasmDppResult<()> { + self.0.set_amount(try_to_u64(amount, "amount")?); + Ok(()) + } + + /// Identity credited with the unshielded tokens. + #[wasm_bindgen(getter = "recipientId")] + pub fn recipient_id(&self) -> IdentifierWasm { + self.0.recipient_id().into() + } + + #[wasm_bindgen(setter = "recipientId")] + pub fn set_recipient_id(&mut self, recipient_id: IdentifierLikeJs) -> WasmDppResult<()> { + let recipient_id: Identifier = recipient_id.try_into()?; + self.0.set_recipient_id(recipient_id); + Ok(()) + } + + /// The serialized Orchard actions. + #[wasm_bindgen(getter = "actions")] + pub fn actions(&self) -> Vec { + self.0 + .actions() + .iter() + .cloned() + .map(SerializedOrchardActionWasm::from) + .collect() + } + + /// The Orchard anchor (32-byte note commitment tree root) the bundle was built against. + #[wasm_bindgen(getter = "anchor")] + pub fn anchor(&self) -> Vec { + self.0.anchor().to_vec() + } + + /// The Halo 2 proof bytes. + #[wasm_bindgen(getter = "proof")] + pub fn proof(&self) -> Vec { + self.0.proof().to_vec() + } + + /// The RedPallas binding signature (64 bytes). + #[wasm_bindgen(getter = "bindingSignature")] + pub fn binding_signature(&self) -> Vec { + self.0.binding_signature().to_vec() + } +} + +impl_try_from_js_value!(TokenUnshieldTransitionWasm, "TokenUnshieldTransition"); +impl_wasm_type_info!(TokenUnshieldTransitionWasm, TokenUnshieldTransition); diff --git a/packages/wasm-dpp2/src/tokens/configuration/token_configuration.rs b/packages/wasm-dpp2/src/tokens/configuration/token_configuration.rs index 61648a0ba68..f653862e78c 100644 --- a/packages/wasm-dpp2/src/tokens/configuration/token_configuration.rs +++ b/packages/wasm-dpp2/src/tokens/configuration/token_configuration.rs @@ -13,6 +13,9 @@ use dpp::balances::credits::TokenAmount; use dpp::data_contract::associated_token::token_configuration::accessors::v0::{ TokenConfigurationV0Getters, TokenConfigurationV0Setters, }; +use dpp::data_contract::associated_token::token_configuration::accessors::v1::{ + TokenConfigurationV1Getters, TokenConfigurationV1Setters, +}; use dpp::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; use dpp::data_contract::{GroupContractPosition, TokenConfiguration, TokenContractPosition}; use dpp::prelude::Identifier; @@ -35,6 +38,8 @@ struct TokenConfigurationOptions { main_control_group: Option, #[serde(default)] description: Option, + #[serde(default)] + has_shielded_pool: bool, } #[wasm_bindgen(typescript_custom_section)] @@ -59,6 +64,11 @@ export interface TokenConfigurationOptions { mainControlGroup?: number; mainControlGroupCanBeModified: AuthorizedActionTakers; description?: string; + /** + * Give the token its own shielded pool (protocol version 14+). Produces a + * format-version-1 token configuration; the flag cannot change after creation. + */ + hasShieldedPool?: boolean; } "#; @@ -121,29 +131,33 @@ impl TokenConfigurationWasm { let opts: TokenConfigurationOptions = serde_wasm_bindgen::from_value(options.into()) .map_err(|e| WasmDppError::invalid_argument(e.to_string()))?; - Ok(TokenConfigurationWasm(TokenConfiguration::V0( - TokenConfigurationV0 { - conventions: conventions.into(), - conventions_change_rules: conventions_change_rules.into(), - base_supply: opts.base_supply, - max_supply: opts.max_supply, - keeps_history: keeps_history.into(), - start_as_paused: opts.is_started_as_paused, - allow_transfer_to_frozen_balance: opts.is_allowed_transfer_to_frozen_balance, - max_supply_change_rules: max_supply_change_rules.into(), - distribution_rules: distribution_rules.into(), - marketplace_rules: marketplace_rules.into(), - manual_minting_rules: manual_minting_rules.into(), - manual_burning_rules: manual_burning_rules.into(), - freeze_rules: freeze_rules.into(), - unfreeze_rules: unfreeze_rules.into(), - destroy_frozen_funds_rules: destroy_frozen_funds_rules.into(), - emergency_action_rules: emergency_action_rules.into(), - main_control_group: opts.main_control_group, - main_control_group_can_be_modified: main_control_group_can_be_modified.into(), - description: opts.description, - }, - ))) + let mut configuration = TokenConfiguration::V0(TokenConfigurationV0 { + conventions: conventions.into(), + conventions_change_rules: conventions_change_rules.into(), + base_supply: opts.base_supply, + max_supply: opts.max_supply, + keeps_history: keeps_history.into(), + start_as_paused: opts.is_started_as_paused, + allow_transfer_to_frozen_balance: opts.is_allowed_transfer_to_frozen_balance, + max_supply_change_rules: max_supply_change_rules.into(), + distribution_rules: distribution_rules.into(), + marketplace_rules: marketplace_rules.into(), + manual_minting_rules: manual_minting_rules.into(), + manual_burning_rules: manual_burning_rules.into(), + freeze_rules: freeze_rules.into(), + unfreeze_rules: unfreeze_rules.into(), + destroy_frozen_funds_rules: destroy_frozen_funds_rules.into(), + emergency_action_rules: emergency_action_rules.into(), + main_control_group: opts.main_control_group, + main_control_group_can_be_modified: main_control_group_can_be_modified.into(), + description: opts.description, + }); + + if opts.has_shielded_pool { + configuration.set_has_shielded_pool(true); + } + + Ok(TokenConfigurationWasm(configuration)) } #[wasm_bindgen(getter = "conventions")] @@ -193,9 +207,19 @@ impl TokenConfigurationWasm { #[wasm_bindgen(getter = "marketplaceRules")] pub fn marketplace_rules(&self) -> TokenMarketplaceRulesWasm { - match self.0.clone() { - TokenConfiguration::V0(v0) => v0.marketplace_rules.clone().into(), - } + self.0.as_v0().marketplace_rules.clone().into() + } + + /// Whether the token has its own shielded pool (format version 1 configurations only). + #[wasm_bindgen(getter = "hasShieldedPool")] + pub fn has_shielded_pool(&self) -> bool { + self.0.has_shielded_pool() + } + + /// The token configuration format version: 0 without a shielded pool, 1 with one. + #[wasm_bindgen(getter = "formatVersion")] + pub fn format_version(&self) -> u16 { + self.0.format_version() } #[wasm_bindgen(getter = "manualMintingRules")] @@ -302,13 +326,13 @@ impl TokenConfigurationWasm { #[wasm_bindgen(setter = "marketplaceRules")] pub fn set_marketplace_rules(&mut self, marketplace_rules: &TokenMarketplaceRulesWasm) { - self.0 = match self.0.clone() { - TokenConfiguration::V0(mut v0) => { - v0.marketplace_rules = marketplace_rules.clone().into(); + self.0.as_v0_mut().marketplace_rules = marketplace_rules.clone().into(); + } - TokenConfiguration::V0(v0) - } - } + /// Enabling upgrades a format-version-0 configuration to version 1 in place. + #[wasm_bindgen(setter = "hasShieldedPool")] + pub fn set_has_shielded_pool(&mut self, has_shielded_pool: bool) { + self.0.set_has_shielded_pool(has_shielded_pool) } #[wasm_bindgen(setter = "manualMintingRules")] From c04045729ec558ed7f71770727e6512f67c242b3 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 15 Sep 2026 15:20:56 +0700 Subject: [PATCH 02/94] fix(dpp): append the token pool state error as the last StateError variant StateError is encoded by variant position, so the new TokenShieldedPoolNotEnabledError must be the last variant rather than sit in the token block, or every later variant's wire discriminant shifts; the frozen-discriminant test now pins it at 101. Co-Authored-By: Claude Fable 5.1 --- .../src/errors/consensus/state/state_error.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/rs-dpp/src/errors/consensus/state/state_error.rs b/packages/rs-dpp/src/errors/consensus/state/state_error.rs index 5578daa6736..4f3880f43b8 100644 --- a/packages/rs-dpp/src/errors/consensus/state/state_error.rs +++ b/packages/rs-dpp/src/errors/consensus/state/state_error.rs @@ -338,9 +338,6 @@ pub enum StateError { #[error(transparent)] TokenTransferRecipientIdentityNotExistError(TokenTransferRecipientIdentityNotExistError), - #[error(transparent)] - TokenShieldedPoolNotEnabledError(TokenShieldedPoolNotEnabledError), - #[error(transparent)] PreProgrammedDistributionTimestampInPastError(PreProgrammedDistributionTimestampInPastError), @@ -590,6 +587,11 @@ pub enum StateError { // Requirements on a referenced contract (protocol version 14). #[error(transparent)] ReferencedContractRequirementNotMetError(ReferencedContractRequirementNotMetError), + + /// Variants are encoded by position: new ones go at the end (see the frozen + /// discriminant test below). + #[error(transparent)] + TokenShieldedPoolNotEnabledError(TokenShieldedPoolNotEnabledError), } impl From for ConsensusError { @@ -1101,5 +1103,11 @@ mod tests { )), 143 ); + assert_eq!( + discriminant_of(StateError::TokenShieldedPoolNotEnabledError( + TokenShieldedPoolNotEnabledError::new(Identifier::from([1; 32])) + )), + 144 + ); } } From 150ce1562d97541f01e9ac81f1a743c27fc41e79 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 15 Sep 2026 15:32:51 +0700 Subject: [PATCH 03/94] feat(dpp): require a pooled token to disable freeze and confiscation rules Shielded notes belong to no identity account, so freezing and destroying frozen funds cannot reach them; a holder who expects a freeze simply shields first. A token with hasShieldedPool must therefore set freezeRules, unfreezeRules and destroyFrozenFundsRules to no action takers and no admins, so no later configuration update can enable them. Contract create and update reject anything else with TokenShieldedPoolIncompatibleRulesError (10277). Co-Authored-By: Claude Fable 5.1 --- book/src/data-model/token-shielded-pools.md | 11 ++ .../token_configuration/mod.rs | 141 ++++++++++++++++++ .../src/errors/consensus/basic/basic_error.rs | 3 + .../consensus/basic/data_contract/mod.rs | 2 + ..._shielded_pool_incompatible_rules_error.rs | 64 ++++++++ packages/rs-dpp/src/errors/consensus/codes.rs | 1 + .../batch/tests/token/shielded/mod.rs | 43 ++++++ .../basic_structure/v0/mod.rs | 7 + .../basic_structure/v0/mod.rs | 7 + .../src/errors/consensus/consensus_error.rs | 5 +- .../configuration/token_configuration.rs | 4 +- 11 files changed, 286 insertions(+), 2 deletions(-) create mode 100644 packages/rs-dpp/src/errors/consensus/basic/data_contract/token_shielded_pool_incompatible_rules_error.rs diff --git a/book/src/data-model/token-shielded-pools.md b/book/src/data-model/token-shielded-pools.md index b03ddf458e9..de538c77bba 100644 --- a/book/src/data-model/token-shielded-pools.md +++ b/book/src/data-model/token-shielded-pools.md @@ -54,6 +54,17 @@ The flag is immutable. A contract update that changes it is rejected with enabled can hold notes that would become unspendable, and a pool that is enabled late would need a tree created under an existing token. +A pool also makes freezing and confiscation unenforceable: shielded notes belong to no identity +account, so a holder who expects a freeze shields first and nothing can freeze, destroy or even +see those notes. Rather than let an issuer advertise controls that cover only transparent +balances, a token with `hasShieldedPool` must disable them permanently: `freezeRules`, +`unfreezeRules` and `destroyFrozenFundsRules` must each authorize no one to take the action and +have no admin action takers, so no later configuration update can switch them on. Contract +create and update reject anything else with `TokenShieldedPoolIncompatibleRulesError` (10277). +Pausing still works: shield, unshield and shielded transfer are all rejected while the token is +paused. The frozen-account checks in the shield and unshield validators remain as defence in +depth for state written before this rule. + ## Transitions The three operations are `TokenTransition` variants inside a `Batch` transition, like every diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs index b8d6bd79816..9059973b060 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs @@ -1,6 +1,12 @@ +use crate::consensus::basic::data_contract::TokenShieldedPoolIncompatibleRulesError; use crate::consensus::basic::unsupported_version_error::UnsupportedVersionError; +use crate::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters; use crate::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; use crate::data_contract::associated_token::token_configuration::v1::TokenConfigurationV1; +use crate::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; +use crate::data_contract::change_control_rules::ChangeControlRules; +use crate::data_contract::TokenContractPosition; #[cfg(feature = "json-conversion")] use crate::serialization::JsonConvertible; #[cfg(feature = "value-conversion")] @@ -77,6 +83,42 @@ impl TokenConfiguration { SimpleConsensusValidationResult::new() } } + + /// A shielded pool makes freezing and confiscation unenforceable: shielded notes belong to + /// no identity account, so a holder who expects a freeze simply shields first. Rather than + /// let an issuer advertise controls that only cover transparent balances, a token with + /// `hasShieldedPool` must permanently disable `freezeRules`, `unfreezeRules` and + /// `destroyFrozenFundsRules`: no one may take the action and no one may administer the + /// rule, so no configuration update can ever switch them on. Checked on contract create + /// and update; the flag itself is immutable. + pub fn validate_shielded_pool_rules( + &self, + token_contract_position: TokenContractPosition, + ) -> SimpleConsensusValidationResult { + if !self.has_shielded_pool() { + return SimpleConsensusValidationResult::new(); + } + let rules: [(&ChangeControlRules, &str); 3] = [ + (self.freeze_rules(), "freezeRules"), + (self.unfreeze_rules(), "unfreezeRules"), + (self.destroy_frozen_funds_rules(), "destroyFrozenFundsRules"), + ]; + for (rule, name) in rules { + let disabled = *rule.authorized_to_make_change_action_takers() + == AuthorizedActionTakers::NoOne + && *rule.admin_action_takers() == AuthorizedActionTakers::NoOne; + if !disabled { + return SimpleConsensusValidationResult::new_with_error( + TokenShieldedPoolIncompatibleRulesError::new( + token_contract_position, + name.to_string(), + ) + .into(), + ); + } + } + SimpleConsensusValidationResult::new() + } } impl fmt::Display for TokenConfiguration { @@ -221,4 +263,103 @@ mod json_convertible_tests { let recovered = TokenConfiguration::from_object(value).expect("from_object"); assert_eq!(original, recovered); } + + mod shielded_pool_rules { + use super::*; + use crate::consensus::basic::BasicError; + use crate::consensus::ConsensusError; + use crate::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Setters; + use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters; + use crate::data_contract::change_control_rules::v0::ChangeControlRulesV0; + use crate::prelude::Identifier; + + fn rules( + authorized: AuthorizedActionTakers, + admin: AuthorizedActionTakers, + ) -> ChangeControlRules { + ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: authorized, + admin_action_takers: admin, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }) + } + + fn pooled_configuration() -> TokenConfiguration { + let mut configuration = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + configuration.set_has_shielded_pool(true); + configuration + } + + fn rejected_rule(configuration: &TokenConfiguration) -> Option { + let result = configuration.validate_shielded_pool_rules(3); + match result.errors.as_slice() { + [] => None, + [ConsensusError::BasicError( + BasicError::TokenShieldedPoolIncompatibleRulesError(error), + )] => { + assert_eq!(error.token_contract_position(), 3); + Some(error.rule().to_string()) + } + other => panic!("unexpected errors: {other:?}"), + } + } + + #[test] + fn most_restrictive_defaults_are_compatible_with_a_pool() { + assert_eq!(rejected_rule(&pooled_configuration()), None); + } + + #[test] + fn a_token_without_a_pool_may_keep_freeze_rules() { + let mut configuration = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + configuration.set_freeze_rules(rules( + AuthorizedActionTakers::ContractOwner, + AuthorizedActionTakers::ContractOwner, + )); + assert_eq!(rejected_rule(&configuration), None); + } + + #[test] + fn a_pooled_token_rejects_an_authorized_freezer() { + let mut configuration = pooled_configuration(); + configuration.set_freeze_rules(rules( + AuthorizedActionTakers::ContractOwner, + AuthorizedActionTakers::NoOne, + )); + assert_eq!( + rejected_rule(&configuration), + Some("freezeRules".to_string()) + ); + } + + #[test] + fn a_pooled_token_rejects_an_admin_who_could_enable_unfreezing_later() { + let mut configuration = pooled_configuration(); + configuration.set_unfreeze_rules(rules( + AuthorizedActionTakers::NoOne, + AuthorizedActionTakers::MainGroup, + )); + assert_eq!( + rejected_rule(&configuration), + Some("unfreezeRules".to_string()) + ); + } + + #[test] + fn a_pooled_token_rejects_frozen_funds_destruction() { + let mut configuration = pooled_configuration(); + configuration.set_destroy_frozen_funds_rules(rules( + AuthorizedActionTakers::Identity(Identifier::from([1u8; 32])), + AuthorizedActionTakers::NoOne, + )); + assert_eq!( + rejected_rule(&configuration), + Some("destroyFrozenFundsRules".to_string()) + ); + } + } } diff --git a/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs b/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs index 3eb229a6ebf..0b5233c23e0 100644 --- a/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs +++ b/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs @@ -39,6 +39,7 @@ use crate::consensus::basic::data_contract::{ NewTokensDestinationIdentityOptionRequiredError, NonContiguousContractGroupPositionsError, NonContiguousContractTokenPositionsError, PreProgrammedDistributionAmountOverLimitError, RedundantDocumentPaidForByTokenWithContractId, SystemPropertyIndexAlreadyPresentError, + TokenShieldedPoolIncompatibleRulesError, UndefinedIndexPropertyError, UniqueIndicesLimitReachedError, UnknownDocumentCreationRestrictionModeError, UnknownGasFeesPaidByError, UnknownSecurityLevelError, UnknownStorageKeyRequirementsError, UnknownTradeModeError, @@ -805,6 +806,8 @@ pub enum BasicError { // Documents cited by a contract moderation reason (protocol version 14). #[error(transparent)] InvalidContractModerationReasonDocumentsError(InvalidContractModerationReasonDocumentsError), + #[error(transparent)] + TokenShieldedPoolIncompatibleRulesError(TokenShieldedPoolIncompatibleRulesError), } impl From for ConsensusError { diff --git a/packages/rs-dpp/src/errors/consensus/basic/data_contract/mod.rs b/packages/rs-dpp/src/errors/consensus/basic/data_contract/mod.rs index d30276402c7..24fe9e6c414 100644 --- a/packages/rs-dpp/src/errors/consensus/basic/data_contract/mod.rs +++ b/packages/rs-dpp/src/errors/consensus/basic/data_contract/mod.rs @@ -51,6 +51,7 @@ mod redundant_document_paid_for_by_token_with_contract_id; mod system_property_index_already_present_error; mod token_decimals_over_limit_error; mod token_payment_by_burning_only_allowed_on_internal_token_error; +mod token_shielded_pool_incompatible_rules_error; mod undefined_index_property_error; mod unique_indices_limit_reached_error; mod unknown_document_action_token_effect_error; @@ -117,6 +118,7 @@ pub use pre_programmed_distribution_amount_over_limit_error::*; pub use redundant_document_paid_for_by_token_with_contract_id::*; pub use token_decimals_over_limit_error::*; pub use token_payment_by_burning_only_allowed_on_internal_token_error::*; +pub use token_shielded_pool_incompatible_rules_error::*; pub use unknown_document_action_token_effect_error::*; pub use unknown_document_creation_restriction_mode_error::*; pub use unknown_gas_fees_paid_by_error::*; diff --git a/packages/rs-dpp/src/errors/consensus/basic/data_contract/token_shielded_pool_incompatible_rules_error.rs b/packages/rs-dpp/src/errors/consensus/basic/data_contract/token_shielded_pool_incompatible_rules_error.rs new file mode 100644 index 00000000000..c5dbbc58df7 --- /dev/null +++ b/packages/rs-dpp/src/errors/consensus/basic/data_contract/token_shielded_pool_incompatible_rules_error.rs @@ -0,0 +1,64 @@ +use crate::consensus::basic::BasicError; +use crate::data_contract::TokenContractPosition; +use crate::errors::ProtocolError; +use platform_serialization_derive::{ + PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, +}; +use thiserror::Error; + +use crate::consensus::ConsensusError; + +use bincode::{Decode, DecodeUntrusted, Encode}; + +/// A token with a shielded pool cannot promise freezing or confiscation: shielded notes belong +/// to no identity account, so `freezeRules`, `unfreezeRules` and `destroyFrozenFundsRules` must +/// authorize no one (as change taker and as admin) when `hasShieldedPool` is set. +#[derive( + Error, + Debug, + Clone, + PartialEq, + Eq, + Encode, + Decode, + PlatformSerialize, + PlatformDeserializeTrusted, + PlatformDeserializeUntrusted, + DecodeUntrusted, +)] +#[error( + "Token at position {token_contract_position} has a shielded pool, so {rule} must authorize no one to act and have no admin action takers: shielded notes cannot be frozen or destroyed" +)] +#[platform_serialize(unversioned)] +pub struct TokenShieldedPoolIncompatibleRulesError { + /* + + DO NOT CHANGE ORDER OF FIELDS WITHOUT INTRODUCING OF NEW VERSION + + */ + token_contract_position: TokenContractPosition, + rule: String, +} + +impl TokenShieldedPoolIncompatibleRulesError { + pub fn new(token_contract_position: TokenContractPosition, rule: String) -> Self { + Self { + token_contract_position, + rule, + } + } + + pub fn token_contract_position(&self) -> TokenContractPosition { + self.token_contract_position + } + + pub fn rule(&self) -> &str { + &self.rule + } +} + +impl From for ConsensusError { + fn from(err: TokenShieldedPoolIncompatibleRulesError) -> Self { + Self::BasicError(BasicError::TokenShieldedPoolIncompatibleRulesError(err)) + } +} diff --git a/packages/rs-dpp/src/errors/consensus/codes.rs b/packages/rs-dpp/src/errors/consensus/codes.rs index 4427884aa6b..4c45ee9d942 100644 --- a/packages/rs-dpp/src/errors/consensus/codes.rs +++ b/packages/rs-dpp/src/errors/consensus/codes.rs @@ -122,6 +122,7 @@ impl ErrorWithCode for BasicError { Self::RedundantDocumentPaidForByTokenWithContractId(_) => 10275, Self::DataContractInvalidRequiredFieldsUpdateError { .. } => 10276, Self::PreProgrammedDistributionAmountOverLimitError(_) => 10277, + Self::TokenShieldedPoolIncompatibleRulesError(_) => 10278, // Group Errors: 10350-10399 Self::GroupPositionDoesNotExistError(_) => 10350, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 117e3bd5baf..8b91414ee34 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -1024,6 +1024,49 @@ mod token_shielded_pool_tests { ); } + #[tokio::test] + async fn test_contract_create_with_shielded_pool_token_rejects_freeze_rules() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9011); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let mut contract = shielded_token_contract(identity.id(), platform_version); + contract + .token_configuration_mut(0) + .expect("token configuration") + .set_freeze_rules(ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::ContractOwner, + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + })); + + let create = DataContractCreateTransition::new_from_data_contract( + contract, + 1, + &identity.clone().into_partial_identity_info(), + key.id(), + &signer, + platform_version, + None, + ) + .await + .expect("contract create transition"); + + let result = process(&platform, &create); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::BasicError(BasicError::TokenShieldedPoolIncompatibleRulesError( + error + )) + )] if error.token_contract_position() == 0 && error.rule() == "freezeRules" + ); + } + /// `hasShieldedPool` is a format-version-1 field, so clients see it in the contract JSON /// and it survives the round trip. #[test] diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs index f1a60ac113d..b5aebe758d7 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs @@ -85,6 +85,13 @@ impl DataContractCreateStateTransitionBasicStructureValidationV0 for DataContrac return Ok(validation_result); } + let validation_result = + token_configuration.validate_shielded_pool_rules(*token_contract_position); + + if !validation_result.is_valid() { + return Ok(validation_result); + } + let validation_result = token_configuration .conventions() .validate_localizations(platform_version)?; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v0/mod.rs index 69afeb1c5a2..e19d7bc469d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v0/mod.rs @@ -70,6 +70,13 @@ impl DataContractUpdateStateTransitionBasicStructureValidationV0 for DataContrac return Ok(validation_result); } + let validation_result = + token_configuration.validate_shielded_pool_rules(*token_contract_position); + + if !validation_result.is_valid() { + return Ok(validation_result); + } + let validation_result = token_configuration .conventions() .validate_localizations(platform_version)?; diff --git a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs index 9eb801e884b..6e2da204469 100644 --- a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs +++ b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs @@ -66,7 +66,7 @@ use dpp::consensus::state::data_trigger::DataTriggerError::{ DataTriggerConditionError, DataTriggerExecutionError, DataTriggerInvalidResultError, }; use wasm_bindgen::{JsError, JsValue}; -use dpp::consensus::basic::data_contract::{ContestedUniqueIndexOnMutableDocumentTypeError, DataContractInvalidRequiredFieldsUpdateError, ContestedUniqueIndexWithUniqueIndexError, DataContractTokenConfigurationUpdateError, DecimalsOverLimitError, DuplicateKeywordsError, GroupExceedsMaxMembersError, GroupHasTooFewMembersError, GroupMemberHasPowerOfZeroError, GroupMemberHasPowerOverLimitError, GroupNonUnilateralMemberPowerHasLessThanRequiredPowerError, GroupPositionDoesNotExistError, GroupRequiredPowerIsInvalidError, GroupTotalPowerLessThanRequiredError, InvalidDescriptionLengthError, InvalidDocumentTypeRequiredSecurityLevelError, InvalidKeywordCharacterError, InvalidKeywordLengthError, InvalidTokenBaseSupplyError, InvalidTokenDistributionFunctionDivideByZeroError, InvalidTokenDistributionFunctionIncoherenceError, InvalidTokenDistributionFunctionInvalidParameterError, InvalidTokenDistributionFunctionInvalidParameterTupleError, InvalidTokenLanguageCodeError, InvalidTokenNameCharacterError, InvalidTokenNameLengthError, MainGroupIsNotDefinedError, NewTokensDestinationIdentityOptionRequiredError, NonContiguousContractGroupPositionsError, NonContiguousContractTokenPositionsError, PreProgrammedDistributionAmountOverLimitError, RedundantDocumentPaidForByTokenWithContractId, TokenPaymentByBurningOnlyAllowedOnInternalTokenError, TooManyKeywordsError, UnknownDocumentActionTokenEffectError, UnknownDocumentCreationRestrictionModeError, UnknownGasFeesPaidByError, UnknownSecurityLevelError, UnknownStorageKeyRequirementsError, UnknownTradeModeError, UnknownTransferableTypeError}; +use dpp::consensus::basic::data_contract::{ContestedUniqueIndexOnMutableDocumentTypeError, DataContractInvalidRequiredFieldsUpdateError, ContestedUniqueIndexWithUniqueIndexError, DataContractTokenConfigurationUpdateError, DecimalsOverLimitError, DuplicateKeywordsError, GroupExceedsMaxMembersError, GroupHasTooFewMembersError, GroupMemberHasPowerOfZeroError, GroupMemberHasPowerOverLimitError, GroupNonUnilateralMemberPowerHasLessThanRequiredPowerError, GroupPositionDoesNotExistError, GroupRequiredPowerIsInvalidError, GroupTotalPowerLessThanRequiredError, InvalidDescriptionLengthError, InvalidDocumentTypeRequiredSecurityLevelError, InvalidKeywordCharacterError, InvalidKeywordLengthError, InvalidTokenBaseSupplyError, InvalidTokenDistributionFunctionDivideByZeroError, InvalidTokenDistributionFunctionIncoherenceError, InvalidTokenDistributionFunctionInvalidParameterError, InvalidTokenDistributionFunctionInvalidParameterTupleError, InvalidTokenLanguageCodeError, InvalidTokenNameCharacterError, InvalidTokenNameLengthError, MainGroupIsNotDefinedError, NewTokensDestinationIdentityOptionRequiredError, NonContiguousContractGroupPositionsError, NonContiguousContractTokenPositionsError, PreProgrammedDistributionAmountOverLimitError, RedundantDocumentPaidForByTokenWithContractId, TokenPaymentByBurningOnlyAllowedOnInternalTokenError, TooManyKeywordsError, UnknownDocumentActionTokenEffectError, UnknownDocumentCreationRestrictionModeError, UnknownGasFeesPaidByError, UnknownSecurityLevelError, UnknownStorageKeyRequirementsError, UnknownTradeModeError, UnknownTransferableTypeError, TokenShieldedPoolIncompatibleRulesError}; use dpp::consensus::basic::document::{ContestedDocumentsTemporarilyNotAllowedError, DocumentCreationNotAllowedError, DocumentFieldMaxSizeExceededError, MaxDocumentsTransitionsExceededError, MissingPositionsInDocumentTypePropertiesError}; use dpp::consensus::basic::group::GroupActionNotAllowedOnTransitionError; use dpp::consensus::basic::identity::{DataContractBoundsNotPresentError, DisablingKeyIdAlsoBeingAddedInSameTransitionError, InvalidIdentityCreditWithdrawalTransitionAmountError, InvalidIdentityUpdateTransitionDisableKeysError, InvalidIdentityUpdateTransitionEmptyError, InvalidKeyPurposeForContractBoundsError, TooManyMasterPublicKeyError, WithdrawalOutputScriptNotAllowedWhenSigningWithOwnerKeyError}; @@ -1267,6 +1267,9 @@ fn from_basic_error(basic_error: &BasicError) -> JsValue { BasicError::InvalidContractModerationReasonDocumentsError(e) => { generic_consensus_error!(InvalidContractModerationReasonDocumentsError, e).into() } + BasicError::TokenShieldedPoolIncompatibleRulesError(e) => { + generic_consensus_error!(TokenShieldedPoolIncompatibleRulesError, e).into() + } } } diff --git a/packages/wasm-dpp2/src/tokens/configuration/token_configuration.rs b/packages/wasm-dpp2/src/tokens/configuration/token_configuration.rs index f653862e78c..211a3fbbba7 100644 --- a/packages/wasm-dpp2/src/tokens/configuration/token_configuration.rs +++ b/packages/wasm-dpp2/src/tokens/configuration/token_configuration.rs @@ -66,7 +66,9 @@ export interface TokenConfigurationOptions { description?: string; /** * Give the token its own shielded pool (protocol version 14+). Produces a - * format-version-1 token configuration; the flag cannot change after creation. + * format-version-1 token configuration; the flag cannot change after creation, and + * freezeRules, unfreezeRules and destroyFrozenFundsRules must then authorize no one + * (no action takers and no admins): shielded notes cannot be frozen or destroyed. */ hasShieldedPool?: boolean; } From 8f47d59b675b51e6bdf196b41515f514e41c0fcd Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 16 Sep 2026 13:55:04 +0700 Subject: [PATCH 04/94] feat(platform): mint, burn, claim and purchase into token shielded pools Four more batch token transitions move tokens straight into or out of a token's Orchard pool while an identity still signs and pays credits: TokenMintToPool and TokenBurnFromPool follow the manual minting and burning rules (group actions store a digest of the notes so every signer commits to the same bundle), TokenClaimToPool releases a distribution into a note (a perpetual claim names the cycle-aligned moment it claims up to so the amount is provable), and TokenDirectPurchaseToPool pays credits for tokens delivered shielded. Outputs-only bundles bind nothing extra; a burn binds token id, owner id and amount into its sighash. The shielded verification fee is charged by the action transformers so CheckTx admission and block execution price a bundle identically. Co-Authored-By: Claude Fable 5.1 --- .../protos/platform/v0/platform.proto | 16 + .../token_configuration/accessors/mod.rs | 18 +- packages/rs-dpp/src/shielded/builder/mod.rs | 8 + .../shielded/builder/token_burn_from_pool.rs | 171 +++++ .../shielded/builder/token_claim_to_pool.rs | 135 ++++ .../builder/token_direct_purchase_to_pool.rs | 121 ++++ .../shielded/builder/token_mint_to_pool.rs | 121 ++++ .../src/shielded/builder/token_shield.rs | 16 +- .../builder/token_shielded_transfer.rs | 9 +- .../src/shielded/builder/token_unshield.rs | 9 +- packages/rs-dpp/src/shielded/mod.rs | 20 +- packages/rs-dpp/src/shielded/sighash.rs | 43 ++ packages/rs-dpp/src/state_transition/mod.rs | 12 + .../src/state_transition/proof_result.rs | 3 + .../batched_transition/document_transition.rs | 21 +- .../batched_transition/mod.rs | 4 + .../batched_transition/resolvers.rs | 71 +- .../token_burn_from_pool_transition/mod.rs | 101 +++ .../token_burn_from_pool_transition/v0/mod.rs | 65 ++ .../v0/v0_methods.rs | 94 +++ .../v0_methods.rs | 115 +++ .../validate_structure/mod.rs | 36 + .../validate_structure/v0/mod.rs | 181 +++++ .../token_claim_to_pool_transition/mod.rs | 104 +++ .../token_claim_to_pool_transition/v0/mod.rs | 68 ++ .../v0/v0_methods.rs | 80 +++ .../v0_methods.rs | 84 +++ .../validate_structure/mod.rs | 36 + .../validate_structure/v0/mod.rs | 164 +++++ .../mod.rs | 101 +++ .../v0/mod.rs | 67 ++ .../v0/v0_methods.rs | 66 ++ .../v0_methods.rs | 72 ++ .../validate_structure/mod.rs | 38 + .../validate_structure/v0/mod.rs | 157 ++++ .../token_mint_to_pool_transition/mod.rs | 102 +++ .../token_mint_to_pool_transition/v0/mod.rs | 65 ++ .../v0/v0_methods.rs | 92 +++ .../v0_methods.rs | 113 +++ .../validate_structure/mod.rs | 36 + .../validate_structure/v0/mod.rs | 181 +++++ .../batched_transition/token_transition.rs | 101 ++- .../token_transition_action_type.rs | 28 + .../document/batch_transition/methods/mod.rs | 235 ++++++ .../batch_transition/methods/v1/mod.rs | 88 +++ .../document/batch_transition/mod.rs | 14 +- .../batch_transition/resolvers/v0/mod.rs | 16 +- .../batch_transition/v1/v1_methods.rs | 339 ++++++++- .../validate_basic_structure/v0/mod.rs | 16 + packages/rs-dpp/src/tokens/token_event.rs | 120 ++++ .../v0/mod.rs | 10 + .../process_raw_state_transitions/v0/mod.rs | 15 +- .../processor/traits/is_allowed.rs | 12 + .../processor/traits/shielded_proof.rs | 64 +- .../batch/action_validation/token/mod.rs | 4 + .../mod.rs | 64 ++ .../state_v0/mod.rs | 252 +++++++ .../mod.rs | 64 ++ .../state_v0/mod.rs | 121 ++++ .../mod.rs | 66 ++ .../state_v0/mod.rs | 122 ++++ .../mod.rs | 64 ++ .../state_v0/mod.rs | 181 +++++ .../state_v0/mod.rs | 2 - .../token/token_shielded_pool_common/mod.rs | 13 +- .../state_v0/mod.rs | 2 - .../state_v0/mod.rs | 2 - .../state_transitions/batch/state/v0/mod.rs | 48 ++ .../batch/tests/token/shielded/mod.rs | 677 +++++++++++++++++- .../batch/transformer/v0/mod.rs | 44 ++ .../group_queries/group_actions/v0/mod.rs | 24 +- packages/rs-drive/src/drive/shielded/paths.rs | 12 + .../shielded_pool/burn_from_pool/mod.rs | 48 ++ .../shielded_pool/burn_from_pool/v0/mod.rs | 48 ++ .../tokens/shielded_pool/mint_to_pool/mod.rs | 48 ++ .../shielded_pool/mint_to_pool/v0/mod.rs | 51 ++ .../src/drive/tokens/shielded_pool/mod.rs | 149 ++++ .../batch/token/mod.rs | 4 + .../token/token_burn_from_pool_transition.rs | 103 +++ .../token/token_claim_to_pool_transition.rs | 79 ++ ...oken_direct_purchase_to_pool_transition.rs | 73 ++ .../token/token_mint_to_pool_transition.rs | 103 +++ .../batch/token/token_transition.rs | 34 + .../token_transition/mod.rs | 42 +- .../mod.rs | 72 ++ .../transformer.rs | 53 ++ .../v0/mod.rs | 142 ++++ .../v0/transformer.rs | 120 ++++ .../mod.rs | 78 ++ .../transformer.rs | 53 ++ .../v0/mod.rs | 150 ++++ .../v0/transformer.rs | 158 ++++ .../token_claim_transition_action/mod.rs | 1 + .../token_claim_transition_action/v0/mod.rs | 1 + .../v0/transformer.rs | 663 ++++++++--------- .../mod.rs | 70 ++ .../transformer.rs | 53 ++ .../v0/mod.rs | 140 ++++ .../v0/transformer.rs | 153 ++++ .../mod.rs | 1 + .../v0/mod.rs | 1 + .../v0/transformer.rs | 198 ++--- .../mod.rs | 72 ++ .../transformer.rs | 53 ++ .../v0/mod.rs | 142 ++++ .../v0/transformer.rs | 120 ++++ .../v0/transformer.rs | 11 +- .../v0/transformer.rs | 11 +- .../token_transition_action_type.rs | 6 + .../v0/transformer.rs | 11 +- .../src/util/batch/drive_op_batch/token.rs | 50 ++ .../token_transition.rs | 20 +- .../v0/mod.rs | 55 +- .../drive_abci_validation_versions/mod.rs | 8 + .../drive_abci_validation_versions/v1.rs | 8 + .../drive_abci_validation_versions/v10.rs | 8 + .../drive_abci_validation_versions/v2.rs | 8 + .../drive_abci_validation_versions/v3.rs | 8 + .../drive_abci_validation_versions/v4.rs | 8 + .../drive_abci_validation_versions/v5.rs | 8 + .../drive_abci_validation_versions/v6.rs | 8 + .../drive_abci_validation_versions/v7.rs | 8 + .../drive_abci_validation_versions/v8.rs | 8 + .../drive_abci_validation_versions/v9.rs | 8 + .../mod.rs | 4 + .../v1.rs | 4 + .../v2.rs | 4 + .../v3.rs | 4 + .../v4.rs | 4 + .../drive_token_method_versions/mod.rs | 4 + .../drive_token_method_versions/v1.rs | 2 + .../token_transition/burn_from_pool.rs | 46 ++ .../token_transition/claim_to_pool.rs | 55 ++ .../direct_purchase_to_pool.rs | 46 ++ .../token_transition/mint_to_pool.rs | 46 ++ .../batch_transition/token_transition/mod.rs | 29 + packages/wasm-dpp2/src/group/token_event.rs | 12 + .../batch/token_transition.rs | 57 +- .../batch/token_transitions/mod.rs | 4 + .../token_transitions/token_burn_from_pool.rs | 166 +++++ .../token_transitions/token_claim_to_pool.rs | 168 +++++ .../token_direct_purchase_to_pool.rs | 153 ++++ .../token_transitions/token_mint_to_pool.rs | 163 +++++ .../state_transitions/proof_result/convert.rs | 12 +- .../state_transitions/proof_result/token.rs | 31 + 145 files changed, 9842 insertions(+), 536 deletions(-) create mode 100644 packages/rs-dpp/src/shielded/builder/token_burn_from_pool.rs create mode 100644 packages/rs-dpp/src/shielded/builder/token_claim_to_pool.rs create mode 100644 packages/rs-dpp/src/shielded/builder/token_direct_purchase_to_pool.rs create mode 100644 packages/rs-dpp/src/shielded/builder/token_mint_to_pool.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/v0/v0_methods.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/v0_methods.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/validate_structure/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/validate_structure/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/v0/v0_methods.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/v0_methods.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/validate_structure/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/validate_structure/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/v0/v0_methods.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/v0_methods.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/validate_structure/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/validate_structure/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/v0/v0_methods.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/v0_methods.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/validate_structure/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/validate_structure/v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_burn_from_pool_transition_action/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_burn_from_pool_transition_action/state_v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs create mode 100644 packages/rs-drive/src/drive/tokens/shielded_pool/burn_from_pool/mod.rs create mode 100644 packages/rs-drive/src/drive/tokens/shielded_pool/burn_from_pool/v0/mod.rs create mode 100644 packages/rs-drive/src/drive/tokens/shielded_pool/mint_to_pool/mod.rs create mode 100644 packages/rs-drive/src/drive/tokens/shielded_pool/mint_to_pool/v0/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_burn_from_pool_transition.rs create mode 100644 packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_claim_to_pool_transition.rs create mode 100644 packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_direct_purchase_to_pool_transition.rs create mode 100644 packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_mint_to_pool_transition.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_from_pool_transition_action/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_from_pool_transition_action/transformer.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_from_pool_transition_action/v0/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_from_pool_transition_action/v0/transformer.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/transformer.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/v0/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/v0/transformer.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_to_pool_transition_action/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_to_pool_transition_action/transformer.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_to_pool_transition_action/v0/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_to_pool_transition_action/v0/transformer.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_to_pool_transition_action/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_to_pool_transition_action/transformer.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_to_pool_transition_action/v0/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_to_pool_transition_action/v0/transformer.rs create mode 100644 packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/burn_from_pool.rs create mode 100644 packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/claim_to_pool.rs create mode 100644 packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/direct_purchase_to_pool.rs create mode 100644 packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/mint_to_pool.rs create mode 100644 packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_burn_from_pool.rs create mode 100644 packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_claim_to_pool.rs create mode 100644 packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_direct_purchase_to_pool.rs create mode 100644 packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_mint_to_pool.rs diff --git a/packages/dapi-grpc/protos/platform/v0/platform.proto b/packages/dapi-grpc/protos/platform/v0/platform.proto index 971c15c5827..0f21adbdc85 100644 --- a/packages/dapi-grpc/protos/platform/v0/platform.proto +++ b/packages/dapi-grpc/protos/platform/v0/platform.proto @@ -3558,6 +3558,20 @@ message GetGroupActionsResponse { optional string public_note = 3; // Public note } + // Mint straight into the token's shielded pool (protocol version 14+) + message MintToPoolEvent { + uint64 amount = 1; // Amount minted into the pool + bytes actions_digest = 2; // Digest of the Orchard actions every signer commits to + optional string public_note = 3; // Public note + } + + // Burn of notes held in the token's shielded pool (protocol version 14+) + message BurnFromPoolEvent { + uint64 amount = 1; // Amount destroyed + bytes actions_digest = 2; // Digest of the Orchard actions every signer commits to + optional string public_note = 3; // Public note + } + // Event associated with this action message GroupActionEvent { oneof event_type { @@ -3597,6 +3611,8 @@ message GetGroupActionsResponse { 7; // Token configuration update details UpdateDirectPurchasePriceEvent update_price = 8; // Updating the token direct selling price + MintToPoolEvent mint_to_pool = 9; // Mint into the token shielded pool + BurnFromPoolEvent burn_from_pool = 10; // Burn from the token shielded pool } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/mod.rs index 6ed02bc2e40..310cfd1a448 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/mod.rs @@ -245,8 +245,10 @@ impl TokenConfigurationV1Getters for TokenConfiguration { } impl TokenConfigurationV1Setters for TokenConfiguration { - /// Enabling the pool on a V0 configuration upgrades it in place to V1; disabling it on a - /// V0 configuration is a no-op (a V0 never has a pool). + /// The representation stays canonical: enabling the pool on a V0 configuration upgrades + /// it in place to V1, and disabling it on a V1 configuration downgrades it back to V0, so + /// a configuration without a pool always serializes as format version 0 and is accepted + /// by every protocol version. fn set_has_shielded_pool(&mut self, has_shielded_pool: bool) { match self { TokenConfiguration::V0(v0) => { @@ -256,7 +258,17 @@ impl TokenConfigurationV1Setters for TokenConfiguration { *self = TokenConfiguration::V1(TokenConfigurationV1::from_v0(base, true)); } } - TokenConfiguration::V1(v1) => v1.set_has_shielded_pool(has_shielded_pool), + TokenConfiguration::V1(v1) => { + if has_shielded_pool { + v1.set_has_shielded_pool(true); + } else { + let base = std::mem::replace( + &mut v1.base, + TokenConfigurationV0::default_most_restrictive(), + ); + *self = TokenConfiguration::V0(base); + } + } } } } diff --git a/packages/rs-dpp/src/shielded/builder/mod.rs b/packages/rs-dpp/src/shielded/builder/mod.rs index d26b49348bd..a5af790e119 100644 --- a/packages/rs-dpp/src/shielded/builder/mod.rs +++ b/packages/rs-dpp/src/shielded/builder/mod.rs @@ -36,6 +36,10 @@ mod shield_from_asset_lock; mod shield_from_identity; mod shielded_transfer; mod shielded_withdrawal; +mod token_burn_from_pool; +mod token_claim_to_pool; +mod token_direct_purchase_to_pool; +mod token_mint_to_pool; mod token_shield; mod token_shielded_transfer; mod token_unshield; @@ -52,6 +56,10 @@ pub use shield_from_asset_lock::build_shield_from_asset_lock_transition_with_sig pub use shield_from_identity::build_shield_from_identity_transition; pub use shielded_transfer::build_shielded_transfer_transition; pub use shielded_withdrawal::build_shielded_withdrawal_transition; +pub use token_burn_from_pool::build_token_burn_from_pool_transition; +pub use token_claim_to_pool::build_token_claim_to_pool_transition; +pub use token_direct_purchase_to_pool::build_token_direct_purchase_to_pool_transition; +pub use token_mint_to_pool::build_token_mint_to_pool_transition; pub use token_shield::build_token_shield_transition; pub use token_shielded_transfer::build_token_shielded_transfer_transition; pub use token_unshield::build_token_unshield_transition; diff --git a/packages/rs-dpp/src/shielded/builder/token_burn_from_pool.rs b/packages/rs-dpp/src/shielded/builder/token_burn_from_pool.rs new file mode 100644 index 00000000000..ddbbbbbc601 --- /dev/null +++ b/packages/rs-dpp/src/shielded/builder/token_burn_from_pool.rs @@ -0,0 +1,171 @@ +use grovedb_commitment_tree::{Anchor, FullViewingKey, SpendAuthorizingKey}; + +use crate::address_funds::OrchardAddress; +use crate::balances::credits::TokenAmount; +use crate::group::GroupStateTransitionInfoStatus; +use crate::identity::signer::Signer; +use crate::identity::IdentityPublicKey; +use crate::prelude::{Identifier, IdentityNonce, UserFeeIncrease}; +use crate::shielded::{token_burn_from_pool_extra_sighash_data, OrchardBundleParams}; +use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1; +use crate::state_transition::batch_transition::BatchTransition; +use crate::state_transition::StateTransition; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +use super::{build_spend_bundle, serialize_authorized_bundle, OrchardProver, SpendableNote}; + +/// Builds a `TokenBurnFromPool` batch transition: spends `spends` inside the token's shielded +/// pool, destroys `amount` and returns the remainder to `change_address`, then wraps the bundle +/// in a batch transition signed by `owner_id`, who must be authorized to burn and pays the fee +/// in credits. The token id, owner id and amount are bound into the Orchard sighash. +#[allow(clippy::too_many_arguments)] +pub async fn build_token_burn_from_pool_transition< + S: Signer, + P: OrchardProver, +>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + spends: Vec, + amount: TokenAmount, + change_address: &OrchardAddress, + fvk: &FullViewingKey, + ask: &SpendAuthorizingKey, + anchor: Anchor, + memo: [u8; 36], + public_note: Option, + using_group_info: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + prover: &P, + platform_version: &PlatformVersion, +) -> Result { + if amount == 0 { + return Err(ProtocolError::ShieldedBuildError( + "token burn from pool amount must be greater than zero".to_string(), + )); + } + if amount > i64::MAX as u64 { + return Err(ProtocolError::ShieldedBuildError(format!( + "token burn from pool amount {} exceeds maximum allowed value {}", + amount, + i64::MAX as u64 + ))); + } + + let total_spent: u64 = spends + .iter() + .try_fold(0u64, |total, spend| { + total.checked_add(spend.note.value().inner()) + }) + .ok_or_else(|| { + ProtocolError::ShieldedBuildError("total spendable value overflows u64".to_string()) + })?; + if amount > total_spent { + return Err(ProtocolError::ShieldedBuildError(format!( + "token burn from pool amount {} exceeds total spendable value {}", + amount, total_spent + ))); + } + let change_amount = total_spent - amount; + + let extra_sighash_data = token_burn_from_pool_extra_sighash_data( + &token_id.to_buffer(), + &owner_id.to_buffer(), + amount, + platform_version, + )?; + + let bundle = build_spend_bundle( + spends, + change_address, + change_amount, + memo, + fvk, + ask, + anchor, + prover, + &extra_sighash_data, + )?; + let sb = serialize_authorized_bundle(&bundle); + + if sb.value_balance != amount as i64 { + return Err(ProtocolError::ShieldedBuildError(format!( + "token burn from pool bundle value balance {} does not equal the amount {}", + sb.value_balance, amount + ))); + } + + BatchTransition::new_token_burn_from_pool_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + amount, + OrchardBundleParams { + actions: sb.actions, + anchor: sb.anchor, + proof: sb.proof, + binding_signature: sb.binding_signature, + }, + public_note, + using_group_info, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + None, + ) + .await +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::shielded::builder::test_helpers::{ + test_identity_key, test_orchard_address, test_spendable_note, DummyIdentitySigner, + TestProver, + }; + use grovedb_commitment_tree::SpendingKey; + + #[tokio::test] + async fn rejects_amount_above_spendable_value() { + let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes"); + let fvk = FullViewingKey::from(&sk); + let ask = SpendAuthorizingKey::from(&sk); + let key = test_identity_key(); + let err = build_token_burn_from_pool_transition( + Identifier::from([1u8; 32]), + Identifier::from([2u8; 32]), + Identifier::from([3u8; 32]), + 0, + vec![test_spendable_note(100)], + 1_000, + &test_orchard_address(), + &fvk, + &ask, + Anchor::empty_tree(), + [0u8; 36], + None, + None, + &key, + 1, + 0, + &DummyIdentitySigner, + &TestProver, + PlatformVersion::latest(), + ) + .await + .expect_err("overspend must be rejected") + .to_string(); + assert!( + err.contains("exceeds total spendable value"), + "unexpected error: {err}" + ); + } +} diff --git a/packages/rs-dpp/src/shielded/builder/token_claim_to_pool.rs b/packages/rs-dpp/src/shielded/builder/token_claim_to_pool.rs new file mode 100644 index 00000000000..414e17eec64 --- /dev/null +++ b/packages/rs-dpp/src/shielded/builder/token_claim_to_pool.rs @@ -0,0 +1,135 @@ +use grovedb_commitment_tree::OutgoingViewingKey; + +use crate::address_funds::OrchardAddress; +use crate::balances::credits::TokenAmount; +use crate::data_contract::associated_token::token_distribution_key::TokenDistributionType; +use crate::identity::signer::Signer; +use crate::identity::IdentityPublicKey; +use crate::prelude::{Identifier, IdentityNonce, UserFeeIncrease}; +use crate::shielded::OrchardBundleParams; +use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1; +use crate::state_transition::batch_transition::BatchTransition; +use crate::state_transition::StateTransition; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +use super::{build_output_only_bundle, serialize_authorized_bundle, OrchardProver}; + +/// Builds a `TokenClaimToPool` batch transition: proves an outputs-only Orchard bundle creating +/// `amount` of the token as a note for `recipient` inside the token's shielded pool, then wraps +/// it in a batch transition signed by `owner_id`, the claimant, who pays the fee in credits. +/// +/// `amount` must be exactly what consensus will pay out: the next pre-programmed release, or for +/// a perpetual distribution the rewards from the last claim up to `claim_up_to` (a cycle-aligned +/// moment in the distribution's own unit, not after the current interval). A mismatch is a paid +/// failure, so the client should compute the amount from the same state it read the moment from. +#[allow(clippy::too_many_arguments)] +pub async fn build_token_claim_to_pool_transition< + S: Signer, + P: OrchardProver, +>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + recipient: &OrchardAddress, + amount: TokenAmount, + distribution_type: TokenDistributionType, + claim_up_to: Option, + memo: [u8; 36], + sender_ovk: Option, + public_note: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + prover: &P, + platform_version: &PlatformVersion, +) -> Result { + if amount == 0 { + return Err(ProtocolError::ShieldedBuildError( + "token claim to pool amount must be greater than zero".to_string(), + )); + } + if amount > i64::MAX as u64 { + return Err(ProtocolError::ShieldedBuildError(format!( + "token claim to pool amount {} exceeds maximum allowed value {}", + amount, + i64::MAX as u64 + ))); + } + if distribution_type == TokenDistributionType::Perpetual && claim_up_to.is_none() { + return Err(ProtocolError::ShieldedBuildError( + "a perpetual claim into the pool must name the moment it claims up to".to_string(), + )); + } + + let bundle = build_output_only_bundle(recipient, amount, memo, sender_ovk, 0, prover)?; + let sb = serialize_authorized_bundle(&bundle); + + if sb.value_balance != -(amount as i64) { + return Err(ProtocolError::ShieldedBuildError(format!( + "token claim to pool amount bundle value balance {} does not equal -{}", + sb.value_balance, amount + ))); + } + + BatchTransition::new_token_claim_to_pool_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + distribution_type, + claim_up_to, + OrchardBundleParams { + actions: sb.actions, + anchor: sb.anchor, + proof: sb.proof, + binding_signature: sb.binding_signature, + }, + public_note, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + None, + ) + .await +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::shielded::builder::test_helpers::{ + test_identity_key, test_orchard_address, DummyIdentitySigner, TestProver, + }; + + #[tokio::test] + async fn perpetual_claim_needs_a_moment() { + let key = test_identity_key(); + let err = build_token_claim_to_pool_transition( + Identifier::from([1u8; 32]), + Identifier::from([2u8; 32]), + Identifier::from([3u8; 32]), + 0, + &test_orchard_address(), + 100, + TokenDistributionType::Perpetual, + None, + [0u8; 36], + None, + None, + &key, + 1, + 0, + &DummyIdentitySigner, + &TestProver, + PlatformVersion::latest(), + ) + .await + .expect_err("perpetual claim without a moment must be rejected") + .to_string(); + assert!(err.contains("claims up to"), "unexpected error: {err}"); + } +} diff --git a/packages/rs-dpp/src/shielded/builder/token_direct_purchase_to_pool.rs b/packages/rs-dpp/src/shielded/builder/token_direct_purchase_to_pool.rs new file mode 100644 index 00000000000..011b2cba269 --- /dev/null +++ b/packages/rs-dpp/src/shielded/builder/token_direct_purchase_to_pool.rs @@ -0,0 +1,121 @@ +use grovedb_commitment_tree::OutgoingViewingKey; + +use crate::address_funds::OrchardAddress; +use crate::balances::credits::TokenAmount; +use crate::fee::Credits; +use crate::identity::signer::Signer; +use crate::identity::IdentityPublicKey; +use crate::prelude::{Identifier, IdentityNonce, UserFeeIncrease}; +use crate::shielded::OrchardBundleParams; +use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1; +use crate::state_transition::batch_transition::BatchTransition; +use crate::state_transition::StateTransition; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +use super::{build_output_only_bundle, serialize_authorized_bundle, OrchardProver}; + +/// Builds a `TokenDirectPurchaseToPool` batch transition: proves an outputs-only Orchard bundle +/// creating `token_count` of the token as a note for `recipient` inside the token's shielded +/// pool, then wraps it in a batch transition signed by `owner_id`, the buyer, who pays at most +/// `total_agreed_price` credits to the contract owner plus the fee. +#[allow(clippy::too_many_arguments)] +pub async fn build_token_direct_purchase_to_pool_transition< + S: Signer, + P: OrchardProver, +>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + recipient: &OrchardAddress, + token_count: TokenAmount, + total_agreed_price: Credits, + memo: [u8; 36], + sender_ovk: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + prover: &P, + platform_version: &PlatformVersion, +) -> Result { + if token_count == 0 { + return Err(ProtocolError::ShieldedBuildError( + "token direct purchase to pool count must be greater than zero".to_string(), + )); + } + if token_count > i64::MAX as u64 { + return Err(ProtocolError::ShieldedBuildError(format!( + "token direct purchase to pool count {} exceeds maximum allowed value {}", + token_count, + i64::MAX as u64 + ))); + } + + let bundle = build_output_only_bundle(recipient, token_count, memo, sender_ovk, 0, prover)?; + let sb = serialize_authorized_bundle(&bundle); + + if sb.value_balance != -(token_count as i64) { + return Err(ProtocolError::ShieldedBuildError(format!( + "token direct purchase to pool count bundle value balance {} does not equal -{}", + sb.value_balance, token_count + ))); + } + + BatchTransition::new_token_direct_purchase_to_pool_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + token_count, + total_agreed_price, + OrchardBundleParams { + actions: sb.actions, + anchor: sb.anchor, + proof: sb.proof, + binding_signature: sb.binding_signature, + }, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + None, + ) + .await +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::shielded::builder::test_helpers::{ + test_identity_key, test_orchard_address, DummyIdentitySigner, TestProver, + }; + + #[tokio::test] + async fn rejects_zero_count() { + let key = test_identity_key(); + let err = build_token_direct_purchase_to_pool_transition( + Identifier::from([1u8; 32]), + Identifier::from([2u8; 32]), + Identifier::from([3u8; 32]), + 0, + &test_orchard_address(), + 0, + 1_000, + [0u8; 36], + None, + &key, + 1, + 0, + &DummyIdentitySigner, + &TestProver, + PlatformVersion::latest(), + ) + .await + .expect_err("zero count must be rejected") + .to_string(); + assert!(err.contains("greater than zero"), "unexpected error: {err}"); + } +} diff --git a/packages/rs-dpp/src/shielded/builder/token_mint_to_pool.rs b/packages/rs-dpp/src/shielded/builder/token_mint_to_pool.rs new file mode 100644 index 00000000000..5c0b6acef30 --- /dev/null +++ b/packages/rs-dpp/src/shielded/builder/token_mint_to_pool.rs @@ -0,0 +1,121 @@ +use grovedb_commitment_tree::OutgoingViewingKey; + +use crate::address_funds::OrchardAddress; +use crate::balances::credits::TokenAmount; +use crate::group::GroupStateTransitionInfoStatus; +use crate::identity::signer::Signer; +use crate::identity::IdentityPublicKey; +use crate::prelude::{Identifier, IdentityNonce, UserFeeIncrease}; +use crate::shielded::OrchardBundleParams; +use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1; +use crate::state_transition::batch_transition::BatchTransition; +use crate::state_transition::StateTransition; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +use super::{build_output_only_bundle, serialize_authorized_bundle, OrchardProver}; + +/// Builds a `TokenMintToPool` batch transition: proves an outputs-only Orchard bundle creating +/// `amount` of the token as a note for `recipient` inside the token's shielded pool, then wraps +/// it in a batch transition signed by `owner_id`, who must be authorized to mint and pays the +/// fee in credits. `using_group_info` sets up a group action exactly like a transparent mint. +#[allow(clippy::too_many_arguments)] +pub async fn build_token_mint_to_pool_transition, P: OrchardProver>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + recipient: &OrchardAddress, + amount: TokenAmount, + memo: [u8; 36], + sender_ovk: Option, + public_note: Option, + using_group_info: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + prover: &P, + platform_version: &PlatformVersion, +) -> Result { + if amount == 0 { + return Err(ProtocolError::ShieldedBuildError( + "token mint to pool amount must be greater than zero".to_string(), + )); + } + if amount > i64::MAX as u64 { + return Err(ProtocolError::ShieldedBuildError(format!( + "token mint to pool amount {} exceeds maximum allowed value {}", + amount, + i64::MAX as u64 + ))); + } + + let bundle = build_output_only_bundle(recipient, amount, memo, sender_ovk, 0, prover)?; + let sb = serialize_authorized_bundle(&bundle); + + if sb.value_balance != -(amount as i64) { + return Err(ProtocolError::ShieldedBuildError(format!( + "token mint to pool amount bundle value balance {} does not equal -{}", + sb.value_balance, amount + ))); + } + + BatchTransition::new_token_mint_to_pool_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + amount, + OrchardBundleParams { + actions: sb.actions, + anchor: sb.anchor, + proof: sb.proof, + binding_signature: sb.binding_signature, + }, + public_note, + using_group_info, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + None, + ) + .await +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::shielded::builder::test_helpers::{ + test_identity_key, test_orchard_address, DummyIdentitySigner, TestProver, + }; + + #[tokio::test] + async fn rejects_zero_amount() { + let key = test_identity_key(); + let err = build_token_mint_to_pool_transition( + Identifier::from([1u8; 32]), + Identifier::from([2u8; 32]), + Identifier::from([3u8; 32]), + 0, + &test_orchard_address(), + 0, + [0u8; 36], + None, + None, + None, + &key, + 1, + 0, + &DummyIdentitySigner, + &TestProver, + PlatformVersion::latest(), + ) + .await + .expect_err("zero amount must be rejected") + .to_string(); + assert!(err.contains("greater than zero"), "unexpected error: {err}"); + } +} diff --git a/packages/rs-dpp/src/shielded/builder/token_shield.rs b/packages/rs-dpp/src/shielded/builder/token_shield.rs index ad347ba39f4..ef8b73d8e0a 100644 --- a/packages/rs-dpp/src/shielded/builder/token_shield.rs +++ b/packages/rs-dpp/src/shielded/builder/token_shield.rs @@ -44,16 +44,30 @@ pub async fn build_token_shield_transition, P: Orch "token shield amount must be greater than zero".to_string(), )); } + if amount > i64::MAX as u64 { + return Err(ProtocolError::ShieldedBuildError(format!( + "token shield amount {} exceeds maximum allowed value {}", + amount, + i64::MAX as u64 + ))); + } let bundle = build_output_only_bundle(recipient, amount, memo, sender_ovk, 0, prover)?; let sb = serialize_authorized_bundle(&bundle); + if sb.value_balance != -(amount as i64) { + return Err(ProtocolError::ShieldedBuildError(format!( + "token shield amount bundle value balance {} does not equal -{}", + sb.value_balance, amount + ))); + } + BatchTransition::new_token_shield_transition( token_id, owner_id, data_contract_id, token_contract_position, - sb.value_balance.unsigned_abs(), + amount, OrchardBundleParams { actions: sb.actions, anchor: sb.anchor, diff --git a/packages/rs-dpp/src/shielded/builder/token_shielded_transfer.rs b/packages/rs-dpp/src/shielded/builder/token_shielded_transfer.rs index b67369d6a38..cb7c29957b7 100644 --- a/packages/rs-dpp/src/shielded/builder/token_shielded_transfer.rs +++ b/packages/rs-dpp/src/shielded/builder/token_shielded_transfer.rs @@ -56,7 +56,14 @@ pub async fn build_token_shielded_transfer_transition< )); } - let total_spent: u64 = spends.iter().map(|s| s.note.value().inner()).sum(); + let total_spent: u64 = spends + .iter() + .try_fold(0u64, |total, spend| { + total.checked_add(spend.note.value().inner()) + }) + .ok_or_else(|| { + ProtocolError::ShieldedBuildError("total spendable value overflows u64".to_string()) + })?; if transfer_amount > total_spent { return Err(ProtocolError::ShieldedBuildError(format!( "token shielded transfer amount {} exceeds total spendable value {}", diff --git a/packages/rs-dpp/src/shielded/builder/token_unshield.rs b/packages/rs-dpp/src/shielded/builder/token_unshield.rs index c377f8eac88..c8a33afe969 100644 --- a/packages/rs-dpp/src/shielded/builder/token_unshield.rs +++ b/packages/rs-dpp/src/shielded/builder/token_unshield.rs @@ -56,7 +56,14 @@ pub async fn build_token_unshield_transition, P: Or ))); } - let total_spent: u64 = spends.iter().map(|s| s.note.value().inner()).sum(); + let total_spent: u64 = spends + .iter() + .try_fold(0u64, |total, spend| { + total.checked_add(spend.note.value().inner()) + }) + .ok_or_else(|| { + ProtocolError::ShieldedBuildError("total spendable value overflows u64".to_string()) + })?; if amount > total_spent { return Err(ProtocolError::ShieldedBuildError(format!( "token unshield amount {} exceeds total spendable value {}", diff --git a/packages/rs-dpp/src/shielded/mod.rs b/packages/rs-dpp/src/shielded/mod.rs index ba9accde943..451e4260945 100644 --- a/packages/rs-dpp/src/shielded/mod.rs +++ b/packages/rs-dpp/src/shielded/mod.rs @@ -5,6 +5,7 @@ mod compute_minimum_shielded_fee; pub mod memo; mod sighash; +use crate::util::hash::hash_single; pub use memo::{ShieldedMemo, MEMO_PAYLOAD_SIZE, MEMO_SIZE}; use bincode::{Decode, DecodeUntrusted, Encode}; @@ -23,12 +24,29 @@ pub use compute_minimum_shielded_fee::{ // Re-exported so the public paths stay `dpp::shielded::` after moving the sighash preimage // builders into their own file. Both the version-dispatching wrappers and their `_v0` impls are // re-exported (callers use the wrappers; byte-layout tests use the `_v0` impls). +/// A digest of serialized Orchard actions in wire order: every field of every action, hashed +/// once. A group action stores it so every signer commits to exactly the same notes, and a +/// pool mint or burn folds it into its group action id. +pub fn serialized_actions_digest(actions: &[SerializedAction]) -> [u8; 32] { + let mut bytes = Vec::new(); + for action in actions { + bytes.extend_from_slice(&action.nullifier); + bytes.extend_from_slice(&action.rk); + bytes.extend_from_slice(&action.cmx); + bytes.extend_from_slice(&action.encrypted_note); + bytes.extend_from_slice(&action.cv_net); + bytes.extend_from_slice(&action.spend_auth_sig); + } + hash_single(bytes) +} + pub use sighash::{ compute_platform_sighash, identity_create_from_shielded_extra_sighash_data, identity_create_from_shielded_extra_sighash_data_v0, identity_top_up_from_shielded_extra_sighash_data, identity_top_up_from_shielded_extra_sighash_data_v0, shielded_withdrawal_extra_sighash_data, - shielded_withdrawal_extra_sighash_data_v0, token_shielded_transfer_extra_sighash_data, + shielded_withdrawal_extra_sighash_data_v0, token_burn_from_pool_extra_sighash_data, + token_burn_from_pool_extra_sighash_data_v0, token_shielded_transfer_extra_sighash_data, token_shielded_transfer_extra_sighash_data_v0, token_unshield_extra_sighash_data, token_unshield_extra_sighash_data_v0, unshield_extra_sighash_data, unshield_extra_sighash_data_v0, diff --git a/packages/rs-dpp/src/shielded/sighash.rs b/packages/rs-dpp/src/shielded/sighash.rs index 95617294330..daf879e752b 100644 --- a/packages/rs-dpp/src/shielded/sighash.rs +++ b/packages/rs-dpp/src/shielded/sighash.rs @@ -346,6 +346,40 @@ pub fn token_unshield_extra_sighash_data_v0( data } +/// Extra sighash data of a batch `TokenBurnFromPool`: the token id, the burning identity and the +/// amount destroyed, so a bundle proven for one burn cannot be replayed for another token, +/// signer or amount (72 bytes; no other layout has that length). +pub fn token_burn_from_pool_extra_sighash_data( + token_id: &[u8; 32], + owner_id: &[u8; 32], + amount: u64, + platform_version: &PlatformVersion, +) -> Result, ProtocolError> { + match platform_version.dpp.methods.shielded_extra_sighash_data { + 0 => Ok(token_burn_from_pool_extra_sighash_data_v0( + token_id, owner_id, amount, + )), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "token_burn_from_pool_extra_sighash_data".to_string(), + known_versions: vec![0], + received: version, + }), + } +} + +/// Version 0 layout: `token_id (32) || owner_id (32) || amount (8, little endian)`. +pub fn token_burn_from_pool_extra_sighash_data_v0( + token_id: &[u8; 32], + owner_id: &[u8; 32], + amount: u64, +) -> Vec { + let mut data = Vec::with_capacity(32 + 32 + 8); + data.extend_from_slice(token_id); + data.extend_from_slice(owner_id); + data.extend_from_slice(&amount.to_le_bytes()); + data +} + /// Builds the transparent `extra_data` bound into a `TokenShieldedTransfer`'s platform sighash, /// with the byte layout `token_id (32) || owner_id (32)`. /// @@ -688,4 +722,13 @@ mod tests { assert_eq!(&data[data.len() - 32..], &[0x44u8; 32]); } } + + #[test] + fn token_burn_from_pool_layout_is_token_owner_amount() { + let data = token_burn_from_pool_extra_sighash_data_v0(&[1u8; 32], &[2u8; 32], 300); + assert_eq!(data.len(), 72); + assert_eq!(&data[..32], &[1u8; 32]); + assert_eq!(&data[32..64], &[2u8; 32]); + assert_eq!(&data[64..], &300u64.to_le_bytes()); + } } diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index c750134e58b..e40a76a63da 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -1170,6 +1170,18 @@ impl StateTransition { BatchedTransitionRef::Token(TokenTransition::ShieldedTransfer(_)) => { "TokenShieldedTransfer" } + BatchedTransitionRef::Token(TokenTransition::MintToPool(_)) => { + "TokenMintToPool" + } + BatchedTransitionRef::Token(TokenTransition::BurnFromPool(_)) => { + "TokenBurnFromPool" + } + BatchedTransitionRef::Token(TokenTransition::ClaimToPool(_)) => { + "TokenClaimToPool" + } + BatchedTransitionRef::Token(TokenTransition::DirectPurchaseToPool(_)) => { + "TokenDirectPurchaseToPool" + } }; document_transition_types.push(document_transition_name); } diff --git a/packages/rs-dpp/src/state_transition/proof_result.rs b/packages/rs-dpp/src/state_transition/proof_result.rs index 53b2a75f7ec..2a05cca192f 100644 --- a/packages/rs-dpp/src/state_transition/proof_result.rs +++ b/packages/rs-dpp/src/state_transition/proof_result.rs @@ -104,6 +104,9 @@ pub enum StateTransitionProofResult { ), VerifiedAssetLockConsumed(StoredAssetLockInfo), VerifiedShieldedNullifiers(Vec<(Vec, bool)>), + /// The proven total balance of a token's shielded pool (token id, balance). Returned by the + /// pool transitions that only create notes (mint, claim and purchase into the pool). + VerifiedTokenShieldedPoolBalance(Identifier, TokenAmount), VerifiedShieldedNullifiersWithAddressInfos( Vec<(Vec, bool)>, #[cfg_attr( diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs index d02cad104a0..3dab3276371 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_transition.rs @@ -5,7 +5,8 @@ use derive_more::{Display, From}; use serde::{Deserialize, Serialize}; use bincode::{Encode, Decode, DecodeUntrusted}; use crate::prelude::{IdentityNonce, Revision}; -use crate::state_transition::batch_transition::{DocumentCreateTransition, DocumentDeleteTransition, DocumentReplaceTransition, TokenBurnTransition, TokenConfigUpdateTransition, TokenDestroyFrozenFundsTransition, TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintTransition, TokenClaimTransition, TokenTransferTransition, TokenUnfreezeTransition, TokenDirectPurchaseTransition, TokenSetPriceForDirectPurchaseTransition, TokenShieldTransition, TokenShieldedTransferTransition, TokenUnshieldTransition}; +use crate::state_transition::batch_transition::{DocumentCreateTransition, DocumentDeleteTransition, DocumentReplaceTransition, TokenBurnTransition, TokenConfigUpdateTransition, TokenDestroyFrozenFundsTransition, TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintTransition, TokenClaimTransition, TokenTransferTransition, TokenUnfreezeTransition, TokenDirectPurchaseTransition, TokenSetPriceForDirectPurchaseTransition, TokenShieldTransition, TokenMintToPoolTransition, TokenBurnFromPoolTransition, + TokenClaimToPoolTransition, TokenDirectPurchaseToPoolTransition, TokenShieldedTransferTransition, TokenUnshieldTransition}; use crate::state_transition::batch_transition::batched_transition::{DocumentIndexOnlyDeleteTransition, DocumentPurchaseTransition, DocumentTransferTransition, DocumentUpdatePriceTransition}; use crate::state_transition::batch_transition::batched_transition::document_index_only_delete_transition::v0::v0_methods::DocumentIndexOnlyDeleteTransitionV0Methods; use crate::state_transition::batch_transition::batched_transition::document_purchase_transition::v0::v0_methods::DocumentPurchaseTransitionV0Methods; @@ -284,6 +285,24 @@ impl BatchTransitionResolversV0 for DocumentTransition { fn as_transition_token_shielded_transfer(&self) -> Option<&TokenShieldedTransferTransition> { None } + + fn as_transition_token_mint_to_pool(&self) -> Option<&TokenMintToPoolTransition> { + None + } + + fn as_transition_token_burn_from_pool(&self) -> Option<&TokenBurnFromPoolTransition> { + None + } + + fn as_transition_token_claim_to_pool(&self) -> Option<&TokenClaimToPoolTransition> { + None + } + + fn as_transition_token_direct_purchase_to_pool( + &self, + ) -> Option<&TokenDirectPurchaseToPoolTransition> { + None + } } pub trait DocumentTransitionV0Methods { diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs index 3bce73e6f2f..915fea2a187 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/mod.rs @@ -16,13 +16,17 @@ pub mod document_update_price_transition; pub mod multi_party_action; mod resolvers; pub mod token_base_transition; +pub mod token_burn_from_pool_transition; pub mod token_burn_transition; +pub mod token_claim_to_pool_transition; pub mod token_claim_transition; pub mod token_config_update_transition; pub mod token_destroy_frozen_funds_transition; +pub mod token_direct_purchase_to_pool_transition; pub mod token_direct_purchase_transition; pub mod token_emergency_action_transition; pub mod token_freeze_transition; +pub mod token_mint_to_pool_transition; pub mod token_mint_transition; pub mod token_set_price_for_direct_purchase_transition; pub mod token_shield_transition; diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/resolvers.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/resolvers.rs index 794e6133bd4..787ef54613c 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/resolvers.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/resolvers.rs @@ -4,10 +4,11 @@ use crate::state_transition::batch_transition::batched_transition::{ use crate::state_transition::batch_transition::resolvers::v0::BatchTransitionResolversV0; use crate::state_transition::batch_transition::{ DocumentCreateTransition, DocumentDeleteTransition, DocumentReplaceTransition, - TokenBurnTransition, TokenClaimTransition, TokenConfigUpdateTransition, - TokenDestroyFrozenFundsTransition, TokenDirectPurchaseTransition, - TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintTransition, - TokenSetPriceForDirectPurchaseTransition, TokenShieldTransition, + TokenBurnFromPoolTransition, TokenBurnTransition, TokenClaimToPoolTransition, + TokenClaimTransition, TokenConfigUpdateTransition, TokenDestroyFrozenFundsTransition, + TokenDirectPurchaseToPoolTransition, TokenDirectPurchaseTransition, + TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintToPoolTransition, + TokenMintTransition, TokenSetPriceForDirectPurchaseTransition, TokenShieldTransition, TokenShieldedTransferTransition, TokenTransferTransition, TokenUnfreezeTransition, TokenUnshieldTransition, }; @@ -151,6 +152,36 @@ impl BatchTransitionResolversV0 for BatchedTransition { BatchedTransition::Token(token) => token.as_transition_token_shielded_transfer(), } } + + fn as_transition_token_mint_to_pool(&self) -> Option<&TokenMintToPoolTransition> { + match self { + BatchedTransition::Document(_) => None, + BatchedTransition::Token(token) => token.as_transition_token_mint_to_pool(), + } + } + + fn as_transition_token_burn_from_pool(&self) -> Option<&TokenBurnFromPoolTransition> { + match self { + BatchedTransition::Document(_) => None, + BatchedTransition::Token(token) => token.as_transition_token_burn_from_pool(), + } + } + + fn as_transition_token_claim_to_pool(&self) -> Option<&TokenClaimToPoolTransition> { + match self { + BatchedTransition::Document(_) => None, + BatchedTransition::Token(token) => token.as_transition_token_claim_to_pool(), + } + } + + fn as_transition_token_direct_purchase_to_pool( + &self, + ) -> Option<&TokenDirectPurchaseToPoolTransition> { + match self { + BatchedTransition::Document(_) => None, + BatchedTransition::Token(token) => token.as_transition_token_direct_purchase_to_pool(), + } + } } impl BatchTransitionResolversV0 for BatchedTransitionRef<'_> { @@ -292,6 +323,38 @@ impl BatchTransitionResolversV0 for BatchedTransitionRef<'_> { BatchedTransitionRef::Token(token) => token.as_transition_token_shielded_transfer(), } } + + fn as_transition_token_mint_to_pool(&self) -> Option<&TokenMintToPoolTransition> { + match self { + BatchedTransitionRef::Document(_) => None, + BatchedTransitionRef::Token(token) => token.as_transition_token_mint_to_pool(), + } + } + + fn as_transition_token_burn_from_pool(&self) -> Option<&TokenBurnFromPoolTransition> { + match self { + BatchedTransitionRef::Document(_) => None, + BatchedTransitionRef::Token(token) => token.as_transition_token_burn_from_pool(), + } + } + + fn as_transition_token_claim_to_pool(&self) -> Option<&TokenClaimToPoolTransition> { + match self { + BatchedTransitionRef::Document(_) => None, + BatchedTransitionRef::Token(token) => token.as_transition_token_claim_to_pool(), + } + } + + fn as_transition_token_direct_purchase_to_pool( + &self, + ) -> Option<&TokenDirectPurchaseToPoolTransition> { + match self { + BatchedTransitionRef::Document(_) => None, + BatchedTransitionRef::Token(token) => { + token.as_transition_token_direct_purchase_to_pool() + } + } + } } #[cfg(test)] diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/mod.rs new file mode 100644 index 00000000000..96d0a5a3ce4 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/mod.rs @@ -0,0 +1,101 @@ +pub mod v0; +mod v0_methods; +pub mod validate_structure; + +use bincode::{Decode, DecodeUntrusted, Encode}; +use derive_more::{Display, From}; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; +pub use v0::TokenBurnFromPoolTransitionV0; + +/// Destroys `amount` of a token held inside the token's shielded pool. +/// +/// The Orchard bundle spends notes and has a value balance of `+amount`; the tokens leave the pool +/// and the total supply. The burner must be authorized by `manualBurningRules` like a transparent +/// burn, and the token id, owner id and amount are bound into the Orchard sighash. +#[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From, DecodeUntrusted)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] +pub enum TokenBurnFromPoolTransition { + #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] + V0(TokenBurnFromPoolTransitionV0), +} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenBurnFromPoolTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenBurnFromPoolTransition {} + +impl Default for TokenBurnFromPoolTransition { + fn default() -> Self { + TokenBurnFromPoolTransition::V0(TokenBurnFromPoolTransitionV0::default()) + } +} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::shielded::SerializedAction; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::TokenBaseTransition; + use platform_value::Identifier; + + pub(crate) fn fixture() -> TokenBurnFromPoolTransition { + TokenBurnFromPoolTransition::V0(TokenBurnFromPoolTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 15, + token_contract_position: 1, + data_contract_id: Identifier::new([0xa1; 32]), + token_id: Identifier::new([0xb2; 32]), + using_group_info: None, + }), + amount: 4_000, + actions: vec![SerializedAction { + nullifier: [0x11; 32], + rk: [0x22; 32], + cmx: [0x33; 32], + encrypted_note: vec![0x44; 216], + cv_net: [0x55; 32], + spend_auth_sig: [0x66; 64], + }], + anchor: [0x77; 32], + proof: vec![0x88; 8], + binding_signature: [0x99; 64], + public_note: Some("note".to_string()), + }) + } + + #[test] + fn json_round_trip_keeps_envelope_and_bundle() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + assert_eq!(json["$formatVersion"], "0"); + assert_eq!(json["$baseFormatVersion"], "0"); + assert_eq!(json["$tokenContractPosition"], 1); + assert_eq!(json["amount"], 4_000); + assert!(json["actions"].is_array()); + assert!(json["anchor"].is_string(), "byte fields are base64 in JSON"); + let recovered = TokenBurnFromPoolTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + let recovered = TokenBurnFromPoolTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/v0/mod.rs new file mode 100644 index 00000000000..0c10925800a --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/v0/mod.rs @@ -0,0 +1,65 @@ +pub mod v0_methods; + +use crate::balances::credits::TokenAmount; +use crate::shielded::SerializedAction; +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +use bincode::{Decode, DecodeUntrusted, Encode}; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; +use std::fmt; + +/// The Identifier fields in [`TokenBurnFromPoolTransition`] +pub use super::super::document_base_transition::IDENTIFIER_FIELDS; + +#[derive(Debug, Clone, Encode, Decode, PartialEq, DecodeUntrusted)] +// Auto-injects `json_safe_u64` on the u64 fields and `serde_bytes` on the fixed byte arrays / +// `serde_bytes_var` on `proof` (base64 strings in JSON, raw bytes in Value). +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +pub struct TokenBurnFromPoolTransitionV0 { + /// Token base transition (nonce, contract, token position, token id). + #[cfg_attr(feature = "serde-conversion", serde(flatten))] + pub base: TokenBaseTransition, + /// Tokens entering or leaving the pool. Equals the absolute value of the bundle's value balance. + pub amount: TokenAmount, + /// Orchard actions (spend-output pairs). + pub actions: Vec, + /// Sinsemilla root of the token pool's note commitment tree (Orchard anchor). + pub anchor: [u8; 32], + /// Halo 2 proof bytes. + pub proof: Vec, + /// RedPallas binding signature. + pub binding_signature: [u8; 64], + /// Optional public note. Only a group action proposer may set one. + pub public_note: Option, +} + +impl Default for TokenBurnFromPoolTransitionV0 { + fn default() -> Self { + Self { + base: TokenBaseTransition::default(), + amount: 0, + actions: vec![], + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + public_note: None, + } + } +} + +impl fmt::Display for TokenBurnFromPoolTransitionV0 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Token Burn From Pool, base: {}, amount: {}, actions: {}", + self.base, + self.amount, + self.actions.len() + ) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/v0/v0_methods.rs new file mode 100644 index 00000000000..a96d7d703fb --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/v0/v0_methods.rs @@ -0,0 +1,94 @@ +use crate::balances::credits::TokenAmount; +use crate::shielded::SerializedAction; +use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +use crate::state_transition::batch_transition::token_burn_from_pool_transition::TokenBurnFromPoolTransition; +use crate::state_transition::batch_transition::token_burn_from_pool_transition::TokenBurnFromPoolTransitionV0; +use crate::state_transition::batch_transition::batched_transition::multi_party_action::AllowedAsMultiPartyAction; +use crate::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; +use platform_value::Identifier; +use platform_version::version::PlatformVersion; +use crate::ProtocolError; + +impl TokenBaseTransitionAccessors for TokenBurnFromPoolTransitionV0 { + fn base(&self) -> &TokenBaseTransition { + &self.base + } + + fn base_mut(&mut self) -> &mut TokenBaseTransition { + &mut self.base + } + + fn set_base(&mut self, base: TokenBaseTransition) { + self.base = base; + } +} + +pub trait TokenBurnFromPoolTransitionV0Methods: TokenBaseTransitionAccessors { + /// Tokens entering or leaving the pool. Equals the absolute value of the bundle's value balance. + fn amount(&self) -> TokenAmount; + /// Orchard actions. + fn actions(&self) -> &[SerializedAction]; + /// Sinsemilla root of the token pool's note commitment tree (Orchard anchor). + fn anchor(&self) -> &[u8; 32]; + /// Halo 2 proof bytes. + fn proof(&self) -> &[u8]; + /// RedPallas binding signature. + fn binding_signature(&self) -> &[u8; 64]; + /// Optional public note. Only a group action proposer may set one. + fn public_note(&self) -> Option<&String>; + /// Takes the public note. + fn public_note_owned(self) -> Option; + /// Sets `amount`. + fn set_amount(&mut self, amount: TokenAmount); + /// Sets `public_note`. + fn set_public_note(&mut self, public_note: Option); +} + +impl TokenBurnFromPoolTransitionV0Methods for TokenBurnFromPoolTransitionV0 { + fn amount(&self) -> TokenAmount { + self.amount + } + fn actions(&self) -> &[SerializedAction] { + &self.actions + } + fn anchor(&self) -> &[u8; 32] { + &self.anchor + } + fn proof(&self) -> &[u8] { + &self.proof + } + fn binding_signature(&self) -> &[u8; 64] { + &self.binding_signature + } + fn public_note(&self) -> Option<&String> { + self.public_note.as_ref() + } + fn public_note_owned(self) -> Option { + self.public_note + } + fn set_amount(&mut self, amount: TokenAmount) { + self.amount = amount; + } + fn set_public_note(&mut self, public_note: Option) { + self.public_note = public_note; + } +} + +impl AllowedAsMultiPartyAction for TokenBurnFromPoolTransitionV0 { + fn calculate_action_id( + &self, + owner_id: Identifier, + _platform_version: &PlatformVersion, + ) -> Result { + Ok( + TokenBurnFromPoolTransition::calculate_action_id_with_fields( + self.base.token_id().as_bytes(), + owner_id.as_bytes(), + self.base.identity_contract_nonce(), + self.amount, + &crate::shielded::serialized_actions_digest(&self.actions), + ), + ) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/v0_methods.rs new file mode 100644 index 00000000000..dc91bed35c9 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/v0_methods.rs @@ -0,0 +1,115 @@ +use crate::balances::credits::TokenAmount; +use crate::shielded::SerializedAction; +use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +use crate::state_transition::batch_transition::token_burn_from_pool_transition::v0::v0_methods::TokenBurnFromPoolTransitionV0Methods; +use crate::state_transition::batch_transition::TokenBurnFromPoolTransition; +use crate::prelude::IdentityNonce; +use crate::state_transition::batch_transition::batched_transition::multi_party_action::AllowedAsMultiPartyAction; +use crate::util::hash::hash_double; +use crate::ProtocolError; +use platform_value::Identifier; +use platform_version::version::PlatformVersion; + +impl TokenBaseTransitionAccessors for TokenBurnFromPoolTransition { + fn base(&self) -> &TokenBaseTransition { + match self { + TokenBurnFromPoolTransition::V0(v0) => &v0.base, + } + } + + fn base_mut(&mut self) -> &mut TokenBaseTransition { + match self { + TokenBurnFromPoolTransition::V0(v0) => &mut v0.base, + } + } + + fn set_base(&mut self, base: TokenBaseTransition) { + match self { + TokenBurnFromPoolTransition::V0(v0) => v0.base = base, + } + } +} + +impl TokenBurnFromPoolTransitionV0Methods for TokenBurnFromPoolTransition { + fn amount(&self) -> TokenAmount { + match self { + TokenBurnFromPoolTransition::V0(v0) => v0.amount(), + } + } + fn actions(&self) -> &[SerializedAction] { + match self { + TokenBurnFromPoolTransition::V0(v0) => v0.actions(), + } + } + fn anchor(&self) -> &[u8; 32] { + match self { + TokenBurnFromPoolTransition::V0(v0) => v0.anchor(), + } + } + fn proof(&self) -> &[u8] { + match self { + TokenBurnFromPoolTransition::V0(v0) => v0.proof(), + } + } + fn binding_signature(&self) -> &[u8; 64] { + match self { + TokenBurnFromPoolTransition::V0(v0) => v0.binding_signature(), + } + } + fn public_note(&self) -> Option<&String> { + match self { + TokenBurnFromPoolTransition::V0(v0) => v0.public_note(), + } + } + fn public_note_owned(self) -> Option { + match self { + TokenBurnFromPoolTransition::V0(v0) => v0.public_note_owned(), + } + } + fn set_amount(&mut self, amount: TokenAmount) { + match self { + TokenBurnFromPoolTransition::V0(v0) => v0.set_amount(amount), + } + } + fn set_public_note(&mut self, public_note: Option) { + match self { + TokenBurnFromPoolTransition::V0(v0) => v0.set_public_note(public_note), + } + } +} + +impl AllowedAsMultiPartyAction for TokenBurnFromPoolTransition { + fn calculate_action_id( + &self, + owner_id: Identifier, + platform_version: &PlatformVersion, + ) -> Result { + match self { + TokenBurnFromPoolTransition::V0(v0) => { + v0.calculate_action_id(owner_id, platform_version) + } + } + } +} + +impl TokenBurnFromPoolTransition { + /// The group action id: unlike a transparent burn, the digest of the Orchard + /// actions is part of it, so two proposals that differ only in the notes never share an id. + pub fn calculate_action_id_with_fields( + token_id: &[u8; 32], + owner_id: &[u8; 32], + identity_contract_nonce: IdentityNonce, + amount: TokenAmount, + actions_digest: &[u8; 32], + ) -> Identifier { + let mut bytes = b"action_token_burn_from_pool".to_vec(); + bytes.extend_from_slice(token_id); + bytes.extend_from_slice(owner_id); + bytes.extend_from_slice(&identity_contract_nonce.to_be_bytes()); + bytes.extend_from_slice(&amount.to_be_bytes()); + bytes.extend_from_slice(actions_digest); + + hash_double(bytes).into() + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/validate_structure/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/validate_structure/mod.rs new file mode 100644 index 00000000000..738f72ae2a5 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/validate_structure/mod.rs @@ -0,0 +1,36 @@ +use crate::state_transition::batch_transition::token_burn_from_pool_transition::validate_structure::v0::TokenBurnFromPoolTransitionActionStructureValidationV0; +use crate::state_transition::batch_transition::TokenBurnFromPoolTransition; +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +mod v0; + +pub trait TokenBurnFromPoolTransitionStructureValidation { + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenBurnFromPoolTransitionStructureValidation for TokenBurnFromPoolTransition { + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .token_burn_from_pool_transition_structure_validation + { + 0 => self.validate_structure_v0(platform_version), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "TokenBurnFromPoolTransition::validate_structure".to_string(), + known_versions: vec![0], + received: version, + }), + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/validate_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/validate_structure/v0/mod.rs new file mode 100644 index 00000000000..524f8748ccd --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/validate_structure/v0/mod.rs @@ -0,0 +1,181 @@ +use crate::consensus::basic::token::{InvalidTokenAmountError, InvalidTokenNoteTooBigError, TokenNoteOnlyAllowedWhenProposerError}; +use crate::consensus::basic::BasicError; +use crate::consensus::ConsensusError; +use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::MAX_DISTRIBUTION_PARAM; +use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use crate::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; +use crate::state_transition::batch_transition::token_burn_from_pool_transition::v0::v0_methods::TokenBurnFromPoolTransitionV0Methods; +use crate::state_transition::batch_transition::TokenBurnFromPoolTransition; +use crate::state_transition::state_transitions::shielded::common_validation::{ + validate_actions_count, validate_anchor_not_zero, validate_encrypted_note_sizes, + validate_proof_not_empty, +}; +use crate::tokens::MAX_TOKEN_NOTE_LEN; +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +pub(super) trait TokenBurnFromPoolTransitionActionStructureValidationV0 { + fn validate_structure_v0( + &self, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenBurnFromPoolTransitionActionStructureValidationV0 for TokenBurnFromPoolTransition { + /// The bounds match the equivalent transparent token transition; the bundle checks are the + /// same stateless checks the other pool transitions run (action count, note sizes, + /// non-empty proof, non-zero anchor). The proof itself is verified against state, where its + /// cost can be charged. + fn validate_structure_v0( + &self, + platform_version: &PlatformVersion, + ) -> Result { + if self.amount() > MAX_DISTRIBUTION_PARAM || self.amount() == 0 { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::BasicError(BasicError::InvalidTokenAmountError( + InvalidTokenAmountError::new(MAX_DISTRIBUTION_PARAM, self.amount()), + )), + )); + } + + if let Some(public_note) = self.public_note() { + if public_note.len() > MAX_TOKEN_NOTE_LEN { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError( + InvalidTokenNoteTooBigError::new( + MAX_TOKEN_NOTE_LEN as u32, + "public_note", + public_note.len() as u32, + ), + )), + )); + } + if let Some(group_state_transition_info) = self.base().using_group_info() { + if !group_state_transition_info.action_is_proposer { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::BasicError( + BasicError::TokenNoteOnlyAllowedWhenProposerError( + TokenNoteOnlyAllowedWhenProposerError::new(), + ), + ), + )); + } + } + } + + let result = validate_actions_count( + self.actions(), + platform_version + .system_limits + .max_shielded_transition_actions, + ); + if !result.is_valid() { + return Ok(result); + } + + let result = validate_encrypted_note_sizes(self.actions()); + if !result.is_valid() { + return Ok(result); + } + + let result = validate_proof_not_empty(self.proof()); + if !result.is_valid() { + return Ok(result); + } + + Ok(validate_anchor_not_zero(self.anchor())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::shielded::SerializedAction; + use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::token_burn_from_pool_transition::v0::TokenBurnFromPoolTransitionV0; + use platform_value::Identifier; + + fn action() -> SerializedAction { + SerializedAction { + nullifier: [1u8; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + } + } + + fn make_transition(amount: u64) -> TokenBurnFromPoolTransition { + TokenBurnFromPoolTransition::V0(TokenBurnFromPoolTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::default(), + token_id: Identifier::default(), + using_group_info: None, + }), + amount: amount, + actions: vec![action()], + anchor: [9u8; 32], + proof: vec![1u8; 10], + binding_signature: [7u8; 64], + public_note: None, + }) + } + + #[test] + fn valid_transition_passes() { + let result = make_transition(100) + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(result.is_valid(), "{:?}", result.errors); + } + + #[test] + fn zero_amount_is_rejected() { + let result = make_transition(0) + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::InvalidTokenAmountError(_) + )) + )); + } + + #[test] + fn empty_actions_are_rejected() { + let mut transition = make_transition(100); + let TokenBurnFromPoolTransition::V0(v0) = &mut transition; + v0.actions.clear(); + let result = transition + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::ShieldedNoActionsError(_) + )) + )); + } + + #[test] + fn empty_proof_is_rejected() { + let mut transition = make_transition(100); + let TokenBurnFromPoolTransition::V0(v0) = &mut transition; + v0.proof.clear(); + let result = transition + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::ShieldedEmptyProofError(_) + )) + )); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/mod.rs new file mode 100644 index 00000000000..e1c7f9015c6 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/mod.rs @@ -0,0 +1,104 @@ +pub mod v0; +mod v0_methods; +pub mod validate_structure; + +use bincode::{Decode, DecodeUntrusted, Encode}; +use derive_more::{Display, From}; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; +pub use v0::TokenClaimToPoolTransitionV0; + +/// Claims a distribution straight into the token's shielded pool. +/// +/// Consensus computes the claimable amount exactly as `TokenClaim` does and requires the +/// outputs-only bundle's value balance to equal it. A perpetual claim names the moment it claims +/// up to (`claim_up_to`, in the distribution's own unit: block height, time in milliseconds or +/// epoch index) so the client can predict the amount it proves; it must be cycle aligned and not +/// after the current interval. +#[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From, DecodeUntrusted)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] +pub enum TokenClaimToPoolTransition { + #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] + V0(TokenClaimToPoolTransitionV0), +} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenClaimToPoolTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenClaimToPoolTransition {} + +impl Default for TokenClaimToPoolTransition { + fn default() -> Self { + TokenClaimToPoolTransition::V0(TokenClaimToPoolTransitionV0::default()) + } +} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::data_contract::associated_token::token_distribution_key::TokenDistributionType; + use crate::shielded::SerializedAction; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::TokenBaseTransition; + use platform_value::Identifier; + + pub(crate) fn fixture() -> TokenClaimToPoolTransition { + TokenClaimToPoolTransition::V0(TokenClaimToPoolTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 15, + token_contract_position: 1, + data_contract_id: Identifier::new([0xa1; 32]), + token_id: Identifier::new([0xb2; 32]), + using_group_info: None, + }), + distribution_type: TokenDistributionType::PreProgrammed, + claim_up_to: Some(1_000), + actions: vec![SerializedAction { + nullifier: [0x11; 32], + rk: [0x22; 32], + cmx: [0x33; 32], + encrypted_note: vec![0x44; 216], + cv_net: [0x55; 32], + spend_auth_sig: [0x66; 64], + }], + anchor: [0x77; 32], + proof: vec![0x88; 8], + binding_signature: [0x99; 64], + public_note: Some("note".to_string()), + }) + } + + #[test] + fn json_round_trip_keeps_envelope_and_bundle() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + assert_eq!(json["$formatVersion"], "0"); + assert_eq!(json["$baseFormatVersion"], "0"); + assert_eq!(json["$tokenContractPosition"], 1); + assert!(json["actions"].is_array()); + assert!(json["anchor"].is_string(), "byte fields are base64 in JSON"); + let recovered = TokenClaimToPoolTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + let recovered = TokenClaimToPoolTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/v0/mod.rs new file mode 100644 index 00000000000..35095206277 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/v0/mod.rs @@ -0,0 +1,68 @@ +pub mod v0_methods; + +use crate::data_contract::associated_token::token_distribution_key::TokenDistributionType; +use crate::shielded::SerializedAction; +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +use bincode::{Decode, DecodeUntrusted, Encode}; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; +use std::fmt; + +/// The Identifier fields in [`TokenClaimToPoolTransition`] +pub use super::super::document_base_transition::IDENTIFIER_FIELDS; + +#[derive(Debug, Clone, Encode, Decode, PartialEq, DecodeUntrusted)] +// Auto-injects `json_safe_u64` on the u64 fields and `serde_bytes` on the fixed byte arrays / +// `serde_bytes_var` on `proof` (base64 strings in JSON, raw bytes in Value). +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +pub struct TokenClaimToPoolTransitionV0 { + /// Token base transition (nonce, contract, token position, token id). + #[cfg_attr(feature = "serde-conversion", serde(flatten))] + pub base: TokenBaseTransition, + /// Which distribution is claimed. + pub distribution_type: TokenDistributionType, + /// For a perpetual distribution, the cycle-aligned moment to claim up to. Ignored for pre-programmed distributions. + pub claim_up_to: Option, + /// Orchard actions (spend-output pairs). + pub actions: Vec, + /// Sinsemilla root of the token pool's note commitment tree (Orchard anchor). + pub anchor: [u8; 32], + /// Halo 2 proof bytes. + pub proof: Vec, + /// RedPallas binding signature. + pub binding_signature: [u8; 64], + /// Optional public note. Only a group action proposer may set one. + pub public_note: Option, +} + +impl Default for TokenClaimToPoolTransitionV0 { + fn default() -> Self { + Self { + base: TokenBaseTransition::default(), + distribution_type: TokenDistributionType::default(), + claim_up_to: None, + actions: vec![], + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + public_note: None, + } + } +} + +impl fmt::Display for TokenClaimToPoolTransitionV0 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Token Claim To Pool, base: {}, distribution: {:?}, actions: {}", + self.base, + self.distribution_type, + self.actions.len() + ) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/v0/v0_methods.rs new file mode 100644 index 00000000000..9d2ecec82c9 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/v0/v0_methods.rs @@ -0,0 +1,80 @@ +use crate::data_contract::associated_token::token_distribution_key::TokenDistributionType; +use crate::shielded::SerializedAction; +use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +use crate::state_transition::batch_transition::token_claim_to_pool_transition::TokenClaimToPoolTransitionV0; + +impl TokenBaseTransitionAccessors for TokenClaimToPoolTransitionV0 { + fn base(&self) -> &TokenBaseTransition { + &self.base + } + + fn base_mut(&mut self) -> &mut TokenBaseTransition { + &mut self.base + } + + fn set_base(&mut self, base: TokenBaseTransition) { + self.base = base; + } +} + +pub trait TokenClaimToPoolTransitionV0Methods: TokenBaseTransitionAccessors { + /// Which distribution is claimed. + fn distribution_type(&self) -> TokenDistributionType; + /// For a perpetual distribution, the cycle-aligned moment to claim up to. Ignored for pre-programmed distributions. + fn claim_up_to(&self) -> Option; + /// Orchard actions. + fn actions(&self) -> &[SerializedAction]; + /// Sinsemilla root of the token pool's note commitment tree (Orchard anchor). + fn anchor(&self) -> &[u8; 32]; + /// Halo 2 proof bytes. + fn proof(&self) -> &[u8]; + /// RedPallas binding signature. + fn binding_signature(&self) -> &[u8; 64]; + /// Optional public note. Only a group action proposer may set one. + fn public_note(&self) -> Option<&String>; + /// Takes the public note. + fn public_note_owned(self) -> Option; + /// Sets `distribution_type`. + fn set_distribution_type(&mut self, distribution_type: TokenDistributionType); + /// Sets `claim_up_to`. + fn set_claim_up_to(&mut self, claim_up_to: Option); + /// Sets `public_note`. + fn set_public_note(&mut self, public_note: Option); +} + +impl TokenClaimToPoolTransitionV0Methods for TokenClaimToPoolTransitionV0 { + fn distribution_type(&self) -> TokenDistributionType { + self.distribution_type + } + fn claim_up_to(&self) -> Option { + self.claim_up_to + } + fn actions(&self) -> &[SerializedAction] { + &self.actions + } + fn anchor(&self) -> &[u8; 32] { + &self.anchor + } + fn proof(&self) -> &[u8] { + &self.proof + } + fn binding_signature(&self) -> &[u8; 64] { + &self.binding_signature + } + fn public_note(&self) -> Option<&String> { + self.public_note.as_ref() + } + fn public_note_owned(self) -> Option { + self.public_note + } + fn set_distribution_type(&mut self, distribution_type: TokenDistributionType) { + self.distribution_type = distribution_type; + } + fn set_claim_up_to(&mut self, claim_up_to: Option) { + self.claim_up_to = claim_up_to; + } + fn set_public_note(&mut self, public_note: Option) { + self.public_note = public_note; + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/v0_methods.rs new file mode 100644 index 00000000000..6bb4d176216 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/v0_methods.rs @@ -0,0 +1,84 @@ +use crate::data_contract::associated_token::token_distribution_key::TokenDistributionType; +use crate::shielded::SerializedAction; +use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +use crate::state_transition::batch_transition::token_claim_to_pool_transition::v0::v0_methods::TokenClaimToPoolTransitionV0Methods; +use crate::state_transition::batch_transition::TokenClaimToPoolTransition; + +impl TokenBaseTransitionAccessors for TokenClaimToPoolTransition { + fn base(&self) -> &TokenBaseTransition { + match self { + TokenClaimToPoolTransition::V0(v0) => &v0.base, + } + } + + fn base_mut(&mut self) -> &mut TokenBaseTransition { + match self { + TokenClaimToPoolTransition::V0(v0) => &mut v0.base, + } + } + + fn set_base(&mut self, base: TokenBaseTransition) { + match self { + TokenClaimToPoolTransition::V0(v0) => v0.base = base, + } + } +} + +impl TokenClaimToPoolTransitionV0Methods for TokenClaimToPoolTransition { + fn distribution_type(&self) -> TokenDistributionType { + match self { + TokenClaimToPoolTransition::V0(v0) => v0.distribution_type(), + } + } + fn claim_up_to(&self) -> Option { + match self { + TokenClaimToPoolTransition::V0(v0) => v0.claim_up_to(), + } + } + fn actions(&self) -> &[SerializedAction] { + match self { + TokenClaimToPoolTransition::V0(v0) => v0.actions(), + } + } + fn anchor(&self) -> &[u8; 32] { + match self { + TokenClaimToPoolTransition::V0(v0) => v0.anchor(), + } + } + fn proof(&self) -> &[u8] { + match self { + TokenClaimToPoolTransition::V0(v0) => v0.proof(), + } + } + fn binding_signature(&self) -> &[u8; 64] { + match self { + TokenClaimToPoolTransition::V0(v0) => v0.binding_signature(), + } + } + fn public_note(&self) -> Option<&String> { + match self { + TokenClaimToPoolTransition::V0(v0) => v0.public_note(), + } + } + fn public_note_owned(self) -> Option { + match self { + TokenClaimToPoolTransition::V0(v0) => v0.public_note_owned(), + } + } + fn set_distribution_type(&mut self, distribution_type: TokenDistributionType) { + match self { + TokenClaimToPoolTransition::V0(v0) => v0.set_distribution_type(distribution_type), + } + } + fn set_claim_up_to(&mut self, claim_up_to: Option) { + match self { + TokenClaimToPoolTransition::V0(v0) => v0.set_claim_up_to(claim_up_to), + } + } + fn set_public_note(&mut self, public_note: Option) { + match self { + TokenClaimToPoolTransition::V0(v0) => v0.set_public_note(public_note), + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/validate_structure/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/validate_structure/mod.rs new file mode 100644 index 00000000000..2b37d9e9c89 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/validate_structure/mod.rs @@ -0,0 +1,36 @@ +use crate::state_transition::batch_transition::token_claim_to_pool_transition::validate_structure::v0::TokenClaimToPoolTransitionActionStructureValidationV0; +use crate::state_transition::batch_transition::TokenClaimToPoolTransition; +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +mod v0; + +pub trait TokenClaimToPoolTransitionStructureValidation { + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenClaimToPoolTransitionStructureValidation for TokenClaimToPoolTransition { + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .token_claim_to_pool_transition_structure_validation + { + 0 => self.validate_structure_v0(platform_version), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "TokenClaimToPoolTransition::validate_structure".to_string(), + known_versions: vec![0], + received: version, + }), + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/validate_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/validate_structure/v0/mod.rs new file mode 100644 index 00000000000..8e15b81fab2 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_claim_to_pool_transition/validate_structure/v0/mod.rs @@ -0,0 +1,164 @@ +use crate::consensus::basic::token::{ + InvalidTokenNoteTooBigError, TokenNoteOnlyAllowedWhenProposerError, +}; +use crate::consensus::basic::BasicError; +use crate::consensus::ConsensusError; + +use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use crate::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; +use crate::state_transition::batch_transition::token_claim_to_pool_transition::v0::v0_methods::TokenClaimToPoolTransitionV0Methods; +use crate::state_transition::batch_transition::TokenClaimToPoolTransition; +use crate::state_transition::state_transitions::shielded::common_validation::{ + validate_actions_count, validate_anchor_not_zero, validate_encrypted_note_sizes, + validate_proof_not_empty, +}; +use crate::tokens::MAX_TOKEN_NOTE_LEN; +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +pub(super) trait TokenClaimToPoolTransitionActionStructureValidationV0 { + fn validate_structure_v0( + &self, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenClaimToPoolTransitionActionStructureValidationV0 for TokenClaimToPoolTransition { + /// The bounds match the equivalent transparent token transition; the bundle checks are the + /// same stateless checks the other pool transitions run (action count, note sizes, + /// non-empty proof, non-zero anchor). The proof itself is verified against state, where its + /// cost can be charged. + fn validate_structure_v0( + &self, + platform_version: &PlatformVersion, + ) -> Result { + if let Some(public_note) = self.public_note() { + if public_note.len() > MAX_TOKEN_NOTE_LEN { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError( + InvalidTokenNoteTooBigError::new( + MAX_TOKEN_NOTE_LEN as u32, + "public_note", + public_note.len() as u32, + ), + )), + )); + } + if let Some(group_state_transition_info) = self.base().using_group_info() { + if !group_state_transition_info.action_is_proposer { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::BasicError( + BasicError::TokenNoteOnlyAllowedWhenProposerError( + TokenNoteOnlyAllowedWhenProposerError::new(), + ), + ), + )); + } + } + } + + let result = validate_actions_count( + self.actions(), + platform_version + .system_limits + .max_shielded_transition_actions, + ); + if !result.is_valid() { + return Ok(result); + } + + let result = validate_encrypted_note_sizes(self.actions()); + if !result.is_valid() { + return Ok(result); + } + + let result = validate_proof_not_empty(self.proof()); + if !result.is_valid() { + return Ok(result); + } + + Ok(validate_anchor_not_zero(self.anchor())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_contract::associated_token::token_distribution_key::TokenDistributionType; + use crate::shielded::SerializedAction; + use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::token_claim_to_pool_transition::v0::TokenClaimToPoolTransitionV0; + use platform_value::Identifier; + + fn action() -> SerializedAction { + SerializedAction { + nullifier: [1u8; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + } + } + + fn make_transition(_amount: u64) -> TokenClaimToPoolTransition { + TokenClaimToPoolTransition::V0(TokenClaimToPoolTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::default(), + token_id: Identifier::default(), + using_group_info: None, + }), + distribution_type: TokenDistributionType::PreProgrammed, + claim_up_to: Some(1_000), + actions: vec![action()], + anchor: [9u8; 32], + proof: vec![1u8; 10], + binding_signature: [7u8; 64], + public_note: None, + }) + } + + #[test] + fn valid_transition_passes() { + let result = make_transition(100) + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(result.is_valid(), "{:?}", result.errors); + } + + #[test] + fn empty_actions_are_rejected() { + let mut transition = make_transition(100); + let TokenClaimToPoolTransition::V0(v0) = &mut transition; + v0.actions.clear(); + let result = transition + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::ShieldedNoActionsError(_) + )) + )); + } + + #[test] + fn empty_proof_is_rejected() { + let mut transition = make_transition(100); + let TokenClaimToPoolTransition::V0(v0) = &mut transition; + v0.proof.clear(); + let result = transition + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::ShieldedEmptyProofError(_) + )) + )); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/mod.rs new file mode 100644 index 00000000000..bc23fdb6d53 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/mod.rs @@ -0,0 +1,101 @@ +pub mod v0; +mod v0_methods; +pub mod validate_structure; + +use bincode::{Decode, DecodeUntrusted, Encode}; +use derive_more::{Display, From}; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; +pub use v0::TokenDirectPurchaseToPoolTransitionV0; + +/// Buys `token_count` tokens for `total_agreed_price` credits and receives them shielded. +/// +/// The buyer's identity pays the credits (to the contract owner) and the fee; the outputs-only +/// bundle's value balance is `-token_count`. Pricing and supply checks match `TokenDirectPurchase`. +#[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From, DecodeUntrusted)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] +pub enum TokenDirectPurchaseToPoolTransition { + #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] + V0(TokenDirectPurchaseToPoolTransitionV0), +} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenDirectPurchaseToPoolTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenDirectPurchaseToPoolTransition {} + +impl Default for TokenDirectPurchaseToPoolTransition { + fn default() -> Self { + TokenDirectPurchaseToPoolTransition::V0(TokenDirectPurchaseToPoolTransitionV0::default()) + } +} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::shielded::SerializedAction; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::TokenBaseTransition; + use platform_value::Identifier; + + pub(crate) fn fixture() -> TokenDirectPurchaseToPoolTransition { + TokenDirectPurchaseToPoolTransition::V0(TokenDirectPurchaseToPoolTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 15, + token_contract_position: 1, + data_contract_id: Identifier::new([0xa1; 32]), + token_id: Identifier::new([0xb2; 32]), + using_group_info: None, + }), + token_count: 4_000, + total_agreed_price: 9_000_000, + actions: vec![SerializedAction { + nullifier: [0x11; 32], + rk: [0x22; 32], + cmx: [0x33; 32], + encrypted_note: vec![0x44; 216], + cv_net: [0x55; 32], + spend_auth_sig: [0x66; 64], + }], + anchor: [0x77; 32], + proof: vec![0x88; 8], + binding_signature: [0x99; 64], + }) + } + + #[test] + fn json_round_trip_keeps_envelope_and_bundle() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + assert_eq!(json["$formatVersion"], "0"); + assert_eq!(json["$baseFormatVersion"], "0"); + assert_eq!(json["$tokenContractPosition"], 1); + assert_eq!(json["tokenCount"], 4_000); + assert!(json["actions"].is_array()); + assert!(json["anchor"].is_string(), "byte fields are base64 in JSON"); + let recovered = TokenDirectPurchaseToPoolTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + let recovered = + TokenDirectPurchaseToPoolTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/v0/mod.rs new file mode 100644 index 00000000000..297313eddc0 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/v0/mod.rs @@ -0,0 +1,67 @@ +pub mod v0_methods; + +use crate::balances::credits::TokenAmount; +use crate::fee::Credits; +use crate::shielded::SerializedAction; +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +use bincode::{Decode, DecodeUntrusted, Encode}; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; +use std::fmt; + +/// The Identifier fields in [`TokenDirectPurchaseToPoolTransition`] +pub use super::super::document_base_transition::IDENTIFIER_FIELDS; + +#[derive(Debug, Clone, Encode, Decode, PartialEq, DecodeUntrusted)] +// Auto-injects `json_safe_u64` on the u64 fields and `serde_bytes` on the fixed byte arrays / +// `serde_bytes_var` on `proof` (base64 strings in JSON, raw bytes in Value). +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +pub struct TokenDirectPurchaseToPoolTransitionV0 { + /// Token base transition (nonce, contract, token position, token id). + #[cfg_attr(feature = "serde-conversion", serde(flatten))] + pub base: TokenBaseTransition, + /// Tokens purchased and minted into the pool. + pub token_count: TokenAmount, + /// The most credits the buyer agrees to pay for `token_count`. + pub total_agreed_price: Credits, + /// Orchard actions (spend-output pairs). + pub actions: Vec, + /// Sinsemilla root of the token pool's note commitment tree (Orchard anchor). + pub anchor: [u8; 32], + /// Halo 2 proof bytes. + pub proof: Vec, + /// RedPallas binding signature. + pub binding_signature: [u8; 64], +} + +impl Default for TokenDirectPurchaseToPoolTransitionV0 { + fn default() -> Self { + Self { + base: TokenBaseTransition::default(), + token_count: 0, + total_agreed_price: 0, + actions: vec![], + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + } + } +} + +impl fmt::Display for TokenDirectPurchaseToPoolTransitionV0 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Token Direct Purchase To Pool, base: {}, count: {}, price: {}, actions: {}", + self.base, + self.token_count, + self.total_agreed_price, + self.actions.len() + ) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/v0/v0_methods.rs new file mode 100644 index 00000000000..3665cba6d47 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/v0/v0_methods.rs @@ -0,0 +1,66 @@ +use crate::balances::credits::TokenAmount; +use crate::fee::Credits; +use crate::shielded::SerializedAction; +use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +use crate::state_transition::batch_transition::token_direct_purchase_to_pool_transition::TokenDirectPurchaseToPoolTransitionV0; + +impl TokenBaseTransitionAccessors for TokenDirectPurchaseToPoolTransitionV0 { + fn base(&self) -> &TokenBaseTransition { + &self.base + } + + fn base_mut(&mut self) -> &mut TokenBaseTransition { + &mut self.base + } + + fn set_base(&mut self, base: TokenBaseTransition) { + self.base = base; + } +} + +pub trait TokenDirectPurchaseToPoolTransitionV0Methods: TokenBaseTransitionAccessors { + /// Tokens purchased and minted into the pool. + fn token_count(&self) -> TokenAmount; + /// The most credits the buyer agrees to pay for `token_count`. + fn total_agreed_price(&self) -> Credits; + /// Orchard actions. + fn actions(&self) -> &[SerializedAction]; + /// Sinsemilla root of the token pool's note commitment tree (Orchard anchor). + fn anchor(&self) -> &[u8; 32]; + /// Halo 2 proof bytes. + fn proof(&self) -> &[u8]; + /// RedPallas binding signature. + fn binding_signature(&self) -> &[u8; 64]; + /// Sets `token_count`. + fn set_token_count(&mut self, token_count: TokenAmount); + /// Sets `total_agreed_price`. + fn set_total_agreed_price(&mut self, total_agreed_price: Credits); +} + +impl TokenDirectPurchaseToPoolTransitionV0Methods for TokenDirectPurchaseToPoolTransitionV0 { + fn token_count(&self) -> TokenAmount { + self.token_count + } + fn total_agreed_price(&self) -> Credits { + self.total_agreed_price + } + fn actions(&self) -> &[SerializedAction] { + &self.actions + } + fn anchor(&self) -> &[u8; 32] { + &self.anchor + } + fn proof(&self) -> &[u8] { + &self.proof + } + fn binding_signature(&self) -> &[u8; 64] { + &self.binding_signature + } + fn set_token_count(&mut self, token_count: TokenAmount) { + self.token_count = token_count; + } + fn set_total_agreed_price(&mut self, total_agreed_price: Credits) { + self.total_agreed_price = total_agreed_price; + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/v0_methods.rs new file mode 100644 index 00000000000..bfa2615a636 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/v0_methods.rs @@ -0,0 +1,72 @@ +use crate::balances::credits::TokenAmount; +use crate::fee::Credits; +use crate::shielded::SerializedAction; +use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +use crate::state_transition::batch_transition::token_direct_purchase_to_pool_transition::v0::v0_methods::TokenDirectPurchaseToPoolTransitionV0Methods; +use crate::state_transition::batch_transition::TokenDirectPurchaseToPoolTransition; + +impl TokenBaseTransitionAccessors for TokenDirectPurchaseToPoolTransition { + fn base(&self) -> &TokenBaseTransition { + match self { + TokenDirectPurchaseToPoolTransition::V0(v0) => &v0.base, + } + } + + fn base_mut(&mut self) -> &mut TokenBaseTransition { + match self { + TokenDirectPurchaseToPoolTransition::V0(v0) => &mut v0.base, + } + } + + fn set_base(&mut self, base: TokenBaseTransition) { + match self { + TokenDirectPurchaseToPoolTransition::V0(v0) => v0.base = base, + } + } +} + +impl TokenDirectPurchaseToPoolTransitionV0Methods for TokenDirectPurchaseToPoolTransition { + fn token_count(&self) -> TokenAmount { + match self { + TokenDirectPurchaseToPoolTransition::V0(v0) => v0.token_count(), + } + } + fn total_agreed_price(&self) -> Credits { + match self { + TokenDirectPurchaseToPoolTransition::V0(v0) => v0.total_agreed_price(), + } + } + fn actions(&self) -> &[SerializedAction] { + match self { + TokenDirectPurchaseToPoolTransition::V0(v0) => v0.actions(), + } + } + fn anchor(&self) -> &[u8; 32] { + match self { + TokenDirectPurchaseToPoolTransition::V0(v0) => v0.anchor(), + } + } + fn proof(&self) -> &[u8] { + match self { + TokenDirectPurchaseToPoolTransition::V0(v0) => v0.proof(), + } + } + fn binding_signature(&self) -> &[u8; 64] { + match self { + TokenDirectPurchaseToPoolTransition::V0(v0) => v0.binding_signature(), + } + } + fn set_token_count(&mut self, token_count: TokenAmount) { + match self { + TokenDirectPurchaseToPoolTransition::V0(v0) => v0.set_token_count(token_count), + } + } + fn set_total_agreed_price(&mut self, total_agreed_price: Credits) { + match self { + TokenDirectPurchaseToPoolTransition::V0(v0) => { + v0.set_total_agreed_price(total_agreed_price) + } + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/validate_structure/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/validate_structure/mod.rs new file mode 100644 index 00000000000..1183a974f39 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/validate_structure/mod.rs @@ -0,0 +1,38 @@ +use crate::state_transition::batch_transition::token_direct_purchase_to_pool_transition::validate_structure::v0::TokenDirectPurchaseToPoolTransitionActionStructureValidationV0; +use crate::state_transition::batch_transition::TokenDirectPurchaseToPoolTransition; +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +mod v0; + +pub trait TokenDirectPurchaseToPoolTransitionStructureValidation { + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenDirectPurchaseToPoolTransitionStructureValidation + for TokenDirectPurchaseToPoolTransition +{ + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .token_direct_purchase_to_pool_transition_structure_validation + { + 0 => self.validate_structure_v0(platform_version), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "TokenDirectPurchaseToPoolTransition::validate_structure".to_string(), + known_versions: vec![0], + received: version, + }), + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/validate_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/validate_structure/v0/mod.rs new file mode 100644 index 00000000000..a111201d35f --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_direct_purchase_to_pool_transition/validate_structure/v0/mod.rs @@ -0,0 +1,157 @@ +use crate::consensus::basic::token::InvalidTokenAmountError; +use crate::consensus::basic::BasicError; +use crate::consensus::ConsensusError; +use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::MAX_DISTRIBUTION_PARAM; + +use crate::state_transition::batch_transition::token_direct_purchase_to_pool_transition::v0::v0_methods::TokenDirectPurchaseToPoolTransitionV0Methods; +use crate::state_transition::batch_transition::TokenDirectPurchaseToPoolTransition; +use crate::state_transition::state_transitions::shielded::common_validation::{ + validate_actions_count, validate_anchor_not_zero, validate_encrypted_note_sizes, + validate_proof_not_empty, +}; + +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +pub(super) trait TokenDirectPurchaseToPoolTransitionActionStructureValidationV0 { + fn validate_structure_v0( + &self, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenDirectPurchaseToPoolTransitionActionStructureValidationV0 + for TokenDirectPurchaseToPoolTransition +{ + /// The bounds match the equivalent transparent token transition; the bundle checks are the + /// same stateless checks the other pool transitions run (action count, note sizes, + /// non-empty proof, non-zero anchor). The proof itself is verified against state, where its + /// cost can be charged. + fn validate_structure_v0( + &self, + platform_version: &PlatformVersion, + ) -> Result { + if self.token_count() > MAX_DISTRIBUTION_PARAM || self.token_count() == 0 { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::BasicError(BasicError::InvalidTokenAmountError( + InvalidTokenAmountError::new(MAX_DISTRIBUTION_PARAM, self.token_count()), + )), + )); + } + + let result = validate_actions_count( + self.actions(), + platform_version + .system_limits + .max_shielded_transition_actions, + ); + if !result.is_valid() { + return Ok(result); + } + + let result = validate_encrypted_note_sizes(self.actions()); + if !result.is_valid() { + return Ok(result); + } + + let result = validate_proof_not_empty(self.proof()); + if !result.is_valid() { + return Ok(result); + } + + Ok(validate_anchor_not_zero(self.anchor())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::shielded::SerializedAction; + use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::token_direct_purchase_to_pool_transition::v0::TokenDirectPurchaseToPoolTransitionV0; + use platform_value::Identifier; + + fn action() -> SerializedAction { + SerializedAction { + nullifier: [1u8; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + } + } + + fn make_transition(amount: u64) -> TokenDirectPurchaseToPoolTransition { + TokenDirectPurchaseToPoolTransition::V0(TokenDirectPurchaseToPoolTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::default(), + token_id: Identifier::default(), + using_group_info: None, + }), + token_count: amount, + total_agreed_price: 9_000_000, + actions: vec![action()], + anchor: [9u8; 32], + proof: vec![1u8; 10], + binding_signature: [7u8; 64], + }) + } + + #[test] + fn valid_transition_passes() { + let result = make_transition(100) + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(result.is_valid(), "{:?}", result.errors); + } + + #[test] + fn zero_amount_is_rejected() { + let result = make_transition(0) + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::InvalidTokenAmountError(_) + )) + )); + } + + #[test] + fn empty_actions_are_rejected() { + let mut transition = make_transition(100); + let TokenDirectPurchaseToPoolTransition::V0(v0) = &mut transition; + v0.actions.clear(); + let result = transition + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::ShieldedNoActionsError(_) + )) + )); + } + + #[test] + fn empty_proof_is_rejected() { + let mut transition = make_transition(100); + let TokenDirectPurchaseToPoolTransition::V0(v0) = &mut transition; + v0.proof.clear(); + let result = transition + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::ShieldedEmptyProofError(_) + )) + )); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/mod.rs new file mode 100644 index 00000000000..e37bac52baa --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/mod.rs @@ -0,0 +1,102 @@ +pub mod v0; +mod v0_methods; +pub mod validate_structure; + +use bincode::{Decode, DecodeUntrusted, Encode}; +use derive_more::{Display, From}; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; +pub use v0::TokenMintToPoolTransitionV0; + +/// Mints `amount` of a token straight into the token's shielded pool. +/// +/// The Orchard bundle is outputs-only (spends disabled) with a value balance of `-amount`: the +/// new notes are created for whoever the minter addressed them to. The minter must be authorized +/// by `manualMintingRules` like a transparent mint, the max supply applies, and a group action +/// carries the digest of the actions so every signer commits to the same notes. +#[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From, DecodeUntrusted)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] +pub enum TokenMintToPoolTransition { + #[display("V0({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] + V0(TokenMintToPoolTransitionV0), +} + +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenMintToPoolTransition {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenMintToPoolTransition {} + +impl Default for TokenMintToPoolTransition { + fn default() -> Self { + TokenMintToPoolTransition::V0(TokenMintToPoolTransitionV0::default()) + } +} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::shielded::SerializedAction; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_base_transition::TokenBaseTransition; + use platform_value::Identifier; + + pub(crate) fn fixture() -> TokenMintToPoolTransition { + TokenMintToPoolTransition::V0(TokenMintToPoolTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 15, + token_contract_position: 1, + data_contract_id: Identifier::new([0xa1; 32]), + token_id: Identifier::new([0xb2; 32]), + using_group_info: None, + }), + amount: 4_000, + actions: vec![SerializedAction { + nullifier: [0x11; 32], + rk: [0x22; 32], + cmx: [0x33; 32], + encrypted_note: vec![0x44; 216], + cv_net: [0x55; 32], + spend_auth_sig: [0x66; 64], + }], + anchor: [0x77; 32], + proof: vec![0x88; 8], + binding_signature: [0x99; 64], + public_note: Some("note".to_string()), + }) + } + + #[test] + fn json_round_trip_keeps_envelope_and_bundle() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + assert_eq!(json["$formatVersion"], "0"); + assert_eq!(json["$baseFormatVersion"], "0"); + assert_eq!(json["$tokenContractPosition"], 1); + assert_eq!(json["amount"], 4_000); + assert!(json["actions"].is_array()); + assert!(json["anchor"].is_string(), "byte fields are base64 in JSON"); + let recovered = TokenMintToPoolTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + let recovered = TokenMintToPoolTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/v0/mod.rs new file mode 100644 index 00000000000..4827b1abdec --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/v0/mod.rs @@ -0,0 +1,65 @@ +pub mod v0_methods; + +use crate::balances::credits::TokenAmount; +use crate::shielded::SerializedAction; +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +use bincode::{Decode, DecodeUntrusted, Encode}; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; +use std::fmt; + +/// The Identifier fields in [`TokenMintToPoolTransition`] +pub use super::super::document_base_transition::IDENTIFIER_FIELDS; + +#[derive(Debug, Clone, Encode, Decode, PartialEq, DecodeUntrusted)] +// Auto-injects `json_safe_u64` on the u64 fields and `serde_bytes` on the fixed byte arrays / +// `serde_bytes_var` on `proof` (base64 strings in JSON, raw bytes in Value). +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +pub struct TokenMintToPoolTransitionV0 { + /// Token base transition (nonce, contract, token position, token id). + #[cfg_attr(feature = "serde-conversion", serde(flatten))] + pub base: TokenBaseTransition, + /// Tokens entering or leaving the pool. Equals the absolute value of the bundle's value balance. + pub amount: TokenAmount, + /// Orchard actions (spend-output pairs). + pub actions: Vec, + /// Sinsemilla root of the token pool's note commitment tree (Orchard anchor). + pub anchor: [u8; 32], + /// Halo 2 proof bytes. + pub proof: Vec, + /// RedPallas binding signature. + pub binding_signature: [u8; 64], + /// Optional public note. Only a group action proposer may set one. + pub public_note: Option, +} + +impl Default for TokenMintToPoolTransitionV0 { + fn default() -> Self { + Self { + base: TokenBaseTransition::default(), + amount: 0, + actions: vec![], + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + public_note: None, + } + } +} + +impl fmt::Display for TokenMintToPoolTransitionV0 { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "Token Mint To Pool, base: {}, amount: {}, actions: {}", + self.base, + self.amount, + self.actions.len() + ) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/v0/v0_methods.rs new file mode 100644 index 00000000000..31c110815ac --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/v0/v0_methods.rs @@ -0,0 +1,92 @@ +use crate::balances::credits::TokenAmount; +use crate::shielded::SerializedAction; +use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +use crate::state_transition::batch_transition::token_mint_to_pool_transition::TokenMintToPoolTransition; +use crate::state_transition::batch_transition::token_mint_to_pool_transition::TokenMintToPoolTransitionV0; +use crate::state_transition::batch_transition::batched_transition::multi_party_action::AllowedAsMultiPartyAction; +use crate::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; +use platform_value::Identifier; +use platform_version::version::PlatformVersion; +use crate::ProtocolError; + +impl TokenBaseTransitionAccessors for TokenMintToPoolTransitionV0 { + fn base(&self) -> &TokenBaseTransition { + &self.base + } + + fn base_mut(&mut self) -> &mut TokenBaseTransition { + &mut self.base + } + + fn set_base(&mut self, base: TokenBaseTransition) { + self.base = base; + } +} + +pub trait TokenMintToPoolTransitionV0Methods: TokenBaseTransitionAccessors { + /// Tokens entering or leaving the pool. Equals the absolute value of the bundle's value balance. + fn amount(&self) -> TokenAmount; + /// Orchard actions. + fn actions(&self) -> &[SerializedAction]; + /// Sinsemilla root of the token pool's note commitment tree (Orchard anchor). + fn anchor(&self) -> &[u8; 32]; + /// Halo 2 proof bytes. + fn proof(&self) -> &[u8]; + /// RedPallas binding signature. + fn binding_signature(&self) -> &[u8; 64]; + /// Optional public note. Only a group action proposer may set one. + fn public_note(&self) -> Option<&String>; + /// Takes the public note. + fn public_note_owned(self) -> Option; + /// Sets `amount`. + fn set_amount(&mut self, amount: TokenAmount); + /// Sets `public_note`. + fn set_public_note(&mut self, public_note: Option); +} + +impl TokenMintToPoolTransitionV0Methods for TokenMintToPoolTransitionV0 { + fn amount(&self) -> TokenAmount { + self.amount + } + fn actions(&self) -> &[SerializedAction] { + &self.actions + } + fn anchor(&self) -> &[u8; 32] { + &self.anchor + } + fn proof(&self) -> &[u8] { + &self.proof + } + fn binding_signature(&self) -> &[u8; 64] { + &self.binding_signature + } + fn public_note(&self) -> Option<&String> { + self.public_note.as_ref() + } + fn public_note_owned(self) -> Option { + self.public_note + } + fn set_amount(&mut self, amount: TokenAmount) { + self.amount = amount; + } + fn set_public_note(&mut self, public_note: Option) { + self.public_note = public_note; + } +} + +impl AllowedAsMultiPartyAction for TokenMintToPoolTransitionV0 { + fn calculate_action_id( + &self, + owner_id: Identifier, + _platform_version: &PlatformVersion, + ) -> Result { + Ok(TokenMintToPoolTransition::calculate_action_id_with_fields( + self.base.token_id().as_bytes(), + owner_id.as_bytes(), + self.base.identity_contract_nonce(), + self.amount, + &crate::shielded::serialized_actions_digest(&self.actions), + )) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/v0_methods.rs new file mode 100644 index 00000000000..83a6b5117bb --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/v0_methods.rs @@ -0,0 +1,113 @@ +use crate::balances::credits::TokenAmount; +use crate::shielded::SerializedAction; +use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; +use crate::state_transition::batch_transition::token_mint_to_pool_transition::v0::v0_methods::TokenMintToPoolTransitionV0Methods; +use crate::state_transition::batch_transition::TokenMintToPoolTransition; +use crate::prelude::IdentityNonce; +use crate::state_transition::batch_transition::batched_transition::multi_party_action::AllowedAsMultiPartyAction; +use crate::util::hash::hash_double; +use crate::ProtocolError; +use platform_value::Identifier; +use platform_version::version::PlatformVersion; + +impl TokenBaseTransitionAccessors for TokenMintToPoolTransition { + fn base(&self) -> &TokenBaseTransition { + match self { + TokenMintToPoolTransition::V0(v0) => &v0.base, + } + } + + fn base_mut(&mut self) -> &mut TokenBaseTransition { + match self { + TokenMintToPoolTransition::V0(v0) => &mut v0.base, + } + } + + fn set_base(&mut self, base: TokenBaseTransition) { + match self { + TokenMintToPoolTransition::V0(v0) => v0.base = base, + } + } +} + +impl TokenMintToPoolTransitionV0Methods for TokenMintToPoolTransition { + fn amount(&self) -> TokenAmount { + match self { + TokenMintToPoolTransition::V0(v0) => v0.amount(), + } + } + fn actions(&self) -> &[SerializedAction] { + match self { + TokenMintToPoolTransition::V0(v0) => v0.actions(), + } + } + fn anchor(&self) -> &[u8; 32] { + match self { + TokenMintToPoolTransition::V0(v0) => v0.anchor(), + } + } + fn proof(&self) -> &[u8] { + match self { + TokenMintToPoolTransition::V0(v0) => v0.proof(), + } + } + fn binding_signature(&self) -> &[u8; 64] { + match self { + TokenMintToPoolTransition::V0(v0) => v0.binding_signature(), + } + } + fn public_note(&self) -> Option<&String> { + match self { + TokenMintToPoolTransition::V0(v0) => v0.public_note(), + } + } + fn public_note_owned(self) -> Option { + match self { + TokenMintToPoolTransition::V0(v0) => v0.public_note_owned(), + } + } + fn set_amount(&mut self, amount: TokenAmount) { + match self { + TokenMintToPoolTransition::V0(v0) => v0.set_amount(amount), + } + } + fn set_public_note(&mut self, public_note: Option) { + match self { + TokenMintToPoolTransition::V0(v0) => v0.set_public_note(public_note), + } + } +} + +impl AllowedAsMultiPartyAction for TokenMintToPoolTransition { + fn calculate_action_id( + &self, + owner_id: Identifier, + platform_version: &PlatformVersion, + ) -> Result { + match self { + TokenMintToPoolTransition::V0(v0) => v0.calculate_action_id(owner_id, platform_version), + } + } +} + +impl TokenMintToPoolTransition { + /// The group action id: unlike a transparent mint, the digest of the Orchard + /// actions is part of it, so two proposals that differ only in the notes never share an id. + pub fn calculate_action_id_with_fields( + token_id: &[u8; 32], + owner_id: &[u8; 32], + identity_contract_nonce: IdentityNonce, + amount: TokenAmount, + actions_digest: &[u8; 32], + ) -> Identifier { + let mut bytes = b"action_token_mint_to_pool".to_vec(); + bytes.extend_from_slice(token_id); + bytes.extend_from_slice(owner_id); + bytes.extend_from_slice(&identity_contract_nonce.to_be_bytes()); + bytes.extend_from_slice(&amount.to_be_bytes()); + bytes.extend_from_slice(actions_digest); + + hash_double(bytes).into() + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/validate_structure/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/validate_structure/mod.rs new file mode 100644 index 00000000000..288b3fbb814 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/validate_structure/mod.rs @@ -0,0 +1,36 @@ +use crate::state_transition::batch_transition::token_mint_to_pool_transition::validate_structure::v0::TokenMintToPoolTransitionActionStructureValidationV0; +use crate::state_transition::batch_transition::TokenMintToPoolTransition; +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +mod v0; + +pub trait TokenMintToPoolTransitionStructureValidation { + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenMintToPoolTransitionStructureValidation for TokenMintToPoolTransition { + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .token_mint_to_pool_transition_structure_validation + { + 0 => self.validate_structure_v0(platform_version), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "TokenMintToPoolTransition::validate_structure".to_string(), + known_versions: vec![0], + received: version, + }), + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/validate_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/validate_structure/v0/mod.rs new file mode 100644 index 00000000000..92ab368d1e2 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/validate_structure/v0/mod.rs @@ -0,0 +1,181 @@ +use crate::consensus::basic::token::{InvalidTokenAmountError, InvalidTokenNoteTooBigError, TokenNoteOnlyAllowedWhenProposerError}; +use crate::consensus::basic::BasicError; +use crate::consensus::ConsensusError; +use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::MAX_DISTRIBUTION_PARAM; +use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use crate::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; +use crate::state_transition::batch_transition::token_mint_to_pool_transition::v0::v0_methods::TokenMintToPoolTransitionV0Methods; +use crate::state_transition::batch_transition::TokenMintToPoolTransition; +use crate::state_transition::state_transitions::shielded::common_validation::{ + validate_actions_count, validate_anchor_not_zero, validate_encrypted_note_sizes, + validate_proof_not_empty, +}; +use crate::tokens::MAX_TOKEN_NOTE_LEN; +use crate::validation::SimpleConsensusValidationResult; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +pub(super) trait TokenMintToPoolTransitionActionStructureValidationV0 { + fn validate_structure_v0( + &self, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenMintToPoolTransitionActionStructureValidationV0 for TokenMintToPoolTransition { + /// The bounds match the equivalent transparent token transition; the bundle checks are the + /// same stateless checks the other pool transitions run (action count, note sizes, + /// non-empty proof, non-zero anchor). The proof itself is verified against state, where its + /// cost can be charged. + fn validate_structure_v0( + &self, + platform_version: &PlatformVersion, + ) -> Result { + if self.amount() > MAX_DISTRIBUTION_PARAM || self.amount() == 0 { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::BasicError(BasicError::InvalidTokenAmountError( + InvalidTokenAmountError::new(MAX_DISTRIBUTION_PARAM, self.amount()), + )), + )); + } + + if let Some(public_note) = self.public_note() { + if public_note.len() > MAX_TOKEN_NOTE_LEN { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::BasicError(BasicError::InvalidTokenNoteTooBigError( + InvalidTokenNoteTooBigError::new( + MAX_TOKEN_NOTE_LEN as u32, + "public_note", + public_note.len() as u32, + ), + )), + )); + } + if let Some(group_state_transition_info) = self.base().using_group_info() { + if !group_state_transition_info.action_is_proposer { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::BasicError( + BasicError::TokenNoteOnlyAllowedWhenProposerError( + TokenNoteOnlyAllowedWhenProposerError::new(), + ), + ), + )); + } + } + } + + let result = validate_actions_count( + self.actions(), + platform_version + .system_limits + .max_shielded_transition_actions, + ); + if !result.is_valid() { + return Ok(result); + } + + let result = validate_encrypted_note_sizes(self.actions()); + if !result.is_valid() { + return Ok(result); + } + + let result = validate_proof_not_empty(self.proof()); + if !result.is_valid() { + return Ok(result); + } + + Ok(validate_anchor_not_zero(self.anchor())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::shielded::SerializedAction; + use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; + use crate::state_transition::batch_transition::token_mint_to_pool_transition::v0::TokenMintToPoolTransitionV0; + use platform_value::Identifier; + + fn action() -> SerializedAction { + SerializedAction { + nullifier: [1u8; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + } + } + + fn make_transition(amount: u64) -> TokenMintToPoolTransition { + TokenMintToPoolTransition::V0(TokenMintToPoolTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::default(), + token_id: Identifier::default(), + using_group_info: None, + }), + amount: amount, + actions: vec![action()], + anchor: [9u8; 32], + proof: vec![1u8; 10], + binding_signature: [7u8; 64], + public_note: None, + }) + } + + #[test] + fn valid_transition_passes() { + let result = make_transition(100) + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(result.is_valid(), "{:?}", result.errors); + } + + #[test] + fn zero_amount_is_rejected() { + let result = make_transition(0) + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::InvalidTokenAmountError(_) + )) + )); + } + + #[test] + fn empty_actions_are_rejected() { + let mut transition = make_transition(100); + let TokenMintToPoolTransition::V0(v0) = &mut transition; + v0.actions.clear(); + let result = transition + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::ShieldedNoActionsError(_) + )) + )); + } + + #[test] + fn empty_proof_is_rejected() { + let mut transition = make_transition(100); + let TokenMintToPoolTransition::V0(v0) = &mut transition; + v0.proof.clear(); + let result = transition + .validate_structure_v0(PlatformVersion::latest()) + .unwrap(); + assert!(matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::ShieldedEmptyProofError(_) + )) + )); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs index d4869026cf5..5e514e3902b 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs @@ -19,7 +19,10 @@ use crate::data_contract::document_type::DocumentTypeRef; use crate::document::Document; use crate::prelude::IdentityNonce; use crate::ProtocolError; -use crate::state_transition::batch_transition::{DocumentCreateTransition, DocumentDeleteTransition, DocumentReplaceTransition, TokenBurnTransition, TokenConfigUpdateTransition, TokenDestroyFrozenFundsTransition, TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintTransition, TokenClaimTransition, TokenTransferTransition, TokenSetPriceForDirectPurchaseTransition, TokenShieldTransition, TokenShieldedTransferTransition, TokenUnshieldTransition}; +use crate::state_transition::batch_transition::{DocumentCreateTransition, DocumentDeleteTransition, DocumentReplaceTransition, TokenBurnTransition, TokenConfigUpdateTransition, TokenDestroyFrozenFundsTransition, TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintTransition, TokenClaimTransition, TokenTransferTransition, TokenSetPriceForDirectPurchaseTransition, TokenShieldTransition, TokenShieldedTransferTransition, TokenUnshieldTransition, + TokenMintToPoolTransition, TokenBurnFromPoolTransition, TokenClaimToPoolTransition, + TokenDirectPurchaseToPoolTransition, +}; use crate::state_transition::batch_transition::batched_transition::{DocumentPurchaseTransition, DocumentTransferTransition}; use crate::state_transition::batch_transition::batched_transition::multi_party_action::AllowedAsMultiPartyAction; use crate::state_transition::batch_transition::batched_transition::token_unfreeze_transition::TokenUnfreezeTransition; @@ -38,7 +41,11 @@ use crate::state_transition::batch_transition::token_direct_purchase_transition: use crate::state_transition::batch_transition::token_direct_purchase_transition::v0::v0_methods::TokenDirectPurchaseTransitionV0Methods; use crate::state_transition::batch_transition::token_set_price_for_direct_purchase_transition::v0::v0_methods::TokenSetPriceForDirectPurchaseTransitionV0Methods; use crate::state_transition::batch_transition::token_transfer_transition::v0::v0_methods::TokenTransferTransitionV0Methods; +use crate::shielded::serialized_actions_digest; use crate::state_transition::batch_transition::token_shield_transition::v0::v0_methods::TokenShieldTransitionV0Methods; +use crate::state_transition::batch_transition::token_mint_to_pool_transition::v0::v0_methods::TokenMintToPoolTransitionV0Methods; +use crate::state_transition::batch_transition::token_burn_from_pool_transition::v0::v0_methods::TokenBurnFromPoolTransitionV0Methods; +use crate::state_transition::batch_transition::token_direct_purchase_to_pool_transition::v0::v0_methods::TokenDirectPurchaseToPoolTransitionV0Methods; use crate::state_transition::batch_transition::token_unshield_transition::v0::v0_methods::TokenUnshieldTransitionV0Methods; use crate::state_transition::batch_transition::token_unfreeze_transition::v0::v0_methods::TokenUnfreezeTransitionV0Methods; use crate::tokens::token_event::TokenEvent; @@ -101,6 +108,18 @@ pub enum TokenTransition { #[display("TokenShieldedTransferTransition({})", "_0")] ShieldedTransfer(TokenShieldedTransferTransition), + + #[display("TokenMintToPoolTransition({})", "_0")] + MintToPool(TokenMintToPoolTransition), + + #[display("TokenBurnFromPoolTransition({})", "_0")] + BurnFromPool(TokenBurnFromPoolTransition), + + #[display("TokenClaimToPoolTransition({})", "_0")] + ClaimToPool(TokenClaimToPoolTransition), + + #[display("TokenDirectPurchaseToPoolTransition({})", "_0")] + DirectPurchaseToPool(TokenDirectPurchaseToPoolTransition), } #[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] @@ -417,6 +436,40 @@ impl BatchTransitionResolversV0 for TokenTransition { None } } + + fn as_transition_token_mint_to_pool(&self) -> Option<&TokenMintToPoolTransition> { + if let Self::MintToPool(ref t) = self { + Some(t) + } else { + None + } + } + + fn as_transition_token_burn_from_pool(&self) -> Option<&TokenBurnFromPoolTransition> { + if let Self::BurnFromPool(ref t) = self { + Some(t) + } else { + None + } + } + + fn as_transition_token_claim_to_pool(&self) -> Option<&TokenClaimToPoolTransition> { + if let Self::ClaimToPool(ref t) = self { + Some(t) + } else { + None + } + } + + fn as_transition_token_direct_purchase_to_pool( + &self, + ) -> Option<&TokenDirectPurchaseToPoolTransition> { + if let Self::DirectPurchaseToPool(ref t) = self { + Some(t) + } else { + None + } + } } pub trait TokenTransitionV0Methods { @@ -488,6 +541,10 @@ impl TokenTransitionV0Methods for TokenTransition { TokenTransition::Shield(t) => t.base(), TokenTransition::Unshield(t) => t.base(), TokenTransition::ShieldedTransfer(t) => t.base(), + TokenTransition::MintToPool(t) => t.base(), + TokenTransition::BurnFromPool(t) => t.base(), + TokenTransition::ClaimToPool(t) => t.base(), + TokenTransition::DirectPurchaseToPool(t) => t.base(), } } @@ -507,6 +564,10 @@ impl TokenTransitionV0Methods for TokenTransition { TokenTransition::Shield(t) => t.base_mut(), TokenTransition::Unshield(t) => t.base_mut(), TokenTransition::ShieldedTransfer(t) => t.base_mut(), + TokenTransition::MintToPool(t) => t.base_mut(), + TokenTransition::BurnFromPool(t) => t.base_mut(), + TokenTransition::ClaimToPool(t) => t.base_mut(), + TokenTransition::DirectPurchaseToPool(t) => t.base_mut(), } } @@ -539,9 +600,17 @@ impl TokenTransitionV0Methods for TokenTransition { TokenTransition::SetPriceForDirectPurchase(t) => { Some(t.calculate_action_id(owner_id, platform_version)) } + TokenTransition::MintToPool(t) => { + Some(t.calculate_action_id(owner_id, platform_version)) + } + TokenTransition::BurnFromPool(t) => { + Some(t.calculate_action_id(owner_id, platform_version)) + } TokenTransition::Shield(_) | TokenTransition::Unshield(_) - | TokenTransition::ShieldedTransfer(_) => None, + | TokenTransition::ShieldedTransfer(_) + | TokenTransition::ClaimToPool(_) + | TokenTransition::DirectPurchaseToPool(_) => None, } } @@ -554,13 +623,17 @@ impl TokenTransitionV0Methods for TokenTransition { | TokenTransition::DestroyFrozenFunds(_) | TokenTransition::EmergencyAction(_) | TokenTransition::ConfigUpdate(_) - | TokenTransition::SetPriceForDirectPurchase(_) => true, + | TokenTransition::SetPriceForDirectPurchase(_) + | TokenTransition::MintToPool(_) + | TokenTransition::BurnFromPool(_) => true, TokenTransition::Transfer(_) | TokenTransition::Claim(_) | TokenTransition::DirectPurchase(_) | TokenTransition::Shield(_) | TokenTransition::Unshield(_) - | TokenTransition::ShieldedTransfer(_) => false, + | TokenTransition::ShieldedTransfer(_) + | TokenTransition::ClaimToPool(_) + | TokenTransition::DirectPurchaseToPool(_) => false, } } @@ -601,6 +674,10 @@ impl TokenTransitionV0Methods for TokenTransition { TokenTransition::Shield(_) => "shield", TokenTransition::Unshield(_) => "unshield", TokenTransition::ShieldedTransfer(_) => "shieldedTransfer", + TokenTransition::MintToPool(_) => "mintToPool", + TokenTransition::BurnFromPool(_) => "burnFromPool", + TokenTransition::ClaimToPool(_) => "claimToPool", + TokenTransition::DirectPurchaseToPool(_) => "directPurchaseToPool", } } @@ -745,6 +822,22 @@ impl TokenTransitionV0Methods for TokenTransition { TokenEvent::Unshield(unshield.recipient_id(), unshield.amount()) } TokenTransition::ShieldedTransfer(_) => TokenEvent::ShieldedTransfer, + TokenTransition::MintToPool(mint) => TokenEvent::MintToPool( + mint.amount(), + serialized_actions_digest(mint.actions()).into(), + mint.public_note().cloned(), + ), + TokenTransition::BurnFromPool(burn) => TokenEvent::BurnFromPool( + burn.amount(), + serialized_actions_digest(burn.actions()).into(), + burn.public_note().cloned(), + ), + // we do not know how much will be released + TokenTransition::ClaimToPool(_) => TokenEvent::ClaimToPool(TokenAmount::MAX), + TokenTransition::DirectPurchaseToPool(purchase) => TokenEvent::DirectPurchaseToPool( + purchase.token_count(), + purchase.total_agreed_price(), + ), TokenTransition::DirectPurchase(direct_purchase) => TokenEvent::DirectPurchase( direct_purchase.token_count(), direct_purchase.total_agreed_price(), diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition_action_type.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition_action_type.rs index e95a042ab44..cb797562b82 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition_action_type.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition_action_type.rs @@ -49,6 +49,18 @@ pub enum TokenTransitionActionType { /// Indicates tokens moving from an identity balance into the token's shielded pool. Shield, + /// Indicates a mint straight into the token's shielded pool. + MintToPool, + + /// Indicates a burn of notes held in the token's shielded pool. + BurnFromPool, + + /// Indicates a distribution claim paid into the token's shielded pool. + ClaimToPool, + + /// Indicates a direct purchase paid into the token's shielded pool. + DirectPurchaseToPool, + /// Indicates tokens moving from the token's shielded pool to an identity balance. Unshield, @@ -73,6 +85,10 @@ impl fmt::Display for TokenTransitionActionType { TokenTransitionActionType::Shield => "Shield", TokenTransitionActionType::Unshield => "Unshield", TokenTransitionActionType::ShieldedTransfer => "ShieldedTransfer", + TokenTransitionActionType::MintToPool => "MintToPool", + TokenTransitionActionType::BurnFromPool => "BurnFromPool", + TokenTransitionActionType::ClaimToPool => "ClaimToPool", + TokenTransitionActionType::DirectPurchaseToPool => "DirectPurchaseToPool", }; write!(f, "{}", action_str) } @@ -101,6 +117,12 @@ impl TokenTransitionActionTypeGetter for TokenTransition { TokenTransition::Shield(_) => TokenTransitionActionType::Shield, TokenTransition::Unshield(_) => TokenTransitionActionType::Unshield, TokenTransition::ShieldedTransfer(_) => TokenTransitionActionType::ShieldedTransfer, + TokenTransition::MintToPool(_) => TokenTransitionActionType::MintToPool, + TokenTransition::BurnFromPool(_) => TokenTransitionActionType::BurnFromPool, + TokenTransition::ClaimToPool(_) => TokenTransitionActionType::ClaimToPool, + TokenTransition::DirectPurchaseToPool(_) => { + TokenTransitionActionType::DirectPurchaseToPool + } } } } @@ -132,6 +154,12 @@ impl TryFrom<&str> for TokenTransitionActionType { "shielded_transfer" | "shieldedTransfer" => { Ok(TokenTransitionActionType::ShieldedTransfer) } + "mint_to_pool" | "mintToPool" => Ok(TokenTransitionActionType::MintToPool), + "burn_from_pool" | "burnFromPool" => Ok(TokenTransitionActionType::BurnFromPool), + "claim_to_pool" | "claimToPool" => Ok(TokenTransitionActionType::ClaimToPool), + "direct_purchase_to_pool" | "directPurchaseToPool" => { + Ok(TokenTransitionActionType::DirectPurchaseToPool) + } action_type => Err(ProtocolError::Generic(format!( "unknown token transition action type {action_type}" ))), diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs index c125acdc7e9..1974f12f723 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs @@ -737,6 +737,241 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransition { } } + #[cfg(feature = "state-transition-signing")] + async fn new_token_mint_to_pool_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + amount: TokenAmount, + bundle: OrchardBundleParams, + public_note: Option, + using_group_info: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let resolved_options = options.unwrap_or_default(); + match resolved_options.batch_feature_version.unwrap_or( + platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .default_current_version, + ) { + 1 | 0 + if platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .max_version + >= 1 => + { + BatchTransitionV1::new_token_mint_to_pool_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + amount, + bundle, + public_note, + using_group_info, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + options, + ) + .await + } + version => Err(ProtocolError::UnknownVersionMismatch { + method: "DocumentsBatchTransition::new_token_mint_to_pool_transition".to_string(), + known_versions: vec![1], + received: version, + }), + } + } + + #[cfg(feature = "state-transition-signing")] + async fn new_token_burn_from_pool_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + amount: TokenAmount, + bundle: OrchardBundleParams, + public_note: Option, + using_group_info: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let resolved_options = options.unwrap_or_default(); + match resolved_options.batch_feature_version.unwrap_or( + platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .default_current_version, + ) { + 1 | 0 + if platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .max_version + >= 1 => + { + BatchTransitionV1::new_token_burn_from_pool_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + amount, + bundle, + public_note, + using_group_info, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + options, + ) + .await + } + version => Err(ProtocolError::UnknownVersionMismatch { + method: "DocumentsBatchTransition::new_token_burn_from_pool_transition".to_string(), + known_versions: vec![1], + received: version, + }), + } + } + + #[cfg(feature = "state-transition-signing")] + async fn new_token_claim_to_pool_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + distribution_type: TokenDistributionType, + claim_up_to: Option, + bundle: OrchardBundleParams, + public_note: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let resolved_options = options.unwrap_or_default(); + match resolved_options.batch_feature_version.unwrap_or( + platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .default_current_version, + ) { + 1 | 0 + if platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .max_version + >= 1 => + { + BatchTransitionV1::new_token_claim_to_pool_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + distribution_type, + claim_up_to, + bundle, + public_note, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + options, + ) + .await + } + version => Err(ProtocolError::UnknownVersionMismatch { + method: "DocumentsBatchTransition::new_token_claim_to_pool_transition".to_string(), + known_versions: vec![1], + received: version, + }), + } + } + + #[cfg(feature = "state-transition-signing")] + async fn new_token_direct_purchase_to_pool_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + token_count: TokenAmount, + total_agreed_price: Credits, + bundle: OrchardBundleParams, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let resolved_options = options.unwrap_or_default(); + match resolved_options.batch_feature_version.unwrap_or( + platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .default_current_version, + ) { + 1 | 0 + if platform_version + .dpp + .state_transition_serialization_versions + .batch_state_transition + .max_version + >= 1 => + { + BatchTransitionV1::new_token_direct_purchase_to_pool_transition( + token_id, + owner_id, + data_contract_id, + token_contract_position, + token_count, + total_agreed_price, + bundle, + identity_public_key, + identity_contract_nonce, + user_fee_increase, + signer, + platform_version, + options, + ) + .await + } + version => Err(ProtocolError::UnknownVersionMismatch { + method: "DocumentsBatchTransition::new_token_direct_purchase_to_pool_transition" + .to_string(), + known_versions: vec![1], + received: version, + }), + } + } + #[cfg(feature = "state-transition-signing")] async fn new_token_unshield_transition>( token_id: Identifier, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs index 25071ca34c8..2a12c597283 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs @@ -170,6 +170,94 @@ pub trait DocumentsBatchTransitionMethodsV1: DocumentsBatchTransitionAccessorsV0 options: Option, ) -> Result; + /// Creates a `StateTransition` minting `amount` of a token straight into the token's shielded + /// pool as the notes in `bundle`. `owner_id` must be authorized to mint; it signs and pays the + /// credits fee. A group action is set up exactly like a transparent mint. + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + async fn new_token_mint_to_pool_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + amount: TokenAmount, + bundle: OrchardBundleParams, + public_note: Option, + using_group_info: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result; + + /// Creates a `StateTransition` destroying `amount` of a token held in the token's shielded + /// pool by spending the notes in `bundle`. `owner_id` must be authorized to burn; it signs + /// and pays the credits fee. A group action is set up exactly like a transparent burn. + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + async fn new_token_burn_from_pool_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + amount: TokenAmount, + bundle: OrchardBundleParams, + public_note: Option, + using_group_info: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result; + + /// Creates a `StateTransition` claiming a distribution straight into the token's shielded + /// pool as the notes in `bundle`. `claim_up_to` names the cycle-aligned moment a perpetual + /// claim pays out to, so the bundle's value can be predicted; `owner_id` signs and pays the + /// credits fee. + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + async fn new_token_claim_to_pool_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + distribution_type: TokenDistributionType, + claim_up_to: Option, + bundle: OrchardBundleParams, + public_note: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result; + + /// Creates a `StateTransition` buying `token_count` tokens for at most `total_agreed_price` + /// credits and receiving them shielded as the notes in `bundle`. `owner_id` pays the price + /// and the credits fee. + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + async fn new_token_direct_purchase_to_pool_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + token_count: TokenAmount, + total_agreed_price: Credits, + bundle: OrchardBundleParams, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result; + /// Creates a `StateTransition` moving `amount` of a token out of the token's shielded pool /// into `recipient_id`'s identity token balance. `owner_id` signs and pays the credits fee. /// diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs index ae88e85149c..4ef5591bb94 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/mod.rs @@ -17,16 +17,22 @@ pub use self::batched_transition::{ document_delete_transition::DocumentDeleteTransition, document_index_only_delete_transition, document_index_only_delete_transition::DocumentIndexOnlyDeleteTransition, document_replace_transition, document_replace_transition::DocumentReplaceTransition, - token_base_transition, token_burn_transition, token_burn_transition::TokenBurnTransition, - token_claim_transition, token_claim_transition::TokenClaimTransition, - token_config_update_transition, token_config_update_transition::TokenConfigUpdateTransition, + token_base_transition, token_burn_from_pool_transition, + token_burn_from_pool_transition::TokenBurnFromPoolTransition, token_burn_transition, + token_burn_transition::TokenBurnTransition, token_claim_to_pool_transition, + token_claim_to_pool_transition::TokenClaimToPoolTransition, token_claim_transition, + token_claim_transition::TokenClaimTransition, token_config_update_transition, + token_config_update_transition::TokenConfigUpdateTransition, token_destroy_frozen_funds_transition, token_destroy_frozen_funds_transition::TokenDestroyFrozenFundsTransition, + token_direct_purchase_to_pool_transition, + token_direct_purchase_to_pool_transition::TokenDirectPurchaseToPoolTransition, token_direct_purchase_transition, token_direct_purchase_transition::TokenDirectPurchaseTransition, token_emergency_action_transition, token_emergency_action_transition::TokenEmergencyActionTransition, token_freeze_transition, - token_freeze_transition::TokenFreezeTransition, token_mint_transition, + token_freeze_transition::TokenFreezeTransition, token_mint_to_pool_transition, + token_mint_to_pool_transition::TokenMintToPoolTransition, token_mint_transition, token_mint_transition::TokenMintTransition, token_set_price_for_direct_purchase_transition, token_set_price_for_direct_purchase_transition::TokenSetPriceForDirectPurchaseTransition, token_shield_transition, token_shield_transition::TokenShieldTransition, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/resolvers/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/resolvers/v0/mod.rs index d830506aa75..a5a91272f8a 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/resolvers/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/resolvers/v0/mod.rs @@ -5,10 +5,12 @@ use crate::state_transition::batch_transition::batched_transition::{ use crate::state_transition::batch_transition::token_direct_purchase_transition::TokenDirectPurchaseTransition; use crate::state_transition::batch_transition::{ DocumentCreateTransition, DocumentDeleteTransition, DocumentReplaceTransition, - TokenBurnTransition, TokenClaimTransition, TokenConfigUpdateTransition, - TokenDestroyFrozenFundsTransition, TokenEmergencyActionTransition, TokenFreezeTransition, - TokenMintTransition, TokenSetPriceForDirectPurchaseTransition, TokenShieldTransition, - TokenShieldedTransferTransition, TokenTransferTransition, TokenUnshieldTransition, + TokenBurnFromPoolTransition, TokenBurnTransition, TokenClaimToPoolTransition, + TokenClaimTransition, TokenConfigUpdateTransition, TokenDestroyFrozenFundsTransition, + TokenDirectPurchaseToPoolTransition, TokenEmergencyActionTransition, TokenFreezeTransition, + TokenMintToPoolTransition, TokenMintTransition, TokenSetPriceForDirectPurchaseTransition, + TokenShieldTransition, TokenShieldedTransferTransition, TokenTransferTransition, + TokenUnshieldTransition, }; pub trait BatchTransitionResolversV0 { @@ -37,4 +39,10 @@ pub trait BatchTransitionResolversV0 { fn as_transition_token_shield(&self) -> Option<&TokenShieldTransition>; fn as_transition_token_unshield(&self) -> Option<&TokenUnshieldTransition>; fn as_transition_token_shielded_transfer(&self) -> Option<&TokenShieldedTransferTransition>; + fn as_transition_token_mint_to_pool(&self) -> Option<&TokenMintToPoolTransition>; + fn as_transition_token_burn_from_pool(&self) -> Option<&TokenBurnFromPoolTransition>; + fn as_transition_token_claim_to_pool(&self) -> Option<&TokenClaimToPoolTransition>; + fn as_transition_token_direct_purchase_to_pool( + &self, + ) -> Option<&TokenDirectPurchaseToPoolTransition>; } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v1_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v1_methods.rs index 35affa19994..7ac7a07d18f 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v1_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/v1_methods.rs @@ -71,11 +71,19 @@ use crate::shielded::OrchardBundleParams; #[cfg(feature = "state-transition-signing")] use crate::state_transition::batch_transition::token_shield_transition::TokenShieldTransitionV0; #[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_mint_to_pool_transition::TokenMintToPoolTransitionV0; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_burn_from_pool_transition::TokenBurnFromPoolTransitionV0; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_claim_to_pool_transition::TokenClaimToPoolTransitionV0; +#[cfg(feature = "state-transition-signing")] +use crate::state_transition::batch_transition::token_direct_purchase_to_pool_transition::TokenDirectPurchaseToPoolTransitionV0; +#[cfg(feature = "state-transition-signing")] use crate::state_transition::batch_transition::token_shielded_transfer_transition::TokenShieldedTransferTransitionV0; #[cfg(feature = "state-transition-signing")] use crate::state_transition::batch_transition::token_unshield_transition::TokenUnshieldTransitionV0; #[cfg(feature = "state-transition-signing")] -use crate::state_transition::batch_transition::{TokenShieldTransition, TokenShieldedTransferTransition, TokenUnshieldTransition}; +use crate::state_transition::batch_transition::{TokenShieldTransition, TokenShieldedTransferTransition, TokenUnshieldTransition, TokenMintToPoolTransition, TokenBurnFromPoolTransition, TokenClaimToPoolTransition, TokenDirectPurchaseToPoolTransition}; #[cfg(feature = "state-transition-signing")] use crate::state_transition::batch_transition::token_unfreeze_transition::TokenUnfreezeTransitionV0; #[cfg(feature = "state-transition-signing")] @@ -391,6 +399,335 @@ impl DocumentsBatchTransitionMethodsV1 for BatchTransitionV1 { Ok(state_transition) } + #[cfg(feature = "state-transition-signing")] + async fn new_token_mint_to_pool_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + amount: TokenAmount, + bundle: OrchardBundleParams, + public_note: Option, + using_group_info: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let OrchardBundleParams { + actions, + anchor, + proof, + binding_signature, + } = bundle; + + let mut transition = TokenMintToPoolTransition::V0(TokenMintToPoolTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce, + token_contract_position, + data_contract_id, + token_id, + using_group_info: None, + }), + amount, + actions, + anchor, + proof, + binding_signature, + public_note, + }); + + if let Some(using_group_info_status) = using_group_info { + match using_group_info_status { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer( + group_contract_position, + ) => { + let action_id = transition.calculate_action_id(owner_id, platform_version)?; + transition + .base_mut() + .set_using_group_info(Some(GroupStateTransitionInfo { + group_contract_position, + action_id, + action_is_proposer: true, + })) + } + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(info) => { + transition.base_mut().set_using_group_info(Some(info)) + } + } + } + + let documents_batch_transition: BatchTransition = BatchTransitionV1 { + owner_id, + transitions: vec![BatchedTransition::Token(transition.into())], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + + let mut state_transition: StateTransition = documents_batch_transition.into(); + if let Some(options) = options { + state_transition + .sign_external_with_options( + identity_public_key, + signer, + None::, + options.signing_options, + ) + .await?; + } else { + state_transition + .sign_external( + identity_public_key, + signer, + None::, + ) + .await?; + } + + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + async fn new_token_burn_from_pool_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + amount: TokenAmount, + bundle: OrchardBundleParams, + public_note: Option, + using_group_info: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let OrchardBundleParams { + actions, + anchor, + proof, + binding_signature, + } = bundle; + + let mut transition = TokenBurnFromPoolTransition::V0(TokenBurnFromPoolTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce, + token_contract_position, + data_contract_id, + token_id, + using_group_info: None, + }), + amount, + actions, + anchor, + proof, + binding_signature, + public_note, + }); + + if let Some(using_group_info_status) = using_group_info { + match using_group_info_status { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer( + group_contract_position, + ) => { + let action_id = transition.calculate_action_id(owner_id, platform_version)?; + transition + .base_mut() + .set_using_group_info(Some(GroupStateTransitionInfo { + group_contract_position, + action_id, + action_is_proposer: true, + })) + } + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(info) => { + transition.base_mut().set_using_group_info(Some(info)) + } + } + } + + let documents_batch_transition: BatchTransition = BatchTransitionV1 { + owner_id, + transitions: vec![BatchedTransition::Token(transition.into())], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + + let mut state_transition: StateTransition = documents_batch_transition.into(); + if let Some(options) = options { + state_transition + .sign_external_with_options( + identity_public_key, + signer, + None::, + options.signing_options, + ) + .await?; + } else { + state_transition + .sign_external( + identity_public_key, + signer, + None::, + ) + .await?; + } + + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + async fn new_token_claim_to_pool_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + distribution_type: TokenDistributionType, + claim_up_to: Option, + bundle: OrchardBundleParams, + public_note: Option, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + _platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let OrchardBundleParams { + actions, + anchor, + proof, + binding_signature, + } = bundle; + + let transition = TokenClaimToPoolTransition::V0(TokenClaimToPoolTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce, + token_contract_position, + data_contract_id, + token_id, + using_group_info: None, + }), + distribution_type, + claim_up_to, + actions, + anchor, + proof, + binding_signature, + public_note, + }); + + let documents_batch_transition: BatchTransition = BatchTransitionV1 { + owner_id, + transitions: vec![BatchedTransition::Token(transition.into())], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + + let mut state_transition: StateTransition = documents_batch_transition.into(); + if let Some(options) = options { + state_transition + .sign_external_with_options( + identity_public_key, + signer, + None::, + options.signing_options, + ) + .await?; + } else { + state_transition + .sign_external( + identity_public_key, + signer, + None::, + ) + .await?; + } + + Ok(state_transition) + } + + #[cfg(feature = "state-transition-signing")] + async fn new_token_direct_purchase_to_pool_transition>( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + token_contract_position: u16, + token_count: TokenAmount, + total_agreed_price: Credits, + bundle: OrchardBundleParams, + identity_public_key: &IdentityPublicKey, + identity_contract_nonce: IdentityNonce, + user_fee_increase: UserFeeIncrease, + signer: &S, + _platform_version: &PlatformVersion, + options: Option, + ) -> Result { + let OrchardBundleParams { + actions, + anchor, + proof, + binding_signature, + } = bundle; + + let transition = + TokenDirectPurchaseToPoolTransition::V0(TokenDirectPurchaseToPoolTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce, + token_contract_position, + data_contract_id, + token_id, + using_group_info: None, + }), + token_count, + total_agreed_price, + actions, + anchor, + proof, + binding_signature, + }); + + let documents_batch_transition: BatchTransition = BatchTransitionV1 { + owner_id, + transitions: vec![BatchedTransition::Token(transition.into())], + user_fee_increase, + signature_public_key_id: 0, + signature: Default::default(), + } + .into(); + + let mut state_transition: StateTransition = documents_batch_transition.into(); + if let Some(options) = options { + state_transition + .sign_external_with_options( + identity_public_key, + signer, + None::, + options.signing_options, + ) + .await?; + } else { + state_transition + .sign_external( + identity_public_key, + signer, + None::, + ) + .await?; + } + + Ok(state_transition) + } + #[cfg(feature = "state-transition-signing")] async fn new_token_unshield_transition>( token_id: Identifier, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs index 05459a33574..06d1ff1980b 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs @@ -32,6 +32,10 @@ use crate::state_transition::batch_transition::token_claim_transition::validate_ use crate::state_transition::batch_transition::token_direct_purchase_transition::validate_structure::TokenDirectPurchaseTransitionStructureValidation; use crate::state_transition::batch_transition::token_set_price_for_direct_purchase_transition::validate_structure::TokenSetPriceForDirectPurchaseTransitionStructureValidation; use crate::state_transition::batch_transition::token_shield_transition::validate_structure::TokenShieldTransitionStructureValidation; +use crate::state_transition::batch_transition::token_mint_to_pool_transition::validate_structure::TokenMintToPoolTransitionStructureValidation; +use crate::state_transition::batch_transition::token_burn_from_pool_transition::validate_structure::TokenBurnFromPoolTransitionStructureValidation; +use crate::state_transition::batch_transition::token_claim_to_pool_transition::validate_structure::TokenClaimToPoolTransitionStructureValidation; +use crate::state_transition::batch_transition::token_direct_purchase_to_pool_transition::validate_structure::TokenDirectPurchaseToPoolTransitionStructureValidation; use crate::state_transition::batch_transition::token_shielded_transfer_transition::validate_structure::TokenShieldedTransferTransitionStructureValidation; use crate::state_transition::batch_transition::token_unshield_transition::validate_structure::TokenUnshieldTransitionStructureValidation; use crate::state_transition::batch_transition::token_transfer_transition::validate_structure::TokenTransferTransitionStructureValidation; @@ -231,6 +235,18 @@ impl BatchTransition { TokenTransition::ShieldedTransfer(shielded_transfer_transition) => { shielded_transfer_transition.validate_structure(platform_version)? } + TokenTransition::MintToPool(mint_to_pool_transition) => { + mint_to_pool_transition.validate_structure(platform_version)? + } + TokenTransition::BurnFromPool(burn_from_pool_transition) => { + burn_from_pool_transition.validate_structure(platform_version)? + } + TokenTransition::ClaimToPool(claim_to_pool_transition) => { + claim_to_pool_transition.validate_structure(platform_version)? + } + TokenTransition::DirectPurchaseToPool(direct_purchase_to_pool_transition) => { + direct_purchase_to_pool_transition.validate_structure(platform_version)? + } }; if !consensus_result.is_valid() { diff --git a/packages/rs-dpp/src/tokens/token_event.rs b/packages/rs-dpp/src/tokens/token_event.rs index ecc3e42fffb..dc9182bfb90 100644 --- a/packages/rs-dpp/src/tokens/token_event.rs +++ b/packages/rs-dpp/src/tokens/token_event.rs @@ -181,6 +181,31 @@ pub enum TokenEvent { /// Event representing a transfer inside the token's shielded pool. Nothing about the /// transfer (parties, amount) is public. ShieldedTransfer, + + /// Event representing a mint straight into the token's shielded pool. + /// + /// - `TokenAmount`: The amount minted. + /// - `Identifier`: Digest of the Orchard actions, so a group action commits to the notes. + /// - `TokenEventPublicNote`: Optional note associated with the event. + MintToPool(TokenAmount, Identifier, TokenEventPublicNote), + + /// Event representing a burn of notes held in the token's shielded pool. + /// + /// - `TokenAmount`: The amount destroyed. + /// - `Identifier`: Digest of the Orchard actions, so a group action commits to the notes. + /// - `TokenEventPublicNote`: Optional note associated with the event. + BurnFromPool(TokenAmount, Identifier, TokenEventPublicNote), + + /// Event representing a distribution claim paid into the token's shielded pool. + /// + /// - `TokenAmount`: The amount claimed. + ClaimToPool(TokenAmount), + + /// Event representing a direct purchase paid into the token's shielded pool. + /// + /// - `TokenAmount`: The amount of tokens purchased. + /// - `Credits`: The number of credits paid. + DirectPurchaseToPool(TokenAmount, Credits), } // Manual impl because TokenEvent is a flat enum with u64-alias tuple variants @@ -318,6 +343,35 @@ impl serde::Serialize for TokenEvent { m.serialize_entry("$type", "shieldedTransfer")?; m.end() } + TokenEvent::MintToPool(amount, actions_digest, note) => { + let mut m = serializer.serialize_map(Some(4))?; + m.serialize_entry("$type", "mintToPool")?; + m.serialize_entry("amount", &SafeU64(amount))?; + m.serialize_entry("actionsDigest", actions_digest)?; + m.serialize_entry("publicNote", note)?; + m.end() + } + TokenEvent::BurnFromPool(amount, actions_digest, note) => { + let mut m = serializer.serialize_map(Some(4))?; + m.serialize_entry("$type", "burnFromPool")?; + m.serialize_entry("amount", &SafeU64(amount))?; + m.serialize_entry("actionsDigest", actions_digest)?; + m.serialize_entry("publicNote", note)?; + m.end() + } + TokenEvent::ClaimToPool(amount) => { + let mut m = serializer.serialize_map(Some(2))?; + m.serialize_entry("$type", "claimToPool")?; + m.serialize_entry("amount", &SafeU64(amount))?; + m.end() + } + TokenEvent::DirectPurchaseToPool(amount, credits) => { + let mut m = serializer.serialize_map(Some(3))?; + m.serialize_entry("$type", "directPurchaseToPool")?; + m.serialize_entry("amount", &SafeU64(amount))?; + m.serialize_entry("credits", &SafeU64(credits))?; + m.end() + } } } } @@ -358,6 +412,7 @@ impl<'de> serde::Deserialize<'de> for TokenEvent { let mut amount: Option = None; let mut credits: Option = None; let mut recipient: Option = None; + let mut actions_digest: Option = None; let mut burn_from: Option = None; let mut frozen: Option = None; let mut public_note: Option = None; @@ -375,6 +430,7 @@ impl<'de> serde::Deserialize<'de> for TokenEvent { "amount" => amount = Some(map.next_value::()?.0), "credits" => credits = Some(map.next_value::()?.0), "recipient" => recipient = Some(map.next_value()?), + "actionsDigest" => actions_digest = Some(map.next_value()?), "burnFromIdentifier" => burn_from = Some(map.next_value()?), "frozenIdentifier" => frozen = Some(map.next_value()?), "publicNote" => public_note = map.next_value()?, @@ -457,6 +513,23 @@ impl<'de> serde::Deserialize<'de> for TokenEvent { amount.ok_or_else(|| A::Error::missing_field("amount"))?, )), "shieldedTransfer" => Ok(TokenEvent::ShieldedTransfer), + "mintToPool" => Ok(TokenEvent::MintToPool( + amount.ok_or_else(|| A::Error::missing_field("amount"))?, + actions_digest.ok_or_else(|| A::Error::missing_field("actionsDigest"))?, + public_note, + )), + "burnFromPool" => Ok(TokenEvent::BurnFromPool( + amount.ok_or_else(|| A::Error::missing_field("amount"))?, + actions_digest.ok_or_else(|| A::Error::missing_field("actionsDigest"))?, + public_note, + )), + "claimToPool" => Ok(TokenEvent::ClaimToPool( + amount.ok_or_else(|| A::Error::missing_field("amount"))?, + )), + "directPurchaseToPool" => Ok(TokenEvent::DirectPurchaseToPool( + amount.ok_or_else(|| A::Error::missing_field("amount"))?, + credits.ok_or_else(|| A::Error::missing_field("credits"))?, + )), other => Err(A::Error::unknown_variant( other, &[ @@ -474,6 +547,10 @@ impl<'de> serde::Deserialize<'de> for TokenEvent { "shield", "unshield", "shieldedTransfer", + "mintToPool", + "burnFromPool", + "claimToPool", + "directPurchaseToPool", ], )), } @@ -678,6 +755,30 @@ impl fmt::Display for TokenEvent { write!(f, "Unshield {} from the token pool to {}", amount, to) } TokenEvent::ShieldedTransfer => write!(f, "Shielded transfer inside the token pool"), + TokenEvent::MintToPool(amount, _, note) => { + write!( + f, + "Mint {} into the token pool{}", + amount, + format_note(note) + ) + } + TokenEvent::BurnFromPool(amount, _, note) => { + write!( + f, + "Burn {} from the token pool{}", + amount, + format_note(note) + ) + } + TokenEvent::ClaimToPool(amount) => { + write!(f, "Claim {} into the token pool", amount) + } + TokenEvent::DirectPurchaseToPool(amount, credits) => write!( + f, + "Direct purchase of {} into the token pool for {} credits", + amount, credits + ), } } } @@ -706,6 +807,10 @@ impl TokenEvent { TokenEvent::Shield(..) => "shield", TokenEvent::Unshield(..) => "unshield", TokenEvent::ShieldedTransfer => "shieldedTransfer", + TokenEvent::MintToPool(..) => "mintToPool", + TokenEvent::BurnFromPool(..) => "burnFromPool", + TokenEvent::ClaimToPool(..) => "claimToPool", + TokenEvent::DirectPurchaseToPool(..) => "directPurchaseToPool", } } @@ -936,6 +1041,21 @@ impl TokenEvent { TokenEvent::ShieldedTransfer => { BTreeMap::from([("tokenId".to_string(), token_id.into())]) } + TokenEvent::MintToPool(amount, _, _) | TokenEvent::BurnFromPool(amount, _, _) => { + BTreeMap::from([ + ("tokenId".to_string(), token_id.into()), + ("amount".to_string(), amount.into()), + ]) + } + TokenEvent::ClaimToPool(amount) => BTreeMap::from([ + ("tokenId".to_string(), token_id.into()), + ("amount".to_string(), amount.into()), + ]), + TokenEvent::DirectPurchaseToPool(amount, total_cost) => BTreeMap::from([ + ("tokenId".to_string(), token_id.into()), + ("tokenAmount".to_string(), amount.into()), + ("purchaseCost".to_string(), total_cost.into()), + ]), }; let document: Document = DocumentV0 { diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/record_token_shielded_pool_anchors/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/record_token_shielded_pool_anchors/v0/mod.rs index 1dcc94b8bad..6b986ddd2b3 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/record_token_shielded_pool_anchors/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/record_token_shielded_pool_anchors/v0/mod.rs @@ -31,6 +31,16 @@ where .shielded_anchor_retention_blocks; for token_id in token_ids { + // Defence in depth: only successful pool writes are collected, but a pool that + // does not exist must never turn the block end into a storage error. + if !self + .drive + .has_token_shielded_pool(*token_id, Some(transaction), platform_version) + .map_err(Error::Drive)? + { + continue; + } + self.drive .record_token_shielded_pool_anchor_if_changed( *token_id, diff --git a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs index d7c3f2c9356..df72e540818 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs @@ -283,14 +283,13 @@ where } } - // An applied transition (successful, or a paid failure whose writes - // stay in the block) may have appended notes to a token pool. A paid - // failure that only bumped the nonce is recorded too: recording is - // "if changed", so an untouched pool costs one anchor read. + // Only a successful execution writes to a token pool; a paid failure + // bumps the nonce and nothing else, and its pool may not even exist + // (a token without the flag, or a paid rejection before the pool + // check), so it must not reach the block end anchor recorder. if matches!( execution_result, StateTransitionExecutionResult::SuccessfulExecution { .. } - | StateTransitionExecutionResult::PaidConsensusError { .. } ) { processing_result .add_token_shielded_pools_touched(token_shielded_pools_touched); @@ -403,7 +402,11 @@ fn token_shielded_pools_touched(state_transition: &StateTransition) -> Vec<[u8; BatchedTransitionRef::Token( token_transition @ (TokenTransition::Shield(_) | TokenTransition::Unshield(_) - | TokenTransition::ShieldedTransfer(_)), + | TokenTransition::ShieldedTransfer(_) + | TokenTransition::MintToPool(_) + | TokenTransition::BurnFromPool(_) + | TokenTransition::ClaimToPool(_) + | TokenTransition::DirectPurchaseToPool(_)), ) => Some(token_transition.token_id().to_buffer()), _ => None, }) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs index d4e5c6f5a65..a7361c2e9c1 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs @@ -86,6 +86,18 @@ impl StateTransitionIsAllowedValidationV0 for StateTransition { BatchedTransitionRef::Token(TokenTransition::ShieldedTransfer( _, )) => Some("TokenShieldedTransfer"), + BatchedTransitionRef::Token(TokenTransition::MintToPool(_)) => { + Some("TokenMintToPool") + } + BatchedTransitionRef::Token(TokenTransition::BurnFromPool(_)) => { + Some("TokenBurnFromPool") + } + BatchedTransitionRef::Token(TokenTransition::ClaimToPool(_)) => { + Some("TokenClaimToPool") + } + BatchedTransitionRef::Token( + TokenTransition::DirectPurchaseToPool(_), + ) => Some("TokenDirectPurchaseToPool"), _ => None, }) { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs index c3c846f1b64..c3d181e2d21 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs @@ -31,6 +31,10 @@ use dpp::state_transition::batch_transition::token_base_transition::v0::v0_metho use dpp::state_transition::batch_transition::token_shield_transition::v0::v0_methods::TokenShieldTransitionV0Methods; use dpp::state_transition::batch_transition::token_shielded_transfer_transition::v0::v0_methods::TokenShieldedTransferTransitionV0Methods; use dpp::state_transition::batch_transition::token_unshield_transition::v0::v0_methods::TokenUnshieldTransitionV0Methods; +use dpp::state_transition::batch_transition::token_mint_to_pool_transition::v0::v0_methods::TokenMintToPoolTransitionV0Methods; +use dpp::state_transition::batch_transition::token_burn_from_pool_transition::v0::v0_methods::TokenBurnFromPoolTransitionV0Methods; +use dpp::state_transition::batch_transition::token_claim_to_pool_transition::v0::v0_methods::TokenClaimToPoolTransitionV0Methods; +use dpp::state_transition::batch_transition::token_direct_purchase_to_pool_transition::v0::v0_methods::TokenDirectPurchaseToPoolTransitionV0Methods; use dpp::state_transition::batch_transition::BatchTransition; use dpp::state_transition::{StateTransition, StateTransitionOwned}; use dpp::util::hash::hash_single; @@ -184,6 +188,18 @@ impl StateTransitionHasShieldedProofValidationV0 for StateTransition { BatchedTransitionRef::Token(TokenTransition::ShieldedTransfer(t)) => { t.actions().len() } + BatchedTransitionRef::Token(TokenTransition::MintToPool(t)) => { + t.actions().len() + } + BatchedTransitionRef::Token(TokenTransition::BurnFromPool(t)) => { + t.actions().len() + } + BatchedTransitionRef::Token(TokenTransition::ClaimToPool(t)) => { + t.actions().len() + } + BatchedTransitionRef::Token(TokenTransition::DirectPurchaseToPool(t)) => { + t.actions().len() + } _ => 0, }) .sum(), @@ -207,7 +223,11 @@ impl StateTransitionHasShieldedProofValidationV0 for StateTransition { BatchedTransitionRef::Token( token_transition @ (TokenTransition::Shield(_) | TokenTransition::Unshield(_) - | TokenTransition::ShieldedTransfer(_)), + | TokenTransition::ShieldedTransfer(_) + | TokenTransition::MintToPool(_) + | TokenTransition::BurnFromPool(_) + | TokenTransition::ClaimToPool(_) + | TokenTransition::DirectPurchaseToPool(_)), ) => Some(ShieldedProofAdmissionKey::IdentityContract { identity_id: owner_id, contract_id: token_transition.data_contract_id().to_buffer(), @@ -1347,6 +1367,48 @@ fn validate_batch_token_shielded_proofs( &extra_sighash_data, ) } + BatchedTransitionRef::Token(TokenTransition::MintToPool(t)) => { + reconstruct_and_verify_bundle( + t.actions(), + FLAGS_OUTPUTS_ONLY, + -(t.amount() as i64), + t.anchor(), + t.proof(), + t.binding_signature(), + &[], + ) + } + BatchedTransitionRef::Token(TokenTransition::BurnFromPool(t)) => { + let extra_sighash_data = dpp::shielded::token_burn_from_pool_extra_sighash_data( + &t.base().token_id().to_buffer(), + &owner_id, + t.amount(), + platform_version, + )?; + reconstruct_and_verify_bundle( + t.actions(), + FLAGS_SPENDS_AND_OUTPUTS, + t.amount() as i64, + t.anchor(), + t.proof(), + t.binding_signature(), + &extra_sighash_data, + ) + } + // The claimable amount is only known against state, so a claim into the pool is + // verified in block validation only; its proof is still admitted per nonce. + BatchedTransitionRef::Token(TokenTransition::ClaimToPool(_)) => continue, + BatchedTransitionRef::Token(TokenTransition::DirectPurchaseToPool(t)) => { + reconstruct_and_verify_bundle( + t.actions(), + FLAGS_OUTPUTS_ONLY, + -(t.token_count() as i64), + t.anchor(), + t.proof(), + t.binding_signature(), + &[], + ) + } _ => continue, }; if let Err(e) = result { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/mod.rs index 6cdc64363b0..2870bd81b70 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/mod.rs @@ -1,11 +1,15 @@ pub(crate) mod token_base_transition_action; +pub(crate) mod token_burn_from_pool_transition_action; pub(crate) mod token_burn_transition_action; +pub(crate) mod token_claim_to_pool_transition_action; pub(crate) mod token_claim_transition_action; pub(crate) mod token_config_update_transition_action; pub(crate) mod token_destroy_frozen_funds_transition_action; +pub(crate) mod token_direct_purchase_to_pool_transition_action; pub(crate) mod token_direct_purchase_transition_action; pub(crate) mod token_emergency_action_transition_action; pub(crate) mod token_freeze_transition_action; +pub(crate) mod token_mint_to_pool_transition_action; pub(crate) mod token_mint_transition_action; pub(crate) mod token_set_price_for_direct_purchase_transition_action; pub(crate) mod token_shield_transition_action; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_burn_from_pool_transition_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_burn_from_pool_transition_action/mod.rs new file mode 100644 index 00000000000..268cde8744c --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_burn_from_pool_transition_action/mod.rs @@ -0,0 +1,64 @@ +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::token::token_burn_from_pool_transition_action::state_v0::TokenBurnFromPoolTransitionActionStateValidationV0; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; +use dpp::identifier::Identifier; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::batch::batched_transition::token_transition::token_burn_from_pool_transition_action::TokenBurnFromPoolTransitionAction; + +mod state_v0; + +pub trait TokenBurnFromPoolTransitionActionValidation { + #[allow(clippy::too_many_arguments)] + fn validate_state( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenBurnFromPoolTransitionActionValidation for TokenBurnFromPoolTransitionAction { + fn validate_state( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .token_burn_from_pool_transition_state_validation + { + 0 => self.validate_state_v0( + platform, + owner_id, + block_info, + execution_context, + validation_mode, + transaction, + platform_version, + ), + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "TokenBurnFromPoolTransitionAction::validate_state".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_burn_from_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_burn_from_pool_transition_action/state_v0/mod.rs new file mode 100644 index 00000000000..fe32bcc18aa --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_burn_from_pool_transition_action/state_v0/mod.rs @@ -0,0 +1,252 @@ +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::token::token_base_transition_action::TokenBaseTransitionActionValidation; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; +use dpp::prelude::Identifier; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::query::TransactionArg; +use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_pool_common::{ + charge_drive_operations, validate_minimum_token_pool_notes, validate_token_not_paused, + validate_token_pool_anchor_exists, validate_token_pool_nullifiers, + validate_token_shielded_pool_enabled, verify_token_pool_bundle, +}; +use crate::execution::validation::state_transition::state_transitions::shielded_common::FLAGS_SPENDS_AND_OUTPUTS; +use dpp::consensus::state::group::ModificationOfGroupActionMainParametersNotPermittedError; +use dpp::consensus::state::shielded::invalid_shielded_proof_error::InvalidShieldedProofError; +use dpp::consensus::state::state_error::StateError; +use dpp::consensus::ConsensusError; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::accessors::v1::DataContractV1Getters; +use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dpp::group::action_event::GroupActionEvent; +use dpp::group::group_action::GroupActionAccessors; +use dpp::shielded::{serialized_actions_digest, token_burn_from_pool_extra_sighash_data}; +use dpp::tokens::token_event::TokenEvent; +use drive::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::token_transition::token_burn_from_pool_transition_action::{ + TokenBurnFromPoolTransitionAction, TokenBurnFromPoolTransitionActionAccessorsV0, +}; + +pub(in crate::execution::validation::state_transition::state_transitions::batch::action_validation) trait TokenBurnFromPoolTransitionActionStateValidationV0 { + #[allow(clippy::too_many_arguments)] + fn validate_state_v0( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenBurnFromPoolTransitionActionStateValidationV0 for TokenBurnFromPoolTransitionAction { + /// Runs the transparent burn's authorization (group action included, with the actions digest + /// as a group parameter), then the pool spend checks (paused, anchor, nullifiers, balance) + /// and verifies the spend bundle whose value balance is `+amount`, bound to the token, the + /// burner and the amount. + fn validate_state_v0( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let validation_result = self.base().validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let validation_result = validate_token_shielded_pool_enabled(self.base())?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let token_id = self.token_id(); + + let contract = &self.data_contract_fetch_info_ref().contract; + let token_configuration = contract.expected_token_configuration(self.token_position())?; + let rules = token_configuration.manual_burning_rules(); + let main_control_group = token_configuration.main_control_group(); + let validation_result = self.base().validate_group_action( + rules, + owner_id, + contract.owner_id(), + main_control_group, + contract.groups(), + "burnFromPool".to_string(), + token_configuration, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + // Every signer of a group action must commit to the same amount and the same notes. + if let Some(original_group_action) = self.base().original_group_action() { + let changed_internal_fields = match original_group_action.event() { + GroupActionEvent::TokenEvent(TokenEvent::BurnFromPool( + old_group_action_amount, + old_actions_digest, + _, + )) => { + let mut changed = vec![]; + if old_group_action_amount != &self.amount() { + changed.push("amount".to_string()); + } + if old_actions_digest.as_bytes() != &serialized_actions_digest(self.actions()) { + changed.push("actions".to_string()); + } + changed + } + _ => vec![], + }; + let same_event = matches!( + original_group_action.event(), + GroupActionEvent::TokenEvent(TokenEvent::BurnFromPool(..)) + ); + if !same_event || !changed_internal_fields.is_empty() { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::StateError( + StateError::ModificationOfGroupActionMainParametersNotPermittedError( + ModificationOfGroupActionMainParametersNotPermittedError::new( + original_group_action.event().event_name(), + "Token: burnFromPool".to_string(), + changed_internal_fields, + ), + ), + ), + )); + } + } + + let validation_result = validate_token_not_paused( + platform, + token_id, + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let token_id_bytes = token_id.to_buffer(); + let mut drive_operations = vec![]; + + let validation_result = validate_minimum_token_pool_notes( + platform.drive, + &token_id_bytes, + transaction, + &mut drive_operations, + platform_version, + )?; + if !validation_result.is_valid() { + charge_drive_operations( + platform.drive, + block_info, + execution_context, + drive_operations, + platform_version, + )?; + return Ok(validation_result); + } + + let validation_result = validate_token_pool_anchor_exists( + platform.drive, + &token_id_bytes, + self.anchor(), + transaction, + &mut drive_operations, + platform_version, + )?; + if !validation_result.is_valid() { + charge_drive_operations( + platform.drive, + block_info, + execution_context, + drive_operations, + platform_version, + )?; + return Ok(validation_result); + } + + let validation_result = validate_token_pool_nullifiers( + platform.drive, + &token_id_bytes, + &self.nullifiers(), + transaction, + &mut drive_operations, + platform_version, + )?; + if !validation_result.is_valid() { + charge_drive_operations( + platform.drive, + block_info, + execution_context, + drive_operations, + platform_version, + )?; + return Ok(validation_result); + } + + let pool_balance = platform.drive.read_token_shielded_pool_total_balance( + &token_id_bytes, + transaction, + &mut drive_operations, + platform_version, + )?; + + charge_drive_operations( + platform.drive, + block_info, + execution_context, + drive_operations, + platform_version, + )?; + + if pool_balance < self.amount() { + return Ok(SimpleConsensusValidationResult::new_with_error( + StateError::InvalidShieldedProofError(InvalidShieldedProofError::new(format!( + "token shielded pool has insufficient balance: pool has {} but burn requires {}", + pool_balance, + self.amount() + ))) + .into(), + )); + } + + let extra_sighash_data = token_burn_from_pool_extra_sighash_data( + &token_id_bytes, + &owner_id.to_buffer(), + self.amount(), + platform_version, + )?; + + verify_token_pool_bundle( + validation_mode, + self.actions(), + FLAGS_SPENDS_AND_OUTPUTS, + self.amount() as i64, + self.anchor(), + self.proof(), + self.binding_signature(), + &extra_sighash_data, + ) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/mod.rs new file mode 100644 index 00000000000..52c338a3fea --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/mod.rs @@ -0,0 +1,64 @@ +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::token::token_claim_to_pool_transition_action::state_v0::TokenClaimToPoolTransitionActionStateValidationV0; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; +use dpp::identifier::Identifier; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::batch::batched_transition::token_transition::token_claim_to_pool_transition_action::TokenClaimToPoolTransitionAction; + +mod state_v0; + +pub trait TokenClaimToPoolTransitionActionValidation { + #[allow(clippy::too_many_arguments)] + fn validate_state( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenClaimToPoolTransitionActionValidation for TokenClaimToPoolTransitionAction { + fn validate_state( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .token_claim_to_pool_transition_state_validation + { + 0 => self.validate_state_v0( + platform, + owner_id, + block_info, + execution_context, + validation_mode, + transaction, + platform_version, + ), + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "TokenClaimToPoolTransitionAction::validate_state".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs new file mode 100644 index 00000000000..05e0020adad --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs @@ -0,0 +1,121 @@ +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::token::token_base_transition_action::TokenBaseTransitionActionValidation; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; +use dpp::prelude::Identifier; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::query::TransactionArg; +use crate::execution::types::execution_operation::ValidationOperation; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContextMethodsV0; +use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_pool_common::{ + validate_token_shielded_pool_enabled, verify_token_pool_bundle, +}; +use crate::execution::validation::state_transition::state_transitions::shielded_common::FLAGS_OUTPUTS_ONLY; +use dpp::consensus::state::state_error::StateError; +use dpp::consensus::state::token::TokenMintPastMaxSupplyError; +use dpp::consensus::ConsensusError; +use dpp::data_contract::accessors::v1::DataContractV1Getters; +use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use drive::error::drive::DriveError; +use drive::state_transition_action::batch::batched_transition::token_transition::token_claim_to_pool_transition_action::{ + TokenClaimToPoolTransitionAction, TokenClaimToPoolTransitionActionAccessorsV0, +}; + +pub(in crate::execution::validation::state_transition::state_transitions::batch::action_validation) trait TokenClaimToPoolTransitionActionStateValidationV0 { + #[allow(clippy::too_many_arguments)] + fn validate_state_v0( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenClaimToPoolTransitionActionStateValidationV0 for TokenClaimToPoolTransitionAction { + /// The claim itself (who may claim, how much, up to which moment) was resolved when the action + /// was built; here the max supply is checked and the outputs-only bundle must carry exactly + /// the resolved amount. + fn validate_state_v0( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let validation_result = self.base().validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let validation_result = validate_token_shielded_pool_enabled(self.base())?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let token_id = self.token_id(); + + let contract = &self.data_contract_fetch_info_ref().contract; + let token_configuration = contract.expected_token_configuration(self.token_position())?; + + if let Some(max_supply) = token_configuration.max_supply() { + let (token_total_supply, fee) = platform.drive.fetch_token_total_supply_with_cost( + token_id.to_buffer(), + block_info, + transaction, + platform_version, + )?; + execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee)); + let Some(token_total_supply) = token_total_supply else { + return Err(Error::Drive(drive::error::Error::Drive( + DriveError::CorruptedDriveState(format!( + "token {} total supply not found", + token_id + )), + ))); + }; + match token_total_supply.checked_add(self.amount()) { + Some(total_supply_after) if total_supply_after <= max_supply => {} + _ => { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::TokenMintPastMaxSupplyError( + TokenMintPastMaxSupplyError::new( + token_id, + self.amount(), + token_total_supply, + max_supply, + ), + )), + )); + } + } + } + + verify_token_pool_bundle( + validation_mode, + self.actions(), + FLAGS_OUTPUTS_ONLY, + -(self.amount() as i64), + self.anchor(), + self.proof(), + self.binding_signature(), + &[], + ) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/mod.rs new file mode 100644 index 00000000000..eab22ab03da --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/mod.rs @@ -0,0 +1,66 @@ +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::token::token_direct_purchase_to_pool_transition_action::state_v0::TokenDirectPurchaseToPoolTransitionActionStateValidationV0; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; +use dpp::identifier::Identifier; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_to_pool_transition_action::TokenDirectPurchaseToPoolTransitionAction; + +mod state_v0; + +pub trait TokenDirectPurchaseToPoolTransitionActionValidation { + #[allow(clippy::too_many_arguments)] + fn validate_state( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenDirectPurchaseToPoolTransitionActionValidation + for TokenDirectPurchaseToPoolTransitionAction +{ + fn validate_state( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .token_direct_purchase_to_pool_transition_state_validation + { + 0 => self.validate_state_v0( + platform, + owner_id, + block_info, + execution_context, + validation_mode, + transaction, + platform_version, + ), + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "TokenDirectPurchaseToPoolTransitionAction::validate_state".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs new file mode 100644 index 00000000000..3f1609cb676 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs @@ -0,0 +1,122 @@ +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::token::token_base_transition_action::TokenBaseTransitionActionValidation; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; +use dpp::prelude::Identifier; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::query::TransactionArg; +use crate::execution::types::execution_operation::ValidationOperation; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContextMethodsV0; +use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_pool_common::{ + validate_token_shielded_pool_enabled, verify_token_pool_bundle, +}; +use crate::execution::validation::state_transition::state_transitions::shielded_common::FLAGS_OUTPUTS_ONLY; +use dpp::consensus::state::state_error::StateError; +use dpp::consensus::state::token::TokenMintPastMaxSupplyError; +use dpp::consensus::ConsensusError; +use dpp::data_contract::accessors::v1::DataContractV1Getters; +use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use drive::error::drive::DriveError; +use drive::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_to_pool_transition_action::{ + TokenDirectPurchaseToPoolTransitionAction, TokenDirectPurchaseToPoolTransitionActionAccessorsV0, +}; + +pub(in crate::execution::validation::state_transition::state_transitions::batch::action_validation) trait TokenDirectPurchaseToPoolTransitionActionStateValidationV0 { + #[allow(clippy::too_many_arguments)] + fn validate_state_v0( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenDirectPurchaseToPoolTransitionActionStateValidationV0 + for TokenDirectPurchaseToPoolTransitionAction +{ + /// The price was checked when the action was built (as for a transparent purchase); here the + /// max supply is checked and the outputs-only bundle must carry exactly `token_count`. + fn validate_state_v0( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let validation_result = self.base().validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let validation_result = validate_token_shielded_pool_enabled(self.base())?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let token_id = self.token_id(); + + let contract = &self.data_contract_fetch_info_ref().contract; + let token_configuration = contract.expected_token_configuration(self.token_position())?; + + if let Some(max_supply) = token_configuration.max_supply() { + let (token_total_supply, fee) = platform.drive.fetch_token_total_supply_with_cost( + token_id.to_buffer(), + block_info, + transaction, + platform_version, + )?; + execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee)); + let Some(token_total_supply) = token_total_supply else { + return Err(Error::Drive(drive::error::Error::Drive( + DriveError::CorruptedDriveState(format!( + "token {} total supply not found", + token_id + )), + ))); + }; + match token_total_supply.checked_add(self.token_count()) { + Some(total_supply_after) if total_supply_after <= max_supply => {} + _ => { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::TokenMintPastMaxSupplyError( + TokenMintPastMaxSupplyError::new( + token_id, + self.token_count(), + token_total_supply, + max_supply, + ), + )), + )); + } + } + } + + verify_token_pool_bundle( + validation_mode, + self.actions(), + FLAGS_OUTPUTS_ONLY, + -(self.token_count() as i64), + self.anchor(), + self.proof(), + self.binding_signature(), + &[], + ) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/mod.rs new file mode 100644 index 00000000000..d92fe154115 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/mod.rs @@ -0,0 +1,64 @@ +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::token::token_mint_to_pool_transition_action::state_v0::TokenMintToPoolTransitionActionStateValidationV0; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; +use dpp::identifier::Identifier; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::batch::batched_transition::token_transition::token_mint_to_pool_transition_action::TokenMintToPoolTransitionAction; + +mod state_v0; + +pub trait TokenMintToPoolTransitionActionValidation { + #[allow(clippy::too_many_arguments)] + fn validate_state( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenMintToPoolTransitionActionValidation for TokenMintToPoolTransitionAction { + fn validate_state( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .batch_state_transition + .token_mint_to_pool_transition_state_validation + { + 0 => self.validate_state_v0( + platform, + owner_id, + block_info, + execution_context, + validation_mode, + transaction, + platform_version, + ), + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "TokenMintToPoolTransitionAction::validate_state".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs new file mode 100644 index 00000000000..9bf2a508ec2 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs @@ -0,0 +1,181 @@ +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::token::token_base_transition_action::TokenBaseTransitionActionValidation; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; +use dpp::prelude::Identifier; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::query::TransactionArg; +use crate::execution::types::execution_operation::ValidationOperation; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContextMethodsV0; +use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_pool_common::{ + validate_token_shielded_pool_enabled, verify_token_pool_bundle, +}; +use crate::execution::validation::state_transition::state_transitions::shielded_common::FLAGS_OUTPUTS_ONLY; +use dpp::consensus::state::group::ModificationOfGroupActionMainParametersNotPermittedError; +use dpp::consensus::state::state_error::StateError; +use dpp::consensus::state::token::TokenMintPastMaxSupplyError; +use dpp::consensus::ConsensusError; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::accessors::v1::DataContractV1Getters; +use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dpp::group::action_event::GroupActionEvent; +use dpp::group::group_action::GroupActionAccessors; +use dpp::shielded::serialized_actions_digest; +use dpp::tokens::token_event::TokenEvent; +use drive::error::drive::DriveError; +use drive::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionActionAccessorsV0; +use drive::state_transition_action::batch::batched_transition::token_transition::token_mint_to_pool_transition_action::{ + TokenMintToPoolTransitionAction, TokenMintToPoolTransitionActionAccessorsV0, +}; + +pub(in crate::execution::validation::state_transition::state_transitions::batch::action_validation) trait TokenMintToPoolTransitionActionStateValidationV0 { + #[allow(clippy::too_many_arguments)] + fn validate_state_v0( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl TokenMintToPoolTransitionActionStateValidationV0 for TokenMintToPoolTransitionAction { + /// Runs the transparent mint's authorization (group action included, with the actions digest + /// as a group parameter) and max supply checks, then verifies the outputs-only bundle whose + /// value balance is `-amount`. No identity balance is involved. + fn validate_state_v0( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let validation_result = self.base().validate_state( + platform, + owner_id, + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let validation_result = validate_token_shielded_pool_enabled(self.base())?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + let token_id = self.token_id(); + + let contract = &self.data_contract_fetch_info_ref().contract; + let token_configuration = contract.expected_token_configuration(self.token_position())?; + let rules = token_configuration.manual_minting_rules(); + let main_control_group = token_configuration.main_control_group(); + let validation_result = self.base().validate_group_action( + rules, + owner_id, + contract.owner_id(), + main_control_group, + contract.groups(), + "mintToPool".to_string(), + token_configuration, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + + // Every signer of a group action must commit to the same amount and the same notes. + if let Some(original_group_action) = self.base().original_group_action() { + let changed_internal_fields = match original_group_action.event() { + GroupActionEvent::TokenEvent(TokenEvent::MintToPool( + old_group_action_amount, + old_actions_digest, + _, + )) => { + let mut changed = vec![]; + if old_group_action_amount != &self.amount() { + changed.push("amount".to_string()); + } + if old_actions_digest.as_bytes() != &serialized_actions_digest(self.actions()) { + changed.push("actions".to_string()); + } + changed + } + _ => vec![], + }; + let same_event = matches!( + original_group_action.event(), + GroupActionEvent::TokenEvent(TokenEvent::MintToPool(..)) + ); + if !same_event || !changed_internal_fields.is_empty() { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::StateError( + StateError::ModificationOfGroupActionMainParametersNotPermittedError( + ModificationOfGroupActionMainParametersNotPermittedError::new( + original_group_action.event().event_name(), + "Token: mintToPool".to_string(), + changed_internal_fields, + ), + ), + ), + )); + } + } + + if let Some(max_supply) = token_configuration.max_supply() { + let (token_total_supply, fee) = platform.drive.fetch_token_total_supply_with_cost( + token_id.to_buffer(), + block_info, + transaction, + platform_version, + )?; + execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee)); + let Some(token_total_supply) = token_total_supply else { + return Err(Error::Drive(drive::error::Error::Drive( + DriveError::CorruptedDriveState(format!( + "token {} total supply not found", + token_id + )), + ))); + }; + match token_total_supply.checked_add(self.amount()) { + Some(total_supply_after) if total_supply_after <= max_supply => {} + _ => { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::TokenMintPastMaxSupplyError( + TokenMintPastMaxSupplyError::new( + token_id, + self.amount(), + token_total_supply, + max_supply, + ), + )), + )); + } + } + } + + verify_token_pool_bundle( + validation_mode, + self.actions(), + FLAGS_OUTPUTS_ONLY, + -(self.amount() as i64), + self.anchor(), + self.proof(), + self.binding_signature(), + &[], + ) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs index 3cffb8a884f..424c9ace9a4 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs @@ -127,7 +127,6 @@ impl TokenShieldTransitionActionStateValidationV0 for TokenShieldTransitionActio // signature over the batch already binds the bundle to this token, owner and amount, so // no extra sighash data is bound (exactly like `ShieldFromIdentity`). verify_token_pool_bundle( - execution_context, validation_mode, self.actions(), FLAGS_OUTPUTS_ONLY, @@ -136,7 +135,6 @@ impl TokenShieldTransitionActionStateValidationV0 for TokenShieldTransitionActio self.proof(), self.binding_signature(), &[], - platform_version, ) } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs index 07847cd9bec..6e343589982 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs @@ -27,9 +27,8 @@ use dpp::consensus::state::token::{ }; use dpp::consensus::ConsensusError; use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters; -use dpp::fee::fee_result::FeeResult; use dpp::prelude::Identifier; -use dpp::shielded::{compute_shielded_verification_fee, SerializedAction}; +use dpp::shielded::{SerializedAction}; use dpp::tokens::info::v0::IdentityTokenInfoV0Accessors; use dpp::tokens::status::v0::TokenStatusV0Accessors; use dpp::validation::SimpleConsensusValidationResult; @@ -254,7 +253,6 @@ pub(super) fn validate_minimum_token_pool_notes( /// once the fee estimate has shown the identity can pay for it. #[allow(clippy::too_many_arguments)] pub(super) fn verify_token_pool_bundle( - execution_context: &mut StateTransitionExecutionContext, validation_mode: ValidationMode, actions: &[SerializedAction], flags: u8, @@ -263,14 +261,9 @@ pub(super) fn verify_token_pool_bundle( proof: &[u8], binding_signature: &[u8; 64], extra_sighash_data: &[u8], - platform_version: &PlatformVersion, ) -> Result { - let verification_fee = compute_shielded_verification_fee(actions.len(), platform_version)?; - execution_context.add_operation(ValidationOperation::PrecalculatedOperation(FeeResult { - processing_fee: verification_fee, - ..Default::default() - })); - + // The verification fee was charged when the action was built (see the token pool action + // transformers), so CheckTx admission and block execution price it once and identically. if !matches!(validation_mode, ValidationMode::Validator) { return Ok(SimpleConsensusValidationResult::new()); } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_transfer_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_transfer_transition_action/state_v0/mod.rs index fe0e3fcb951..2a1c9370703 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_transfer_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_transfer_transition_action/state_v0/mod.rs @@ -129,7 +129,6 @@ impl TokenShieldedTransferTransitionActionStateValidationV0 )?; verify_token_pool_bundle( - execution_context, validation_mode, self.actions(), FLAGS_SPENDS_AND_OUTPUTS, @@ -138,7 +137,6 @@ impl TokenShieldedTransferTransitionActionStateValidationV0 self.proof(), self.binding_signature(), &extra_sighash_data, - platform_version, ) } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/state_v0/mod.rs index 3f2d13d7247..0fd25f5e117 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/state_v0/mod.rs @@ -215,7 +215,6 @@ impl TokenUnshieldTransitionActionStateValidationV0 for TokenUnshieldTransitionA )?; verify_token_pool_bundle( - execution_context, validation_mode, self.actions(), FLAGS_SPENDS_AND_OUTPUTS, @@ -224,7 +223,6 @@ impl TokenUnshieldTransitionActionStateValidationV0 for TokenUnshieldTransitionA self.proof(), self.binding_signature(), &extra_sighash_data, - platform_version, ) } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs index 27b61ee23d9..f888d46411c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs @@ -36,6 +36,10 @@ use crate::execution::validation::state_transition::batch::action_validation::to use crate::execution::validation::state_transition::batch::action_validation::token::token_shield_transition_action::TokenShieldTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_transfer_transition_action::TokenShieldedTransferTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::token::token_unshield_transition_action::TokenUnshieldTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_mint_to_pool_transition_action::TokenMintToPoolTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_burn_from_pool_transition_action::TokenBurnFromPoolTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_claim_to_pool_transition_action::TokenClaimToPoolTransitionActionValidation; +use crate::execution::validation::state_transition::batch::action_validation::token::token_direct_purchase_to_pool_transition_action::TokenDirectPurchaseToPoolTransitionActionValidation; use crate::execution::validation::state_transition::batch::data_triggers::{data_trigger_bindings_list, DataTriggerExecutionContext, DataTriggerExecutor}; use crate::execution::validation::state_transition::batch::state::v0::index_only_batch_entries::IndexOnlyBatchEntries; use drive::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::DocumentCreateTransitionActionAccessorsV0; @@ -305,6 +309,50 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { platform_version, )? } + TokenTransitionAction::MintToPoolAction(mint_to_pool_action) => { + mint_to_pool_action.validate_state( + platform, + owner_id, + block_info, + execution_context, + validation_mode, + transaction, + platform_version, + )? + } + TokenTransitionAction::BurnFromPoolAction(burn_from_pool_action) => { + burn_from_pool_action.validate_state( + platform, + owner_id, + block_info, + execution_context, + validation_mode, + transaction, + platform_version, + )? + } + TokenTransitionAction::ClaimToPoolAction(claim_to_pool_action) => { + claim_to_pool_action.validate_state( + platform, + owner_id, + block_info, + execution_context, + validation_mode, + transaction, + platform_version, + )? + } + TokenTransitionAction::DirectPurchaseToPoolAction( + direct_purchase_to_pool_action, + ) => direct_purchase_to_pool_action.validate_state( + platform, + owner_id, + block_info, + execution_context, + validation_mode, + transaction, + platform_version, + )?, }, BatchedTransitionAction::BumpIdentityDataContractNonce(_) => { return Err(Error::Execution(ExecutionError::CorruptedCodeExecution( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 8b91414ee34..27bd87bef59 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -38,21 +38,23 @@ mod token_shielded_pool_tests { use platform_version::version::PlatformVersion; /// The basic token fixture mints this much to the contract owner. - const OWNER_INITIAL_BALANCE: u64 = 100_000; - const SHIELD_AMOUNT: u64 = 10_000; + pub(super) const OWNER_INITIAL_BALANCE: u64 = 100_000; + pub(super) const SHIELD_AMOUNT: u64 = 10_000; - fn enable_shielded_pool(configuration: &mut TokenConfiguration) { + pub(super) fn enable_shielded_pool(configuration: &mut TokenConfiguration) { configuration.set_has_shielded_pool(true); } - fn spend_keys() -> (FullViewingKey, SpendAuthorizingKey, PaymentAddress) { + pub(super) fn spend_keys() -> (FullViewingKey, SpendAuthorizingKey, PaymentAddress) { let sk = SpendingKey::from_bytes([7u8; 32]).unwrap(); let fvk = FullViewingKey::from(&sk); let address = fvk.address_at(0u32, Scope::External); (fvk, SpendAuthorizingKey::from(&sk), address) } - fn bundle_params(bundle: &Bundle) -> (OrchardBundleParams, i64) { + pub(super) fn bundle_params( + bundle: &Bundle, + ) -> (OrchardBundleParams, i64) { let (actions, value_balance, anchor, proof, binding_signature) = serialize_authorized_bundle_i64(bundle); ( @@ -68,7 +70,7 @@ mod token_shielded_pool_tests { /// An outputs-only bundle paying `amount` to the test wallet: what a token shield carries. /// The bundle has no spends, so no extra sighash data is bound. - fn build_shield_bundle(amount: u64, seed: u64) -> OrchardBundleParams { + pub(super) fn build_shield_bundle(amount: u64, seed: u64) -> OrchardBundleParams { let mut rng = StdRng::seed_from_u64(seed); let (fvk, _, address) = spend_keys(); let mut builder = Builder::::new( @@ -101,7 +103,7 @@ mod token_shielded_pool_tests { /// A note the test wallet controls, alone in a client-side commitment tree. The returned /// anchor is the tree's root; a spend of the note is only accepted once that anchor is in /// the pool's anchors. - fn spendable_note(value: u64, tag: u8) -> (Note, Anchor, MerklePath) { + pub(super) fn spendable_note(value: u64, tag: u8) -> (Note, Anchor, MerklePath) { let (_, _, address) = spend_keys(); let mut rho_bytes = [0u8; 32]; rho_bytes[0] = tag; @@ -120,7 +122,7 @@ mod token_shielded_pool_tests { /// Spends `note` against `anchor`, sending everything but `leaving_pool` back to the test /// wallet as a change note, bound to `extra_sighash_data`. `leaving_pool` is the bundle's /// value balance: the unshielded amount, or zero for a shielded transfer. - fn build_spend_bundle( + pub(super) fn build_spend_bundle( note: Note, merkle_path: MerklePath, anchor: Anchor, @@ -152,7 +154,7 @@ mod token_shielded_pool_tests { } /// Structurally valid but unprovable: for tests that fail before proof verification. - fn dummy_bundle() -> OrchardBundleParams { + pub(super) fn dummy_bundle() -> OrchardBundleParams { OrchardBundleParams { actions: vec![create_dummy_serialized_action()], anchor: [42u8; 32], @@ -161,7 +163,7 @@ mod token_shielded_pool_tests { } } - fn insert_token_pool_anchor( + pub(super) fn insert_token_pool_anchor( platform: &TempPlatform, token_id: Identifier, anchor: &Anchor, @@ -190,7 +192,10 @@ mod token_shielded_pool_tests { .expect("should commit"); } - fn pool_balance(platform: &TempPlatform, token_id: Identifier) -> u64 { + pub(super) fn pool_balance( + platform: &TempPlatform, + token_id: Identifier, + ) -> u64 { platform .drive .read_token_shielded_pool_total_balance( @@ -202,7 +207,10 @@ mod token_shielded_pool_tests { .expect("pool balance") } - fn pool_notes_count(platform: &TempPlatform, token_id: Identifier) -> u64 { + pub(super) fn pool_notes_count( + platform: &TempPlatform, + token_id: Identifier, + ) -> u64 { platform .drive .token_shielded_pool_notes_count( @@ -214,7 +222,7 @@ mod token_shielded_pool_tests { .expect("pool notes count") } - fn nullifier_is_spent( + pub(super) fn nullifier_is_spent( platform: &TempPlatform, token_id: Identifier, nullifier: &[u8; 32], @@ -231,7 +239,7 @@ mod token_shielded_pool_tests { .expect("nullifier lookup") } - fn identity_token_balance( + pub(super) fn identity_token_balance( platform: &TempPlatform, token_id: Identifier, identity_id: Identifier, @@ -247,7 +255,7 @@ mod token_shielded_pool_tests { .expect("token balance") } - fn assert_tokens_conserved(platform: &TempPlatform) { + pub(super) fn assert_tokens_conserved(platform: &TempPlatform) { let total = platform .drive .calculate_total_tokens_balance(None, PlatformVersion::latest()) @@ -259,7 +267,7 @@ mod token_shielded_pool_tests { } /// Processes one transition in its own transaction and commits it. - fn process( + pub(super) fn process( platform: &TempPlatform, transition: &StateTransition, ) -> StateTransitionsProcessingResult { @@ -290,7 +298,7 @@ mod token_shielded_pool_tests { result } - fn platform_with_latest_version() -> TempPlatform { + pub(super) fn platform_with_latest_version() -> TempPlatform { TestPlatformBuilder::new() .with_latest_protocol_version() .build_with_mock_rpc() @@ -1084,3 +1092,638 @@ mod token_shielded_pool_tests { assert_eq!(&decoded, configuration); } } + +/// Mint, burn, claim and purchase straight into or out of the token shielded pool. +mod token_pool_mint_burn_claim_purchase_tests { + use super::token_shielded_pool_tests::{ + assert_tokens_conserved, build_shield_bundle, build_spend_bundle, dummy_bundle, + enable_shielded_pool, identity_token_balance, insert_token_pool_anchor, + platform_with_latest_version, pool_balance, pool_notes_count, process, spendable_note, + OWNER_INITIAL_BALANCE, + }; + use super::*; + use crate::rpc::core::MockCoreRPCLike; + use crate::test::helpers::fast_forward_to_block::fast_forward_to_block; + use crate::test::helpers::setup::TempPlatform; + use dpp::block::epoch::Epoch; + use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters; + use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; + use dpp::data_contract::associated_token::token_pre_programmed_distribution::v0::TokenPreProgrammedDistributionV0; + use dpp::data_contract::associated_token::token_pre_programmed_distribution::TokenPreProgrammedDistribution; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::shielded::token_burn_from_pool_extra_sighash_data_v0; + use dpp::state_transition::batch_transition::TokenSetPriceForDirectPurchaseTransition; + use dpp::tokens::token_pricing_schedule::TokenPricingSchedule; + use platform_version::version::PlatformVersion; + + fn total_supply(platform: &TempPlatform, token_id: Identifier) -> u64 { + platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, PlatformVersion::latest()) + .expect("total supply") + .expect("supply exists") + } + + #[tokio::test] + async fn test_token_mint_to_pool_by_owner() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9101); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + + let bundle = build_shield_bundle(1_337, 21); + let mint = BatchTransition::new_token_mint_to_pool_transition( + token_id, + identity.id(), + contract.id(), + 0, + 1_337, + bundle.clone(), + Some("private issuance".to_string()), + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token mint to pool transition"); + + let result = process(&platform, &mint); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert!(result + .token_shielded_pools_touched() + .contains(&token_id.to_buffer())); + assert_eq!(pool_balance(&platform, token_id), 1_337); + assert_eq!( + pool_notes_count(&platform, token_id), + bundle.actions.len() as u64 + ); + assert_eq!( + total_supply(&platform, token_id), + OWNER_INITIAL_BALANCE + 1_337 + ); + assert_eq!( + identity_token_balance(&platform, token_id, identity.id()), + Some(OWNER_INITIAL_BALANCE) + ); + assert_tokens_conserved(&platform); + } + + #[tokio::test] + async fn test_token_mint_to_pool_rejected_past_max_supply() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9102); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|configuration: &mut TokenConfiguration| { + configuration.set_has_shielded_pool(true); + configuration.set_max_supply(Some(OWNER_INITIAL_BALANCE + 100)); + }), + None, + None, + None, + platform_version, + ); + + let mint = BatchTransition::new_token_mint_to_pool_transition( + token_id, + identity.id(), + contract.id(), + 0, + 101, + dummy_bundle(), + None, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token mint to pool transition"); + + let result = process(&platform, &mint); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::TokenMintPastMaxSupplyError(_)), + .. + }] + ); + assert_eq!(pool_balance(&platform, token_id), 0); + assert_eq!(total_supply(&platform, token_id), OWNER_INITIAL_BALANCE); + } + + #[tokio::test] + async fn test_token_mint_to_pool_rejected_for_unauthorized_identity() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9103); + + let (owner, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (stranger, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + owner.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + + let mint = BatchTransition::new_token_mint_to_pool_transition( + token_id, + stranger.id(), + contract.id(), + 0, + 10, + dummy_bundle(), + None, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token mint to pool transition"); + + let result = process(&platform, &mint); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::UnauthorizedTokenActionError(_)), + .. + }] + ); + assert_eq!(pool_balance(&platform, token_id), 0); + } + + #[tokio::test] + async fn test_token_burn_from_pool_after_shield() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9104); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + let token = token_id.to_buffer(); + + // Fund the pool first. + let shield_bundle = build_shield_bundle(10_000, 22); + let shield = BatchTransition::new_token_shield_transition( + token_id, + identity.id(), + contract.id(), + 0, + 10_000, + shield_bundle.clone(), + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token shield transition"); + let result = process(&platform, &shield); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + // Burn 4_000 of a 6_000 note the wallet holds; 2_000 comes back as change. + let (note, anchor, merkle_path) = spendable_note(6_000, 3); + insert_token_pool_anchor(&platform, token_id, &anchor); + let burn_amount = 4_000; + let extra = token_burn_from_pool_extra_sighash_data_v0( + &token, + &identity.id().to_buffer(), + burn_amount, + ); + let (burn_bundle, value_balance) = + build_spend_bundle(note, merkle_path, anchor, burn_amount, &extra, 23); + assert_eq!(value_balance, burn_amount as i64); + + let burn = BatchTransition::new_token_burn_from_pool_transition( + token_id, + identity.id(), + contract.id(), + 0, + burn_amount, + burn_bundle.clone(), + None, + None, + &key, + 3, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token burn from pool transition"); + + let result = process(&platform, &burn); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(&platform, token_id), 10_000 - burn_amount); + assert_eq!( + total_supply(&platform, token_id), + OWNER_INITIAL_BALANCE - burn_amount + ); + assert_eq!( + pool_notes_count(&platform, token_id), + (shield_bundle.actions.len() + burn_bundle.actions.len()) as u64 + ); + assert_tokens_conserved(&platform); + + // The same notes cannot be burned twice. + let replay = BatchTransition::new_token_burn_from_pool_transition( + token_id, + identity.id(), + contract.id(), + 0, + burn_amount, + burn_bundle, + None, + None, + &key, + 4, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token burn from pool transition"); + let result = process(&platform, &replay); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::NullifierAlreadySpentError(_)), + .. + }] + ); + } + + #[tokio::test] + async fn test_token_claim_to_pool_pre_programmed() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9105); + + let (owner, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (claimant, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let claimant_id = claimant.id(); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + owner.id(), + Some(move |configuration: &mut TokenConfiguration| { + configuration.set_has_shielded_pool(true); + configuration + .distribution_rules_mut() + .set_pre_programmed_distribution(Some(TokenPreProgrammedDistribution::V0( + TokenPreProgrammedDistributionV0 { + distributions: [(100, [(claimant_id, 445)].into())].into(), + }, + ))); + }), + None, + None, + None, + platform_version, + ); + + fast_forward_to_block(&platform, 100, 40, 42, 1, false); + let platform_state = platform.state.load(); + let block_info = BlockInfo { + time_ms: 200, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }; + + // The bundle must carry exactly the release (445); a wrong amount is a paid failure. + let wrong = BatchTransition::new_token_claim_to_pool_transition( + token_id, + claimant_id, + contract.id(), + 0, + TokenDistributionType::PreProgrammed, + None, + build_shield_bundle(444, 24), + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token claim to pool transition"); + let transaction = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &[wrong.serialize_to_bytes().expect("serialize")], + &platform_state, + &block_info, + &transaction, + platform_version, + false, + None, + ) + .expect("process state transition"); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::InvalidShieldedProofError(_)), + .. + }] + ); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("commit"); + + let claim_bundle = build_shield_bundle(445, 25); + let claim = BatchTransition::new_token_claim_to_pool_transition( + token_id, + claimant_id, + contract.id(), + 0, + TokenDistributionType::PreProgrammed, + None, + claim_bundle.clone(), + None, + &key, + 3, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token claim to pool transition"); + let transaction = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &[claim.serialize_to_bytes().expect("serialize")], + &platform_state, + &block_info, + &transaction, + platform_version, + false, + None, + ) + .expect("process state transition"); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("commit"); + + assert_eq!(pool_balance(&platform, token_id), 445); + assert_eq!( + identity_token_balance(&platform, token_id, claimant_id), + None + ); + assert_eq!( + total_supply(&platform, token_id), + OWNER_INITIAL_BALANCE + 445 + ); + assert_eq!( + pool_notes_count(&platform, token_id), + claim_bundle.actions.len() as u64 + ); + assert_tokens_conserved(&platform); + + // The release is marked distributed: claiming it again finds nothing to claim. + let again = BatchTransition::new_token_claim_to_pool_transition( + token_id, + claimant_id, + contract.id(), + 0, + TokenDistributionType::PreProgrammed, + None, + build_shield_bundle(445, 26), + None, + &key, + 4, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token claim to pool transition"); + let transaction = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &[again.serialize_to_bytes().expect("serialize")], + &platform_state, + &block_info, + &transaction, + platform_version, + false, + None, + ) + .expect("process state transition"); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), + .. + }] + ); + } + + #[tokio::test] + async fn test_token_direct_purchase_to_pool() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9106); + + let (seller, seller_signer, seller_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (buyer, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(1.0)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + seller.id(), + Some(|configuration: &mut TokenConfiguration| { + configuration.set_has_shielded_pool(true); + configuration + .distribution_rules_mut() + .set_change_direct_purchase_pricing_rules(ChangeControlRules::V0( + ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::ContractOwner, + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }, + )); + }), + None, + None, + None, + platform_version, + ); + + let set_price = BatchTransition::new_token_change_direct_purchase_price_transition( + token_id, + seller.id(), + contract.id(), + 0, + Some(TokenPricingSchedule::SinglePrice(dash_to_credits!(0.01))), + None, + None, + &seller_key, + 2, + 0, + &seller_signer, + platform_version, + None, + ) + .await + .expect("set price transition"); + let result = process(&platform, &set_price); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + let seller_credits_before = platform + .drive + .fetch_identity_balance(seller.id().to_buffer(), None, platform_version) + .expect("balance") + .expect("seller exists"); + + let purchase_bundle = build_shield_bundle(3, 27); + let purchase = BatchTransition::new_token_direct_purchase_to_pool_transition( + token_id, + buyer.id(), + contract.id(), + 0, + 3, + dash_to_credits!(0.03), + purchase_bundle.clone(), + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token direct purchase to pool transition"); + let result = process(&platform, &purchase); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + assert_eq!(pool_balance(&platform, token_id), 3); + assert_eq!( + identity_token_balance(&platform, token_id, buyer.id()), + None + ); + assert_eq!(total_supply(&platform, token_id), OWNER_INITIAL_BALANCE + 3); + assert_eq!( + pool_notes_count(&platform, token_id), + purchase_bundle.actions.len() as u64 + ); + let seller_credits_after = platform + .drive + .fetch_identity_balance(seller.id().to_buffer(), None, platform_version) + .expect("balance") + .expect("seller exists"); + assert_eq!( + seller_credits_after, + seller_credits_before + dash_to_credits!(0.03) + ); + assert_tokens_conserved(&platform); + + // Underpaying is rejected before any note is created. + let cheap = BatchTransition::new_token_direct_purchase_to_pool_transition( + token_id, + buyer.id(), + contract.id(), + 0, + 3, + dash_to_credits!(0.02), + build_shield_bundle(3, 28), + &key, + 3, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token direct purchase to pool transition"); + let result = process(&platform, &cheap); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::TokenDirectPurchaseUserPriceTooLow( + _ + )), + .. + }] + ); + assert_eq!(pool_balance(&platform, token_id), 3); + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs index 8a1b56af705..cfd6d27fdfb 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs @@ -95,6 +95,10 @@ use drive::state_transition_action::batch::batched_transition::token_transition: use drive::state_transition_action::batch::batched_transition::token_transition::token_shield_transition_action::TokenShieldTransitionAction; use drive::state_transition_action::batch::batched_transition::token_transition::token_shielded_transfer_transition_action::TokenShieldedTransferTransitionAction; use drive::state_transition_action::batch::batched_transition::token_transition::token_unshield_transition_action::TokenUnshieldTransitionAction; +use drive::state_transition_action::batch::batched_transition::token_transition::token_mint_to_pool_transition_action::TokenMintToPoolTransitionAction; +use drive::state_transition_action::batch::batched_transition::token_transition::token_burn_from_pool_transition_action::TokenBurnFromPoolTransitionAction; +use drive::state_transition_action::batch::batched_transition::token_transition::token_claim_to_pool_transition_action::TokenClaimToPoolTransitionAction; +use drive::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_to_pool_transition_action::TokenDirectPurchaseToPoolTransitionAction; use drive::state_transition_action::batch::batched_transition::token_transition::token_set_price_for_direct_purchase_transition_action::TokenSetPriceForDirectPurchaseTransitionAction; use drive::state_transition_action::batch::batched_transition::token_transition::token_transfer_transition_action::TokenTransferTransitionAction; use drive::state_transition_action::batch::batched_transition::token_transition::token_unfreeze_transition_action::TokenUnfreezeTransitionAction; @@ -755,6 +759,46 @@ impl BatchTransitionInternalTransformerV0 for BatchTransition { Ok(batched_action) } + TokenTransition::MintToPool(token_mint_to_pool) => { + let (batched_action, fee_result) = TokenMintToPoolTransitionAction::try_from_borrowed_token_mint_to_pool_transition_with_contract_lookup(drive, owner_id, token_mint_to_pool, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + }, platform_version)?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action) + } + TokenTransition::BurnFromPool(token_burn_from_pool) => { + let (batched_action, fee_result) = TokenBurnFromPoolTransitionAction::try_from_borrowed_token_burn_from_pool_transition_with_contract_lookup(drive, owner_id, token_burn_from_pool, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + }, platform_version)?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action) + } + TokenTransition::ClaimToPool(token_claim_to_pool) => { + let (batched_action, fee_result) = TokenClaimToPoolTransitionAction::try_from_borrowed_token_claim_to_pool_transition_with_contract_lookup(drive, owner_id, token_claim_to_pool, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + }, platform_version)?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action) + } + TokenTransition::DirectPurchaseToPool(token_direct_purchase_to_pool) => { + let (batched_action, fee_result) = TokenDirectPurchaseToPoolTransitionAction::try_from_borrowed_token_direct_purchase_to_pool_transition_with_contract_lookup(drive, owner_id, token_direct_purchase_to_pool, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { + Ok(data_contract_fetch_info.clone()) + }, platform_version)?; + + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); + + Ok(batched_action) + } TokenTransition::Unshield(token_unshield) => { let (batched_action, fee_result) = TokenUnshieldTransitionAction::try_from_borrowed_token_unshield_transition_with_contract_lookup(drive, owner_id, token_unshield, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { Ok(data_contract_fetch_info.clone()) diff --git a/packages/rs-drive-abci/src/query/group_queries/group_actions/v0/mod.rs b/packages/rs-drive-abci/src/query/group_queries/group_actions/v0/mod.rs index 5835e315571..7fc8666edfb 100644 --- a/packages/rs-drive-abci/src/query/group_queries/group_actions/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/group_queries/group_actions/v0/mod.rs @@ -4,7 +4,7 @@ use crate::platform_types::platform::Platform; use crate::platform_types::platform_state::PlatformState; use crate::query::QueryValidationResult; use dapi_grpc::platform::v0::get_group_actions_request::GetGroupActionsRequestV0; -use dapi_grpc::platform::v0::get_group_actions_response::get_group_actions_response_v0::{emergency_action_event, group_action_event, token_event, BurnEvent, DestroyFrozenFundsEvent, EmergencyActionEvent, FreezeEvent, GroupActionEntry, GroupActionEvent, GroupActions, MintEvent, TokenConfigUpdateEvent, TokenEvent as TokenEventResponse, UnfreezeEvent, UpdateDirectPurchasePriceEvent}; +use dapi_grpc::platform::v0::get_group_actions_response::get_group_actions_response_v0::{emergency_action_event, group_action_event, token_event, BurnEvent, DestroyFrozenFundsEvent, EmergencyActionEvent, FreezeEvent, GroupActionEntry, GroupActionEvent, GroupActions, MintEvent, MintToPoolEvent, BurnFromPoolEvent, TokenConfigUpdateEvent, TokenEvent as TokenEventResponse, UnfreezeEvent, UpdateDirectPurchasePriceEvent}; use dapi_grpc::platform::v0::get_group_actions_response::{ get_group_actions_response_v0, GetGroupActionsResponseV0, }; @@ -224,12 +224,32 @@ impl Platform { })), }) } + TokenEvent::MintToPool(amount, actions_digest, public_note) => { + group_action_event::EventType::TokenEvent(TokenEventResponse { + r#type: Some(token_event::Type::MintToPool(MintToPoolEvent { + amount, + actions_digest: actions_digest.to_vec(), + public_note, + })), + }) + } + TokenEvent::BurnFromPool(amount, actions_digest, public_note) => { + group_action_event::EventType::TokenEvent(TokenEventResponse { + r#type: Some(token_event::Type::BurnFromPool(BurnFromPoolEvent { + amount, + actions_digest: actions_digest.to_vec(), + public_note, + })), + }) + } TokenEvent::Transfer(..) | TokenEvent::DirectPurchase(..) | TokenEvent::Claim(..) | TokenEvent::Shield(..) | TokenEvent::Unshield(..) - | TokenEvent::ShieldedTransfer => { + | TokenEvent::ShieldedTransfer + | TokenEvent::ClaimToPool(..) + | TokenEvent::DirectPurchaseToPool(..) => { return None; }, }, diff --git a/packages/rs-drive/src/drive/shielded/paths.rs b/packages/rs-drive/src/drive/shielded/paths.rs index 79219c8a931..2b09c7cf051 100644 --- a/packages/rs-drive/src/drive/shielded/paths.rs +++ b/packages/rs-drive/src/drive/shielded/paths.rs @@ -503,3 +503,15 @@ mod tests { assert_ne!(pool_path[2], by_height[2]); } } + +/// The proof query for a token pool's total balance: the `TOTAL_BALANCE` item of the pool. +pub fn token_shielded_pool_state_path_query(token_id: [u8; 32]) -> PathQuery { + PathQuery { + path: token_shielded_pool_path_vec(token_id), + query: SizedQuery { + query: Query::new_single_key(vec![SHIELDED_TOTAL_BALANCE_KEY]), + limit: Some(1), + offset: None, + }, + } +} diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/burn_from_pool/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/burn_from_pool/mod.rs new file mode 100644 index 00000000000..0ac60ac98c9 --- /dev/null +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/burn_from_pool/mod.rs @@ -0,0 +1,48 @@ +mod v0; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::state_transition_action::shielded::ShieldedActionNote; +use dpp::balances::credits::TokenAmount; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::{EstimatedLayerInformation, TransactionArg}; +use std::collections::HashMap; + +impl Drive { + /// Destroys `amount` of the token held in its shielded pool: the spent nullifiers are + /// recorded, the change notes appended, and the pool balance and total supply both shrink by + /// `amount`. No identity balance is touched. + #[allow(clippy::too_many_arguments)] + pub fn token_burn_from_pool_operations( + &self, + token_id: [u8; 32], + amount: TokenAmount, + nullifiers: &[[u8; 32]], + notes: &[ShieldedActionNote], + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match platform_version.drive.methods.token.update.burn_from_pool { + 0 => self.token_burn_from_pool_operations_v0( + token_id, + amount, + nullifiers, + notes, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "token_burn_from_pool_operations".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/burn_from_pool/v0/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/burn_from_pool/v0/mod.rs new file mode 100644 index 00000000000..5ac852eb1a3 --- /dev/null +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/burn_from_pool/v0/mod.rs @@ -0,0 +1,48 @@ +use crate::drive::tokens::shielded_pool::TokenPoolBalanceChange; +use crate::drive::Drive; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::state_transition_action::shielded::ShieldedActionNote; +use dpp::balances::credits::TokenAmount; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::{EstimatedLayerInformation, TransactionArg}; +use std::collections::HashMap; + +impl Drive { + /// Version 0: record the nullifiers, append the change notes, debit the pool balance, then + /// lower the total supply by the same amount. + #[allow(clippy::too_many_arguments)] + pub(super) fn token_burn_from_pool_operations_v0( + &self, + token_id: [u8; 32], + amount: TokenAmount, + nullifiers: &[[u8; 32]], + notes: &[ShieldedActionNote], + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let mut drive_operations = self.token_shielded_pool_update_operations( + token_id, + TokenPoolBalanceChange::Remove(amount), + nullifiers, + notes, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?; + + drive_operations.extend(self.remove_from_token_total_supply_operations( + token_id, + amount, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?); + + Ok(drive_operations) + } +} diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/mint_to_pool/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/mint_to_pool/mod.rs new file mode 100644 index 00000000000..275fe452a45 --- /dev/null +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/mint_to_pool/mod.rs @@ -0,0 +1,48 @@ +mod v0; + +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::state_transition_action::shielded::ShieldedActionNote; +use dpp::balances::credits::TokenAmount; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::{EstimatedLayerInformation, TransactionArg}; +use std::collections::HashMap; + +impl Drive { + /// Mints `amount` of the token straight into its shielded pool: the total supply and the pool + /// balance both grow by `amount` and the bundle's output notes are appended. No identity + /// balance is touched. + #[allow(clippy::too_many_arguments)] + pub fn token_mint_to_pool_operations( + &self, + token_id: [u8; 32], + amount: TokenAmount, + allow_first_mint: bool, + notes: &[ShieldedActionNote], + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match platform_version.drive.methods.token.update.mint_to_pool { + 0 => self.token_mint_to_pool_operations_v0( + token_id, + amount, + allow_first_mint, + notes, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ), + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "token_mint_to_pool_operations".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/mint_to_pool/v0/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/mint_to_pool/v0/mod.rs new file mode 100644 index 00000000000..f49eb9af6a9 --- /dev/null +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/mint_to_pool/v0/mod.rs @@ -0,0 +1,51 @@ +use crate::drive::tokens::shielded_pool::TokenPoolBalanceChange; +use crate::drive::Drive; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::state_transition_action::shielded::ShieldedActionNote; +use dpp::balances::credits::TokenAmount; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::{EstimatedLayerInformation, TransactionArg}; +use std::collections::HashMap; + +impl Drive { + /// Version 0: raise the total supply, then append the notes and credit the pool balance. + /// Conservation holds because the supply and the pool grow by the same amount. + #[allow(clippy::too_many_arguments)] + pub(super) fn token_mint_to_pool_operations_v0( + &self, + token_id: [u8; 32], + amount: TokenAmount, + allow_first_mint: bool, + notes: &[ShieldedActionNote], + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let (mut drive_operations, _new_supply) = self.add_to_token_total_supply_operations( + token_id, + amount, + allow_first_mint, + false, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?; + + // An outputs-only bundle spends nothing: its dummy nullifiers are never recorded. + drive_operations.extend(self.token_shielded_pool_update_operations( + token_id, + TokenPoolBalanceChange::Add(amount), + &[], + notes, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?); + + Ok(drive_operations) + } +} diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs index a74d4c4cb9b..722e79a2a46 100644 --- a/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs @@ -12,18 +12,22 @@ //! module holds what is token-specific: pool creation, cost estimation, and the three composite //! operations (`shield`, `unshield`, `shielded_transfer`) the batch token transitions lower to. +mod burn_from_pool; mod create_pool_trees; mod estimated_costs; mod insert_root_tree; +mod mint_to_pool; mod shield; mod shielded_transfer; mod unshield; +use crate::drive::tokens::paths::token_shielded_pools_root_path; use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; use crate::state_transition_action::shielded::ShieldedActionNote; +use crate::util::grove_operations::DirectQueryType; use dpp::balances::credits::TokenAmount; use dpp::version::PlatformVersion; use grovedb::batch::KeyInfoPath; @@ -42,6 +46,23 @@ pub(in crate::drive::tokens) enum TokenPoolBalanceChange { } impl Drive { + /// Whether the token owns a shielded pool subtree, without touching it. + pub fn has_token_shielded_pool( + &self, + token_id: [u8; 32], + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + self.grove_has_raw( + (&token_shielded_pools_root_path()).into(), + &token_id, + DirectQueryType::StatefulDirectQuery, + transaction, + &mut vec![], + &platform_version.drive, + ) + } + /// The operations every token pool write shares: nullifier insertion, note appends and the /// total balance update, with the pool's cost estimation registered when estimating. /// @@ -192,6 +213,19 @@ mod tests { platform_version, ) .expect("create token trees"); + // Keep the supply consistent with the balance handed to the identity, as a mint would. + drive + .add_to_token_total_supply( + TOKEN_ID, + balance, + true, + false, + true, + &block_info, + None, + platform_version, + ) + .expect("add to total supply"); drive .add_to_identity_token_balance( TOKEN_ID, @@ -477,6 +511,121 @@ mod tests { assert!(nullifier_is_spent(&drive, &nullifiers[1])); } + #[test] + fn should_mint_into_the_pool_raising_the_supply() { + let (drive, identity_id) = setup(1_000); + let platform_version = PlatformVersion::latest(); + + let operations = drive + .token_mint_to_pool_operations( + TOKEN_ID, + 250, + false, + &[note(1), note(2)], + &mut None, + None, + platform_version, + ) + .expect("mint to pool operations"); + apply(&drive, operations); + + assert_eq!(pool_balance(&drive), 250); + assert_eq!(notes_count(&drive), 2); + assert_eq!( + drive + .fetch_token_total_supply(TOKEN_ID, None, platform_version) + .expect("supply"), + Some(1_250) + ); + // The minter's own balance is not involved. + assert_eq!(token_balance(&drive, identity_id), Some(1_000)); + assert!(drive + .has_token_shielded_pool(TOKEN_ID, None, platform_version) + .expect("pool lookup")); + assert!(!drive + .has_token_shielded_pool([9u8; 32], None, platform_version) + .expect("pool lookup")); + } + + #[test] + fn should_burn_from_the_pool_lowering_the_supply() { + let (drive, identity_id) = setup(1_000); + let platform_version = PlatformVersion::latest(); + + let operations = drive + .token_shield_operations( + TOKEN_ID, + identity_id, + 400, + &[note(1), note(2)], + &mut None, + None, + platform_version, + ) + .expect("shield operations"); + apply(&drive, operations); + + let nullifiers = [[31u8; 32], [32u8; 32]]; + let operations = drive + .token_burn_from_pool_operations( + TOKEN_ID, + 150, + &nullifiers, + &[note(7)], + &mut None, + None, + platform_version, + ) + .expect("burn from pool operations"); + apply(&drive, operations); + + assert_eq!(pool_balance(&drive), 250); + assert_eq!(notes_count(&drive), 3); + assert_eq!( + drive + .fetch_token_total_supply(TOKEN_ID, None, platform_version) + .expect("supply"), + Some(850) + ); + assert_eq!(token_balance(&drive, identity_id), Some(600)); + assert!(nullifier_is_spent(&drive, &nullifiers[0])); + assert!(nullifier_is_spent(&drive, &nullifiers[1])); + } + + #[test] + fn should_reject_burning_more_than_the_pool_holds() { + let (drive, identity_id) = setup(1_000); + let platform_version = PlatformVersion::latest(); + + let operations = drive + .token_shield_operations( + TOKEN_ID, + identity_id, + 100, + &[note(1)], + &mut None, + None, + platform_version, + ) + .expect("shield operations"); + apply(&drive, operations); + + let result = drive.token_burn_from_pool_operations( + TOKEN_ID, + 101, + &[[41u8; 32]], + &[note(2)], + &mut None, + None, + platform_version, + ); + assert!(matches!( + result, + Err(Error::Drive(DriveError::CorruptedDriveState(_))) + )); + assert_eq!(pool_balance(&drive), 100); + } + #[test] fn should_estimate_without_touching_state() { let (drive, identity_id) = setup(1_000); diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/mod.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/mod.rs index 9ebd49a08d0..dd5b6122a24 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/mod.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/mod.rs @@ -1,10 +1,14 @@ +mod token_burn_from_pool_transition; mod token_burn_transition; +mod token_claim_to_pool_transition; mod token_claim_transition; mod token_config_update_transition; mod token_destroy_frozen_funds_transition; +mod token_direct_purchase_to_pool_transition; mod token_direct_purchase_transition; mod token_emergency_action_transition; mod token_freeze_transition; +mod token_mint_to_pool_transition; mod token_mint_transition; mod token_set_price_for_direct_purchase_transition; mod token_shield_transition; diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_burn_from_pool_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_burn_from_pool_transition.rs new file mode 100644 index 00000000000..978caa7f282 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_burn_from_pool_transition.rs @@ -0,0 +1,103 @@ +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::state_transition_action::action_convert_to_operations::batch::DriveHighLevelBatchOperationConverter; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::token_transition::token_burn_from_pool_transition_action::{TokenBurnFromPoolTransitionAction, TokenBurnFromPoolTransitionActionAccessorsV0}; +use crate::util::batch::drive_op_batch::TokenOperationType; +use crate::util::batch::DriveOperation::{IdentityOperation, TokenOperation}; +use crate::util::batch::{DriveOperation, IdentityOperationType}; +use crate::util::batch::drive_op_batch::GroupOperationType; +use crate::util::batch::DriveOperation::GroupOperation; +use dpp::group::action_event::GroupActionEvent; +use dpp::group::group_action::v0::GroupActionV0; +use dpp::group::group_action::GroupAction; +use dpp::group::GroupStateTransitionResolvedInfo; +use dpp::shielded::serialized_actions_digest; +use dpp::tokens::token_event::TokenEvent; +use dpp::block::epoch::Epoch; +use dpp::identifier::Identifier; +use platform_version::version::PlatformVersion; + +impl DriveHighLevelBatchOperationConverter for TokenBurnFromPoolTransitionAction { + fn into_high_level_batch_drive_operations<'b>( + self, + _epoch: &Epoch, + owner_id: Identifier, + platform_version: &PlatformVersion, + ) -> Result>, Error> { + match platform_version + .drive + .methods + .state_transitions + .convert_to_high_level_operations + .token_burn_from_pool_transition + { + 0 => { + let data_contract_id = self.base().data_contract_id(); + let identity_contract_nonce = self.base().identity_contract_nonce(); + let token_id = self.token_id(); + + let mut ops = vec![IdentityOperation( + IdentityOperationType::UpdateIdentityContractNonce { + identity_id: owner_id.into_buffer(), + contract_id: data_contract_id.into_buffer(), + nonce: identity_contract_nonce, + }, + )]; + + if let Some(GroupStateTransitionResolvedInfo { + group_contract_position, + action_id, + action_is_proposer, + signer_power, + .. + }) = self.base().store_in_group() + { + let event = TokenEvent::BurnFromPool( + self.amount(), + serialized_actions_digest(self.actions()).into(), + self.public_note().cloned(), + ); + + let initialize_with_insert_action_info = if *action_is_proposer { + Some(GroupAction::V0(GroupActionV0 { + contract_id: self.base().data_contract_id(), + proposer_id: owner_id, + token_contract_position: self.base().token_position(), + event: GroupActionEvent::TokenEvent(event), + })) + } else { + None + }; + + ops.push(GroupOperation(GroupOperationType::AddGroupAction { + contract_id: data_contract_id, + group_contract_position: *group_contract_position, + initialize_with_insert_action_info, + action_id: *action_id, + signer_identity_id: owner_id, + signer_power: *signer_power, + closes_group_action: self.base().perform_action(), + })); + } + + if self.base().perform_action() { + ops.push(TokenOperation(TokenOperationType::TokenBurnFromPool { + token_id, + amount: self.amount(), + nullifiers: self.nullifiers(), + notes: self.notes(), + })); + } + + Ok(ops) + } + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "TokenBurnFromPoolTransitionAction::into_high_level_batch_drive_operations" + .to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_claim_to_pool_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_claim_to_pool_transition.rs new file mode 100644 index 00000000000..7b0acc2cf70 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_claim_to_pool_transition.rs @@ -0,0 +1,79 @@ +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::state_transition_action::action_convert_to_operations::batch::DriveHighLevelBatchOperationConverter; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::token_transition::token_claim_to_pool_transition_action::{TokenClaimToPoolTransitionAction, TokenClaimToPoolTransitionActionAccessorsV0}; +use crate::util::batch::drive_op_batch::TokenOperationType; +use crate::util::batch::DriveOperation::{IdentityOperation, TokenOperation}; +use crate::util::batch::{DriveOperation, IdentityOperationType}; +use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionInfo; +use dpp::block::epoch::Epoch; +use dpp::identifier::Identifier; +use platform_version::version::PlatformVersion; + +impl DriveHighLevelBatchOperationConverter for TokenClaimToPoolTransitionAction { + fn into_high_level_batch_drive_operations<'b>( + self, + _epoch: &Epoch, + owner_id: Identifier, + platform_version: &PlatformVersion, + ) -> Result>, Error> { + match platform_version + .drive + .methods + .state_transitions + .convert_to_high_level_operations + .token_claim_to_pool_transition + { + 0 => { + let data_contract_id = self.base().data_contract_id(); + let identity_contract_nonce = self.base().identity_contract_nonce(); + let token_id = self.token_id(); + + let mut ops = vec![IdentityOperation( + IdentityOperationType::UpdateIdentityContractNonce { + identity_id: owner_id.into_buffer(), + contract_id: data_contract_id.into_buffer(), + nonce: identity_contract_nonce, + }, + )]; + + ops.push(TokenOperation(TokenOperationType::TokenMintToPool { + token_id, + amount: self.amount(), + allow_first_mint: false, + notes: self.notes(), + })); + + match self.distribution_info() { + TokenDistributionInfo::PreProgrammed(release_time, recipient) => { + ops.push(TokenOperation( + TokenOperationType::TokenMarkPreProgrammedReleaseAsDistributed { + token_id, + recipient_id: *recipient, + release_time: *release_time, + }, + )); + } + TokenDistributionInfo::Perpetual(claim_moment, _) => { + ops.push(TokenOperation( + TokenOperationType::TokenMarkPerpetualReleaseAsDistributed { + token_id, + recipient_id: owner_id, + cycle_start_moment: *claim_moment, + }, + )); + } + } + + Ok(ops) + } + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "TokenClaimToPoolTransitionAction::into_high_level_batch_drive_operations" + .to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_direct_purchase_to_pool_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_direct_purchase_to_pool_transition.rs new file mode 100644 index 00000000000..ec54b9c25e4 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_direct_purchase_to_pool_transition.rs @@ -0,0 +1,73 @@ +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::state_transition_action::action_convert_to_operations::batch::DriveHighLevelBatchOperationConverter; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_to_pool_transition_action::{TokenDirectPurchaseToPoolTransitionAction, TokenDirectPurchaseToPoolTransitionActionAccessorsV0}; +use crate::util::batch::drive_op_batch::TokenOperationType; +use crate::util::batch::DriveOperation::{IdentityOperation, TokenOperation}; +use crate::util::batch::{DriveOperation, IdentityOperationType}; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::block::epoch::Epoch; +use dpp::identifier::Identifier; +use platform_version::version::PlatformVersion; + +impl DriveHighLevelBatchOperationConverter for TokenDirectPurchaseToPoolTransitionAction { + fn into_high_level_batch_drive_operations<'b>( + self, + _epoch: &Epoch, + owner_id: Identifier, + platform_version: &PlatformVersion, + ) -> Result>, Error> { + match platform_version + .drive + .methods + .state_transitions + .convert_to_high_level_operations + .token_direct_purchase_to_pool_transition + { + 0 => { + let data_contract_id = self.base().data_contract_id(); + let identity_contract_nonce = self.base().identity_contract_nonce(); + let token_id = self.token_id(); + + let mut ops = vec![IdentityOperation( + IdentityOperationType::UpdateIdentityContractNonce { + identity_id: owner_id.into_buffer(), + contract_id: data_contract_id.into_buffer(), + nonce: identity_contract_nonce, + }, + )]; + + ops.push(TokenOperation(TokenOperationType::TokenMintToPool { + token_id, + amount: self.token_count(), + allow_first_mint: true, + notes: self.notes(), + })); + + let contract_owner_id = self.base().data_contract_fetch_info().contract.owner_id(); + if owner_id != contract_owner_id { + ops.push(IdentityOperation( + IdentityOperationType::RemoveFromIdentityBalance { + identity_id: owner_id.to_buffer(), + balance_to_remove: self.total_agreed_price(), + }, + )); + ops.push(IdentityOperation( + IdentityOperationType::AddToIdentityBalance { + identity_id: contract_owner_id.to_buffer(), + added_balance: self.total_agreed_price(), + }, + )); + } + + Ok(ops) + } + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "TokenDirectPurchaseToPoolTransitionAction::into_high_level_batch_drive_operations".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_mint_to_pool_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_mint_to_pool_transition.rs new file mode 100644 index 00000000000..ed3288d9b56 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_mint_to_pool_transition.rs @@ -0,0 +1,103 @@ +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::state_transition_action::action_convert_to_operations::batch::DriveHighLevelBatchOperationConverter; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::token_transition::token_mint_to_pool_transition_action::{TokenMintToPoolTransitionAction, TokenMintToPoolTransitionActionAccessorsV0}; +use crate::util::batch::drive_op_batch::TokenOperationType; +use crate::util::batch::DriveOperation::{IdentityOperation, TokenOperation}; +use crate::util::batch::{DriveOperation, IdentityOperationType}; +use crate::util::batch::drive_op_batch::GroupOperationType; +use crate::util::batch::DriveOperation::GroupOperation; +use dpp::group::action_event::GroupActionEvent; +use dpp::group::group_action::v0::GroupActionV0; +use dpp::group::group_action::GroupAction; +use dpp::group::GroupStateTransitionResolvedInfo; +use dpp::shielded::serialized_actions_digest; +use dpp::tokens::token_event::TokenEvent; +use dpp::block::epoch::Epoch; +use dpp::identifier::Identifier; +use platform_version::version::PlatformVersion; + +impl DriveHighLevelBatchOperationConverter for TokenMintToPoolTransitionAction { + fn into_high_level_batch_drive_operations<'b>( + self, + _epoch: &Epoch, + owner_id: Identifier, + platform_version: &PlatformVersion, + ) -> Result>, Error> { + match platform_version + .drive + .methods + .state_transitions + .convert_to_high_level_operations + .token_mint_to_pool_transition + { + 0 => { + let data_contract_id = self.base().data_contract_id(); + let identity_contract_nonce = self.base().identity_contract_nonce(); + let token_id = self.token_id(); + + let mut ops = vec![IdentityOperation( + IdentityOperationType::UpdateIdentityContractNonce { + identity_id: owner_id.into_buffer(), + contract_id: data_contract_id.into_buffer(), + nonce: identity_contract_nonce, + }, + )]; + + if let Some(GroupStateTransitionResolvedInfo { + group_contract_position, + action_id, + action_is_proposer, + signer_power, + .. + }) = self.base().store_in_group() + { + let event = TokenEvent::MintToPool( + self.amount(), + serialized_actions_digest(self.actions()).into(), + self.public_note().cloned(), + ); + + let initialize_with_insert_action_info = if *action_is_proposer { + Some(GroupAction::V0(GroupActionV0 { + contract_id: self.base().data_contract_id(), + proposer_id: owner_id, + token_contract_position: self.base().token_position(), + event: GroupActionEvent::TokenEvent(event), + })) + } else { + None + }; + + ops.push(GroupOperation(GroupOperationType::AddGroupAction { + contract_id: data_contract_id, + group_contract_position: *group_contract_position, + initialize_with_insert_action_info, + action_id: *action_id, + signer_identity_id: owner_id, + signer_power: *signer_power, + closes_group_action: self.base().perform_action(), + })); + } + + if self.base().perform_action() { + ops.push(TokenOperation(TokenOperationType::TokenMintToPool { + token_id, + amount: self.amount(), + allow_first_mint: false, + notes: self.notes(), + })); + } + + Ok(ops) + } + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "TokenMintToPoolTransitionAction::into_high_level_batch_drive_operations" + .to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_transition.rs index 9bbc300bdae..c63d0d0326b 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_transition.rs @@ -19,6 +19,11 @@ use crate::state_transition_action::batch::batched_transition::token_transition: use crate::state_transition_action::batch::batched_transition::token_transition::token_unfreeze_transition_action::TokenUnfreezeTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::token_transition::token_shield_transition_action::TokenShieldTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::token_transition::token_unshield_transition_action::TokenUnshieldTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::token_transition::token_mint_to_pool_transition_action::TokenMintToPoolTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::token_transition::token_burn_from_pool_transition_action::TokenBurnFromPoolTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::token_transition::token_claim_to_pool_transition_action::TokenClaimToPoolTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_to_pool_transition_action::TokenDirectPurchaseToPoolTransitionActionAccessorsV0; +use dpp::shielded::serialized_actions_digest; impl DriveHighLevelBatchOperationConverter for TokenTransitionAction { fn into_high_level_batch_drive_operations<'b>( @@ -74,6 +79,19 @@ impl DriveHighLevelBatchOperationConverter for TokenTransitionAction { } TokenTransitionAction::ShieldedTransferAction(shielded_transfer) => shielded_transfer .into_high_level_batch_drive_operations(epoch, owner_id, platform_version), + TokenTransitionAction::MintToPoolAction(mint_to_pool) => mint_to_pool + .into_high_level_batch_drive_operations(epoch, owner_id, platform_version), + TokenTransitionAction::BurnFromPoolAction(burn_from_pool) => burn_from_pool + .into_high_level_batch_drive_operations(epoch, owner_id, platform_version), + TokenTransitionAction::ClaimToPoolAction(claim_to_pool) => claim_to_pool + .into_high_level_batch_drive_operations(epoch, owner_id, platform_version), + TokenTransitionAction::DirectPurchaseToPoolAction(direct_purchase_to_pool) => { + direct_purchase_to_pool.into_high_level_batch_drive_operations( + epoch, + owner_id, + platform_version, + ) + } } } } @@ -152,6 +170,22 @@ impl TokenTransitionAction { TokenEvent::Unshield(unshield_action.recipient_id(), unshield_action.amount()) } TokenTransitionAction::ShieldedTransferAction(_) => TokenEvent::ShieldedTransfer, + TokenTransitionAction::MintToPoolAction(action) => TokenEvent::MintToPool( + action.amount(), + serialized_actions_digest(action.actions()).into(), + action.public_note().cloned(), + ), + TokenTransitionAction::BurnFromPoolAction(action) => TokenEvent::BurnFromPool( + action.amount(), + serialized_actions_digest(action.actions()).into(), + action.public_note().cloned(), + ), + TokenTransitionAction::ClaimToPoolAction(action) => { + TokenEvent::ClaimToPool(action.amount()) + } + TokenTransitionAction::DirectPurchaseToPoolAction(action) => { + TokenEvent::DirectPurchaseToPool(action.token_count(), action.total_agreed_price()) + } } } } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/mod.rs index 7b0ce0466d2..a2dcf1f2e81 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/mod.rs @@ -38,6 +38,18 @@ pub mod token_shielded_transfer_transition_action; /// token_unshield_transition_action pub mod token_unshield_transition_action; +/// Mint straight into the token shielded pool +pub mod token_mint_to_pool_transition_action; + +/// Burn of notes held in the token shielded pool +pub mod token_burn_from_pool_transition_action; + +/// Distribution claim paid into the token shielded pool +pub mod token_claim_to_pool_transition_action; + +/// Direct purchase paid into the token shielded pool +pub mod token_direct_purchase_to_pool_transition_action; + use derive_more::From; use dpp::block::block_info::BlockInfo; use dpp::data_contract::accessors::v0::DataContractV0Getters; @@ -68,6 +80,10 @@ use crate::state_transition_action::batch::batched_transition::token_transition: use crate::state_transition_action::batch::batched_transition::token_transition::token_shield_transition_action::{TokenShieldTransitionAction, TokenShieldTransitionActionAccessorsV0}; use crate::state_transition_action::batch::batched_transition::token_transition::token_shielded_transfer_transition_action::{TokenShieldedTransferTransitionAction, TokenShieldedTransferTransitionActionAccessorsV0}; use crate::state_transition_action::batch::batched_transition::token_transition::token_unshield_transition_action::{TokenUnshieldTransitionAction, TokenUnshieldTransitionActionAccessorsV0}; +use crate::state_transition_action::batch::batched_transition::token_transition::token_mint_to_pool_transition_action::{TokenMintToPoolTransitionAction, TokenMintToPoolTransitionActionAccessorsV0}; +use crate::state_transition_action::batch::batched_transition::token_transition::token_burn_from_pool_transition_action::{TokenBurnFromPoolTransitionAction, TokenBurnFromPoolTransitionActionAccessorsV0}; +use crate::state_transition_action::batch::batched_transition::token_transition::token_claim_to_pool_transition_action::{TokenClaimToPoolTransitionAction, TokenClaimToPoolTransitionActionAccessorsV0}; +use crate::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_to_pool_transition_action::{TokenDirectPurchaseToPoolTransitionAction, TokenDirectPurchaseToPoolTransitionActionAccessorsV0}; /// token action #[derive(Debug, Clone, From)] @@ -100,6 +116,14 @@ pub enum TokenTransitionAction { UnshieldAction(TokenUnshieldTransitionAction), /// transfer inside the token shielded pool ShieldedTransferAction(TokenShieldedTransferTransitionAction), + /// mint straight into the token shielded pool + MintToPoolAction(TokenMintToPoolTransitionAction), + /// burn notes held in the token shielded pool + BurnFromPoolAction(TokenBurnFromPoolTransitionAction), + /// distribution claim paid into the token shielded pool + ClaimToPoolAction(TokenClaimToPoolTransitionAction), + /// direct purchase paid into the token shielded pool + DirectPurchaseToPoolAction(TokenDirectPurchaseToPoolTransitionAction), } impl TokenTransitionAction { @@ -120,6 +144,10 @@ impl TokenTransitionAction { TokenTransitionAction::ShieldAction(action) => action.base(), TokenTransitionAction::UnshieldAction(action) => action.base(), TokenTransitionAction::ShieldedTransferAction(action) => action.base(), + TokenTransitionAction::MintToPoolAction(action) => action.base(), + TokenTransitionAction::BurnFromPoolAction(action) => action.base(), + TokenTransitionAction::ClaimToPoolAction(action) => action.base(), + TokenTransitionAction::DirectPurchaseToPoolAction(action) => action.base(), } } @@ -140,6 +168,10 @@ impl TokenTransitionAction { TokenTransitionAction::ShieldAction(action) => action.base_owned(), TokenTransitionAction::UnshieldAction(action) => action.base_owned(), TokenTransitionAction::ShieldedTransferAction(action) => action.base_owned(), + TokenTransitionAction::MintToPoolAction(action) => action.base_owned(), + TokenTransitionAction::BurnFromPoolAction(action) => action.base_owned(), + TokenTransitionAction::ClaimToPoolAction(action) => action.base_owned(), + TokenTransitionAction::DirectPurchaseToPoolAction(action) => action.base_owned(), } } @@ -160,6 +192,10 @@ impl TokenTransitionAction { TokenTransitionAction::ShieldAction(_) => "shield", TokenTransitionAction::UnshieldAction(_) => "unshield", TokenTransitionAction::ShieldedTransferAction(_) => "shieldedTransfer", + TokenTransitionAction::MintToPoolAction(_) => "mintToPool", + TokenTransitionAction::BurnFromPoolAction(_) => "burnFromPool", + TokenTransitionAction::ClaimToPoolAction(_) => "claimToPool", + TokenTransitionAction::DirectPurchaseToPoolAction(_) => "directPurchaseToPool", } } @@ -227,7 +263,11 @@ impl TokenTransitionAction { // no document types for them and a shielded transfer has nothing public to record. TokenTransitionAction::ShieldAction(_) | TokenTransitionAction::UnshieldAction(_) - | TokenTransitionAction::ShieldedTransferAction(_) => Ok(false), + | TokenTransitionAction::ShieldedTransferAction(_) + | TokenTransitionAction::MintToPoolAction(_) + | TokenTransitionAction::BurnFromPoolAction(_) + | TokenTransitionAction::ClaimToPoolAction(_) + | TokenTransitionAction::DirectPurchaseToPoolAction(_) => Ok(false), } } } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_from_pool_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_from_pool_transition_action/mod.rs new file mode 100644 index 00000000000..445e7e57eba --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_from_pool_transition_action/mod.rs @@ -0,0 +1,72 @@ +use derive_more::From; +use dpp::balances::credits::TokenAmount; +use dpp::shielded::SerializedAction; + +/// Builds the action from its transition +pub mod transformer; +mod v0; + +pub use v0::*; + +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; + +/// Versioned `TokenBurnFromPool` action. +#[derive(Debug, Clone, From)] +pub enum TokenBurnFromPoolTransitionAction { + /// Version 0 + V0(TokenBurnFromPoolTransitionActionV0), +} + +impl TokenBurnFromPoolTransitionActionAccessorsV0 for TokenBurnFromPoolTransitionAction { + fn base(&self) -> &TokenBaseTransitionAction { + match self { + Self::V0(v0) => &v0.base, + } + } + + fn base_owned(self) -> TokenBaseTransitionAction { + match self { + Self::V0(v0) => v0.base, + } + } + + fn amount(&self) -> TokenAmount { + match self { + Self::V0(v0) => v0.amount(), + } + } + fn public_note(&self) -> Option<&String> { + match self { + Self::V0(v0) => v0.public_note(), + } + } + fn public_note_owned(self) -> Option { + match self { + Self::V0(v0) => v0.public_note_owned(), + } + } + + fn actions(&self) -> &[SerializedAction] { + match self { + Self::V0(v0) => v0.actions(), + } + } + + fn anchor(&self) -> &[u8; 32] { + match self { + Self::V0(v0) => v0.anchor(), + } + } + + fn proof(&self) -> &[u8] { + match self { + Self::V0(v0) => v0.proof(), + } + } + + fn binding_signature(&self) -> &[u8; 64] { + match self { + Self::V0(v0) => v0.binding_signature(), + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_from_pool_transition_action/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_from_pool_transition_action/transformer.rs new file mode 100644 index 00000000000..d3eb55d8430 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_from_pool_transition_action/transformer.rs @@ -0,0 +1,53 @@ +use crate::drive::contract::DataContractFetchInfo; +use crate::drive::Drive; +use crate::error::Error; +use crate::state_transition_action::batch::batched_transition::token_transition::token_burn_from_pool_transition_action::{TokenBurnFromPoolTransitionAction, TokenBurnFromPoolTransitionActionV0}; +use crate::state_transition_action::batch::BatchedTransitionAction; +use dpp::block::block_info::BlockInfo; +use dpp::fee::fee_result::FeeResult; +use dpp::platform_value::Identifier; +use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; +use dpp::state_transition::batch_transition::token_burn_from_pool_transition::TokenBurnFromPoolTransition; +use dpp::ProtocolError; +use grovedb::TransactionArg; +use platform_version::version::PlatformVersion; +use std::sync::Arc; + +impl TokenBurnFromPoolTransitionAction { + /// Resolves the transition against state (contract lookup, base checks and, for a claim, + /// the amount) into an action, or into a nonce bump carrying the consensus errors. + #[allow(clippy::too_many_arguments)] + pub fn try_from_borrowed_token_burn_from_pool_transition_with_contract_lookup( + drive: &Drive, + owner_id: Identifier, + value: &TokenBurnFromPoolTransition, + approximate_without_state_for_costs: bool, + transaction: TransactionArg, + block_info: &BlockInfo, + user_fee_increase: UserFeeIncrease, + get_data_contract: impl Fn(Identifier) -> Result, ProtocolError>, + platform_version: &PlatformVersion, + ) -> Result< + ( + ConsensusValidationResult, + FeeResult, + ), + Error, + > { + match value { + TokenBurnFromPoolTransition::V0(v0) => { + TokenBurnFromPoolTransitionActionV0::try_from_borrowed_token_burn_from_pool_transition_with_contract_lookup( + drive, + owner_id, + v0, + approximate_without_state_for_costs, + transaction, + block_info, + user_fee_increase, + get_data_contract, + platform_version, + ) + } + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_from_pool_transition_action/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_from_pool_transition_action/v0/mod.rs new file mode 100644 index 00000000000..148abb5b4d4 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_from_pool_transition_action/v0/mod.rs @@ -0,0 +1,142 @@ +mod transformer; + +use std::sync::Arc; + +use dpp::balances::credits::TokenAmount; +use dpp::identifier::Identifier; +use dpp::prelude::IdentityNonce; +use dpp::shielded::SerializedAction; +use crate::drive::contract::DataContractFetchInfo; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{ + TokenBaseTransitionAction, TokenBaseTransitionActionAccessorsV0, +}; +use crate::state_transition_action::shielded::ShieldedActionNote; + +/// The resolved `TokenBurnFromPool` batched transition. +#[derive(Debug, Clone)] +pub struct TokenBurnFromPoolTransitionActionV0 { + /// Base token transition action + pub base: TokenBaseTransitionAction, + /// The amount of tokens + pub amount: TokenAmount, + /// Optional public note + pub public_note: Option, + /// The Orchard actions + pub actions: Vec, + /// The Orchard anchor + pub anchor: [u8; 32], + /// The Halo 2 proof + pub proof: Vec, + /// The RedPallas binding signature + pub binding_signature: [u8; 64], +} + +/// Accessors for the version 0 action. +pub trait TokenBurnFromPoolTransitionActionAccessorsV0 { + /// The base token transition action + fn base(&self) -> &TokenBaseTransitionAction; + + /// Consumes the action and returns its base + fn base_owned(self) -> TokenBaseTransitionAction; + + /// The amount of tokens + fn amount(&self) -> TokenAmount; + /// The public note, if any + fn public_note(&self) -> Option<&String>; + /// Takes the public note + fn public_note_owned(self) -> Option; + + /// The Orchard actions + fn actions(&self) -> &[SerializedAction]; + + /// The Orchard anchor + fn anchor(&self) -> &[u8; 32]; + + /// The Halo 2 proof + fn proof(&self) -> &[u8]; + + /// The RedPallas binding signature + fn binding_signature(&self) -> &[u8; 64]; + + /// The notes every action creates + fn notes(&self) -> Vec { + self.actions() + .iter() + .map(ShieldedActionNote::from) + .collect() + } + + /// The nullifiers every action spends + fn nullifiers(&self) -> Vec<[u8; 32]> { + self.actions() + .iter() + .map(|action| action.nullifier) + .collect() + } + + /// The token position in the contract + fn token_position(&self) -> u16 { + self.base().token_position() + } + + /// The token id + fn token_id(&self) -> Identifier { + self.base().token_id() + } + + /// The data contract id + fn data_contract_id(&self) -> Identifier { + self.base().data_contract_id() + } + + /// A reference to the fetched data contract + fn data_contract_fetch_info_ref(&self) -> &Arc { + self.base().data_contract_fetch_info_ref() + } + + /// The fetched data contract + fn data_contract_fetch_info(&self) -> Arc { + self.base().data_contract_fetch_info() + } + + /// The identity contract nonce + fn identity_contract_nonce(&self) -> IdentityNonce { + self.base().identity_contract_nonce() + } +} + +impl TokenBurnFromPoolTransitionActionAccessorsV0 for TokenBurnFromPoolTransitionActionV0 { + fn base(&self) -> &TokenBaseTransitionAction { + &self.base + } + + fn base_owned(self) -> TokenBaseTransitionAction { + self.base + } + + fn amount(&self) -> TokenAmount { + self.amount + } + fn public_note(&self) -> Option<&String> { + self.public_note.as_ref() + } + fn public_note_owned(self) -> Option { + self.public_note + } + + fn actions(&self) -> &[SerializedAction] { + &self.actions + } + + fn anchor(&self) -> &[u8; 32] { + &self.anchor + } + + fn proof(&self) -> &[u8] { + &self.proof + } + + fn binding_signature(&self) -> &[u8; 64] { + &self.binding_signature + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_from_pool_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_from_pool_transition_action/v0/transformer.rs new file mode 100644 index 00000000000..c59a11c04a7 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_from_pool_transition_action/v0/transformer.rs @@ -0,0 +1,120 @@ +use crate::drive::contract::DataContractFetchInfo; +use crate::drive::Drive; +use crate::error::Error; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; +use crate::state_transition_action::batch::batched_transition::token_transition::token_burn_from_pool_transition_action::{TokenBurnFromPoolTransitionAction, TokenBurnFromPoolTransitionActionV0}; +use crate::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; +use crate::state_transition_action::batch::BatchedTransitionAction; +use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; +use dpp::block::block_info::BlockInfo; +use dpp::fee::fee_result::FeeResult; +use dpp::shielded::compute_shielded_verification_fee; +use dpp::identifier::Identifier; +use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; +use dpp::state_transition::batch_transition::token_burn_from_pool_transition::v0::TokenBurnFromPoolTransitionV0; +use dpp::ProtocolError; +use grovedb::TransactionArg; +use platform_version::version::PlatformVersion; +use std::sync::Arc; + +impl TokenBurnFromPoolTransitionActionV0 { + /// Resolves the version 0 transition against state into an action, or into a nonce bump + /// carrying the consensus errors. + #[allow(clippy::too_many_arguments)] + pub fn try_from_borrowed_token_burn_from_pool_transition_with_contract_lookup( + drive: &Drive, + owner_id: Identifier, + value: &TokenBurnFromPoolTransitionV0, + approximate_without_state_for_costs: bool, + transaction: TransactionArg, + block_info: &BlockInfo, + user_fee_increase: UserFeeIncrease, + get_data_contract: impl Fn(Identifier) -> Result, ProtocolError>, + platform_version: &PlatformVersion, + ) -> Result< + ( + ConsensusValidationResult, + FeeResult, + ), + Error, + > { + let TokenBurnFromPoolTransitionV0 { + base, + amount, + actions, + anchor, + proof, + binding_signature, + public_note, + } = value; + + let mut drive_operations = vec![]; + + let base_action_validation_result = + TokenBaseTransitionAction::try_from_borrowed_base_transition_with_contract_lookup( + drive, + owner_id, + base, + approximate_without_state_for_costs, + transaction, + &mut drive_operations, + get_data_contract, + platform_version, + )?; + + let mut fee_result = Drive::calculate_fee( + None, + Some(drive_operations), + &block_info.epoch, + drive.config.epochs_per_era, + platform_version, + None, + )?; + + // The Halo 2 verification and per-action work GroveDB cannot meter, charged here so + // CheckTx admission and block execution price the bundle identically, whether or not + // the proof is (re)verified on this path. + fee_result.checked_add_assign(FeeResult { + processing_fee: compute_shielded_verification_fee(actions.len(), platform_version)?, + ..Default::default() + })?; + + let (base_action, _change_note) = match base_action_validation_result.is_valid() { + true => base_action_validation_result.into_data()?, + false => { + let bump_action = + BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( + base, + owner_id, + user_fee_increase, + ); + let batched_action = + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); + + return Ok(( + ConsensusValidationResult::new_with_data_and_errors( + batched_action, + base_action_validation_result.errors, + ), + fee_result, + )); + } + }; + + Ok(( + BatchedTransitionAction::TokenAction(TokenTransitionAction::from( + TokenBurnFromPoolTransitionAction::V0(TokenBurnFromPoolTransitionActionV0 { + base: base_action, + amount: *amount, + public_note: public_note.clone(), + actions: actions.clone(), + anchor: *anchor, + proof: proof.clone(), + binding_signature: *binding_signature, + }), + )) + .into(), + fee_result, + )) + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/mod.rs new file mode 100644 index 00000000000..5de32e61f52 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/mod.rs @@ -0,0 +1,78 @@ +use derive_more::From; +use dpp::balances::credits::TokenAmount; +use dpp::shielded::SerializedAction; + +/// Builds the action from its transition +pub mod transformer; +mod v0; + +pub use v0::*; + +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; +use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionInfo; + +/// Versioned `TokenClaimToPool` action. +#[derive(Debug, Clone, From)] +pub enum TokenClaimToPoolTransitionAction { + /// Version 0 + V0(TokenClaimToPoolTransitionActionV0), +} + +impl TokenClaimToPoolTransitionActionAccessorsV0 for TokenClaimToPoolTransitionAction { + fn base(&self) -> &TokenBaseTransitionAction { + match self { + Self::V0(v0) => &v0.base, + } + } + + fn base_owned(self) -> TokenBaseTransitionAction { + match self { + Self::V0(v0) => v0.base, + } + } + + fn amount(&self) -> TokenAmount { + match self { + Self::V0(v0) => v0.amount(), + } + } + fn public_note(&self) -> Option<&String> { + match self { + Self::V0(v0) => v0.public_note(), + } + } + fn public_note_owned(self) -> Option { + match self { + Self::V0(v0) => v0.public_note_owned(), + } + } + fn distribution_info(&self) -> &TokenDistributionInfo { + match self { + Self::V0(v0) => v0.distribution_info(), + } + } + + fn actions(&self) -> &[SerializedAction] { + match self { + Self::V0(v0) => v0.actions(), + } + } + + fn anchor(&self) -> &[u8; 32] { + match self { + Self::V0(v0) => v0.anchor(), + } + } + + fn proof(&self) -> &[u8] { + match self { + Self::V0(v0) => v0.proof(), + } + } + + fn binding_signature(&self) -> &[u8; 64] { + match self { + Self::V0(v0) => v0.binding_signature(), + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/transformer.rs new file mode 100644 index 00000000000..2cf6aee6c6f --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/transformer.rs @@ -0,0 +1,53 @@ +use crate::drive::contract::DataContractFetchInfo; +use crate::drive::Drive; +use crate::error::Error; +use crate::state_transition_action::batch::batched_transition::token_transition::token_claim_to_pool_transition_action::{TokenClaimToPoolTransitionAction, TokenClaimToPoolTransitionActionV0}; +use crate::state_transition_action::batch::BatchedTransitionAction; +use dpp::block::block_info::BlockInfo; +use dpp::fee::fee_result::FeeResult; +use dpp::platform_value::Identifier; +use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; +use dpp::state_transition::batch_transition::token_claim_to_pool_transition::TokenClaimToPoolTransition; +use dpp::ProtocolError; +use grovedb::TransactionArg; +use platform_version::version::PlatformVersion; +use std::sync::Arc; + +impl TokenClaimToPoolTransitionAction { + /// Resolves the transition against state (contract lookup, base checks and, for a claim, + /// the amount) into an action, or into a nonce bump carrying the consensus errors. + #[allow(clippy::too_many_arguments)] + pub fn try_from_borrowed_token_claim_to_pool_transition_with_contract_lookup( + drive: &Drive, + owner_id: Identifier, + value: &TokenClaimToPoolTransition, + approximate_without_state_for_costs: bool, + transaction: TransactionArg, + block_info: &BlockInfo, + user_fee_increase: UserFeeIncrease, + get_data_contract: impl Fn(Identifier) -> Result, ProtocolError>, + platform_version: &PlatformVersion, + ) -> Result< + ( + ConsensusValidationResult, + FeeResult, + ), + Error, + > { + match value { + TokenClaimToPoolTransition::V0(v0) => { + TokenClaimToPoolTransitionActionV0::try_from_borrowed_token_claim_to_pool_transition_with_contract_lookup( + drive, + owner_id, + v0, + approximate_without_state_for_costs, + transaction, + block_info, + user_fee_increase, + get_data_contract, + platform_version, + ) + } + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/v0/mod.rs new file mode 100644 index 00000000000..e009c60671b --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/v0/mod.rs @@ -0,0 +1,150 @@ +mod transformer; + +use std::sync::Arc; + +use dpp::balances::credits::TokenAmount; +use dpp::identifier::Identifier; +use dpp::prelude::IdentityNonce; +use dpp::shielded::SerializedAction; +use crate::drive::contract::DataContractFetchInfo; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{ + TokenBaseTransitionAction, TokenBaseTransitionActionAccessorsV0, +}; +use crate::state_transition_action::shielded::ShieldedActionNote; +use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionInfo; + +/// The resolved `TokenClaimToPool` batched transition. +#[derive(Debug, Clone)] +pub struct TokenClaimToPoolTransitionActionV0 { + /// Base token transition action + pub base: TokenBaseTransitionAction, + /// The amount of tokens + pub amount: TokenAmount, + /// Optional public note + pub public_note: Option, + /// Which distribution was claimed and up to which moment + pub distribution_info: TokenDistributionInfo, + /// The Orchard actions + pub actions: Vec, + /// The Orchard anchor + pub anchor: [u8; 32], + /// The Halo 2 proof + pub proof: Vec, + /// The RedPallas binding signature + pub binding_signature: [u8; 64], +} + +/// Accessors for the version 0 action. +pub trait TokenClaimToPoolTransitionActionAccessorsV0 { + /// The base token transition action + fn base(&self) -> &TokenBaseTransitionAction; + + /// Consumes the action and returns its base + fn base_owned(self) -> TokenBaseTransitionAction; + + /// The amount of tokens + fn amount(&self) -> TokenAmount; + /// The public note, if any + fn public_note(&self) -> Option<&String>; + /// Takes the public note + fn public_note_owned(self) -> Option; + /// Which distribution was claimed + fn distribution_info(&self) -> &TokenDistributionInfo; + + /// The Orchard actions + fn actions(&self) -> &[SerializedAction]; + + /// The Orchard anchor + fn anchor(&self) -> &[u8; 32]; + + /// The Halo 2 proof + fn proof(&self) -> &[u8]; + + /// The RedPallas binding signature + fn binding_signature(&self) -> &[u8; 64]; + + /// The notes every action creates + fn notes(&self) -> Vec { + self.actions() + .iter() + .map(ShieldedActionNote::from) + .collect() + } + + /// The nullifiers every action spends + fn nullifiers(&self) -> Vec<[u8; 32]> { + self.actions() + .iter() + .map(|action| action.nullifier) + .collect() + } + + /// The token position in the contract + fn token_position(&self) -> u16 { + self.base().token_position() + } + + /// The token id + fn token_id(&self) -> Identifier { + self.base().token_id() + } + + /// The data contract id + fn data_contract_id(&self) -> Identifier { + self.base().data_contract_id() + } + + /// A reference to the fetched data contract + fn data_contract_fetch_info_ref(&self) -> &Arc { + self.base().data_contract_fetch_info_ref() + } + + /// The fetched data contract + fn data_contract_fetch_info(&self) -> Arc { + self.base().data_contract_fetch_info() + } + + /// The identity contract nonce + fn identity_contract_nonce(&self) -> IdentityNonce { + self.base().identity_contract_nonce() + } +} + +impl TokenClaimToPoolTransitionActionAccessorsV0 for TokenClaimToPoolTransitionActionV0 { + fn base(&self) -> &TokenBaseTransitionAction { + &self.base + } + + fn base_owned(self) -> TokenBaseTransitionAction { + self.base + } + + fn amount(&self) -> TokenAmount { + self.amount + } + fn public_note(&self) -> Option<&String> { + self.public_note.as_ref() + } + fn public_note_owned(self) -> Option { + self.public_note + } + fn distribution_info(&self) -> &TokenDistributionInfo { + &self.distribution_info + } + + fn actions(&self) -> &[SerializedAction] { + &self.actions + } + + fn anchor(&self) -> &[u8; 32] { + &self.anchor + } + + fn proof(&self) -> &[u8] { + &self.proof + } + + fn binding_signature(&self) -> &[u8; 64] { + &self.binding_signature + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/v0/transformer.rs new file mode 100644 index 00000000000..9e49c291745 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/v0/transformer.rs @@ -0,0 +1,158 @@ +use crate::drive::contract::DataContractFetchInfo; +use crate::drive::Drive; +use crate::error::Error; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; +use crate::state_transition_action::batch::batched_transition::token_transition::token_claim_to_pool_transition_action::{TokenClaimToPoolTransitionAction, TokenClaimToPoolTransitionActionV0}; +use crate::state_transition_action::batch::batched_transition::token_transition::token_claim_transition_action::resolve_token_claim; +use crate::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; +use crate::state_transition_action::batch::BatchedTransitionAction; +use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; +use dpp::block::block_info::BlockInfo; +use dpp::fee::fee_result::FeeResult; +use dpp::shielded::compute_shielded_verification_fee; +use dpp::identifier::Identifier; +use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; +use dpp::state_transition::batch_transition::token_claim_to_pool_transition::v0::TokenClaimToPoolTransitionV0; +use dpp::ProtocolError; +use grovedb::TransactionArg; +use platform_version::version::PlatformVersion; +use std::sync::Arc; + +impl TokenClaimToPoolTransitionActionV0 { + /// Resolves the claim exactly like `TokenClaim` (shared helper), with the client's + /// `claim_up_to` moment bounding a perpetual claim, so the amount the bundle must prove is + /// known before the proof is verified. + /// Resolves the version 0 transition against state into an action, or into a nonce bump + /// carrying the consensus errors. + #[allow(clippy::too_many_arguments)] + pub fn try_from_borrowed_token_claim_to_pool_transition_with_contract_lookup( + drive: &Drive, + owner_id: Identifier, + value: &TokenClaimToPoolTransitionV0, + approximate_without_state_for_costs: bool, + transaction: TransactionArg, + block_info: &BlockInfo, + user_fee_increase: UserFeeIncrease, + get_data_contract: impl Fn(Identifier) -> Result, ProtocolError>, + platform_version: &PlatformVersion, + ) -> Result< + ( + ConsensusValidationResult, + FeeResult, + ), + Error, + > { + let TokenClaimToPoolTransitionV0 { + base, + distribution_type, + claim_up_to, + actions, + anchor, + proof, + binding_signature, + public_note, + } = value; + + let mut drive_operations = vec![]; + + let base_action_validation_result = + TokenBaseTransitionAction::try_from_borrowed_base_transition_with_contract_lookup( + drive, + owner_id, + base, + approximate_without_state_for_costs, + transaction, + &mut drive_operations, + get_data_contract, + platform_version, + )?; + + let mut fee_result = Drive::calculate_fee( + None, + Some(drive_operations), + &block_info.epoch, + drive.config.epochs_per_era, + platform_version, + None, + )?; + + // The Halo 2 verification and per-action work GroveDB cannot meter, charged here so + // CheckTx admission and block execution price the bundle identically, whether or not + // the proof is (re)verified on this path. + fee_result.checked_add_assign(FeeResult { + processing_fee: compute_shielded_verification_fee(actions.len(), platform_version)?, + ..Default::default() + })?; + + let (base_action, _change_note) = match base_action_validation_result.is_valid() { + true => base_action_validation_result.into_data()?, + false => { + let bump_action = + BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( + base, + owner_id, + user_fee_increase, + ); + let batched_action = + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); + + return Ok(( + ConsensusValidationResult::new_with_data_and_errors( + batched_action, + base_action_validation_result.errors, + ), + fee_result, + )); + } + }; + + let (amount, distribution_info) = match resolve_token_claim( + drive, + owner_id, + base, + &base_action, + *distribution_type, + *claim_up_to, + block_info, + transaction, + &mut fee_result, + platform_version, + )? { + Ok(resolved) => resolved, + Err(consensus_error) => { + let bump_action = + BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( + base, + owner_id, + user_fee_increase, + ); + let batched_action = + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); + return Ok(( + ConsensusValidationResult::new_with_data_and_errors( + batched_action, + vec![consensus_error], + ), + fee_result, + )); + } + }; + + Ok(( + BatchedTransitionAction::TokenAction(TokenTransitionAction::from( + TokenClaimToPoolTransitionAction::V0(TokenClaimToPoolTransitionActionV0 { + base: base_action, + amount, + public_note: public_note.clone(), + distribution_info, + actions: actions.clone(), + anchor: *anchor, + proof: proof.clone(), + binding_signature: *binding_signature, + }), + )) + .into(), + fee_result, + )) + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/mod.rs index 00cae9ac3f4..261cb27b6f6 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/mod.rs @@ -7,6 +7,7 @@ use dpp::data_contract::associated_token::token_perpetual_distribution::distribu pub mod transformer; mod v0; +pub(crate) use v0::resolve_token_claim; pub use v0::*; // re-export the v0 module items (including TokenIssuanceTransitionActionV0) use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/mod.rs index eacd31671a0..8d735276776 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/mod.rs @@ -1,4 +1,5 @@ mod transformer; +pub(crate) use transformer::resolve_token_claim; use std::sync::Arc; use dpp::balances::credits::TokenAmount; diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs index ebe280d5673..1e0960e4cc5 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs @@ -26,10 +26,10 @@ use dpp::state_transition::batch_transition::token_claim_transition::v0::TokenCl use dpp::ProtocolError; use crate::drive::contract::DataContractFetchInfo; use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{TokenBaseTransitionAction, TokenBaseTransitionActionAccessorsV0}; +use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; use crate::state_transition_action::batch::batched_transition::token_transition::token_claim_transition_action::v0::TokenClaimTransitionActionV0; use dpp::fee::fee_result::FeeResult; use dpp::prelude::{ConsensusValidationResult, TimestampMillis, UserFeeIncrease}; -use dpp::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; use dpp::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; use platform_version::version::PlatformVersion; use crate::drive::Drive; @@ -191,253 +191,300 @@ impl TokenClaimTransitionActionV0 { } }; - let token_config = base_action.token_configuration()?; - - let (amount, distribution_info) = match distribution_type { - TokenDistributionType::PreProgrammed => { - let Some(pre_programmed_distribution) = token_config - .distribution_rules() - .pre_programmed_distribution() - else { - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( - base, - owner_id, - user_fee_increase, - ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); - - return Ok(( - ConsensusValidationResult::new_with_data_and_errors( - batched_action, - vec![ConsensusError::StateError( - StateError::InvalidTokenClaimPropertyMismatch( - InvalidTokenClaimPropertyMismatch::new( - "pre programmed distribution", - base.token_id(), - ), - ), - )], + let (amount, distribution_info) = match resolve_token_claim( + drive, + owner_id, + base, + &base_action, + *distribution_type, + None, + block_info, + transaction, + &mut fee_result, + platform_version, + )? { + Ok(resolved) => resolved, + Err(consensus_error) => { + let bump_action = + BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( + base, + owner_id, + user_fee_increase, + ); + let batched_action = + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); + return Ok(( + ConsensusValidationResult::new_with_data_and_errors( + batched_action, + vec![consensus_error], + ), + fee_result, + )); + } + }; + + Ok(( + BatchedTransitionAction::TokenAction(TokenTransitionAction::ClaimAction( + TokenClaimTransitionActionV0 { + base: base_action, + amount, + distribution_info, + public_note: public_note.clone(), + } + .into(), + )) + .into(), + fee_result, + )) + } +} + +/// Resolves what a claim of `distribution_type` by `owner_id` pays out right now: the amount and +/// the distribution info to mark as distributed, or the consensus error that rejects the claim. +/// Shared by `TokenClaim` (`claim_up_to = None`, the current interval) and `TokenClaimToPool`, +/// whose bundle must prove the exact amount, so the client names the cycle-aligned moment the +/// perpetual claim pays out to. Read costs are added to `fee_result`. +#[allow(clippy::too_many_arguments)] +pub(crate) fn resolve_token_claim( + drive: &Drive, + owner_id: Identifier, + base: &TokenBaseTransition, + base_action: &TokenBaseTransitionAction, + distribution_type: TokenDistributionType, + claim_up_to: Option, + block_info: &BlockInfo, + transaction: TransactionArg, + fee_result: &mut FeeResult, + platform_version: &PlatformVersion, +) -> Result, Error> { + let token_config = base_action.token_configuration()?; + + let (amount, distribution_info) = match distribution_type { + TokenDistributionType::PreProgrammed => { + let Some(pre_programmed_distribution) = token_config + .distribution_rules() + .pre_programmed_distribution() + else { + return Ok(Err(ConsensusError::StateError( + StateError::InvalidTokenClaimPropertyMismatch( + InvalidTokenClaimPropertyMismatch::new( + "pre programmed distribution", + base.token_id(), ), - fee_result, - )); - }; + ), + ))); + }; - // We need to find the oldest pre-programmed distribution that wasn't yet claimed - // for this identity + // We need to find the oldest pre-programmed distribution that wasn't yet claimed + // for this identity - let times = pre_programmed_distribution.distributions(); + let times = pre_programmed_distribution.distributions(); - let mut last_paid_time_operations = vec![]; + let mut last_paid_time_operations = vec![]; - let last_paid_moment = drive - .fetch_pre_programmed_distribution_last_paid_time_ms_operations( - base.token_id().to_buffer(), - owner_id, - &mut last_paid_time_operations, - transaction, - platform_version, - )?; - - let last_paid_time_fee_result = Drive::calculate_fee( - None, - Some(last_paid_time_operations), - &block_info.epoch, - drive.config.epochs_per_era, + let last_paid_moment = drive + .fetch_pre_programmed_distribution_last_paid_time_ms_operations( + base.token_id().to_buffer(), + owner_id, + &mut last_paid_time_operations, + transaction, platform_version, - None, )?; - fee_result.checked_add_assign(last_paid_time_fee_result)?; - - let distributions_in_past_for_owner: BTreeMap = times - .iter() - .filter_map(|(timestamp, distribution)| { - if timestamp > &block_info.time_ms { - // Don't get the ones in the future - None - } else { - distribution - .get(&owner_id) - .map(|amount| (*timestamp, *amount)) - } - }) - .collect(); - - let distribution_after_last_paid: Option<(TimestampMillis, TokenAmount)> = - if let Some(last_paid) = last_paid_moment { - // Find the earliest distribution after the last paid moment - distributions_in_past_for_owner - .range((last_paid + 1)..) - .next() - .map(|(timestamp, amount)| (*timestamp, *amount)) + let last_paid_time_fee_result = Drive::calculate_fee( + None, + Some(last_paid_time_operations), + &block_info.epoch, + drive.config.epochs_per_era, + platform_version, + None, + )?; + + fee_result.checked_add_assign(last_paid_time_fee_result)?; + + let distributions_in_past_for_owner: BTreeMap = times + .iter() + .filter_map(|(timestamp, distribution)| { + if timestamp > &block_info.time_ms { + // Don't get the ones in the future + None } else { - // If never paid, take the earliest available distribution - distributions_in_past_for_owner - .first_key_value() - .map(|(timestamp, amount)| (*timestamp, *amount)) - }; + distribution + .get(&owner_id) + .map(|amount| (*timestamp, *amount)) + } + }) + .collect(); + + let distribution_after_last_paid: Option<(TimestampMillis, TokenAmount)> = + if let Some(last_paid) = last_paid_moment { + // Find the earliest distribution after the last paid moment + distributions_in_past_for_owner + .range((last_paid + 1)..) + .next() + .map(|(timestamp, amount)| (*timestamp, *amount)) + } else { + // If never paid, take the earliest available distribution + distributions_in_past_for_owner + .first_key_value() + .map(|(timestamp, amount)| (*timestamp, *amount)) + }; - let Some((payout_time, amount)) = distribution_after_last_paid else { - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( - base, + let Some((payout_time, amount)) = distribution_after_last_paid else { + return Ok(Err(ConsensusError::StateError( + StateError::InvalidTokenClaimNoCurrentRewards( + InvalidTokenClaimNoCurrentRewards::new( + base.token_id(), owner_id, - user_fee_increase, - ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); - - return Ok(( - ConsensusValidationResult::new_with_data_and_errors( - batched_action, - vec![ConsensusError::StateError( - StateError::InvalidTokenClaimNoCurrentRewards( - InvalidTokenClaimNoCurrentRewards::new( - value.base().token_id(), - owner_id, - RewardDistributionMoment::TimeBasedMoment( - block_info.time_ms, - ), - last_paid_moment - .map(RewardDistributionMoment::TimeBasedMoment), - ), - ), - )], + RewardDistributionMoment::TimeBasedMoment(block_info.time_ms), + last_paid_moment.map(RewardDistributionMoment::TimeBasedMoment), ), - fee_result, - )); - }; + ), + ))); + }; - ( - amount, - TokenDistributionInfo::PreProgrammed(payout_time, owner_id), - ) - } - TokenDistributionType::Perpetual => { - // we need to validate that we have a perpetual distribution - let Some(perpetual_distribution) = - token_config.distribution_rules().perpetual_distribution() - else { - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( - base, + ( + amount, + TokenDistributionInfo::PreProgrammed(payout_time, owner_id), + ) + } + TokenDistributionType::Perpetual => { + // we need to validate that we have a perpetual distribution + let Some(perpetual_distribution) = + token_config.distribution_rules().perpetual_distribution() + else { + return Ok(Err(ConsensusError::StateError( + StateError::InvalidTokenClaimPropertyMismatch( + InvalidTokenClaimPropertyMismatch::new( + "perpetual distribution", + base.token_id(), + ), + ), + ))); + }; + + // Let's start by checking that we have a valid claimant + let wrong_claimant_error = match perpetual_distribution.distribution_recipient() { + TokenDistributionRecipient::ContractOwner + if base_action.data_contract_fetch_info().contract.owner_id() != owner_id => + { + Some(base_action.data_contract_fetch_info().contract.owner_id()) + } + TokenDistributionRecipient::Identity(identifier) if identifier != owner_id => { + Some(identifier) + } + _ => None, + }; + + if let Some(expected_claimant) = wrong_claimant_error { + return Ok(Err(ConsensusError::StateError( + StateError::InvalidTokenClaimWrongClaimant( + InvalidTokenClaimWrongClaimant::new( + base.token_id(), + expected_claimant, owner_id, - user_fee_increase, - ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); - - return Ok(( - ConsensusValidationResult::new_with_data_and_errors( - batched_action, - vec![ConsensusError::StateError( - StateError::InvalidTokenClaimPropertyMismatch( - InvalidTokenClaimPropertyMismatch::new( - "perpetual distribution", - value.base().token_id(), - ), - ), - )], ), - fee_result, - )); - }; + ), + ))); + } - // Let's start by checking that we have a valid claimant - let wrong_claimant_error = match perpetual_distribution.distribution_recipient() { - TokenDistributionRecipient::ContractOwner - if base_action.data_contract_fetch_info().contract.owner_id() - != owner_id => - { - Some(base_action.data_contract_fetch_info().contract.owner_id()) - } - TokenDistributionRecipient::Identity(identifier) if identifier != owner_id => { - Some(identifier) - } - _ => None, - }; + let mut last_paid_time_operations = vec![]; - if let Some(expected_claimant) = wrong_claimant_error { - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( - base, - owner_id, - user_fee_increase, - ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); - - return Ok(( - ConsensusValidationResult::new_with_data_and_errors( - batched_action, - vec![ConsensusError::StateError( - StateError::InvalidTokenClaimWrongClaimant( - InvalidTokenClaimWrongClaimant::new( - value.base().token_id(), - expected_claimant, - owner_id, - ), - ), - )], - ), - fee_result, - )); - } + let last_paid_moment = drive.fetch_perpetual_distribution_last_paid_moment_operations( + base.token_id().to_buffer(), + owner_id, + perpetual_distribution.distribution_type(), + &mut last_paid_time_operations, + transaction, + platform_version, + )?; - let mut last_paid_time_operations = vec![]; + // if the token has never been paid then we use the token creation - let last_paid_moment = drive - .fetch_perpetual_distribution_last_paid_moment_operations( - base.token_id().to_buffer(), - owner_id, - perpetual_distribution.distribution_type(), - &mut last_paid_time_operations, - transaction, - platform_version, - )?; - - // if the token has never been paid then we use the token creation - - let contract_creation_moment = perpetual_distribution - .distribution_type() - .contract_creation_moment(&base_action.data_contract_fetch_info().contract) - .ok_or(Error::Drive(DriveError::ContractDoesNotHaveAStartMoment( - base_action.data_contract_fetch_info().contract.id(), - )))?; - - let contract_creation_cycle_start = contract_creation_moment - .cycle_start(perpetual_distribution.distribution_type().interval())?; - - let start_from_moment_for_distribution = - last_paid_moment.unwrap_or(contract_creation_cycle_start); - - let last_paid_time_fee_result = Drive::calculate_fee( - None, - Some(last_paid_time_operations), - &block_info.epoch, - drive.config.epochs_per_era, - platform_version, - None, - )?; + let contract_creation_moment = perpetual_distribution + .distribution_type() + .contract_creation_moment(&base_action.data_contract_fetch_info().contract) + .ok_or(Error::Drive(DriveError::ContractDoesNotHaveAStartMoment( + base_action.data_contract_fetch_info().contract.id(), + )))?; - fee_result.checked_add_assign(last_paid_time_fee_result)?; + let contract_creation_cycle_start = contract_creation_moment + .cycle_start(perpetual_distribution.distribution_type().interval())?; - let current_cycle_moment = perpetual_distribution.current_interval(block_info); + let start_from_moment_for_distribution = + last_paid_moment.unwrap_or(contract_creation_cycle_start); - // We need to get the max cycles allowed - let max_cycles = platform_version.system_limits.max_token_redemption_cycles; - let max_cycle_moment = perpetual_distribution - .distribution_type() - .max_cycle_moment( - start_from_moment_for_distribution, - current_cycle_moment, - max_cycles, - platform_version, - )?; + let last_paid_time_fee_result = Drive::calculate_fee( + None, + Some(last_paid_time_operations), + &block_info.epoch, + drive.config.epochs_per_era, + platform_version, + None, + )?; - let (recipient, amount) = match perpetual_distribution.distribution_recipient() { + fee_result.checked_add_assign(last_paid_time_fee_result)?; + + let current_cycle_moment = perpetual_distribution.current_interval(block_info); + // A claim into the pool names the moment it claims up to, so the amount its + // bundle proves is predictable; it must be cycle aligned and not in the future. + let current_cycle_moment = match claim_up_to { + None => current_cycle_moment, + Some(raw) => { + let interval = perpetual_distribution.distribution_type().interval(); + let requested = match interval { + RewardDistributionMoment::BlockBasedMoment(_) => { + RewardDistributionMoment::BlockBasedMoment(raw) + } + RewardDistributionMoment::TimeBasedMoment(_) => { + RewardDistributionMoment::TimeBasedMoment(raw) + } + RewardDistributionMoment::EpochBasedMoment(_) => { + match EpochIndex::try_from(raw) { + Ok(epoch_index) => { + RewardDistributionMoment::EpochBasedMoment(epoch_index) + } + Err(_) => { + return Ok(Err(ConsensusError::StateError( + StateError::InvalidTokenClaimPropertyMismatch( + InvalidTokenClaimPropertyMismatch::new( + "claim up to moment", + base.token_id(), + ), + ), + ))); + } + } + } + }; + if requested > current_cycle_moment + || requested != requested.cycle_start(interval)? + { + return Ok(Err(ConsensusError::StateError( + StateError::InvalidTokenClaimPropertyMismatch( + InvalidTokenClaimPropertyMismatch::new( + "claim up to moment", + base.token_id(), + ), + ), + ))); + } + requested + } + }; + + // We need to get the max cycles allowed + let max_cycles = platform_version.system_limits.max_token_redemption_cycles; + let max_cycle_moment = perpetual_distribution + .distribution_type() + .max_cycle_moment( + start_from_moment_for_distribution, + current_cycle_moment, + max_cycles, + platform_version, + )?; + + let (recipient, amount) = match perpetual_distribution.distribution_recipient() { TokenDistributionRecipient::ContractOwner => ( TokenDistributionResolvedRecipient::ContractOwnerIdentity( base_action.data_contract_fetch_info().contract.owner_id(), @@ -535,142 +582,46 @@ impl TokenClaimTransitionActionV0 { } }; - if amount == 0 { - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( - base, + if amount == 0 { + return Ok(Err(ConsensusError::StateError( + StateError::InvalidTokenClaimNoCurrentRewards( + InvalidTokenClaimNoCurrentRewards::new( + base.token_id(), owner_id, - user_fee_increase, - ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); - - return Ok(( - ConsensusValidationResult::new_with_data_and_errors( - batched_action, - vec![ConsensusError::StateError( - StateError::InvalidTokenClaimNoCurrentRewards( - InvalidTokenClaimNoCurrentRewards::new( - value.base().token_id(), - owner_id, - current_cycle_moment, - last_paid_moment, - ), - ), - )], + current_cycle_moment, + last_paid_moment, ), - fee_result, - )); - } - - ( - amount, - TokenDistributionInfo::Perpetual(max_cycle_moment, recipient), - ) + ), + ))); } - TokenDistributionType::OncePerIdentity => { - // The claimant is whoever signs the claim; the distribution has no configured - // recipient. The amount is fixed by the token configuration, so the only state - // read is whether this identity already claimed. - let Some(once_per_identity_distribution) = token_config - .distribution_rules() - .once_per_identity_distribution() - else { - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( - base, - owner_id, - user_fee_increase, - ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); - - return Ok(( - ConsensusValidationResult::new_with_data_and_errors( - batched_action, - vec![ConsensusError::StateError( - StateError::InvalidTokenClaimPropertyMismatch( - InvalidTokenClaimPropertyMismatch::new( - "once per identity distribution", - base.token_id(), - ), - ), - )], - ), - fee_result, - )); - }; - - let mut claim_operations = vec![]; - - let already_claimed_at = drive - .fetch_once_per_identity_distribution_claim_operations( - base.token_id().to_buffer(), - owner_id, - &mut claim_operations, - transaction, - platform_version, - )?; - - let claim_fee_result = Drive::calculate_fee( - None, - Some(claim_operations), - &block_info.epoch, - drive.config.epochs_per_era, - platform_version, - None, - )?; - - fee_result.checked_add_assign(claim_fee_result)?; - if let Some(claimed_at_ms) = already_claimed_at { - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( - base, - owner_id, - user_fee_increase, - ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); - - return Ok(( - ConsensusValidationResult::new_with_data_and_errors( - batched_action, - vec![ConsensusError::StateError( - StateError::TokenOncePerIdentityDistributionAlreadyClaimedError( - TokenOncePerIdentityDistributionAlreadyClaimedError::new( - base.token_id(), - owner_id, - claimed_at_ms, - ), - ), - )], - ), - fee_result, - )); - } - - ( - once_per_identity_distribution.amount(), - TokenDistributionInfo::OncePerIdentity(block_info.time_ms, owner_id), - ) + ( + amount, + TokenDistributionInfo::Perpetual(max_cycle_moment, recipient), + ) + } + TokenDistributionType::OncePerIdentity => { + let Some(distribution) = token_config.distribution_rules().once_per_identity_distribution() else { + return Ok(Err(StateError::InvalidTokenClaimPropertyMismatch( + InvalidTokenClaimPropertyMismatch::new("once per identity distribution", base.token_id()), + ).into())); + }; + let mut claim_operations = vec![]; + let already_claimed_at = drive.fetch_once_per_identity_distribution_claim_operations( + base.token_id().to_buffer(), owner_id, &mut claim_operations, transaction, platform_version, + )?; + fee_result.checked_add_assign(Drive::calculate_fee( + None, Some(claim_operations), &block_info.epoch, drive.config.epochs_per_era, platform_version, None, + )?)?; + if let Some(claimed_at_ms) = already_claimed_at { + return Ok(Err(StateError::TokenOncePerIdentityDistributionAlreadyClaimedError( + TokenOncePerIdentityDistributionAlreadyClaimedError::new(base.token_id(), owner_id, claimed_at_ms), + ).into())); } - }; - - Ok(( - BatchedTransitionAction::TokenAction(TokenTransitionAction::ClaimAction( - TokenClaimTransitionActionV0 { - base: base_action, - amount, - distribution_info, - public_note: public_note.clone(), - } - .into(), - )) - .into(), - fee_result, - )) - } + (distribution.amount(), TokenDistributionInfo::OncePerIdentity(block_info.time_ms, owner_id)) + } + }; + Ok(Ok((amount, distribution_info))) } #[cfg(test)] diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_to_pool_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_to_pool_transition_action/mod.rs new file mode 100644 index 00000000000..e3de4bc2fbb --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_to_pool_transition_action/mod.rs @@ -0,0 +1,70 @@ +use derive_more::From; +use dpp::balances::credits::TokenAmount; +use dpp::fee::Credits; +use dpp::shielded::SerializedAction; + +/// Builds the action from its transition +pub mod transformer; +mod v0; + +pub use v0::*; + +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; + +/// Versioned `TokenDirectPurchaseToPool` action. +#[derive(Debug, Clone, From)] +pub enum TokenDirectPurchaseToPoolTransitionAction { + /// Version 0 + V0(TokenDirectPurchaseToPoolTransitionActionV0), +} + +impl TokenDirectPurchaseToPoolTransitionActionAccessorsV0 + for TokenDirectPurchaseToPoolTransitionAction +{ + fn base(&self) -> &TokenBaseTransitionAction { + match self { + Self::V0(v0) => &v0.base, + } + } + + fn base_owned(self) -> TokenBaseTransitionAction { + match self { + Self::V0(v0) => v0.base, + } + } + + fn token_count(&self) -> TokenAmount { + match self { + Self::V0(v0) => v0.token_count(), + } + } + fn total_agreed_price(&self) -> Credits { + match self { + Self::V0(v0) => v0.total_agreed_price(), + } + } + + fn actions(&self) -> &[SerializedAction] { + match self { + Self::V0(v0) => v0.actions(), + } + } + + fn anchor(&self) -> &[u8; 32] { + match self { + Self::V0(v0) => v0.anchor(), + } + } + + fn proof(&self) -> &[u8] { + match self { + Self::V0(v0) => v0.proof(), + } + } + + fn binding_signature(&self) -> &[u8; 64] { + match self { + Self::V0(v0) => v0.binding_signature(), + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_to_pool_transition_action/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_to_pool_transition_action/transformer.rs new file mode 100644 index 00000000000..d9b51fb9c4e --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_to_pool_transition_action/transformer.rs @@ -0,0 +1,53 @@ +use crate::drive::contract::DataContractFetchInfo; +use crate::drive::Drive; +use crate::error::Error; +use crate::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_to_pool_transition_action::{TokenDirectPurchaseToPoolTransitionAction, TokenDirectPurchaseToPoolTransitionActionV0}; +use crate::state_transition_action::batch::BatchedTransitionAction; +use dpp::block::block_info::BlockInfo; +use dpp::fee::fee_result::FeeResult; +use dpp::platform_value::Identifier; +use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; +use dpp::state_transition::batch_transition::token_direct_purchase_to_pool_transition::TokenDirectPurchaseToPoolTransition; +use dpp::ProtocolError; +use grovedb::TransactionArg; +use platform_version::version::PlatformVersion; +use std::sync::Arc; + +impl TokenDirectPurchaseToPoolTransitionAction { + /// Resolves the transition against state (contract lookup, base checks and, for a claim, + /// the amount) into an action, or into a nonce bump carrying the consensus errors. + #[allow(clippy::too_many_arguments)] + pub fn try_from_borrowed_token_direct_purchase_to_pool_transition_with_contract_lookup( + drive: &Drive, + owner_id: Identifier, + value: &TokenDirectPurchaseToPoolTransition, + approximate_without_state_for_costs: bool, + transaction: TransactionArg, + block_info: &BlockInfo, + user_fee_increase: UserFeeIncrease, + get_data_contract: impl Fn(Identifier) -> Result, ProtocolError>, + platform_version: &PlatformVersion, + ) -> Result< + ( + ConsensusValidationResult, + FeeResult, + ), + Error, + > { + match value { + TokenDirectPurchaseToPoolTransition::V0(v0) => { + TokenDirectPurchaseToPoolTransitionActionV0::try_from_borrowed_token_direct_purchase_to_pool_transition_with_contract_lookup( + drive, + owner_id, + v0, + approximate_without_state_for_costs, + transaction, + block_info, + user_fee_increase, + get_data_contract, + platform_version, + ) + } + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_to_pool_transition_action/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_to_pool_transition_action/v0/mod.rs new file mode 100644 index 00000000000..6616d5458b0 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_to_pool_transition_action/v0/mod.rs @@ -0,0 +1,140 @@ +mod transformer; + +use std::sync::Arc; + +use dpp::balances::credits::TokenAmount; +use dpp::fee::Credits; +use dpp::identifier::Identifier; +use dpp::prelude::IdentityNonce; +use dpp::shielded::SerializedAction; +use crate::drive::contract::DataContractFetchInfo; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{ + TokenBaseTransitionAction, TokenBaseTransitionActionAccessorsV0, +}; +use crate::state_transition_action::shielded::ShieldedActionNote; + +/// The resolved `TokenDirectPurchaseToPool` batched transition. +#[derive(Debug, Clone)] +pub struct TokenDirectPurchaseToPoolTransitionActionV0 { + /// Base token transition action + pub base: TokenBaseTransitionAction, + /// The amount of tokens purchased + pub token_count: TokenAmount, + /// The most credits the buyer agrees to pay + pub total_agreed_price: Credits, + /// The Orchard actions + pub actions: Vec, + /// The Orchard anchor + pub anchor: [u8; 32], + /// The Halo 2 proof + pub proof: Vec, + /// The RedPallas binding signature + pub binding_signature: [u8; 64], +} + +/// Accessors for the version 0 action. +pub trait TokenDirectPurchaseToPoolTransitionActionAccessorsV0 { + /// The base token transition action + fn base(&self) -> &TokenBaseTransitionAction; + + /// Consumes the action and returns its base + fn base_owned(self) -> TokenBaseTransitionAction; + + /// The amount of tokens purchased + fn token_count(&self) -> TokenAmount; + /// The most credits the buyer agrees to pay + fn total_agreed_price(&self) -> Credits; + + /// The Orchard actions + fn actions(&self) -> &[SerializedAction]; + + /// The Orchard anchor + fn anchor(&self) -> &[u8; 32]; + + /// The Halo 2 proof + fn proof(&self) -> &[u8]; + + /// The RedPallas binding signature + fn binding_signature(&self) -> &[u8; 64]; + + /// The notes every action creates + fn notes(&self) -> Vec { + self.actions() + .iter() + .map(ShieldedActionNote::from) + .collect() + } + + /// The nullifiers every action spends + fn nullifiers(&self) -> Vec<[u8; 32]> { + self.actions() + .iter() + .map(|action| action.nullifier) + .collect() + } + + /// The token position in the contract + fn token_position(&self) -> u16 { + self.base().token_position() + } + + /// The token id + fn token_id(&self) -> Identifier { + self.base().token_id() + } + + /// The data contract id + fn data_contract_id(&self) -> Identifier { + self.base().data_contract_id() + } + + /// A reference to the fetched data contract + fn data_contract_fetch_info_ref(&self) -> &Arc { + self.base().data_contract_fetch_info_ref() + } + + /// The fetched data contract + fn data_contract_fetch_info(&self) -> Arc { + self.base().data_contract_fetch_info() + } + + /// The identity contract nonce + fn identity_contract_nonce(&self) -> IdentityNonce { + self.base().identity_contract_nonce() + } +} + +impl TokenDirectPurchaseToPoolTransitionActionAccessorsV0 + for TokenDirectPurchaseToPoolTransitionActionV0 +{ + fn base(&self) -> &TokenBaseTransitionAction { + &self.base + } + + fn base_owned(self) -> TokenBaseTransitionAction { + self.base + } + + fn token_count(&self) -> TokenAmount { + self.token_count + } + fn total_agreed_price(&self) -> Credits { + self.total_agreed_price + } + + fn actions(&self) -> &[SerializedAction] { + &self.actions + } + + fn anchor(&self) -> &[u8; 32] { + &self.anchor + } + + fn proof(&self) -> &[u8] { + &self.proof + } + + fn binding_signature(&self) -> &[u8; 64] { + &self.binding_signature + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_to_pool_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_to_pool_transition_action/v0/transformer.rs new file mode 100644 index 00000000000..d997a2dc748 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_to_pool_transition_action/v0/transformer.rs @@ -0,0 +1,153 @@ +use crate::drive::contract::DataContractFetchInfo; +use crate::drive::Drive; +use crate::error::Error; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; +use crate::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_to_pool_transition_action::{TokenDirectPurchaseToPoolTransitionAction, TokenDirectPurchaseToPoolTransitionActionV0}; +use crate::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_transition_action::resolve_direct_purchase_price; +use crate::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; +use crate::state_transition_action::batch::BatchedTransitionAction; +use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; +use dpp::block::block_info::BlockInfo; +use dpp::fee::fee_result::FeeResult; +use dpp::shielded::compute_shielded_verification_fee; +use dpp::identifier::Identifier; +use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; +use dpp::state_transition::batch_transition::token_direct_purchase_to_pool_transition::v0::TokenDirectPurchaseToPoolTransitionV0; +use dpp::ProtocolError; +use grovedb::TransactionArg; +use platform_version::version::PlatformVersion; +use std::sync::Arc; + +impl TokenDirectPurchaseToPoolTransitionActionV0 { + /// Resolves the version 0 transition against state into an action, or into a nonce bump + /// carrying the consensus errors. + #[allow(clippy::too_many_arguments)] + pub fn try_from_borrowed_token_direct_purchase_to_pool_transition_with_contract_lookup( + drive: &Drive, + owner_id: Identifier, + value: &TokenDirectPurchaseToPoolTransitionV0, + approximate_without_state_for_costs: bool, + transaction: TransactionArg, + block_info: &BlockInfo, + user_fee_increase: UserFeeIncrease, + get_data_contract: impl Fn(Identifier) -> Result, ProtocolError>, + platform_version: &PlatformVersion, + ) -> Result< + ( + ConsensusValidationResult, + FeeResult, + ), + Error, + > { + let TokenDirectPurchaseToPoolTransitionV0 { + base, + token_count, + total_agreed_price, + actions, + anchor, + proof, + binding_signature, + } = value; + + let mut drive_operations = vec![]; + + let base_action_validation_result = + TokenBaseTransitionAction::try_from_borrowed_base_transition_with_contract_lookup( + drive, + owner_id, + base, + approximate_without_state_for_costs, + transaction, + &mut drive_operations, + get_data_contract, + platform_version, + )?; + + let mut fee_result = Drive::calculate_fee( + None, + Some(drive_operations), + &block_info.epoch, + drive.config.epochs_per_era, + platform_version, + None, + )?; + + // The Halo 2 verification and per-action work GroveDB cannot meter, charged here so + // CheckTx admission and block execution price the bundle identically, whether or not + // the proof is (re)verified on this path. + fee_result.checked_add_assign(FeeResult { + processing_fee: compute_shielded_verification_fee(actions.len(), platform_version)?, + ..Default::default() + })?; + + let (base_action, _change_note) = match base_action_validation_result.is_valid() { + true => base_action_validation_result.into_data()?, + false => { + let bump_action = + BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( + base, + owner_id, + user_fee_increase, + ); + let batched_action = + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); + + return Ok(( + ConsensusValidationResult::new_with_data_and_errors( + batched_action, + base_action_validation_result.errors, + ), + fee_result, + )); + } + }; + + let required_price = match resolve_direct_purchase_price( + drive, + base, + *token_count, + *total_agreed_price, + block_info, + transaction, + &mut fee_result, + platform_version, + )? { + Ok(required_price) => required_price, + Err(error) => { + let bump_action = + BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( + base, + owner_id, + user_fee_increase, + ); + let batched_action = + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); + return Ok(( + ConsensusValidationResult::new_with_data_and_errors( + batched_action, + vec![error], + ), + fee_result, + )); + } + }; + + Ok(( + BatchedTransitionAction::TokenAction(TokenTransitionAction::from( + TokenDirectPurchaseToPoolTransitionAction::V0( + TokenDirectPurchaseToPoolTransitionActionV0 { + base: base_action, + token_count: *token_count, + total_agreed_price: required_price, + actions: actions.clone(), + anchor: *anchor, + proof: proof.clone(), + binding_signature: *binding_signature, + }, + ), + )) + .into(), + fee_result, + )) + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/mod.rs index 56367bcd274..0f08d930f34 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/mod.rs @@ -6,6 +6,7 @@ use dpp::fee::Credits; pub mod transformer; mod v0; +pub(crate) use v0::resolve_direct_purchase_price; pub use v0::*; use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/mod.rs index ef7bd4fce7d..8dba24a07a4 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/mod.rs @@ -1,4 +1,5 @@ mod transformer; +pub(crate) use transformer::resolve_direct_purchase_price; use std::sync::Arc; use dpp::balances::credits::TokenAmount; diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/transformer.rs index 1b92fea51ac..c2e24ee3000 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/transformer.rs @@ -13,6 +13,7 @@ use dpp::balances::credits::TokenAmount; use dpp::fee::Credits; use crate::drive::contract::DataContractFetchInfo; use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; +use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; use crate::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_transition_action::v0::TokenDirectPurchaseTransitionActionV0; use dpp::fee::fee_result::FeeResult; use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; @@ -124,104 +125,33 @@ impl TokenDirectPurchaseTransitionActionV0 { }; // We need to make sure the amount we want to pay is the amount we are expected to pay - let (pricing_schedule, fetch_token_direct_purchase_fee) = drive - .fetch_token_direct_purchase_price_with_costs( - base.token_id().to_buffer(), - block_info, - true, - transaction, - platform_version, - )?; - - fee_result.checked_add_assign(fetch_token_direct_purchase_fee)?; - - let Some(pricing_schedule) = pricing_schedule else { - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( - base, - owner_id, - user_fee_increase, - ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); - - return Ok(( - ConsensusValidationResult::new_with_data_and_errors( - batched_action, - vec![ConsensusError::StateError( - StateError::TokenNotForDirectSale(TokenNotForDirectSale::new( - base.token_id(), - )), - )], - ), - fee_result, - )); - }; - - let required_price = match pricing_schedule { - TokenPricingSchedule::SinglePrice(price_per_token) => { - // We've already checked the user set price in structure validation - // Hence we can do a saturating mul. - let required_price = price_per_token.saturating_mul(*token_count); - if *total_agreed_price < required_price { - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( - base, - owner_id, - user_fee_increase, - ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); - - return Ok(( - ConsensusValidationResult::new_with_data_and_errors( - batched_action, - vec![ConsensusError::StateError( - StateError::TokenDirectPurchaseUserPriceTooLow( - TokenDirectPurchaseUserPriceTooLow::new( - base.token_id(), - *total_agreed_price, - required_price, - ), - ), - )], - ), - fee_result, - )); - } - required_price - } - TokenPricingSchedule::SetPrices(set_prices) => { - // All of the `SetPrices` resolution logic (tier lookup, overflow, under-minimum, - // and the empty-schedule case that must never `.expect()` a key) lives in a pure - // helper so it can be unit-tested directly. On rejection we bump the nonce and - // surface the consensus error. - match resolve_set_prices_direct_purchase_price( - base.token_id(), - &set_prices, - *token_count, - *total_agreed_price, - ) { - Ok(required_total) => required_total, - Err(error) => { - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( - base, - owner_id, - user_fee_increase, - ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); - - return Ok(( - ConsensusValidationResult::new_with_data_and_errors( - batched_action, - vec![error], - ), - fee_result, - )); - } - } + let required_price = match resolve_direct_purchase_price( + drive, + base, + *token_count, + *total_agreed_price, + block_info, + transaction, + &mut fee_result, + platform_version, + )? { + Ok(required_price) => required_price, + Err(error) => { + let bump_action = + BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( + base, + owner_id, + user_fee_increase, + ); + let batched_action = + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); + return Ok(( + ConsensusValidationResult::new_with_data_and_errors( + batched_action, + vec![error], + ), + fee_result, + )); } }; @@ -251,6 +181,78 @@ impl TokenDirectPurchaseTransitionActionV0 { /// `set_prices.keys().next().expect("Map is not empty")`, which panics on an empty map. That /// panic was uncaught during per-state-transition processing and would deterministically halt /// the chain across the quorum. Here an empty schedule resolves to `TokenNotForDirectSale`. + +/// Resolves the credits a direct purchase of `token_count` must pay right now, from the token's +/// current pricing schedule: an error when the token is not for sale, the buyer agreed to too +/// little, or the amount is under the minimum sale amount. Shared by `TokenDirectPurchase` and +/// `TokenDirectPurchaseToPool`. Read costs are added to `fee_result`. +#[allow(clippy::too_many_arguments)] +pub(crate) fn resolve_direct_purchase_price( + drive: &Drive, + base: &TokenBaseTransition, + token_count: TokenAmount, + total_agreed_price: Credits, + block_info: &BlockInfo, + transaction: TransactionArg, + fee_result: &mut FeeResult, + platform_version: &PlatformVersion, +) -> Result, Error> { + let (pricing_schedule, fetch_token_direct_purchase_fee) = drive + .fetch_token_direct_purchase_price_with_costs( + base.token_id().to_buffer(), + block_info, + true, + transaction, + platform_version, + )?; + + fee_result.checked_add_assign(fetch_token_direct_purchase_fee)?; + + let Some(pricing_schedule) = pricing_schedule else { + return Ok(Err(ConsensusError::StateError( + StateError::TokenNotForDirectSale(TokenNotForDirectSale::new(base.token_id())), + ))); + }; + + let required_price = match pricing_schedule { + TokenPricingSchedule::SinglePrice(price_per_token) => { + // We've already checked the user set price in structure validation + // Hence we can do a saturating mul. + let required_price = price_per_token.saturating_mul(token_count); + if total_agreed_price < required_price { + return Ok(Err(ConsensusError::StateError( + StateError::TokenDirectPurchaseUserPriceTooLow( + TokenDirectPurchaseUserPriceTooLow::new( + base.token_id(), + total_agreed_price, + required_price, + ), + ), + ))); + } + required_price + } + TokenPricingSchedule::SetPrices(set_prices) => { + // All of the `SetPrices` resolution logic (tier lookup, overflow, under-minimum, + // and the empty-schedule case that must never `.expect()` a key) lives in a pure + // helper so it can be unit-tested directly. On rejection we bump the nonce and + // surface the consensus error. + match resolve_set_prices_direct_purchase_price( + base.token_id(), + &set_prices, + token_count, + total_agreed_price, + ) { + Ok(required_total) => required_total, + Err(error) => { + return Ok(Err(error)); + } + } + } + }; + Ok(Ok(required_price)) +} + fn resolve_set_prices_direct_purchase_price( token_id: Identifier, set_prices: &BTreeMap, diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_to_pool_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_to_pool_transition_action/mod.rs new file mode 100644 index 00000000000..69ca60aafa0 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_to_pool_transition_action/mod.rs @@ -0,0 +1,72 @@ +use derive_more::From; +use dpp::balances::credits::TokenAmount; +use dpp::shielded::SerializedAction; + +/// Builds the action from its transition +pub mod transformer; +mod v0; + +pub use v0::*; + +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; + +/// Versioned `TokenMintToPool` action. +#[derive(Debug, Clone, From)] +pub enum TokenMintToPoolTransitionAction { + /// Version 0 + V0(TokenMintToPoolTransitionActionV0), +} + +impl TokenMintToPoolTransitionActionAccessorsV0 for TokenMintToPoolTransitionAction { + fn base(&self) -> &TokenBaseTransitionAction { + match self { + Self::V0(v0) => &v0.base, + } + } + + fn base_owned(self) -> TokenBaseTransitionAction { + match self { + Self::V0(v0) => v0.base, + } + } + + fn amount(&self) -> TokenAmount { + match self { + Self::V0(v0) => v0.amount(), + } + } + fn public_note(&self) -> Option<&String> { + match self { + Self::V0(v0) => v0.public_note(), + } + } + fn public_note_owned(self) -> Option { + match self { + Self::V0(v0) => v0.public_note_owned(), + } + } + + fn actions(&self) -> &[SerializedAction] { + match self { + Self::V0(v0) => v0.actions(), + } + } + + fn anchor(&self) -> &[u8; 32] { + match self { + Self::V0(v0) => v0.anchor(), + } + } + + fn proof(&self) -> &[u8] { + match self { + Self::V0(v0) => v0.proof(), + } + } + + fn binding_signature(&self) -> &[u8; 64] { + match self { + Self::V0(v0) => v0.binding_signature(), + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_to_pool_transition_action/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_to_pool_transition_action/transformer.rs new file mode 100644 index 00000000000..e4fb6cd6751 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_to_pool_transition_action/transformer.rs @@ -0,0 +1,53 @@ +use crate::drive::contract::DataContractFetchInfo; +use crate::drive::Drive; +use crate::error::Error; +use crate::state_transition_action::batch::batched_transition::token_transition::token_mint_to_pool_transition_action::{TokenMintToPoolTransitionAction, TokenMintToPoolTransitionActionV0}; +use crate::state_transition_action::batch::BatchedTransitionAction; +use dpp::block::block_info::BlockInfo; +use dpp::fee::fee_result::FeeResult; +use dpp::platform_value::Identifier; +use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; +use dpp::state_transition::batch_transition::token_mint_to_pool_transition::TokenMintToPoolTransition; +use dpp::ProtocolError; +use grovedb::TransactionArg; +use platform_version::version::PlatformVersion; +use std::sync::Arc; + +impl TokenMintToPoolTransitionAction { + /// Resolves the transition against state (contract lookup, base checks and, for a claim, + /// the amount) into an action, or into a nonce bump carrying the consensus errors. + #[allow(clippy::too_many_arguments)] + pub fn try_from_borrowed_token_mint_to_pool_transition_with_contract_lookup( + drive: &Drive, + owner_id: Identifier, + value: &TokenMintToPoolTransition, + approximate_without_state_for_costs: bool, + transaction: TransactionArg, + block_info: &BlockInfo, + user_fee_increase: UserFeeIncrease, + get_data_contract: impl Fn(Identifier) -> Result, ProtocolError>, + platform_version: &PlatformVersion, + ) -> Result< + ( + ConsensusValidationResult, + FeeResult, + ), + Error, + > { + match value { + TokenMintToPoolTransition::V0(v0) => { + TokenMintToPoolTransitionActionV0::try_from_borrowed_token_mint_to_pool_transition_with_contract_lookup( + drive, + owner_id, + v0, + approximate_without_state_for_costs, + transaction, + block_info, + user_fee_increase, + get_data_contract, + platform_version, + ) + } + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_to_pool_transition_action/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_to_pool_transition_action/v0/mod.rs new file mode 100644 index 00000000000..6f5d84a7c23 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_to_pool_transition_action/v0/mod.rs @@ -0,0 +1,142 @@ +mod transformer; + +use std::sync::Arc; + +use dpp::balances::credits::TokenAmount; +use dpp::identifier::Identifier; +use dpp::prelude::IdentityNonce; +use dpp::shielded::SerializedAction; +use crate::drive::contract::DataContractFetchInfo; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{ + TokenBaseTransitionAction, TokenBaseTransitionActionAccessorsV0, +}; +use crate::state_transition_action::shielded::ShieldedActionNote; + +/// The resolved `TokenMintToPool` batched transition. +#[derive(Debug, Clone)] +pub struct TokenMintToPoolTransitionActionV0 { + /// Base token transition action + pub base: TokenBaseTransitionAction, + /// The amount of tokens + pub amount: TokenAmount, + /// Optional public note + pub public_note: Option, + /// The Orchard actions + pub actions: Vec, + /// The Orchard anchor + pub anchor: [u8; 32], + /// The Halo 2 proof + pub proof: Vec, + /// The RedPallas binding signature + pub binding_signature: [u8; 64], +} + +/// Accessors for the version 0 action. +pub trait TokenMintToPoolTransitionActionAccessorsV0 { + /// The base token transition action + fn base(&self) -> &TokenBaseTransitionAction; + + /// Consumes the action and returns its base + fn base_owned(self) -> TokenBaseTransitionAction; + + /// The amount of tokens + fn amount(&self) -> TokenAmount; + /// The public note, if any + fn public_note(&self) -> Option<&String>; + /// Takes the public note + fn public_note_owned(self) -> Option; + + /// The Orchard actions + fn actions(&self) -> &[SerializedAction]; + + /// The Orchard anchor + fn anchor(&self) -> &[u8; 32]; + + /// The Halo 2 proof + fn proof(&self) -> &[u8]; + + /// The RedPallas binding signature + fn binding_signature(&self) -> &[u8; 64]; + + /// The notes every action creates + fn notes(&self) -> Vec { + self.actions() + .iter() + .map(ShieldedActionNote::from) + .collect() + } + + /// The nullifiers every action spends + fn nullifiers(&self) -> Vec<[u8; 32]> { + self.actions() + .iter() + .map(|action| action.nullifier) + .collect() + } + + /// The token position in the contract + fn token_position(&self) -> u16 { + self.base().token_position() + } + + /// The token id + fn token_id(&self) -> Identifier { + self.base().token_id() + } + + /// The data contract id + fn data_contract_id(&self) -> Identifier { + self.base().data_contract_id() + } + + /// A reference to the fetched data contract + fn data_contract_fetch_info_ref(&self) -> &Arc { + self.base().data_contract_fetch_info_ref() + } + + /// The fetched data contract + fn data_contract_fetch_info(&self) -> Arc { + self.base().data_contract_fetch_info() + } + + /// The identity contract nonce + fn identity_contract_nonce(&self) -> IdentityNonce { + self.base().identity_contract_nonce() + } +} + +impl TokenMintToPoolTransitionActionAccessorsV0 for TokenMintToPoolTransitionActionV0 { + fn base(&self) -> &TokenBaseTransitionAction { + &self.base + } + + fn base_owned(self) -> TokenBaseTransitionAction { + self.base + } + + fn amount(&self) -> TokenAmount { + self.amount + } + fn public_note(&self) -> Option<&String> { + self.public_note.as_ref() + } + fn public_note_owned(self) -> Option { + self.public_note + } + + fn actions(&self) -> &[SerializedAction] { + &self.actions + } + + fn anchor(&self) -> &[u8; 32] { + &self.anchor + } + + fn proof(&self) -> &[u8] { + &self.proof + } + + fn binding_signature(&self) -> &[u8; 64] { + &self.binding_signature + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_to_pool_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_to_pool_transition_action/v0/transformer.rs new file mode 100644 index 00000000000..739a21609d8 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_to_pool_transition_action/v0/transformer.rs @@ -0,0 +1,120 @@ +use crate::drive::contract::DataContractFetchInfo; +use crate::drive::Drive; +use crate::error::Error; +use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; +use crate::state_transition_action::batch::batched_transition::token_transition::token_mint_to_pool_transition_action::{TokenMintToPoolTransitionAction, TokenMintToPoolTransitionActionV0}; +use crate::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; +use crate::state_transition_action::batch::BatchedTransitionAction; +use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; +use dpp::block::block_info::BlockInfo; +use dpp::fee::fee_result::FeeResult; +use dpp::shielded::compute_shielded_verification_fee; +use dpp::identifier::Identifier; +use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; +use dpp::state_transition::batch_transition::token_mint_to_pool_transition::v0::TokenMintToPoolTransitionV0; +use dpp::ProtocolError; +use grovedb::TransactionArg; +use platform_version::version::PlatformVersion; +use std::sync::Arc; + +impl TokenMintToPoolTransitionActionV0 { + /// Resolves the version 0 transition against state into an action, or into a nonce bump + /// carrying the consensus errors. + #[allow(clippy::too_many_arguments)] + pub fn try_from_borrowed_token_mint_to_pool_transition_with_contract_lookup( + drive: &Drive, + owner_id: Identifier, + value: &TokenMintToPoolTransitionV0, + approximate_without_state_for_costs: bool, + transaction: TransactionArg, + block_info: &BlockInfo, + user_fee_increase: UserFeeIncrease, + get_data_contract: impl Fn(Identifier) -> Result, ProtocolError>, + platform_version: &PlatformVersion, + ) -> Result< + ( + ConsensusValidationResult, + FeeResult, + ), + Error, + > { + let TokenMintToPoolTransitionV0 { + base, + amount, + actions, + anchor, + proof, + binding_signature, + public_note, + } = value; + + let mut drive_operations = vec![]; + + let base_action_validation_result = + TokenBaseTransitionAction::try_from_borrowed_base_transition_with_contract_lookup( + drive, + owner_id, + base, + approximate_without_state_for_costs, + transaction, + &mut drive_operations, + get_data_contract, + platform_version, + )?; + + let mut fee_result = Drive::calculate_fee( + None, + Some(drive_operations), + &block_info.epoch, + drive.config.epochs_per_era, + platform_version, + None, + )?; + + // The Halo 2 verification and per-action work GroveDB cannot meter, charged here so + // CheckTx admission and block execution price the bundle identically, whether or not + // the proof is (re)verified on this path. + fee_result.checked_add_assign(FeeResult { + processing_fee: compute_shielded_verification_fee(actions.len(), platform_version)?, + ..Default::default() + })?; + + let (base_action, _change_note) = match base_action_validation_result.is_valid() { + true => base_action_validation_result.into_data()?, + false => { + let bump_action = + BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( + base, + owner_id, + user_fee_increase, + ); + let batched_action = + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); + + return Ok(( + ConsensusValidationResult::new_with_data_and_errors( + batched_action, + base_action_validation_result.errors, + ), + fee_result, + )); + } + }; + + Ok(( + BatchedTransitionAction::TokenAction(TokenTransitionAction::from( + TokenMintToPoolTransitionAction::V0(TokenMintToPoolTransitionActionV0 { + base: base_action, + amount: *amount, + public_note: public_note.clone(), + actions: actions.clone(), + anchor: *anchor, + proof: proof.clone(), + binding_signature: *binding_signature, + }), + )) + .into(), + fee_result, + )) + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/v0/transformer.rs index 848761db1f5..27575c8892c 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/v0/transformer.rs @@ -8,6 +8,7 @@ use crate::state_transition_action::batch::BatchedTransitionAction; use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; use dpp::block::block_info::BlockInfo; use dpp::fee::fee_result::FeeResult; +use dpp::shielded::compute_shielded_verification_fee; use dpp::identifier::Identifier; use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; use dpp::state_transition::batch_transition::token_shield_transition::v0::TokenShieldTransitionV0; @@ -61,7 +62,7 @@ impl TokenShieldTransitionActionV0 { platform_version, )?; - let fee_result = Drive::calculate_fee( + let mut fee_result = Drive::calculate_fee( None, Some(drive_operations), &block_info.epoch, @@ -71,6 +72,14 @@ impl TokenShieldTransitionActionV0 { )?; // Shielded token transitions carry no public note to change. + // The Halo 2 verification and per-action work GroveDB cannot meter, charged here so + // CheckTx admission and block execution price the bundle identically, whether or not + // the proof is (re)verified on this path. + fee_result.checked_add_assign(FeeResult { + processing_fee: compute_shielded_verification_fee(actions.len(), platform_version)?, + ..Default::default() + })?; + let (base_action, _change_note) = match base_action_validation_result.is_valid() { true => base_action_validation_result.into_data()?, false => { diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/v0/transformer.rs index bb4f1af95a7..9280f1f4637 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/v0/transformer.rs @@ -8,6 +8,7 @@ use crate::state_transition_action::batch::BatchedTransitionAction; use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; use dpp::block::block_info::BlockInfo; use dpp::fee::fee_result::FeeResult; +use dpp::shielded::compute_shielded_verification_fee; use dpp::identifier::Identifier; use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; use dpp::state_transition::batch_transition::token_shielded_transfer_transition::v0::TokenShieldedTransferTransitionV0; @@ -60,7 +61,7 @@ impl TokenShieldedTransferTransitionActionV0 { platform_version, )?; - let fee_result = Drive::calculate_fee( + let mut fee_result = Drive::calculate_fee( None, Some(drive_operations), &block_info.epoch, @@ -70,6 +71,14 @@ impl TokenShieldedTransferTransitionActionV0 { )?; // Shielded token transitions carry no public note to change. + // The Halo 2 verification and per-action work GroveDB cannot meter, charged here so + // CheckTx admission and block execution price the bundle identically, whether or not + // the proof is (re)verified on this path. + fee_result.checked_add_assign(FeeResult { + processing_fee: compute_shielded_verification_fee(actions.len(), platform_version)?, + ..Default::default() + })?; + let (base_action, _change_note) = match base_action_validation_result.is_valid() { true => base_action_validation_result.into_data()?, false => { diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_transition_action_type.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_transition_action_type.rs index 0bad8ea4c43..d0de2956eb0 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_transition_action_type.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_transition_action_type.rs @@ -30,6 +30,12 @@ impl TokenTransitionActionTypeGetter for TokenTransitionAction { TokenTransitionAction::ShieldedTransferAction(_) => { TokenTransitionActionType::ShieldedTransfer } + TokenTransitionAction::MintToPoolAction(_) => TokenTransitionActionType::MintToPool, + TokenTransitionAction::BurnFromPoolAction(_) => TokenTransitionActionType::BurnFromPool, + TokenTransitionAction::ClaimToPoolAction(_) => TokenTransitionActionType::ClaimToPool, + TokenTransitionAction::DirectPurchaseToPoolAction(_) => { + TokenTransitionActionType::DirectPurchaseToPool + } } } } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/v0/transformer.rs index 72aa62814b1..1082b87a2e9 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/v0/transformer.rs @@ -8,6 +8,7 @@ use crate::state_transition_action::batch::BatchedTransitionAction; use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; use dpp::block::block_info::BlockInfo; use dpp::fee::fee_result::FeeResult; +use dpp::shielded::compute_shielded_verification_fee; use dpp::identifier::Identifier; use dpp::prelude::{ConsensusValidationResult, UserFeeIncrease}; use dpp::state_transition::batch_transition::token_unshield_transition::v0::TokenUnshieldTransitionV0; @@ -62,7 +63,7 @@ impl TokenUnshieldTransitionActionV0 { platform_version, )?; - let fee_result = Drive::calculate_fee( + let mut fee_result = Drive::calculate_fee( None, Some(drive_operations), &block_info.epoch, @@ -72,6 +73,14 @@ impl TokenUnshieldTransitionActionV0 { )?; // Shielded token transitions carry no public note to change. + // The Halo 2 verification and per-action work GroveDB cannot meter, charged here so + // CheckTx admission and block execution price the bundle identically, whether or not + // the proof is (re)verified on this path. + fee_result.checked_add_assign(FeeResult { + processing_fee: compute_shielded_verification_fee(actions.len(), platform_version)?, + ..Default::default() + })?; + let (base_action, _change_note) = match base_action_validation_result.is_valid() { true => base_action_validation_result.into_data()?, false => { diff --git a/packages/rs-drive/src/util/batch/drive_op_batch/token.rs b/packages/rs-drive/src/util/batch/drive_op_batch/token.rs index fc77899e300..6541e9cf0d6 100644 --- a/packages/rs-drive/src/util/batch/drive_op_batch/token.rs +++ b/packages/rs-drive/src/util/batch/drive_op_batch/token.rs @@ -167,6 +167,28 @@ pub enum TokenOperationType { /// The bundle's output notes, appended to the pool's commitment tree notes: Vec, }, + /// Mints tokens straight into the token's shielded pool (supply and pool balance grow). + TokenMintToPool { + /// The token id + token_id: Identifier, + /// The amount minted + amount: TokenAmount, + /// Should we allow this to be the first ever mint + allow_first_mint: bool, + /// The output notes of the bundle + notes: Vec, + }, + /// Burns tokens held in the token's shielded pool (pool balance and supply shrink). + TokenBurnFromPool { + /// The token id + token_id: Identifier, + /// The amount destroyed + amount: TokenAmount, + /// The spent nullifiers + nullifiers: Vec<[u8; 32]>, + /// The change notes of the bundle + notes: Vec, + }, } impl DriveLowLevelOperationConverter for TokenOperationType { @@ -408,6 +430,34 @@ impl DriveLowLevelOperationConverter for TokenOperationType { transaction, platform_version, ), + TokenOperationType::TokenMintToPool { + token_id, + amount, + allow_first_mint, + notes, + } => drive.token_mint_to_pool_operations( + token_id.to_buffer(), + amount, + allow_first_mint, + ¬es, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ), + TokenOperationType::TokenBurnFromPool { + token_id, + amount, + nullifiers, + notes, + } => drive.token_burn_from_pool_operations( + token_id.to_buffer(), + amount, + &nullifiers, + ¬es, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + ), } } } diff --git a/packages/rs-drive/src/verify/state_transition/state_transition_execution_path_queries/token_transition.rs b/packages/rs-drive/src/verify/state_transition/state_transition_execution_path_queries/token_transition.rs index 9494cee0397..71fdc3d7cf0 100644 --- a/packages/rs-drive/src/verify/state_transition/state_transition_execution_path_queries/token_transition.rs +++ b/packages/rs-drive/src/verify/state_transition/state_transition_execution_path_queries/token_transition.rs @@ -1,4 +1,6 @@ -use crate::drive::shielded::paths::token_shielded_pool_nullifiers_path_query; +use crate::drive::shielded::paths::{ + token_shielded_pool_nullifiers_path_query, token_shielded_pool_state_path_query, +}; use crate::drive::Drive; use crate::error::Error; use crate::query::{SingleDocumentDriveQuery, SingleDocumentDriveQueryContestedStatus}; @@ -15,6 +17,7 @@ use dpp::state_transition::batch_transition::batched_transition::token_transitio TokenTransition, TokenTransitionV0Methods, }; use dpp::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; +use dpp::state_transition::batch_transition::token_burn_from_pool_transition::v0::v0_methods::TokenBurnFromPoolTransitionV0Methods; use dpp::state_transition::batch_transition::token_freeze_transition::v0::v0_methods::TokenFreezeTransitionV0Methods; use dpp::state_transition::batch_transition::token_mint_transition::v0::v0_methods::TokenMintTransitionV0Methods; use dpp::state_transition::batch_transition::token_shielded_transfer_transition::v0::v0_methods::TokenShieldedTransferTransitionV0Methods; @@ -182,6 +185,21 @@ impl TryTransitionIntoPathQuery for TokenTransition { .collect(); token_shielded_pool_nullifiers_path_query(token_id.to_buffer(), &nullifiers) } + // Notes created straight into the pool show up in the pool's total balance; a burn + // from the pool is attested by the nullifiers it spent. + TokenTransition::MintToPool(_) + | TokenTransition::ClaimToPool(_) + | TokenTransition::DirectPurchaseToPool(_) => { + token_shielded_pool_state_path_query(token_id.to_buffer()) + } + TokenTransition::BurnFromPool(token_burn_from_pool_transition) => { + let nullifiers: Vec<[u8; 32]> = token_burn_from_pool_transition + .actions() + .iter() + .map(|action| action.nullifier) + .collect(); + token_shielded_pool_nullifiers_path_query(token_id.to_buffer(), &nullifiers) + } }; if let Some(group_state_transition_info) = self.base().using_group_info() { diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs index 55f075e6735..48b85c1a2c2 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs @@ -57,6 +57,7 @@ use dpp::state_transition::batch_transition::token_mint_transition::v0::v0_metho use dpp::state_transition::batch_transition::token_transfer_transition::v0::v0_methods::TokenTransferTransitionV0Methods; use dpp::state_transition::batch_transition::token_shielded_transfer_transition::v0::v0_methods::TokenShieldedTransferTransitionV0Methods; use dpp::state_transition::batch_transition::token_unshield_transition::v0::v0_methods::TokenUnshieldTransitionV0Methods; +use dpp::state_transition::batch_transition::token_burn_from_pool_transition::v0::v0_methods::TokenBurnFromPoolTransitionV0Methods; use dpp::state_transition::batch_transition::token_unfreeze_transition::v0::v0_methods::TokenUnfreezeTransitionV0Methods; use dpp::state_transition::address_credit_withdrawal_transition::accessors::AddressCreditWithdrawalTransitionAccessorsV0; use dpp::state_transition::identity_credit_transfer_transition::accessors::IdentityCreditTransferTransitionAccessorsV0; @@ -64,7 +65,7 @@ use dpp::state_transition::identity_credit_withdrawal_transition::accessors::Ide use dpp::state_transition::identity_topup_transition::accessors::IdentityTopUpTransitionAccessorsV0; use dpp::state_transition::masternode_vote_transition::accessors::MasternodeVoteTransitionAccessorsV0; use dpp::state_transition::proof_result::StateTransitionProofOutcome; -use dpp::state_transition::proof_result::StateTransitionProofResult::{VerifiedAddressInfos, VerifiedContractDocumentRemoval, VerifiedContractFeeClaim, VerifiedContractModerationListStatuses, VerifiedBalanceTransfer, VerifiedDataContract, VerifiedDocuments, VerifiedIdentity, VerifiedIdentityFullWithAddressInfos, VerifiedIdentityWithAddressInfos, VerifiedMasternodeVote, VerifiedPartialIdentity, VerifiedTokenActionWithDocument, VerifiedTokenBalance, VerifiedTokenGroupActionWithDocument, VerifiedTokenGroupActionWithTokenBalance, VerifiedTokenGroupActionWithTokenIdentityInfo, VerifiedTokenGroupActionWithTokenPricingSchedule, VerifiedTokenIdentitiesBalances, VerifiedTokenIdentityInfo, VerifiedTokenPricingSchedule}; +use dpp::state_transition::proof_result::StateTransitionProofResult::{VerifiedAddressInfos, VerifiedContractDocumentRemoval, VerifiedContractFeeClaim, VerifiedContractModerationListStatuses, VerifiedBalanceTransfer, VerifiedDataContract, VerifiedDocuments, VerifiedIdentity, VerifiedIdentityFullWithAddressInfos, VerifiedIdentityWithAddressInfos, VerifiedMasternodeVote, VerifiedPartialIdentity, VerifiedTokenActionWithDocument, VerifiedTokenBalance, VerifiedTokenGroupActionWithDocument, VerifiedTokenGroupActionWithTokenBalance, VerifiedTokenGroupActionWithTokenIdentityInfo, VerifiedTokenGroupActionWithTokenPricingSchedule, VerifiedTokenIdentitiesBalances, VerifiedTokenIdentityInfo, VerifiedTokenPricingSchedule, VerifiedTokenShieldedPoolBalance, VerifiedShieldedNullifiers}; use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; use dpp::tokens::info::v0::IdentityTokenInfoV0Accessors; use dpp::voting::vote_polls::VotePoll; @@ -1079,11 +1080,51 @@ impl Drive { "proof did not show every nullifier of the token shielded transfer as spent".to_string(), ))); } + Ok((root_hash, VerifiedShieldedNullifiers(statuses))) + } + TokenTransition::MintToPool(_) + | TokenTransition::ClaimToPool(_) + | TokenTransition::DirectPurchaseToPool(_) => { + let (root_hash, Some(balance)) = + Drive::verify_token_shielded_pool_state( + proof, + token_id.into_buffer(), + false, + platform_version, + )? + else { + return Err(Error::Proof(ProofError::IncorrectProof( + "proof did not contain the token shielded pool balance expected to exist because of state transition (notes created in the pool)".to_string(), + ))); + }; Ok(( root_hash, - dpp::state_transition::proof_result::StateTransitionProofResult::VerifiedShieldedNullifiers(statuses), + VerifiedTokenShieldedPoolBalance(token_id, balance), )) } + TokenTransition::BurnFromPool(token_burn_from_pool_transition) => { + let nullifiers: Vec> = token_burn_from_pool_transition + .actions() + .iter() + .map(|action| action.nullifier.to_vec()) + .collect(); + let (root_hash, statuses) = + Drive::verify_token_shielded_pool_nullifiers( + proof, + token_id.into_buffer(), + &nullifiers, + false, + platform_version, + )?; + if statuses.len() != nullifiers.len() + || statuses.iter().any(|(_, spent)| !spent) + { + return Err(Error::Proof(ProofError::IncorrectProof( + "proof did not show every nullifier of the token burn from pool as spent".to_string(), + ))); + } + Ok((root_hash, VerifiedShieldedNullifiers(statuses))) + } } } } @@ -2695,8 +2736,14 @@ impl Drive { // Balance snapshots: the moved amount cannot be tied to one shield or // unshield. A shielded transfer's spent nullifiers exist only if it // executed. - TokenTransition::Shield(_) | TokenTransition::Unshield(_) => false, - TokenTransition::ShieldedTransfer(_) => true, + TokenTransition::Shield(_) + | TokenTransition::Unshield(_) + | TokenTransition::MintToPool(_) + | TokenTransition::ClaimToPool(_) + | TokenTransition::DirectPurchaseToPool(_) => false, + TokenTransition::ShieldedTransfer(_) | TokenTransition::BurnFromPool(_) => { + true + } } } }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs index 5ca2e84ce8f..2fbda88a474 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs @@ -296,6 +296,14 @@ pub struct DriveAbciDocumentsStateTransitionValidationVersions { pub token_set_price_for_direct_purchase_transition_state_validation: FeatureVersion, pub token_shield_transition_structure_validation: FeatureVersion, pub token_shield_transition_state_validation: FeatureVersion, + pub token_mint_to_pool_transition_structure_validation: FeatureVersion, + pub token_mint_to_pool_transition_state_validation: FeatureVersion, + pub token_burn_from_pool_transition_structure_validation: FeatureVersion, + pub token_burn_from_pool_transition_state_validation: FeatureVersion, + pub token_claim_to_pool_transition_structure_validation: FeatureVersion, + pub token_claim_to_pool_transition_state_validation: FeatureVersion, + pub token_direct_purchase_to_pool_transition_structure_validation: FeatureVersion, + pub token_direct_purchase_to_pool_transition_state_validation: FeatureVersion, pub token_unshield_transition_structure_validation: FeatureVersion, pub token_unshield_transition_state_validation: FeatureVersion, pub token_shielded_transfer_transition_structure_validation: FeatureVersion, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs index fa8d2a10818..c74980cecd5 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs @@ -195,6 +195,14 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V1: DriveAbciValidationVersions = token_set_price_for_direct_purchase_transition_state_validation: 0, token_shield_transition_structure_validation: 0, token_shield_transition_state_validation: 0, + token_mint_to_pool_transition_structure_validation: 0, + token_mint_to_pool_transition_state_validation: 0, + token_burn_from_pool_transition_structure_validation: 0, + token_burn_from_pool_transition_state_validation: 0, + token_claim_to_pool_transition_structure_validation: 0, + token_claim_to_pool_transition_state_validation: 0, + token_direct_purchase_to_pool_transition_structure_validation: 0, + token_direct_purchase_to_pool_transition_state_validation: 0, token_unshield_transition_structure_validation: 0, token_unshield_transition_state_validation: 0, token_shielded_transfer_transition_structure_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs index ff32be56541..3d439a718a8 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs @@ -264,6 +264,14 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = token_set_price_for_direct_purchase_transition_state_validation: 0, token_shield_transition_structure_validation: 0, token_shield_transition_state_validation: 0, + token_mint_to_pool_transition_structure_validation: 0, + token_mint_to_pool_transition_state_validation: 0, + token_burn_from_pool_transition_structure_validation: 0, + token_burn_from_pool_transition_state_validation: 0, + token_claim_to_pool_transition_structure_validation: 0, + token_claim_to_pool_transition_state_validation: 0, + token_direct_purchase_to_pool_transition_structure_validation: 0, + token_direct_purchase_to_pool_transition_state_validation: 0, token_unshield_transition_structure_validation: 0, token_unshield_transition_state_validation: 0, token_shielded_transfer_transition_structure_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs index c20376a1649..92c0c062401 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs @@ -195,6 +195,14 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V2: DriveAbciValidationVersions = token_set_price_for_direct_purchase_transition_state_validation: 0, token_shield_transition_structure_validation: 0, token_shield_transition_state_validation: 0, + token_mint_to_pool_transition_structure_validation: 0, + token_mint_to_pool_transition_state_validation: 0, + token_burn_from_pool_transition_structure_validation: 0, + token_burn_from_pool_transition_state_validation: 0, + token_claim_to_pool_transition_structure_validation: 0, + token_claim_to_pool_transition_state_validation: 0, + token_direct_purchase_to_pool_transition_structure_validation: 0, + token_direct_purchase_to_pool_transition_state_validation: 0, token_unshield_transition_structure_validation: 0, token_unshield_transition_state_validation: 0, token_shielded_transfer_transition_structure_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs index 53f1a1be613..791f923bf66 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs @@ -195,6 +195,14 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V3: DriveAbciValidationVersions = token_set_price_for_direct_purchase_transition_state_validation: 0, token_shield_transition_structure_validation: 0, token_shield_transition_state_validation: 0, + token_mint_to_pool_transition_structure_validation: 0, + token_mint_to_pool_transition_state_validation: 0, + token_burn_from_pool_transition_structure_validation: 0, + token_burn_from_pool_transition_state_validation: 0, + token_claim_to_pool_transition_structure_validation: 0, + token_claim_to_pool_transition_state_validation: 0, + token_direct_purchase_to_pool_transition_structure_validation: 0, + token_direct_purchase_to_pool_transition_state_validation: 0, token_unshield_transition_structure_validation: 0, token_unshield_transition_state_validation: 0, token_shielded_transfer_transition_structure_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs index 2f84c258196..97e1081e3a4 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs @@ -198,6 +198,14 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V4: DriveAbciValidationVersions = token_set_price_for_direct_purchase_transition_state_validation: 0, token_shield_transition_structure_validation: 0, token_shield_transition_state_validation: 0, + token_mint_to_pool_transition_structure_validation: 0, + token_mint_to_pool_transition_state_validation: 0, + token_burn_from_pool_transition_structure_validation: 0, + token_burn_from_pool_transition_state_validation: 0, + token_claim_to_pool_transition_structure_validation: 0, + token_claim_to_pool_transition_state_validation: 0, + token_direct_purchase_to_pool_transition_structure_validation: 0, + token_direct_purchase_to_pool_transition_state_validation: 0, token_unshield_transition_structure_validation: 0, token_unshield_transition_state_validation: 0, token_shielded_transfer_transition_structure_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs index 23ffdfd9822..b25c4292743 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs @@ -199,6 +199,14 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V5: DriveAbciValidationVersions = token_set_price_for_direct_purchase_transition_state_validation: 0, token_shield_transition_structure_validation: 0, token_shield_transition_state_validation: 0, + token_mint_to_pool_transition_structure_validation: 0, + token_mint_to_pool_transition_state_validation: 0, + token_burn_from_pool_transition_structure_validation: 0, + token_burn_from_pool_transition_state_validation: 0, + token_claim_to_pool_transition_structure_validation: 0, + token_claim_to_pool_transition_state_validation: 0, + token_direct_purchase_to_pool_transition_structure_validation: 0, + token_direct_purchase_to_pool_transition_state_validation: 0, token_unshield_transition_structure_validation: 0, token_unshield_transition_state_validation: 0, token_shielded_transfer_transition_structure_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs index c043c033a05..00e7898b9d5 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs @@ -202,6 +202,14 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V6: DriveAbciValidationVersions = token_set_price_for_direct_purchase_transition_state_validation: 0, token_shield_transition_structure_validation: 0, token_shield_transition_state_validation: 0, + token_mint_to_pool_transition_structure_validation: 0, + token_mint_to_pool_transition_state_validation: 0, + token_burn_from_pool_transition_structure_validation: 0, + token_burn_from_pool_transition_state_validation: 0, + token_claim_to_pool_transition_structure_validation: 0, + token_claim_to_pool_transition_state_validation: 0, + token_direct_purchase_to_pool_transition_structure_validation: 0, + token_direct_purchase_to_pool_transition_state_validation: 0, token_unshield_transition_structure_validation: 0, token_unshield_transition_state_validation: 0, token_shielded_transfer_transition_structure_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs index a0c532c31e6..16cdaed3b0e 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs @@ -196,6 +196,14 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V7: DriveAbciValidationVersions = token_set_price_for_direct_purchase_transition_state_validation: 0, token_shield_transition_structure_validation: 0, token_shield_transition_state_validation: 0, + token_mint_to_pool_transition_structure_validation: 0, + token_mint_to_pool_transition_state_validation: 0, + token_burn_from_pool_transition_structure_validation: 0, + token_burn_from_pool_transition_state_validation: 0, + token_claim_to_pool_transition_structure_validation: 0, + token_claim_to_pool_transition_state_validation: 0, + token_direct_purchase_to_pool_transition_structure_validation: 0, + token_direct_purchase_to_pool_transition_state_validation: 0, token_unshield_transition_structure_validation: 0, token_unshield_transition_state_validation: 0, token_shielded_transfer_transition_structure_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs index 547e49230bb..5abca891eef 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs @@ -250,6 +250,14 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = token_set_price_for_direct_purchase_transition_state_validation: 0, token_shield_transition_structure_validation: 0, token_shield_transition_state_validation: 0, + token_mint_to_pool_transition_structure_validation: 0, + token_mint_to_pool_transition_state_validation: 0, + token_burn_from_pool_transition_structure_validation: 0, + token_burn_from_pool_transition_state_validation: 0, + token_claim_to_pool_transition_structure_validation: 0, + token_claim_to_pool_transition_state_validation: 0, + token_direct_purchase_to_pool_transition_structure_validation: 0, + token_direct_purchase_to_pool_transition_state_validation: 0, token_unshield_transition_structure_validation: 0, token_unshield_transition_state_validation: 0, token_shielded_transfer_transition_structure_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs index c3968675fea..78ac3f9e24a 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs @@ -246,6 +246,14 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V9: DriveAbciValidationVersions = token_set_price_for_direct_purchase_transition_state_validation: 0, token_shield_transition_structure_validation: 0, token_shield_transition_state_validation: 0, + token_mint_to_pool_transition_structure_validation: 0, + token_mint_to_pool_transition_state_validation: 0, + token_burn_from_pool_transition_structure_validation: 0, + token_burn_from_pool_transition_state_validation: 0, + token_claim_to_pool_transition_structure_validation: 0, + token_claim_to_pool_transition_state_validation: 0, + token_direct_purchase_to_pool_transition_structure_validation: 0, + token_direct_purchase_to_pool_transition_state_validation: 0, token_unshield_transition_structure_validation: 0, token_unshield_transition_state_validation: 0, token_shielded_transfer_transition_structure_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs index f94a5b5ea14..f3d992f5a3d 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs @@ -65,6 +65,10 @@ pub struct DriveStateTransitionActionConvertToHighLevelOperationsMethodVersions pub token_shield_transition: FeatureVersion, pub token_unshield_transition: FeatureVersion, pub token_shielded_transfer_transition: FeatureVersion, + pub token_mint_to_pool_transition: FeatureVersion, + pub token_burn_from_pool_transition: FeatureVersion, + pub token_claim_to_pool_transition: FeatureVersion, + pub token_direct_purchase_to_pool_transition: FeatureVersion, pub identity_credit_transfer_to_addresses_transition: FeatureVersion, pub address_funds_transfer_transition: FeatureVersion, pub address_credit_withdrawal_transition: FeatureVersion, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v1.rs index 56835bb83ef..7b37560f4b7 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v1.rs @@ -53,6 +53,10 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V1: DriveStateTransitionMethodV token_shield_transition: 0, token_unshield_transition: 0, token_shielded_transfer_transition: 0, + token_mint_to_pool_transition: 0, + token_burn_from_pool_transition: 0, + token_claim_to_pool_transition: 0, + token_direct_purchase_to_pool_transition: 0, identity_credit_transfer_to_addresses_transition: 0, address_funds_transfer_transition: 0, address_credit_withdrawal_transition: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v2.rs index 8ea5c9e8cac..1510faa63d0 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v2.rs @@ -54,6 +54,10 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V2: DriveStateTransitionMethodV token_shield_transition: 0, token_unshield_transition: 0, token_shielded_transfer_transition: 0, + token_mint_to_pool_transition: 0, + token_burn_from_pool_transition: 0, + token_claim_to_pool_transition: 0, + token_direct_purchase_to_pool_transition: 0, identity_credit_transfer_to_addresses_transition: 0, address_funds_transfer_transition: 0, address_credit_withdrawal_transition: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v3.rs index ecb2f07d6ec..1bdaef8081a 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v3.rs @@ -58,6 +58,10 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V3: DriveStateTransitionMethodV token_shield_transition: 0, token_unshield_transition: 0, token_shielded_transfer_transition: 0, + token_mint_to_pool_transition: 0, + token_burn_from_pool_transition: 0, + token_claim_to_pool_transition: 0, + token_direct_purchase_to_pool_transition: 0, identity_credit_transfer_to_addresses_transition: 0, address_funds_transfer_transition: 0, address_credit_withdrawal_transition: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs index 520abac3ddd..0bfc45d857f 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs @@ -58,6 +58,10 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4: DriveStateTransitionMethodV token_shield_transition: 0, token_unshield_transition: 0, token_shielded_transfer_transition: 0, + token_mint_to_pool_transition: 0, + token_burn_from_pool_transition: 0, + token_claim_to_pool_transition: 0, + token_direct_purchase_to_pool_transition: 0, identity_credit_transfer_to_addresses_transition: 0, address_funds_transfer_transition: 0, address_credit_withdrawal_transition: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/mod.rs index 65df6fe5236..a5c0e65d8c6 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/mod.rs @@ -84,4 +84,8 @@ pub struct DriveTokenUpdateMethodVersions { pub unshield: FeatureVersion, /// Pool-internal token transfer. pub shielded_transfer: FeatureVersion, + /// Mint straight into the token shielded pool (supply and pool balance both grow). + pub mint_to_pool: FeatureVersion, + /// Burn notes held in the token shielded pool (pool balance and supply both shrink). + pub burn_from_pool: FeatureVersion, } diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v1.rs index 87c21b0774a..69853329399 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v1.rs @@ -56,6 +56,8 @@ pub const DRIVE_TOKEN_METHOD_VERSIONS_V1: DriveTokenMethodVersions = DriveTokenM shield: 0, unshield: 0, shielded_transfer: 0, + mint_to_pool: 0, + burn_from_pool: 0, }, calculate_total_tokens_balance: 0, distribution: DriveTokenDistributionMethodVersions { diff --git a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/burn_from_pool.rs b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/burn_from_pool.rs new file mode 100644 index 00000000000..45ae46bc0fc --- /dev/null +++ b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/burn_from_pool.rs @@ -0,0 +1,46 @@ +use dpp::state_transition::batch_transition::token_burn_from_pool_transition::v0::v0_methods::TokenBurnFromPoolTransitionV0Methods; +use dpp::state_transition::batch_transition::TokenBurnFromPoolTransition; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(js_name=TokenBurnFromPoolTransition)] +#[derive(Debug, Clone)] +pub struct TokenBurnFromPoolTransitionWasm(TokenBurnFromPoolTransition); + +impl From for TokenBurnFromPoolTransitionWasm { + fn from(value: TokenBurnFromPoolTransition) -> Self { + Self(value) + } +} + +#[wasm_bindgen(js_class = TokenBurnFromPoolTransition)] +impl TokenBurnFromPoolTransitionWasm { + #[wasm_bindgen(js_name=getAmount)] + pub fn amount(&self) -> u64 { + self.0.amount() + } + + #[wasm_bindgen(js_name=getPublicNote)] + pub fn public_note(&self) -> Option { + self.0.public_note().cloned() + } + + #[wasm_bindgen(js_name=getActionsCount)] + pub fn actions_count(&self) -> u32 { + self.0.actions().len() as u32 + } + + #[wasm_bindgen(js_name=getAnchor)] + pub fn anchor(&self) -> Vec { + self.0.anchor().to_vec() + } + + #[wasm_bindgen(js_name=getProof)] + pub fn proof(&self) -> Vec { + self.0.proof().to_vec() + } + + #[wasm_bindgen(js_name=getBindingSignature)] + pub fn binding_signature(&self) -> Vec { + self.0.binding_signature().to_vec() + } +} diff --git a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/claim_to_pool.rs b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/claim_to_pool.rs new file mode 100644 index 00000000000..2f3e7b4c4e0 --- /dev/null +++ b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/claim_to_pool.rs @@ -0,0 +1,55 @@ +use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; +use dpp::state_transition::batch_transition::token_claim_to_pool_transition::v0::v0_methods::TokenClaimToPoolTransitionV0Methods; +use dpp::state_transition::batch_transition::TokenClaimToPoolTransition; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(js_name=TokenClaimToPoolTransition)] +#[derive(Debug, Clone)] +pub struct TokenClaimToPoolTransitionWasm(TokenClaimToPoolTransition); + +impl From for TokenClaimToPoolTransitionWasm { + fn from(value: TokenClaimToPoolTransition) -> Self { + Self(value) + } +} + +#[wasm_bindgen(js_class = TokenClaimToPoolTransition)] +impl TokenClaimToPoolTransitionWasm { + #[wasm_bindgen(js_name=getDistributionType)] + pub fn distribution_type(&self) -> u8 { + match self.0.distribution_type() { + TokenDistributionType::PreProgrammed => 0, + TokenDistributionType::Perpetual => 1, + } + } + + #[wasm_bindgen(js_name=getClaimUpTo)] + pub fn claim_up_to(&self) -> Option { + self.0.claim_up_to() + } + + #[wasm_bindgen(js_name=getPublicNote)] + pub fn public_note(&self) -> Option { + self.0.public_note().cloned() + } + + #[wasm_bindgen(js_name=getActionsCount)] + pub fn actions_count(&self) -> u32 { + self.0.actions().len() as u32 + } + + #[wasm_bindgen(js_name=getAnchor)] + pub fn anchor(&self) -> Vec { + self.0.anchor().to_vec() + } + + #[wasm_bindgen(js_name=getProof)] + pub fn proof(&self) -> Vec { + self.0.proof().to_vec() + } + + #[wasm_bindgen(js_name=getBindingSignature)] + pub fn binding_signature(&self) -> Vec { + self.0.binding_signature().to_vec() + } +} diff --git a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/direct_purchase_to_pool.rs b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/direct_purchase_to_pool.rs new file mode 100644 index 00000000000..3ac95ad9c4a --- /dev/null +++ b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/direct_purchase_to_pool.rs @@ -0,0 +1,46 @@ +use dpp::state_transition::batch_transition::token_direct_purchase_to_pool_transition::v0::v0_methods::TokenDirectPurchaseToPoolTransitionV0Methods; +use dpp::state_transition::batch_transition::TokenDirectPurchaseToPoolTransition; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(js_name=TokenDirectPurchaseToPoolTransition)] +#[derive(Debug, Clone)] +pub struct TokenDirectPurchaseToPoolTransitionWasm(TokenDirectPurchaseToPoolTransition); + +impl From for TokenDirectPurchaseToPoolTransitionWasm { + fn from(value: TokenDirectPurchaseToPoolTransition) -> Self { + Self(value) + } +} + +#[wasm_bindgen(js_class = TokenDirectPurchaseToPoolTransition)] +impl TokenDirectPurchaseToPoolTransitionWasm { + #[wasm_bindgen(js_name=getTokenCount)] + pub fn token_count(&self) -> u64 { + self.0.token_count() + } + + #[wasm_bindgen(js_name=getTotalAgreedPrice)] + pub fn total_agreed_price(&self) -> u64 { + self.0.total_agreed_price() + } + + #[wasm_bindgen(js_name=getActionsCount)] + pub fn actions_count(&self) -> u32 { + self.0.actions().len() as u32 + } + + #[wasm_bindgen(js_name=getAnchor)] + pub fn anchor(&self) -> Vec { + self.0.anchor().to_vec() + } + + #[wasm_bindgen(js_name=getProof)] + pub fn proof(&self) -> Vec { + self.0.proof().to_vec() + } + + #[wasm_bindgen(js_name=getBindingSignature)] + pub fn binding_signature(&self) -> Vec { + self.0.binding_signature().to_vec() + } +} diff --git a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/mint_to_pool.rs b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/mint_to_pool.rs new file mode 100644 index 00000000000..be22dee4481 --- /dev/null +++ b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/mint_to_pool.rs @@ -0,0 +1,46 @@ +use dpp::state_transition::batch_transition::token_mint_to_pool_transition::v0::v0_methods::TokenMintToPoolTransitionV0Methods; +use dpp::state_transition::batch_transition::TokenMintToPoolTransition; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(js_name=TokenMintToPoolTransition)] +#[derive(Debug, Clone)] +pub struct TokenMintToPoolTransitionWasm(TokenMintToPoolTransition); + +impl From for TokenMintToPoolTransitionWasm { + fn from(value: TokenMintToPoolTransition) -> Self { + Self(value) + } +} + +#[wasm_bindgen(js_class = TokenMintToPoolTransition)] +impl TokenMintToPoolTransitionWasm { + #[wasm_bindgen(js_name=getAmount)] + pub fn amount(&self) -> u64 { + self.0.amount() + } + + #[wasm_bindgen(js_name=getPublicNote)] + pub fn public_note(&self) -> Option { + self.0.public_note().cloned() + } + + #[wasm_bindgen(js_name=getActionsCount)] + pub fn actions_count(&self) -> u32 { + self.0.actions().len() as u32 + } + + #[wasm_bindgen(js_name=getAnchor)] + pub fn anchor(&self) -> Vec { + self.0.anchor().to_vec() + } + + #[wasm_bindgen(js_name=getProof)] + pub fn proof(&self) -> Vec { + self.0.proof().to_vec() + } + + #[wasm_bindgen(js_name=getBindingSignature)] + pub fn binding_signature(&self) -> Vec { + self.0.binding_signature().to_vec() + } +} diff --git a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/mod.rs b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/mod.rs index c8d8cae5f2e..c1d5a7a1356 100644 --- a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/mod.rs +++ b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/mod.rs @@ -1,11 +1,15 @@ pub mod burn; +pub mod burn_from_pool; mod claim; +pub mod claim_to_pool; pub mod config; pub mod destroy; pub mod direct_purchase; +pub mod direct_purchase_to_pool; pub mod emergency_action; pub mod freeze; pub mod mint; +pub mod mint_to_pool; pub mod set_price_for_direct_purchase; pub mod shield; pub mod shielded_transfer; @@ -14,13 +18,17 @@ pub mod unfreeze; pub mod unshield; use crate::batch_transition::token_transition::burn::TokenBurnTransitionWasm; +use crate::batch_transition::token_transition::burn_from_pool::TokenBurnFromPoolTransitionWasm; use crate::batch_transition::token_transition::claim::TokenClaimTransitionWasm; +use crate::batch_transition::token_transition::claim_to_pool::TokenClaimToPoolTransitionWasm; use crate::batch_transition::token_transition::config::TokenConfigUpdateTransitionWasm; use crate::batch_transition::token_transition::destroy::TokenDestroyFrozenFundsTransitionWasm; use crate::batch_transition::token_transition::direct_purchase::TokenDirectPurchaseTransitionWasm; +use crate::batch_transition::token_transition::direct_purchase_to_pool::TokenDirectPurchaseToPoolTransitionWasm; use crate::batch_transition::token_transition::emergency_action::TokenEmergencyActionTransitionWasm; use crate::batch_transition::token_transition::freeze::TokenFreezeTransitionWasm; use crate::batch_transition::token_transition::mint::TokenMintTransitionWasm; +use crate::batch_transition::token_transition::mint_to_pool::TokenMintToPoolTransitionWasm; use crate::batch_transition::token_transition::set_price_for_direct_purchase::TokenSetPriceForDirectPurchaseTransitionWasm; use crate::batch_transition::token_transition::shield::TokenShieldTransitionWasm; use crate::batch_transition::token_transition::shielded_transfer::TokenShieldedTransferTransitionWasm; @@ -53,6 +61,10 @@ pub enum TokenTransitionType { Shield, Unshield, ShieldedTransfer, + MintToPool, + BurnFromPool, + ClaimToPool, + DirectPurchaseToPool, } impl From<&TokenTransition> for TokenTransitionType { @@ -74,6 +86,10 @@ impl From<&TokenTransition> for TokenTransitionType { TokenTransition::Shield(_) => TokenTransitionType::Shield, TokenTransition::Unshield(_) => TokenTransitionType::Unshield, TokenTransition::ShieldedTransfer(_) => TokenTransitionType::ShieldedTransfer, + TokenTransition::MintToPool(_) => TokenTransitionType::MintToPool, + TokenTransition::BurnFromPool(_) => TokenTransitionType::BurnFromPool, + TokenTransition::ClaimToPool(_) => TokenTransitionType::ClaimToPool, + TokenTransition::DirectPurchaseToPool(_) => TokenTransitionType::DirectPurchaseToPool, } } } @@ -170,6 +186,19 @@ impl TokenTransitionWasm { TokenTransition::ShieldedTransfer(shielded_transfer) => { TokenShieldedTransferTransitionWasm::from(shielded_transfer.clone()).into() } + TokenTransition::MintToPool(mint_to_pool) => { + TokenMintToPoolTransitionWasm::from(mint_to_pool.clone()).into() + } + TokenTransition::BurnFromPool(burn_from_pool) => { + TokenBurnFromPoolTransitionWasm::from(burn_from_pool.clone()).into() + } + TokenTransition::ClaimToPool(claim_to_pool) => { + TokenClaimToPoolTransitionWasm::from(claim_to_pool.clone()).into() + } + TokenTransition::DirectPurchaseToPool(direct_purchase_to_pool) => { + TokenDirectPurchaseToPoolTransitionWasm::from(direct_purchase_to_pool.clone()) + .into() + } } } } diff --git a/packages/wasm-dpp2/src/group/token_event.rs b/packages/wasm-dpp2/src/group/token_event.rs index 1c91f3e74ee..74d0fb8071d 100644 --- a/packages/wasm-dpp2/src/group/token_event.rs +++ b/packages/wasm-dpp2/src/group/token_event.rs @@ -28,6 +28,10 @@ const TS_TYPES: &str = r#" * - Shield: { $type: "shield", amount } * - Unshield:{ $type: "unshield", recipientId, amount } * - ShieldedTransfer: { $type: "shieldedTransfer" } + * - MintToPool: { $type: "mintToPool", amount, actionsDigest, publicNote } + * - BurnFromPool: { $type: "burnFromPool", amount, actionsDigest, publicNote } + * - ClaimToPool: { $type: "claimToPool", amount } + * - DirectPurchaseToPool: { $type: "directPurchaseToPool", amount, credits } * * `amount`/`credits` are routed through json_safe_u64 — small numbers, JS * BigInt-safe stringification above 2^53. Identifier fields use base58 in @@ -75,6 +79,10 @@ pub enum TokenEventVariant { Shield = 11, Unshield = 12, ShieldedTransfer = 13, + MintToPool = 14, + BurnFromPool = 15, + ClaimToPool = 16, + DirectPurchaseToPool = 17, } #[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] @@ -115,6 +123,10 @@ impl TokenEventWasm { TokenEvent::Shield(..) => TokenEventVariant::Shield, TokenEvent::Unshield(..) => TokenEventVariant::Unshield, TokenEvent::ShieldedTransfer => TokenEventVariant::ShieldedTransfer, + TokenEvent::MintToPool(..) => TokenEventVariant::MintToPool, + TokenEvent::BurnFromPool(..) => TokenEventVariant::BurnFromPool, + TokenEvent::ClaimToPool(..) => TokenEventVariant::ClaimToPool, + TokenEvent::DirectPurchaseToPool(..) => TokenEventVariant::DirectPurchaseToPool, } } } diff --git a/packages/wasm-dpp2/src/state_transitions/batch/token_transition.rs b/packages/wasm-dpp2/src/state_transitions/batch/token_transition.rs index 9f609413cf7..500b38224f2 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/token_transition.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/token_transition.rs @@ -7,11 +7,15 @@ use crate::state_transitions::batch::token_transitions::config_update::TokenConf use crate::state_transitions::batch::token_transitions::direct_purchase::TokenDirectPurchaseTransitionWasm; use crate::state_transitions::batch::token_transitions::set_price_for_direct_purchase::TokenSetPriceForDirectPurchaseTransitionWasm; use crate::state_transitions::batch::token_transitions::token_burn::TokenBurnTransitionWasm; +use crate::state_transitions::batch::token_transitions::token_burn_from_pool::TokenBurnFromPoolTransitionWasm; use crate::state_transitions::batch::token_transitions::token_claim::TokenClaimTransitionWasm; +use crate::state_transitions::batch::token_transitions::token_claim_to_pool::TokenClaimToPoolTransitionWasm; use crate::state_transitions::batch::token_transitions::token_destroy_frozen_funds::TokenDestroyFrozenFundsTransitionWasm; +use crate::state_transitions::batch::token_transitions::token_direct_purchase_to_pool::TokenDirectPurchaseToPoolTransitionWasm; use crate::state_transitions::batch::token_transitions::token_emergency_action::TokenEmergencyActionTransitionWasm; use crate::state_transitions::batch::token_transitions::token_freeze::TokenFreezeTransitionWasm; use crate::state_transitions::batch::token_transitions::token_mint::TokenMintTransitionWasm; +use crate::state_transitions::batch::token_transitions::token_mint_to_pool::TokenMintToPoolTransitionWasm; use crate::state_transitions::batch::token_transitions::token_shield::TokenShieldTransitionWasm; use crate::state_transitions::batch::token_transitions::token_shielded_transfer::TokenShieldedTransferTransitionWasm; use crate::state_transitions::batch::token_transitions::token_transfer::TokenTransferTransitionWasm; @@ -23,10 +27,11 @@ use dpp::state_transition::batch_transition::batched_transition::token_transitio TokenTransition, TokenTransitionV0Methods, }; use dpp::state_transition::batch_transition::{ - TokenBurnTransition, TokenClaimTransition, TokenConfigUpdateTransition, - TokenDestroyFrozenFundsTransition, TokenDirectPurchaseTransition, - TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintTransition, - TokenSetPriceForDirectPurchaseTransition, TokenShieldTransition, + TokenBurnFromPoolTransition, TokenBurnTransition, TokenClaimToPoolTransition, + TokenClaimTransition, TokenConfigUpdateTransition, TokenDestroyFrozenFundsTransition, + TokenDirectPurchaseToPoolTransition, TokenDirectPurchaseTransition, + TokenEmergencyActionTransition, TokenFreezeTransition, TokenMintToPoolTransition, + TokenMintTransition, TokenSetPriceForDirectPurchaseTransition, TokenShieldTransition, TokenShieldedTransferTransition, TokenTransferTransition, TokenUnfreezeTransition, TokenUnshieldTransition, }; @@ -35,7 +40,7 @@ use wasm_bindgen::prelude::wasm_bindgen; #[wasm_bindgen(typescript_custom_section)] const TOKEN_TRANSITION_TYPES_TS: &str = r#" -export type TokenTransitionLike = TokenMintTransition | TokenBurnTransition | TokenTransferTransition | TokenFreezeTransition | TokenUnFreezeTransition | TokenDestroyFrozenFundsTransition | TokenClaimTransition | TokenEmergencyActionTransition | TokenConfigUpdateTransition | TokenDirectPurchaseTransition | TokenSetPriceForDirectPurchaseTransition | TokenShieldTransition | TokenUnshieldTransition | TokenShieldedTransferTransition; +export type TokenTransitionLike = TokenMintTransition | TokenBurnTransition | TokenTransferTransition | TokenFreezeTransition | TokenUnFreezeTransition | TokenDestroyFrozenFundsTransition | TokenClaimTransition | TokenEmergencyActionTransition | TokenConfigUpdateTransition | TokenDirectPurchaseTransition | TokenSetPriceForDirectPurchaseTransition | TokenShieldTransition | TokenUnshieldTransition | TokenShieldedTransferTransition | TokenMintToPoolTransition | TokenBurnFromPoolTransition | TokenClaimToPoolTransition | TokenDirectPurchaseToPoolTransition; "#; /// Extern type for flexible TokenTransition input @@ -61,6 +66,10 @@ impl_from_for_extern_type!( TokenShieldTransitionWasm, TokenUnshieldTransitionWasm, TokenShieldedTransferTransitionWasm, + TokenMintToPoolTransitionWasm, + TokenBurnFromPoolTransitionWasm, + TokenClaimToPoolTransitionWasm, + TokenDirectPurchaseToPoolTransitionWasm, ); #[derive(Debug, Clone, PartialEq)] @@ -143,6 +152,24 @@ impl TokenTransitionWasm { TokenShieldedTransferTransitionWasm::try_from(&transition)?, )) } + "TokenMintToPoolTransition" => TokenTransition::from(TokenMintToPoolTransition::from( + TokenMintToPoolTransitionWasm::try_from(&transition)?, + )), + "TokenBurnFromPoolTransition" => { + TokenTransition::from(TokenBurnFromPoolTransition::from( + TokenBurnFromPoolTransitionWasm::try_from(&transition)?, + )) + } + "TokenClaimToPoolTransition" => { + TokenTransition::from(TokenClaimToPoolTransition::from( + TokenClaimToPoolTransitionWasm::try_from(&transition)?, + )) + } + "TokenDirectPurchaseToPoolTransition" => { + TokenTransition::from(TokenDirectPurchaseToPoolTransition::from( + TokenDirectPurchaseToPoolTransitionWasm::try_from(&transition)?, + )) + } _ => { return Err(WasmDppError::invalid_argument("Bad token transition input")); } @@ -196,6 +223,18 @@ impl TokenTransitionWasm { TokenTransition::ShieldedTransfer(token_transition) => { TokenShieldedTransferTransitionWasm::from(token_transition).into() } + TokenTransition::MintToPool(token_transition) => { + TokenMintToPoolTransitionWasm::from(token_transition).into() + } + TokenTransition::BurnFromPool(token_transition) => { + TokenBurnFromPoolTransitionWasm::from(token_transition).into() + } + TokenTransition::ClaimToPool(token_transition) => { + TokenClaimToPoolTransitionWasm::from(token_transition).into() + } + TokenTransition::DirectPurchaseToPool(token_transition) => { + TokenDirectPurchaseToPoolTransitionWasm::from(token_transition).into() + } } } @@ -216,6 +255,10 @@ impl TokenTransitionWasm { TokenTransition::Shield(_) => 11, TokenTransition::Unshield(_) => 12, TokenTransition::ShieldedTransfer(_) => 13, + TokenTransition::MintToPool(_) => 14, + TokenTransition::BurnFromPool(_) => 15, + TokenTransition::ClaimToPool(_) => 16, + TokenTransition::DirectPurchaseToPool(_) => 17, } } @@ -238,6 +281,10 @@ impl TokenTransitionWasm { TokenTransition::Shield(_) => "Shield".to_string(), TokenTransition::Unshield(_) => "Unshield".to_string(), TokenTransition::ShieldedTransfer(_) => "ShieldedTransfer".to_string(), + TokenTransition::MintToPool(_) => "MintToPool".to_string(), + TokenTransition::BurnFromPool(_) => "BurnFromPool".to_string(), + TokenTransition::ClaimToPool(_) => "ClaimToPool".to_string(), + TokenTransition::DirectPurchaseToPool(_) => "DirectPurchaseToPool".to_string(), } } diff --git a/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/mod.rs b/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/mod.rs index 482fa4064f7..8127328dc8b 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/mod.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/mod.rs @@ -2,11 +2,15 @@ pub mod config_update; pub mod direct_purchase; pub mod set_price_for_direct_purchase; pub mod token_burn; +pub mod token_burn_from_pool; pub mod token_claim; +pub mod token_claim_to_pool; pub mod token_destroy_frozen_funds; +pub mod token_direct_purchase_to_pool; pub mod token_emergency_action; pub mod token_freeze; pub mod token_mint; +pub mod token_mint_to_pool; pub mod token_shield; pub mod token_shielded_transfer; pub mod token_transfer; diff --git a/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_burn_from_pool.rs b/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_burn_from_pool.rs new file mode 100644 index 00000000000..dd6c7ae59b8 --- /dev/null +++ b/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_burn_from_pool.rs @@ -0,0 +1,166 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::impl_try_from_js_value; +use crate::impl_wasm_type_info; +use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; +use crate::state_transitions::batch::token_base_transition::TokenBaseTransitionWasm; +use crate::utils::{try_from_options, try_vec_to_fixed_bytes}; +use crate::utils::{try_from_options_optional_with, try_to_string, try_to_u64}; +use dpp::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use dpp::state_transition::batch_transition::token_burn_from_pool_transition::v0::v0_methods::TokenBurnFromPoolTransitionV0Methods; +use dpp::state_transition::batch_transition::token_burn_from_pool_transition::TokenBurnFromPoolTransitionV0; +use dpp::state_transition::batch_transition::TokenBurnFromPoolTransition; +use serde::Deserialize; +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(typescript_custom_section)] +const TOKEN_BURN_FROM_POOL_OPTIONS_TS: &str = r#" +/** + * Options for constructing a TokenBurnFromPoolTransition. Destroys `amount` tokens held in the token's shielded pool by spending the given notes; the signer must be authorized to burn. + */ +export interface TokenBurnFromPoolTransitionOptions { + base: TokenBaseTransition; + amount: bigint; + publicNote?: string; + actions: SerializedOrchardAction[]; + anchor: Uint8Array; + proof: Uint8Array; + bindingSignature: Uint8Array; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "TokenBurnFromPoolTransitionOptions")] + pub type TokenBurnFromPoolTransitionOptionsJs; +} + +/// Non-WASM-instance fields extracted from the constructor options via serde. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct TokenBurnFromPoolTransitionSimpleFields { + amount: u64, + anchor: Vec, + proof: Vec, + binding_signature: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +#[wasm_bindgen(js_name = "TokenBurnFromPoolTransition")] +pub struct TokenBurnFromPoolTransitionWasm(TokenBurnFromPoolTransition); + +impl From for TokenBurnFromPoolTransitionWasm { + fn from(transition: TokenBurnFromPoolTransition) -> Self { + Self(transition) + } +} + +impl From for TokenBurnFromPoolTransition { + fn from(transition: TokenBurnFromPoolTransitionWasm) -> Self { + transition.0 + } +} + +#[wasm_bindgen(js_class = TokenBurnFromPoolTransition)] +impl TokenBurnFromPoolTransitionWasm { + #[wasm_bindgen(constructor)] + pub fn constructor( + options: TokenBurnFromPoolTransitionOptionsJs, + ) -> WasmDppResult { + let js_opts: &JsValue = options.as_ref(); + + let base: TokenBaseTransitionWasm = try_from_options(js_opts, "base")?; + let public_note: Option = + try_from_options_optional_with(js_opts, "publicNote", |v| { + try_to_string(v, "publicNote") + })?; + let actions = actions_from_js_options(js_opts, "actions")?; + + let fields: TokenBurnFromPoolTransitionSimpleFields = + serde_wasm_bindgen::from_value(options.into()) + .map_err(|e| WasmDppError::invalid_argument(e.to_string()))?; + + let anchor: [u8; 32] = try_vec_to_fixed_bytes(fields.anchor, "anchor")?; + let binding_signature: [u8; 64] = + try_vec_to_fixed_bytes(fields.binding_signature, "bindingSignature")?; + + Ok(TokenBurnFromPoolTransitionWasm( + TokenBurnFromPoolTransition::V0(TokenBurnFromPoolTransitionV0 { + base: base.into(), + amount: fields.amount, + public_note, + actions: actions.into_iter().map(Into::into).collect(), + anchor, + proof: fields.proof, + binding_signature, + }), + )) + } + + #[wasm_bindgen(getter = "base")] + pub fn base(&self) -> TokenBaseTransitionWasm { + self.0.base().clone().into() + } + + #[wasm_bindgen(setter = "base")] + pub fn set_base(&mut self, base: TokenBaseTransitionWasm) { + self.0.set_base(base.into()) + } + + /// Tokens crossing the pool boundary. + #[wasm_bindgen(getter = "amount")] + pub fn amount(&self) -> u64 { + self.0.amount() + } + + #[wasm_bindgen(setter = "amount")] + pub fn set_amount(&mut self, amount: &js_sys::BigInt) -> WasmDppResult<()> { + self.0.set_amount(try_to_u64(amount, "amount")?); + Ok(()) + } + + #[wasm_bindgen(getter = "publicNote")] + pub fn public_note(&self) -> Option { + self.0.public_note().cloned() + } + + #[wasm_bindgen(setter = "publicNote")] + pub fn set_public_note(&mut self, note: Option) { + self.0.set_public_note(note) + } + + /// The serialized Orchard actions. + #[wasm_bindgen(getter = "actions")] + pub fn actions(&self) -> Vec { + self.0 + .actions() + .iter() + .cloned() + .map(SerializedOrchardActionWasm::from) + .collect() + } + + /// The Orchard anchor (32-byte note commitment tree root) the bundle was built against. + #[wasm_bindgen(getter = "anchor")] + pub fn anchor(&self) -> Vec { + self.0.anchor().to_vec() + } + + /// The Halo 2 proof bytes. + #[wasm_bindgen(getter = "proof")] + pub fn proof(&self) -> Vec { + self.0.proof().to_vec() + } + + /// The RedPallas binding signature (64 bytes). + #[wasm_bindgen(getter = "bindingSignature")] + pub fn binding_signature(&self) -> Vec { + self.0.binding_signature().to_vec() + } +} + +impl_try_from_js_value!( + TokenBurnFromPoolTransitionWasm, + "TokenBurnFromPoolTransition" +); +impl_wasm_type_info!(TokenBurnFromPoolTransitionWasm, TokenBurnFromPoolTransition); diff --git a/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_claim_to_pool.rs b/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_claim_to_pool.rs new file mode 100644 index 00000000000..fe258476d2c --- /dev/null +++ b/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_claim_to_pool.rs @@ -0,0 +1,168 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::impl_try_from_js_value; +use crate::impl_wasm_type_info; +use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; +use crate::state_transitions::batch::token_base_transition::TokenBaseTransitionWasm; +use crate::utils::{try_from_options, try_vec_to_fixed_bytes}; +use crate::enums::token::distribution_type::TokenDistributionTypeWasm; +use crate::utils::{try_from_options_optional, try_from_options_optional_with, try_to_string}; +use dpp::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use dpp::state_transition::batch_transition::token_claim_to_pool_transition::v0::v0_methods::TokenClaimToPoolTransitionV0Methods; +use dpp::state_transition::batch_transition::token_claim_to_pool_transition::TokenClaimToPoolTransitionV0; +use dpp::state_transition::batch_transition::TokenClaimToPoolTransition; +use serde::Deserialize; +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(typescript_custom_section)] +const TOKEN_CLAIM_TO_POOL_OPTIONS_TS: &str = r#" +/** + * Options for constructing a TokenClaimToPoolTransition. Claims a distribution straight into the token's shielded pool; a perpetual claim names the cycle-aligned moment it claims up to. + */ +export interface TokenClaimToPoolTransitionOptions { + base: TokenBaseTransition; + distributionType?: TokenDistributionTypeLike; + claimUpTo?: bigint; + publicNote?: string; + actions: SerializedOrchardAction[]; + anchor: Uint8Array; + proof: Uint8Array; + bindingSignature: Uint8Array; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "TokenClaimToPoolTransitionOptions")] + pub type TokenClaimToPoolTransitionOptionsJs; +} + +/// Non-WASM-instance fields extracted from the constructor options via serde. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct TokenClaimToPoolTransitionSimpleFields { + #[serde(default)] + claim_up_to: Option, + anchor: Vec, + proof: Vec, + binding_signature: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +#[wasm_bindgen(js_name = "TokenClaimToPoolTransition")] +pub struct TokenClaimToPoolTransitionWasm(TokenClaimToPoolTransition); + +impl From for TokenClaimToPoolTransitionWasm { + fn from(transition: TokenClaimToPoolTransition) -> Self { + Self(transition) + } +} + +impl From for TokenClaimToPoolTransition { + fn from(transition: TokenClaimToPoolTransitionWasm) -> Self { + transition.0 + } +} + +#[wasm_bindgen(js_class = TokenClaimToPoolTransition)] +impl TokenClaimToPoolTransitionWasm { + #[wasm_bindgen(constructor)] + pub fn constructor( + options: TokenClaimToPoolTransitionOptionsJs, + ) -> WasmDppResult { + let js_opts: &JsValue = options.as_ref(); + + let base: TokenBaseTransitionWasm = try_from_options(js_opts, "base")?; + let public_note: Option = + try_from_options_optional_with(js_opts, "publicNote", |v| { + try_to_string(v, "publicNote") + })?; + let distribution_type: TokenDistributionTypeWasm = + try_from_options_optional(js_opts, "distributionType")?.unwrap_or_default(); + let actions = actions_from_js_options(js_opts, "actions")?; + + let fields: TokenClaimToPoolTransitionSimpleFields = + serde_wasm_bindgen::from_value(options.into()) + .map_err(|e| WasmDppError::invalid_argument(e.to_string()))?; + + let anchor: [u8; 32] = try_vec_to_fixed_bytes(fields.anchor, "anchor")?; + let binding_signature: [u8; 64] = + try_vec_to_fixed_bytes(fields.binding_signature, "bindingSignature")?; + + Ok(TokenClaimToPoolTransitionWasm( + TokenClaimToPoolTransition::V0(TokenClaimToPoolTransitionV0 { + base: base.into(), + distribution_type: distribution_type.into(), + claim_up_to: fields.claim_up_to, + public_note, + actions: actions.into_iter().map(Into::into).collect(), + anchor, + proof: fields.proof, + binding_signature, + }), + )) + } + + #[wasm_bindgen(getter = "base")] + pub fn base(&self) -> TokenBaseTransitionWasm { + self.0.base().clone().into() + } + + #[wasm_bindgen(setter = "base")] + pub fn set_base(&mut self, base: TokenBaseTransitionWasm) { + self.0.set_base(base.into()) + } + + #[wasm_bindgen(getter = "distributionType")] + pub fn distribution_type(&self) -> String { + TokenDistributionTypeWasm::from(self.0.distribution_type()).into() + } + + /// The moment a perpetual claim pays out to, in the distribution's own unit. + #[wasm_bindgen(getter = "claimUpTo")] + pub fn claim_up_to(&self) -> Option { + self.0.claim_up_to() + } + + #[wasm_bindgen(getter = "publicNote")] + pub fn public_note(&self) -> Option { + self.0.public_note().cloned() + } + + #[wasm_bindgen(setter = "publicNote")] + pub fn set_public_note(&mut self, note: Option) { + self.0.set_public_note(note) + } + + /// The serialized Orchard actions. + #[wasm_bindgen(getter = "actions")] + pub fn actions(&self) -> Vec { + self.0 + .actions() + .iter() + .cloned() + .map(SerializedOrchardActionWasm::from) + .collect() + } + + /// The Orchard anchor (32-byte note commitment tree root) the bundle was built against. + #[wasm_bindgen(getter = "anchor")] + pub fn anchor(&self) -> Vec { + self.0.anchor().to_vec() + } + + /// The Halo 2 proof bytes. + #[wasm_bindgen(getter = "proof")] + pub fn proof(&self) -> Vec { + self.0.proof().to_vec() + } + + /// The RedPallas binding signature (64 bytes). + #[wasm_bindgen(getter = "bindingSignature")] + pub fn binding_signature(&self) -> Vec { + self.0.binding_signature().to_vec() + } +} + +impl_try_from_js_value!(TokenClaimToPoolTransitionWasm, "TokenClaimToPoolTransition"); +impl_wasm_type_info!(TokenClaimToPoolTransitionWasm, TokenClaimToPoolTransition); diff --git a/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_direct_purchase_to_pool.rs b/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_direct_purchase_to_pool.rs new file mode 100644 index 00000000000..98ecc655920 --- /dev/null +++ b/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_direct_purchase_to_pool.rs @@ -0,0 +1,153 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::impl_try_from_js_value; +use crate::impl_wasm_type_info; +use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; +use crate::state_transitions::batch::token_base_transition::TokenBaseTransitionWasm; +use crate::utils::{try_from_options, try_vec_to_fixed_bytes}; +use dpp::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use dpp::state_transition::batch_transition::token_direct_purchase_to_pool_transition::v0::v0_methods::TokenDirectPurchaseToPoolTransitionV0Methods; +use dpp::state_transition::batch_transition::token_direct_purchase_to_pool_transition::TokenDirectPurchaseToPoolTransitionV0; +use dpp::state_transition::batch_transition::TokenDirectPurchaseToPoolTransition; +use serde::Deserialize; +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(typescript_custom_section)] +const TOKEN_DIRECT_PURCHASE_TO_POOL_OPTIONS_TS: &str = r#" +/** + * Options for constructing a TokenDirectPurchaseToPoolTransition. Buys `tokenCount` tokens for at most `totalAgreedPrice` credits and receives them shielded as the given notes. + */ +export interface TokenDirectPurchaseToPoolTransitionOptions { + base: TokenBaseTransition; + tokenCount: bigint; + totalAgreedPrice: bigint; + actions: SerializedOrchardAction[]; + anchor: Uint8Array; + proof: Uint8Array; + bindingSignature: Uint8Array; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "TokenDirectPurchaseToPoolTransitionOptions")] + pub type TokenDirectPurchaseToPoolTransitionOptionsJs; +} + +/// Non-WASM-instance fields extracted from the constructor options via serde. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct TokenDirectPurchaseToPoolTransitionSimpleFields { + token_count: u64, + total_agreed_price: u64, + anchor: Vec, + proof: Vec, + binding_signature: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +#[wasm_bindgen(js_name = "TokenDirectPurchaseToPoolTransition")] +pub struct TokenDirectPurchaseToPoolTransitionWasm(TokenDirectPurchaseToPoolTransition); + +impl From for TokenDirectPurchaseToPoolTransitionWasm { + fn from(transition: TokenDirectPurchaseToPoolTransition) -> Self { + Self(transition) + } +} + +impl From for TokenDirectPurchaseToPoolTransition { + fn from(transition: TokenDirectPurchaseToPoolTransitionWasm) -> Self { + transition.0 + } +} + +#[wasm_bindgen(js_class = TokenDirectPurchaseToPoolTransition)] +impl TokenDirectPurchaseToPoolTransitionWasm { + #[wasm_bindgen(constructor)] + pub fn constructor( + options: TokenDirectPurchaseToPoolTransitionOptionsJs, + ) -> WasmDppResult { + let js_opts: &JsValue = options.as_ref(); + + let base: TokenBaseTransitionWasm = try_from_options(js_opts, "base")?; + let actions = actions_from_js_options(js_opts, "actions")?; + + let fields: TokenDirectPurchaseToPoolTransitionSimpleFields = + serde_wasm_bindgen::from_value(options.into()) + .map_err(|e| WasmDppError::invalid_argument(e.to_string()))?; + + let anchor: [u8; 32] = try_vec_to_fixed_bytes(fields.anchor, "anchor")?; + let binding_signature: [u8; 64] = + try_vec_to_fixed_bytes(fields.binding_signature, "bindingSignature")?; + + Ok(TokenDirectPurchaseToPoolTransitionWasm( + TokenDirectPurchaseToPoolTransition::V0(TokenDirectPurchaseToPoolTransitionV0 { + base: base.into(), + token_count: fields.token_count, + total_agreed_price: fields.total_agreed_price, + actions: actions.into_iter().map(Into::into).collect(), + anchor, + proof: fields.proof, + binding_signature, + }), + )) + } + + #[wasm_bindgen(getter = "base")] + pub fn base(&self) -> TokenBaseTransitionWasm { + self.0.base().clone().into() + } + + #[wasm_bindgen(setter = "base")] + pub fn set_base(&mut self, base: TokenBaseTransitionWasm) { + self.0.set_base(base.into()) + } + + #[wasm_bindgen(getter = "tokenCount")] + pub fn token_count(&self) -> u64 { + self.0.token_count() + } + + #[wasm_bindgen(getter = "totalAgreedPrice")] + pub fn total_agreed_price(&self) -> u64 { + self.0.total_agreed_price() + } + + /// The serialized Orchard actions. + #[wasm_bindgen(getter = "actions")] + pub fn actions(&self) -> Vec { + self.0 + .actions() + .iter() + .cloned() + .map(SerializedOrchardActionWasm::from) + .collect() + } + + /// The Orchard anchor (32-byte note commitment tree root) the bundle was built against. + #[wasm_bindgen(getter = "anchor")] + pub fn anchor(&self) -> Vec { + self.0.anchor().to_vec() + } + + /// The Halo 2 proof bytes. + #[wasm_bindgen(getter = "proof")] + pub fn proof(&self) -> Vec { + self.0.proof().to_vec() + } + + /// The RedPallas binding signature (64 bytes). + #[wasm_bindgen(getter = "bindingSignature")] + pub fn binding_signature(&self) -> Vec { + self.0.binding_signature().to_vec() + } +} + +impl_try_from_js_value!( + TokenDirectPurchaseToPoolTransitionWasm, + "TokenDirectPurchaseToPoolTransition" +); +impl_wasm_type_info!( + TokenDirectPurchaseToPoolTransitionWasm, + TokenDirectPurchaseToPoolTransition +); diff --git a/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_mint_to_pool.rs b/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_mint_to_pool.rs new file mode 100644 index 00000000000..acd9b19afbd --- /dev/null +++ b/packages/wasm-dpp2/src/state_transitions/batch/token_transitions/token_mint_to_pool.rs @@ -0,0 +1,163 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::impl_try_from_js_value; +use crate::impl_wasm_type_info; +use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; +use crate::state_transitions::batch::token_base_transition::TokenBaseTransitionWasm; +use crate::utils::{try_from_options, try_vec_to_fixed_bytes}; +use crate::utils::{try_from_options_optional_with, try_to_string, try_to_u64}; +use dpp::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; +use dpp::state_transition::batch_transition::token_mint_to_pool_transition::v0::v0_methods::TokenMintToPoolTransitionV0Methods; +use dpp::state_transition::batch_transition::token_mint_to_pool_transition::TokenMintToPoolTransitionV0; +use dpp::state_transition::batch_transition::TokenMintToPoolTransition; +use serde::Deserialize; +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(typescript_custom_section)] +const TOKEN_MINT_TO_POOL_OPTIONS_TS: &str = r#" +/** + * Options for constructing a TokenMintToPoolTransition. Mints `amount` tokens straight into the token's shielded pool as the given notes; the signer must be authorized to mint. + */ +export interface TokenMintToPoolTransitionOptions { + base: TokenBaseTransition; + amount: bigint; + publicNote?: string; + actions: SerializedOrchardAction[]; + anchor: Uint8Array; + proof: Uint8Array; + bindingSignature: Uint8Array; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "TokenMintToPoolTransitionOptions")] + pub type TokenMintToPoolTransitionOptionsJs; +} + +/// Non-WASM-instance fields extracted from the constructor options via serde. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct TokenMintToPoolTransitionSimpleFields { + amount: u64, + anchor: Vec, + proof: Vec, + binding_signature: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +#[wasm_bindgen(js_name = "TokenMintToPoolTransition")] +pub struct TokenMintToPoolTransitionWasm(TokenMintToPoolTransition); + +impl From for TokenMintToPoolTransitionWasm { + fn from(transition: TokenMintToPoolTransition) -> Self { + Self(transition) + } +} + +impl From for TokenMintToPoolTransition { + fn from(transition: TokenMintToPoolTransitionWasm) -> Self { + transition.0 + } +} + +#[wasm_bindgen(js_class = TokenMintToPoolTransition)] +impl TokenMintToPoolTransitionWasm { + #[wasm_bindgen(constructor)] + pub fn constructor( + options: TokenMintToPoolTransitionOptionsJs, + ) -> WasmDppResult { + let js_opts: &JsValue = options.as_ref(); + + let base: TokenBaseTransitionWasm = try_from_options(js_opts, "base")?; + let public_note: Option = + try_from_options_optional_with(js_opts, "publicNote", |v| { + try_to_string(v, "publicNote") + })?; + let actions = actions_from_js_options(js_opts, "actions")?; + + let fields: TokenMintToPoolTransitionSimpleFields = + serde_wasm_bindgen::from_value(options.into()) + .map_err(|e| WasmDppError::invalid_argument(e.to_string()))?; + + let anchor: [u8; 32] = try_vec_to_fixed_bytes(fields.anchor, "anchor")?; + let binding_signature: [u8; 64] = + try_vec_to_fixed_bytes(fields.binding_signature, "bindingSignature")?; + + Ok(TokenMintToPoolTransitionWasm( + TokenMintToPoolTransition::V0(TokenMintToPoolTransitionV0 { + base: base.into(), + amount: fields.amount, + public_note, + actions: actions.into_iter().map(Into::into).collect(), + anchor, + proof: fields.proof, + binding_signature, + }), + )) + } + + #[wasm_bindgen(getter = "base")] + pub fn base(&self) -> TokenBaseTransitionWasm { + self.0.base().clone().into() + } + + #[wasm_bindgen(setter = "base")] + pub fn set_base(&mut self, base: TokenBaseTransitionWasm) { + self.0.set_base(base.into()) + } + + /// Tokens crossing the pool boundary. + #[wasm_bindgen(getter = "amount")] + pub fn amount(&self) -> u64 { + self.0.amount() + } + + #[wasm_bindgen(setter = "amount")] + pub fn set_amount(&mut self, amount: &js_sys::BigInt) -> WasmDppResult<()> { + self.0.set_amount(try_to_u64(amount, "amount")?); + Ok(()) + } + + #[wasm_bindgen(getter = "publicNote")] + pub fn public_note(&self) -> Option { + self.0.public_note().cloned() + } + + #[wasm_bindgen(setter = "publicNote")] + pub fn set_public_note(&mut self, note: Option) { + self.0.set_public_note(note) + } + + /// The serialized Orchard actions. + #[wasm_bindgen(getter = "actions")] + pub fn actions(&self) -> Vec { + self.0 + .actions() + .iter() + .cloned() + .map(SerializedOrchardActionWasm::from) + .collect() + } + + /// The Orchard anchor (32-byte note commitment tree root) the bundle was built against. + #[wasm_bindgen(getter = "anchor")] + pub fn anchor(&self) -> Vec { + self.0.anchor().to_vec() + } + + /// The Halo 2 proof bytes. + #[wasm_bindgen(getter = "proof")] + pub fn proof(&self) -> Vec { + self.0.proof().to_vec() + } + + /// The RedPallas binding signature (64 bytes). + #[wasm_bindgen(getter = "bindingSignature")] + pub fn binding_signature(&self) -> Vec { + self.0.binding_signature().to_vec() + } +} + +impl_try_from_js_value!(TokenMintToPoolTransitionWasm, "TokenMintToPoolTransition"); +impl_wasm_type_info!(TokenMintToPoolTransitionWasm, TokenMintToPoolTransition); diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result/convert.rs b/packages/wasm-dpp2/src/state_transitions/proof_result/convert.rs index 774761e38c2..7e3ccb30e35 100644 --- a/packages/wasm-dpp2/src/state_transitions/proof_result/convert.rs +++ b/packages/wasm-dpp2/src/state_transitions/proof_result/convert.rs @@ -28,7 +28,8 @@ use super::token::{ VerifiedTokenGroupActionWithDocumentWasm, VerifiedTokenGroupActionWithTokenBalanceWasm, VerifiedTokenGroupActionWithTokenIdentityInfoWasm, VerifiedTokenGroupActionWithTokenPricingScheduleWasm, VerifiedTokenIdentitiesBalancesWasm, - VerifiedTokenIdentityInfoWasm, VerifiedTokenPricingScheduleWasm, VerifiedTokenStatusWasm, + VerifiedTokenIdentityInfoWasm, VerifiedTokenPricingScheduleWasm, + VerifiedTokenShieldedPoolBalanceWasm, VerifiedTokenStatusWasm, }; use super::voting::{VerifiedMasternodeVoteWasm, VerifiedNextDistributionWasm}; use crate::IdentifierWasm; @@ -71,6 +72,7 @@ export type StateTransitionProofResultType = | VerifiedAssetLockConsumed | VerifiedAssetLockConsumedWithAddressInfos | VerifiedShieldedNullifiers + | VerifiedTokenShieldedPoolBalance | VerifiedShieldedNullifiersWithAddressInfos | VerifiedShieldedNullifiersWithWithdrawalDocument | VerifiedIdentityWithShieldedNullifiers @@ -290,6 +292,14 @@ pub fn convert_proof_result( VerifiedShieldedNullifiersWasm::from_map(build_nullifier_map(nullifiers)).into() } + StateTransitionProofResult::VerifiedTokenShieldedPoolBalance(id, amount) => { + VerifiedTokenShieldedPoolBalanceWasm { + token_id: id.into(), + balance: amount, + } + .into() + } + StateTransitionProofResult::VerifiedShieldedNullifiersWithAddressInfos( nullifiers, infos, diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result/token.rs b/packages/wasm-dpp2/src/state_transitions/proof_result/token.rs index 206a5e6c669..6b5498a22c0 100644 --- a/packages/wasm-dpp2/src/state_transitions/proof_result/token.rs +++ b/packages/wasm-dpp2/src/state_transitions/proof_result/token.rs @@ -50,6 +50,37 @@ impl VerifiedTokenBalanceWasm { impl_wasm_type_info!(VerifiedTokenBalanceWasm, VerifiedTokenBalance); impl_wasm_conversions_serde!(VerifiedTokenBalanceWasm, VerifiedTokenBalance); +// --- VerifiedTokenShieldedPoolBalance --- + +/// The proven total balance of a token's shielded pool, returned by the pool transitions that +/// only create notes (mint, claim and purchase into the pool). +#[dpp_json_convertible_derive::json_safe_fields(crate = "dpp")] +#[wasm_bindgen(js_name = "VerifiedTokenShieldedPoolBalance")] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifiedTokenShieldedPoolBalanceWasm { + #[wasm_bindgen(getter_with_clone, js_name = "tokenId")] + pub token_id: IdentifierWasm, + pub(super) balance: u64, +} + +#[wasm_bindgen(js_class = VerifiedTokenShieldedPoolBalance)] +impl VerifiedTokenShieldedPoolBalanceWasm { + #[wasm_bindgen(getter)] + pub fn balance(&self) -> JsValue { + BigInt::from(self.balance).into() + } +} + +impl_wasm_type_info!( + VerifiedTokenShieldedPoolBalanceWasm, + VerifiedTokenShieldedPoolBalance +); +impl_wasm_conversions_serde!( + VerifiedTokenShieldedPoolBalanceWasm, + VerifiedTokenShieldedPoolBalance +); + // --- VerifiedTokenIdentityInfo --- #[wasm_bindgen(js_name = "VerifiedTokenIdentityInfo")] From 6a85f42e340444720bbdbd6ae966c1a4c9a2f2cf Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 16 Sep 2026 18:43:53 +0700 Subject: [PATCH 05/94] feat(platform)!: gate token shielded pools on protocol version 15 and pay document token costs from a pool The PR now targets v4.3-dev, so the feature moves from protocol version 14 to 15: v15.rs is added with DRIVE_VERSION_V10 (genesis structure v4 and the token pool method versions), DRIVE_ABCI_METHOD_VERSIONS_V11 (anchor recording), DRIVE_ABCI_VALIDATION_VERSIONS_V11 and CONTRACT_VERSIONS_V7 (token configuration format 1); the v14 tables return to their released values and the pools root is inserted by transition_to_version_15, with a genesis-versus-upgrade equivalence test. TokenPaymentInfo gains a format version 1 carrying a TokenShieldedPayment: an Orchard spend bundle in the payment token's pool whose value balance is the document action's token cost. The document base action carries it, the transformer checks the amount against the document type's cost, state validation v1 skips the owner's balance checks and validates the pool side (pool exists, token not paused, anchor, unspent nullifiers, pool balance, proof bound to token, owner, contract, document and amount), and the lowering pays the cost out of the pool: to the contract owner's balance or out of the supply. CheckTx admits the bundle under the identity contract nonce like the batch token pool transitions. Co-Authored-By: Claude Fable 5.1 --- book/src/data-model/token-shielded-pools.md | 8 +- book/src/fees/shielded-fees.md | 2 +- .../src/balances/total_tokens_balance/mod.rs | 2 +- .../token_configuration/mod.rs | 2 +- .../document/extended_document/accessors.rs | 2 +- .../src/document/extended_document/v0/mod.rs | 4 +- packages/rs-dpp/src/errors/consensus/codes.rs | 2 + .../src/errors/consensus/state/state_error.rs | 28 +- .../src/errors/consensus/state/token/mod.rs | 4 + ..._shielded_payment_amount_mismatch_error.rs | 77 +++ ...ken_shielded_payment_not_required_error.rs | 55 ++ .../builder/document_token_payment.rs | 164 ++++++ packages/rs-dpp/src/shielded/builder/mod.rs | 2 + packages/rs-dpp/src/shielded/mod.rs | 3 +- packages/rs-dpp/src/shielded/sighash.rs | 60 +++ .../document_base_transition/v1/v1_methods.rs | 2 +- .../document_base_transition/v1_methods.rs | 4 +- .../validate_basic_structure/v0/mod.rs | 64 +++ .../src/tokens/token_payment_info/mod.rs | 117 +++- .../src/tokens/token_payment_info/v1/mod.rs | 237 +++++++++ .../token_payment_info/v1/v1_accessors.rs | 13 + .../v0/mod.rs | 7 +- .../v0/mod.rs | 74 ++- .../process_raw_state_transitions/v0/mod.rs | 16 +- .../processor/traits/is_allowed.rs | 15 +- .../processor/traits/shielded_proof.rs | 59 +++ .../document_base_transaction_action/mod.rs | 13 +- .../state_v1/mod.rs | 56 ++ .../advanced_structure_v1/mod.rs | 1 + .../document_shielded_token_payment/mod.rs | 201 +++++++ .../batch/action_validation/document/mod.rs | 1 + .../token/token_shielded_pool_common/mod.rs | 16 +- .../triggers/withdrawals/v0/mod.rs | 2 + .../triggers/withdrawals/v2/mod.rs | 1 + .../state_transitions/batch/state/v0/mod.rs | 24 +- .../batch/tests/token/shielded/mod.rs | 500 +++++++++++++++++- .../batch/transformer/v0/mod.rs | 25 + .../rs-drive-abci/src/query/shielded/mod.rs | 2 +- packages/rs-drive-proof-verifier/src/types.rs | 2 +- .../drive/document/index_uniqueness/mod.rs | 1 + .../src/drive/initialization/v4/mod.rs | 4 +- .../calculate_total_tokens_balance/v0/mod.rs | 2 +- .../calculate_total_tokens_balance/v1/mod.rs | 2 +- packages/rs-drive/src/drive/tokens/paths.rs | 2 +- .../tokens/shielded_pool/insert_root_tree.rs | 4 +- .../src/drive/tokens/shielded_pool/mod.rs | 10 +- .../document/document_create_transition.rs | 32 +- .../document/document_delete_transition.rs | 32 +- .../document_index_only_delete_transition.rs | 31 +- .../document/document_purchase_transition.rs | 60 +-- .../document/document_replace_transition.rs | 32 +- .../document/document_transfer_transition.rs | 58 +- .../document_update_price_transition.rs | 58 +- .../document_base_transition_action/mod.rs | 79 +++ .../document_base_transition_action/v0/mod.rs | 6 + .../v0/transformer.rs | 36 +- .../state_transition_action/batch/tests.rs | 3 + .../transformer.rs | 2 + .../v0/transformer.rs | 2 + .../dpp_versions/dpp_contract_versions/mod.rs | 1 + .../dpp_versions/dpp_contract_versions/v6.rs | 4 +- .../dpp_versions/dpp_contract_versions/v7.rs | 96 ++++ .../drive_abci_method_versions/mod.rs | 1 + .../drive_abci_method_versions/v10.rs | 2 +- .../drive_abci_method_versions/v11.rs | 143 +++++ .../drive_abci_validation_versions/mod.rs | 1 + .../drive_abci_validation_versions/v11.rs | 405 ++++++++++++++ .../src/version/drive_versions/mod.rs | 1 + .../src/version/drive_versions/v10.rs | 144 +++++ .../feature_initial_protocol_versions.rs | 6 +- .../rs-platform-version/src/version/mod.rs | 5 +- .../src/version/protocol_version.rs | 4 +- .../rs-platform-version/src/version/v14.rs | 19 +- .../rs-platform-version/src/version/v15.rs | 93 ++++ packages/rs-sdk/src/platform/query.rs | 2 +- .../src/errors/consensus/consensus_error.rs | 8 +- .../src/state_transitions/batch/mod.rs | 1 + .../batch/token_payment_info.rs | 48 +- .../batch/token_shielded_payment.rs | 165 ++++++ .../configuration/token_configuration.rs | 2 +- 80 files changed, 3114 insertions(+), 360 deletions(-) create mode 100644 packages/rs-dpp/src/errors/consensus/state/token/token_shielded_payment_amount_mismatch_error.rs create mode 100644 packages/rs-dpp/src/errors/consensus/state/token/token_shielded_payment_not_required_error.rs create mode 100644 packages/rs-dpp/src/shielded/builder/document_token_payment.rs create mode 100644 packages/rs-dpp/src/tokens/token_payment_info/v1/mod.rs create mode 100644 packages/rs-dpp/src/tokens/token_payment_info/v1/v1_accessors.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_base_transaction_action/state_v1/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_shielded_token_payment/mod.rs create mode 100644 packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v7.rs create mode 100644 packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs create mode 100644 packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs create mode 100644 packages/rs-platform-version/src/version/drive_versions/v10.rs create mode 100644 packages/rs-platform-version/src/version/v15.rs create mode 100644 packages/wasm-dpp2/src/state_transitions/batch/token_shielded_payment.rs diff --git a/book/src/data-model/token-shielded-pools.md b/book/src/data-model/token-shielded-pools.md index de538c77bba..5346ab4f77e 100644 --- a/book/src/data-model/token-shielded-pools.md +++ b/book/src/data-model/token-shielded-pools.md @@ -1,6 +1,6 @@ # Token Shielded Pools -From protocol version 14 a token can own a shielded pool: an Orchard pool that holds that token +From protocol version 15 a token can own a shielded pool: an Orchard pool that holds that token instead of credits. Holders move tokens between their identity balance and the pool with three token transitions inside a batch, and transfer inside the pool without revealing amounts or counterparties. This chapter describes the storage, the configuration flag, the transitions, the @@ -35,7 +35,7 @@ a pool-agnostic form taking the pool path, and a token twin that supplies the to root of all token pools is a BigSumTree so the amount of every token that is shielded is one sum, which the token conservation check reads. -The root tree is created by the version 14 upgrade transition (`transition_to_version_14`) on +The root tree is created by the version 15 upgrade transition (`transition_to_version_15`) on an existing chain and by `create_initial_state_structure` version 4 on a new one. A pool's five trees are created when a contract with the flag is inserted or updated. @@ -45,7 +45,7 @@ trees are created when a contract with the flag is inserted or updated. `hasShieldedPool: bool`. A version 0 configuration behaves as `hasShieldedPool: false`. The format version is admitted by `dpp.contract_versions.token_versions.token_configuration_format`: protocol versions 13 and -below allow only version 0, protocol version 14 allows versions 0 and 1. Contract create and +below allow only version 0, protocol version 15 allows versions 0 and 1. Contract create and update reject a token configuration outside the bounds with `UnsupportedVersionError`, so a pre-14 network never stores the flag. @@ -148,7 +148,7 @@ The six shielded pool queries (`getShieldedPoolState`, `getShieldedNotesCount`, `getShieldedAnchors`, `getMostRecentShieldedAnchor`, `getShieldedEncryptedNotes`, `getShieldedNullifiers`) take an optional `token_id`. Without it they target the credit pool; with a 32-byte token id they target that token's pool and answer with the same response shape. -A token id is rejected with `InvalidArgument` before protocol version 14 or when it is not 32 +A token id is rejected with `InvalidArgument` before protocol version 15 or when it is not 32 bytes. The proof verifier routes on the same field to the token twins of the verify functions (`verify_token_shielded_pool_state` and the rest), and the Rust SDK exposes `TokenShieldedPoolQuery`, `TokenShieldedEncryptedNotesQuery` and `TokenShieldedNullifiersQuery`. diff --git a/book/src/fees/shielded-fees.md b/book/src/fees/shielded-fees.md index 57b9bf1c12d..551814f571b 100644 --- a/book/src/fees/shielded-fees.md +++ b/book/src/fees/shielded-fees.md @@ -48,7 +48,7 @@ The fee is derived differently depending on the shielded transition type: ### Token shielded pool fees -Token pools (protocol version 14, see [Token Shielded Pools](../data-model/token-shielded-pools.md)) +Token pools (protocol version 15, see [Token Shielded Pools](../data-model/token-shielded-pools.md)) hold tokens, and tokens cannot pay fees, so none of the three token pool transitions carves a fee from the bundle. They are `TokenTransition` variants inside a `Batch`, and the batch's signing identity pays in credits through the standard identity-paid path. diff --git a/packages/rs-dpp/src/balances/total_tokens_balance/mod.rs b/packages/rs-dpp/src/balances/total_tokens_balance/mod.rs index 7f920242e54..02cafebabd8 100644 --- a/packages/rs-dpp/src/balances/total_tokens_balance/mod.rs +++ b/packages/rs-dpp/src/balances/total_tokens_balance/mod.rs @@ -9,7 +9,7 @@ pub struct TotalTokensBalance { pub total_tokens_in_platform: SumTokenAmount, /// all the tokens in identity token balances pub total_identity_token_balances: SumTokenAmount, - /// all the tokens held in token shielded pools (0 before protocol version 14) + /// all the tokens held in token shielded pools (0 before protocol version 15) pub total_token_shielded_pool_balances: SumTokenAmount, } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs index 9059973b060..ff2e681a38f 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs @@ -33,7 +33,7 @@ pub mod v1; pub enum TokenConfiguration { #[serde(rename = "0")] V0(TokenConfigurationV0), - /// V0 plus the per-token shielded pool opt-in. Admitted from protocol version 14 + /// V0 plus the per-token shielded pool opt-in. Admitted from protocol version 15 /// (`token_versions.token_configuration_format`). #[serde(rename = "1")] V1(TokenConfigurationV1), diff --git a/packages/rs-dpp/src/document/extended_document/accessors.rs b/packages/rs-dpp/src/document/extended_document/accessors.rs index 1c693abccbb..34cc90aeb68 100644 --- a/packages/rs-dpp/src/document/extended_document/accessors.rs +++ b/packages/rs-dpp/src/document/extended_document/accessors.rs @@ -156,7 +156,7 @@ impl ExtendedDocument { /// Returns a reference to the actual document object containing the data. pub fn token_payment_info(&self) -> Option { match self { - ExtendedDocument::V0(v0) => v0.token_payment_info, + ExtendedDocument::V0(v0) => v0.token_payment_info.clone(), } } diff --git a/packages/rs-dpp/src/document/extended_document/v0/mod.rs b/packages/rs-dpp/src/document/extended_document/v0/mod.rs index ba2fd2002b0..862fcaf487c 100644 --- a/packages/rs-dpp/src/document/extended_document/v0/mod.rs +++ b/packages/rs-dpp/src/document/extended_document/v0/mod.rs @@ -403,10 +403,10 @@ impl ExtendedDocumentV0 { property_names::DATA_CONTRACT_ID.to_string(), Value::Identifier(self.data_contract_id.to_buffer()), ); - if let Some(token_payment_info) = self.token_payment_info { + if let Some(token_payment_info) = &self.token_payment_info { object.insert( property_names::TOKEN_PAYMENT_INFO.to_string(), - token_payment_info.try_into()?, + token_payment_info.clone().try_into()?, ); } Ok(object) diff --git a/packages/rs-dpp/src/errors/consensus/codes.rs b/packages/rs-dpp/src/errors/consensus/codes.rs index 4c45ee9d942..2dea3aa2711 100644 --- a/packages/rs-dpp/src/errors/consensus/codes.rs +++ b/packages/rs-dpp/src/errors/consensus/codes.rs @@ -433,6 +433,8 @@ impl ErrorWithCode for StateError { Self::TokenNotForDirectSale(_) => 40721, Self::TokenOncePerIdentityDistributionAlreadyClaimedError(_) => 40722, Self::TokenShieldedPoolNotEnabledError(_) => 40723, + Self::TokenShieldedPaymentAmountMismatchError(_) => 40724, + Self::TokenShieldedPaymentNotRequiredError(_) => 40725, // Group errors: 40800-40899 Self::IdentityNotMemberOfGroupError(_) => 40800, diff --git a/packages/rs-dpp/src/errors/consensus/state/state_error.rs b/packages/rs-dpp/src/errors/consensus/state/state_error.rs index 4f3880f43b8..2460330dd87 100644 --- a/packages/rs-dpp/src/errors/consensus/state/state_error.rs +++ b/packages/rs-dpp/src/errors/consensus/state/state_error.rs @@ -93,7 +93,7 @@ use crate::consensus::state::identity::missing_transfer_key_error::MissingTransf use crate::consensus::state::identity::no_transfer_key_for_core_withdrawal_available_error::NoTransferKeyForCoreWithdrawalAvailableError; use crate::consensus::state::prefunded_specialized_balances::prefunded_specialized_balance_insufficient_error::PrefundedSpecializedBalanceInsufficientError; use crate::consensus::state::prefunded_specialized_balances::prefunded_specialized_balance_not_found_error::PrefundedSpecializedBalanceNotFoundError; -use crate::consensus::state::token::{IdentityDoesNotHaveEnoughTokenBalanceError, IdentityTokenAccountFrozenError, IdentityTokenAccountNotFrozenError, InvalidGroupPositionError, NewAuthorizedActionTakerGroupDoesNotExistError, NewAuthorizedActionTakerIdentityDoesNotExistError, NewAuthorizedActionTakerMainGroupNotSetError, NewTokensDestinationIdentityDoesNotExistError, TokenMintPastMaxSupplyError, TokenSettingMaxSupplyToLessThanCurrentSupplyError, UnauthorizedTokenActionError, IdentityTokenAccountAlreadyFrozenError, TokenAlreadyPausedError, TokenIsPausedError, TokenNotPausedError, InvalidTokenClaimPropertyMismatch, InvalidTokenClaimNoCurrentRewards, InvalidTokenClaimWrongClaimant, PreProgrammedDistributionTimestampInPastError, TokenTransferRecipientIdentityNotExistError, IdentityHasNotAgreedToPayRequiredTokenAmountError, RequiredTokenPaymentInfoNotSetError, IdentityTryingToPayWithWrongTokenError, TokenDirectPurchaseUserPriceTooLow, TokenAmountUnderMinimumSaleAmount, TokenNotForDirectSale, InvalidTokenPositionStateError, TokenOncePerIdentityDistributionAlreadyClaimedError, TokenShieldedPoolNotEnabledError}; +use crate::consensus::state::token::{IdentityDoesNotHaveEnoughTokenBalanceError, IdentityTokenAccountFrozenError, IdentityTokenAccountNotFrozenError, InvalidGroupPositionError, NewAuthorizedActionTakerGroupDoesNotExistError, NewAuthorizedActionTakerIdentityDoesNotExistError, NewAuthorizedActionTakerMainGroupNotSetError, NewTokensDestinationIdentityDoesNotExistError, TokenMintPastMaxSupplyError, TokenSettingMaxSupplyToLessThanCurrentSupplyError, UnauthorizedTokenActionError, IdentityTokenAccountAlreadyFrozenError, TokenAlreadyPausedError, TokenIsPausedError, TokenNotPausedError, InvalidTokenClaimPropertyMismatch, InvalidTokenClaimNoCurrentRewards, InvalidTokenClaimWrongClaimant, PreProgrammedDistributionTimestampInPastError, TokenTransferRecipientIdentityNotExistError, IdentityHasNotAgreedToPayRequiredTokenAmountError, RequiredTokenPaymentInfoNotSetError, IdentityTryingToPayWithWrongTokenError, TokenDirectPurchaseUserPriceTooLow, TokenAmountUnderMinimumSaleAmount, TokenNotForDirectSale, InvalidTokenPositionStateError, TokenOncePerIdentityDistributionAlreadyClaimedError, TokenShieldedPoolNotEnabledError, TokenShieldedPaymentAmountMismatchError, TokenShieldedPaymentNotRequiredError}; use crate::consensus::state::voting::masternode_incorrect_voter_identity_id_error::MasternodeIncorrectVoterIdentityIdError; use crate::consensus::state::voting::masternode_incorrect_voting_address_error::MasternodeIncorrectVotingAddressError; use crate::consensus::state::voting::masternode_not_found_error::MasternodeNotFoundError; @@ -592,6 +592,12 @@ pub enum StateError { /// discriminant test below). #[error(transparent)] TokenShieldedPoolNotEnabledError(TokenShieldedPoolNotEnabledError), + + #[error(transparent)] + TokenShieldedPaymentAmountMismatchError(TokenShieldedPaymentAmountMismatchError), + + #[error(transparent)] + TokenShieldedPaymentNotRequiredError(TokenShieldedPaymentNotRequiredError), } impl From for ConsensusError { @@ -1109,5 +1115,25 @@ mod tests { )), 144 ); + assert_eq!( + discriminant_of(StateError::TokenShieldedPaymentAmountMismatchError( + TokenShieldedPaymentAmountMismatchError::new( + Identifier::from([1; 32]), + 10, + 9, + "create".to_string(), + ) + )), + 102 + ); + assert_eq!( + discriminant_of(StateError::TokenShieldedPaymentNotRequiredError( + TokenShieldedPaymentNotRequiredError::new( + Identifier::from([1; 32]), + "create".to_string(), + ) + )), + 103 + ); } } diff --git a/packages/rs-dpp/src/errors/consensus/state/token/mod.rs b/packages/rs-dpp/src/errors/consensus/state/token/mod.rs index 3e3b9e720ef..c9a33400965 100644 --- a/packages/rs-dpp/src/errors/consensus/state/token/mod.rs +++ b/packages/rs-dpp/src/errors/consensus/state/token/mod.rs @@ -26,6 +26,8 @@ mod token_not_for_direct_sale; mod token_not_paused_error; mod token_once_per_identity_distribution_already_claimed_error; mod token_setting_max_supply_to_less_than_current_supply_error; +mod token_shielded_payment_amount_mismatch_error; +mod token_shielded_payment_not_required_error; mod token_shielded_pool_not_enabled_error; mod token_transfer_recipient_identity_not_exist_error; mod unauthorized_token_action_error; @@ -58,6 +60,8 @@ pub use token_not_for_direct_sale::*; pub use token_not_paused_error::*; pub use token_once_per_identity_distribution_already_claimed_error::*; pub use token_setting_max_supply_to_less_than_current_supply_error::*; +pub use token_shielded_payment_amount_mismatch_error::*; +pub use token_shielded_payment_not_required_error::*; pub use token_shielded_pool_not_enabled_error::*; pub use token_transfer_recipient_identity_not_exist_error::*; pub use unauthorized_token_action_error::*; diff --git a/packages/rs-dpp/src/errors/consensus/state/token/token_shielded_payment_amount_mismatch_error.rs b/packages/rs-dpp/src/errors/consensus/state/token/token_shielded_payment_amount_mismatch_error.rs new file mode 100644 index 00000000000..ae3a4221645 --- /dev/null +++ b/packages/rs-dpp/src/errors/consensus/state/token/token_shielded_payment_amount_mismatch_error.rs @@ -0,0 +1,77 @@ +use crate::consensus::state::state_error::StateError; +use crate::consensus::ConsensusError; +use crate::ProtocolError; +use bincode::{Decode, DecodeUntrusted, Encode}; +use platform_serialization_derive::{ + PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, +}; +use platform_value::Identifier; +use thiserror::Error; + +/// A document action's shielded token payment proves a different amount than the token cost +/// the document type requires for that action. +#[derive( + Error, + Debug, + Clone, + PartialEq, + Eq, + Encode, + Decode, + PlatformSerialize, + PlatformDeserializeTrusted, + PlatformDeserializeUntrusted, + DecodeUntrusted, +)] +#[error( + "Document {} requires a token payment of {} of token {}, the shielded payment pays {}", + action, + required_amount, + token_id, + paid_amount +)] +#[platform_serialize(unversioned)] +pub struct TokenShieldedPaymentAmountMismatchError { + token_id: Identifier, + required_amount: u64, + paid_amount: u64, + action: String, +} + +impl TokenShieldedPaymentAmountMismatchError { + pub fn new( + token_id: Identifier, + required_amount: u64, + paid_amount: u64, + action: String, + ) -> Self { + Self { + token_id, + required_amount, + paid_amount, + action, + } + } + + pub fn token_id(&self) -> &Identifier { + &self.token_id + } + + pub fn required_amount(&self) -> u64 { + self.required_amount + } + + pub fn paid_amount(&self) -> u64 { + self.paid_amount + } + + pub fn action(&self) -> &str { + &self.action + } +} + +impl From for ConsensusError { + fn from(err: TokenShieldedPaymentAmountMismatchError) -> Self { + Self::StateError(StateError::TokenShieldedPaymentAmountMismatchError(err)) + } +} diff --git a/packages/rs-dpp/src/errors/consensus/state/token/token_shielded_payment_not_required_error.rs b/packages/rs-dpp/src/errors/consensus/state/token/token_shielded_payment_not_required_error.rs new file mode 100644 index 00000000000..79a46bd8604 --- /dev/null +++ b/packages/rs-dpp/src/errors/consensus/state/token/token_shielded_payment_not_required_error.rs @@ -0,0 +1,55 @@ +use crate::consensus::state::state_error::StateError; +use crate::consensus::ConsensusError; +use crate::ProtocolError; +use bincode::{Decode, DecodeUntrusted, Encode}; +use platform_serialization_derive::{ + PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, +}; +use platform_value::Identifier; +use thiserror::Error; + +/// A document transition carries a shielded token payment but the document type has no token +/// cost for that action, so there is nothing for the bundle to pay. +#[derive( + Error, + Debug, + Clone, + PartialEq, + Eq, + Encode, + Decode, + PlatformSerialize, + PlatformDeserializeTrusted, + PlatformDeserializeUntrusted, + DecodeUntrusted, +)] +#[error( + "Document {} carries a shielded payment of token {} but has no token cost to pay", + action, + token_id +)] +#[platform_serialize(unversioned)] +pub struct TokenShieldedPaymentNotRequiredError { + token_id: Identifier, + action: String, +} + +impl TokenShieldedPaymentNotRequiredError { + pub fn new(token_id: Identifier, action: String) -> Self { + Self { token_id, action } + } + + pub fn token_id(&self) -> &Identifier { + &self.token_id + } + + pub fn action(&self) -> &str { + &self.action + } +} + +impl From for ConsensusError { + fn from(err: TokenShieldedPaymentNotRequiredError) -> Self { + Self::StateError(StateError::TokenShieldedPaymentNotRequiredError(err)) + } +} diff --git a/packages/rs-dpp/src/shielded/builder/document_token_payment.rs b/packages/rs-dpp/src/shielded/builder/document_token_payment.rs new file mode 100644 index 00000000000..c41b07ca4b2 --- /dev/null +++ b/packages/rs-dpp/src/shielded/builder/document_token_payment.rs @@ -0,0 +1,164 @@ +use grovedb_commitment_tree::{Anchor, FullViewingKey, SpendAuthorizingKey}; + +use crate::address_funds::OrchardAddress; +use crate::balances::credits::TokenAmount; +use crate::prelude::Identifier; +use crate::shielded::document_token_payment_extra_sighash_data; +use crate::tokens::token_payment_info::v1::TokenShieldedPayment; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +use super::{build_spend_bundle, serialize_authorized_bundle, OrchardProver, SpendableNote}; + +/// Builds the shielded payment of a document action whose token cost is `amount`: spends +/// `spends` from the token's shielded pool, returns the remainder to `change_address` as a +/// new note and binds the token id, the batch owner, the document's contract and id and the +/// amount into the Orchard sighash. Put the result into a `TokenPaymentInfo::V1` on the +/// document transition's base; the identity signing the batch still pays the credit fee. +/// +/// `value_balance == amount` exactly; the pool pays the cost, nothing is carved for fees. +#[allow(clippy::too_many_arguments)] +pub fn build_document_shielded_token_payment( + token_id: Identifier, + owner_id: Identifier, + data_contract_id: Identifier, + document_id: Identifier, + spends: Vec, + amount: TokenAmount, + change_address: &OrchardAddress, + fvk: &FullViewingKey, + ask: &SpendAuthorizingKey, + anchor: Anchor, + memo: [u8; 36], + prover: &P, + platform_version: &PlatformVersion, +) -> Result { + if amount == 0 { + return Err(ProtocolError::ShieldedBuildError( + "document token payment amount must be greater than zero".to_string(), + )); + } + if amount > i64::MAX as u64 { + return Err(ProtocolError::ShieldedBuildError(format!( + "document token payment amount {} exceeds maximum allowed value {}", + amount, + i64::MAX as u64 + ))); + } + + let total_spent: u64 = spends + .iter() + .try_fold(0u64, |total, spend| { + total.checked_add(spend.note.value().inner()) + }) + .ok_or_else(|| { + ProtocolError::ShieldedBuildError("total spendable value overflows u64".to_string()) + })?; + if amount > total_spent { + return Err(ProtocolError::ShieldedBuildError(format!( + "document token payment amount {} exceeds total spendable value {}", + amount, total_spent + ))); + } + let change_amount = total_spent - amount; + + let extra_sighash_data = document_token_payment_extra_sighash_data( + &token_id.to_buffer(), + &owner_id.to_buffer(), + &data_contract_id.to_buffer(), + &document_id.to_buffer(), + amount, + platform_version, + )?; + + let bundle = build_spend_bundle( + spends, + change_address, + change_amount, + memo, + fvk, + ask, + anchor, + prover, + &extra_sighash_data, + )?; + let sb = serialize_authorized_bundle(&bundle); + + if sb.value_balance != amount as i64 { + return Err(ProtocolError::ShieldedBuildError(format!( + "document token payment bundle value balance {} does not equal the amount {}", + sb.value_balance, amount + ))); + } + + Ok(TokenShieldedPayment { + amount, + actions: sb.actions, + anchor: sb.anchor, + proof: sb.proof, + binding_signature: sb.binding_signature, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::shielded::builder::test_helpers::{ + test_orchard_address, test_spendable_note, TestProver, + }; + use grovedb_commitment_tree::SpendingKey; + + fn keys() -> (FullViewingKey, SpendAuthorizingKey) { + let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes"); + (FullViewingKey::from(&sk), SpendAuthorizingKey::from(&sk)) + } + + #[test] + fn rejects_amount_above_spendable_value() { + let (fvk, ask) = keys(); + let err = build_document_shielded_token_payment( + Identifier::from([1u8; 32]), + Identifier::from([2u8; 32]), + Identifier::from([3u8; 32]), + Identifier::from([4u8; 32]), + vec![test_spendable_note(100)], + 1_000, + &test_orchard_address(), + &fvk, + &ask, + Anchor::empty_tree(), + [0u8; 36], + &TestProver, + PlatformVersion::latest(), + ) + .expect_err("overspend must be rejected") + .to_string(); + assert!( + err.contains("exceeds total spendable value"), + "unexpected error: {err}" + ); + } + + #[test] + fn rejects_zero_amount() { + let (fvk, ask) = keys(); + let err = build_document_shielded_token_payment( + Identifier::from([1u8; 32]), + Identifier::from([2u8; 32]), + Identifier::from([3u8; 32]), + Identifier::from([4u8; 32]), + vec![test_spendable_note(100)], + 0, + &test_orchard_address(), + &fvk, + &ask, + Anchor::empty_tree(), + [0u8; 36], + &TestProver, + PlatformVersion::latest(), + ) + .expect_err("zero amount must be rejected") + .to_string(); + assert!(err.contains("greater than zero"), "unexpected error: {err}"); + } +} diff --git a/packages/rs-dpp/src/shielded/builder/mod.rs b/packages/rs-dpp/src/shielded/builder/mod.rs index a5af790e119..2dbce0f8d2f 100644 --- a/packages/rs-dpp/src/shielded/builder/mod.rs +++ b/packages/rs-dpp/src/shielded/builder/mod.rs @@ -29,6 +29,7 @@ //! )?; //! ``` +mod document_token_payment; mod identity_create_from_shielded_pool; mod identity_top_up_from_shielded_pool; mod shield; @@ -46,6 +47,7 @@ mod token_unshield; mod unshield; pub use self::shield::build_shield_transition; +pub use document_token_payment::build_document_shielded_token_payment; pub use identity_create_from_shielded_pool::{ build_identity_create_from_shielded_pool_transition, IdentityCreateFromShieldedPoolBuildResult, }; diff --git a/packages/rs-dpp/src/shielded/mod.rs b/packages/rs-dpp/src/shielded/mod.rs index 451e4260945..7fe3f737d82 100644 --- a/packages/rs-dpp/src/shielded/mod.rs +++ b/packages/rs-dpp/src/shielded/mod.rs @@ -41,7 +41,8 @@ pub fn serialized_actions_digest(actions: &[SerializedAction]) -> [u8; 32] { } pub use sighash::{ - compute_platform_sighash, identity_create_from_shielded_extra_sighash_data, + compute_platform_sighash, document_token_payment_extra_sighash_data, + document_token_payment_extra_sighash_data_v0, identity_create_from_shielded_extra_sighash_data, identity_create_from_shielded_extra_sighash_data_v0, identity_top_up_from_shielded_extra_sighash_data, identity_top_up_from_shielded_extra_sighash_data_v0, shielded_withdrawal_extra_sighash_data, diff --git a/packages/rs-dpp/src/shielded/sighash.rs b/packages/rs-dpp/src/shielded/sighash.rs index daf879e752b..544f332de81 100644 --- a/packages/rs-dpp/src/shielded/sighash.rs +++ b/packages/rs-dpp/src/shielded/sighash.rs @@ -380,6 +380,53 @@ pub fn token_burn_from_pool_extra_sighash_data_v0( data } +/// Extra sighash data of a document action paid from a token shielded pool +/// (`TokenPaymentInfo::V1`): the token id, the batch owner, the document's contract and id +/// and the amount paid, so a bundle proven for one document cannot be replayed for another +/// document, batch owner, token or cost. +pub fn document_token_payment_extra_sighash_data( + token_id: &[u8; 32], + owner_id: &[u8; 32], + data_contract_id: &[u8; 32], + document_id: &[u8; 32], + amount: u64, + platform_version: &PlatformVersion, +) -> Result, ProtocolError> { + match platform_version.dpp.methods.shielded_extra_sighash_data { + 0 => Ok(document_token_payment_extra_sighash_data_v0( + token_id, + owner_id, + data_contract_id, + document_id, + amount, + )), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "document_token_payment_extra_sighash_data".to_string(), + known_versions: vec![0], + received: version, + }), + } +} + +/// Version 0 layout: `token_id (32) || owner_id (32) || data_contract_id (32) || +/// document_id (32) || amount (8, little endian)`. Frozen: never mutate; a layout change +/// requires a new `_v1` + version bump. +pub fn document_token_payment_extra_sighash_data_v0( + token_id: &[u8; 32], + owner_id: &[u8; 32], + data_contract_id: &[u8; 32], + document_id: &[u8; 32], + amount: u64, +) -> Vec { + let mut data = Vec::with_capacity(32 * 4 + 8); + data.extend_from_slice(token_id); + data.extend_from_slice(owner_id); + data.extend_from_slice(data_contract_id); + data.extend_from_slice(document_id); + data.extend_from_slice(&amount.to_le_bytes()); + data +} + /// Builds the transparent `extra_data` bound into a `TokenShieldedTransfer`'s platform sighash, /// with the byte layout `token_id (32) || owner_id (32)`. /// @@ -723,6 +770,19 @@ mod tests { } } + #[test] + fn document_token_payment_layout_is_token_owner_contract_document_amount() { + let data = document_token_payment_extra_sighash_data_v0( + &[1u8; 32], &[2u8; 32], &[3u8; 32], &[4u8; 32], 10, + ); + assert_eq!(data.len(), 136); + assert_eq!(&data[..32], &[1u8; 32]); + assert_eq!(&data[32..64], &[2u8; 32]); + assert_eq!(&data[64..96], &[3u8; 32]); + assert_eq!(&data[96..128], &[4u8; 32]); + assert_eq!(&data[128..], &10u64.to_le_bytes()); + } + #[test] fn token_burn_from_pool_layout_is_token_owner_amount() { let data = token_burn_from_pool_extra_sighash_data_v0(&[1u8; 32], &[2u8; 32], 300); diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v1/v1_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v1/v1_methods.rs index 1d185790619..083cbb3f560 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v1/v1_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v1/v1_methods.rs @@ -21,7 +21,7 @@ pub trait DocumentBaseTransitionV1Methods: DocumentBaseTransitionV0Methods { impl DocumentBaseTransitionV1Methods for DocumentBaseTransitionV1 { fn token_payment_info(&self) -> Option { - self.token_payment_info + self.token_payment_info.clone() } fn token_payment_info_ref(&self) -> &Option { diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v1_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v1_methods.rs index 0727515aed7..cf33af29cf8 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v1_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v1_methods.rs @@ -6,8 +6,8 @@ impl DocumentBaseTransitionV1Methods for DocumentBaseTransition { fn token_payment_info(&self) -> Option { match self { DocumentBaseTransition::V0(_) => None, - DocumentBaseTransition::V1(v1) => v1.token_payment_info, - DocumentBaseTransition::V2(v2) => v2.token_payment_info, + DocumentBaseTransition::V1(v1) => v1.token_payment_info.clone(), + DocumentBaseTransition::V2(v2) => v2.token_payment_info.clone(), } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs index 06d1ff1980b..b5bc68dbcff 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs @@ -9,6 +9,16 @@ use crate::state_transition::batch_transition::batched_transition::DocumentIndex use crate::identity::identity_nonce::MISSING_IDENTITY_REVISIONS_FILTER; use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; use crate::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; +use crate::state_transition::batch_transition::document_base_transition::v1::v1_methods::DocumentBaseTransitionV1Methods; +use crate::consensus::basic::token::InvalidTokenAmountError; +use crate::consensus::ConsensusError; +use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::MAX_DISTRIBUTION_PARAM; +use crate::state_transition::state_transitions::shielded::common_validation::{ + validate_actions_count, validate_anchor_not_zero, validate_encrypted_note_sizes, + validate_proof_not_empty, +}; +use crate::tokens::token_payment_info::v1::TokenShieldedPayment; +use crate::tokens::token_payment_info::TokenPaymentInfo; use crate::state_transition::batch_transition::validation::find_duplicates_by_id::find_duplicates_by_id; use crate::state_transition::batch_transition::BatchTransition; use crate::validation::SimpleConsensusValidationResult; @@ -112,6 +122,21 @@ impl BatchTransition { )); } + // A shielded token payment (`TokenPaymentInfo::V1`) carries an Orchard spend + // bundle: the same stateless checks the token pool transitions run. The amount + // it pays is matched against the document type's token cost in the transformer. + if let Some(TokenPaymentInfo::V1(payment_info)) = + transition.base().token_payment_info_ref() + { + let payment_result = validate_shielded_token_payment_structure( + payment_info.shielded_payment(), + platform_version, + ); + if !payment_result.is_valid() { + result.merge(payment_result); + } + } + // The indexOnlyDelete kind joined the wire at PV14. Old // software cannot decode it at all, so no historical block // can contain one — this check exists so that NEW software @@ -290,6 +315,45 @@ impl BatchTransition { } } +/// The stateless checks of a document's shielded token payment bundle: a non-zero amount within +/// the token amount bound, a bounded action count, note sizes, a non-empty proof and a non-zero +/// anchor. The proof itself is verified against state, where its cost can be charged. +fn validate_shielded_token_payment_structure( + payment: &TokenShieldedPayment, + platform_version: &PlatformVersion, +) -> SimpleConsensusValidationResult { + if payment.amount > MAX_DISTRIBUTION_PARAM || payment.amount == 0 { + return SimpleConsensusValidationResult::new_with_error(ConsensusError::BasicError( + BasicError::InvalidTokenAmountError(InvalidTokenAmountError::new( + MAX_DISTRIBUTION_PARAM, + payment.amount, + )), + )); + } + + let result = validate_actions_count( + &payment.actions, + platform_version + .system_limits + .max_shielded_transition_actions, + ); + if !result.is_valid() { + return result; + } + + let result = validate_encrypted_note_sizes(&payment.actions); + if !result.is_valid() { + return result; + } + + let result = validate_proof_not_empty(&payment.proof); + if !result.is_valid() { + return result; + } + + validate_anchor_not_zero(&payment.anchor) +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/rs-dpp/src/tokens/token_payment_info/mod.rs b/packages/rs-dpp/src/tokens/token_payment_info/mod.rs index 26c00466984..806d774ce56 100644 --- a/packages/rs-dpp/src/tokens/token_payment_info/mod.rs +++ b/packages/rs-dpp/src/tokens/token_payment_info/mod.rs @@ -5,8 +5,10 @@ //! transferring, purchasing, or updating the price of a document/NFT). //! It captures which token to use, optional price bounds, and who covers gas fees. //! -//! The enum is versioned to allow future evolution without breaking callers. The -//! current implementation is [`v0::TokenPaymentInfoV0`]. Accessors are provided via +//! The enum is versioned to allow future evolution without breaking callers. +//! [`v0::TokenPaymentInfoV0`] pays from the identity's token balance; [`v1::TokenPaymentInfoV1`] +//! adds a [`v1::TokenShieldedPayment`], a spend bundle that pays the cost out of the token's +//! shielded pool instead (protocol version 15 and up). Accessors are provided via //! [`v0::v0_accessors::TokenPaymentInfoAccessorsV0`], and convenience methods (such //! as `token_id()` and `is_valid_for_required_cost()`) are available through //! [`methods::v0::TokenPaymentInfoMethodsV0`]. @@ -40,6 +42,9 @@ //! - `maximumTokenCost` (`u64`) //! - `gasFeesPaidBy` (one of: `"DocumentOwner"`, `"ContractOwner"`, `"PreferContractOwner"`) //! +//! For V1 the map additionally carries `shieldedPayment` (`amount`, `actions`, `anchor`, +//! `proof`, `bindingSignature`). +//! //! Unknown `$formatVersion` values yield an `UnknownVersionMismatch` error. //! use crate::balances::credits::TokenAmount; @@ -48,6 +53,8 @@ use crate::tokens::gas_fees_paid_by::GasFeesPaidBy; use crate::tokens::token_payment_info::methods::v0::TokenPaymentInfoMethodsV0; use crate::tokens::token_payment_info::v0::v0_accessors::TokenPaymentInfoAccessorsV0; use crate::tokens::token_payment_info::v0::TokenPaymentInfoV0; +use crate::tokens::token_payment_info::v1::v1_accessors::TokenPaymentInfoAccessorsV1; +use crate::tokens::token_payment_info::v1::{TokenPaymentInfoV1, TokenShieldedPayment}; use crate::ProtocolError; use bincode::{Decode, DecodeUntrusted, Encode}; use derive_more::{Display, From}; @@ -64,11 +71,11 @@ use std::collections::BTreeMap; pub mod methods; pub mod v0; +pub mod v1; #[derive( Debug, Clone, - Copy, Encode, Decode, PlatformDeserializeTrusted, @@ -97,6 +104,10 @@ pub enum TokenPaymentInfo { #[display("V0({})", "_0")] #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(TokenPaymentInfoV0), + /// `V0` plus a shielded payment: the token cost is paid out of the token's shielded pool. + #[display("V1({})", "_0")] + #[cfg_attr(feature = "serde-conversion", serde(rename = "1"))] + V1(TokenPaymentInfoV1), } #[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] @@ -107,41 +118,71 @@ impl crate::serialization::ValueConvertible for TokenPaymentInfo {} impl TokenPaymentInfoMethodsV0 for TokenPaymentInfo {} +impl TokenPaymentInfoAccessorsV1 for TokenPaymentInfo { + fn shielded_payment(&self) -> Option<&TokenShieldedPayment> { + match self { + TokenPaymentInfo::V0(_) => None, + TokenPaymentInfo::V1(v1) => Some(v1.shielded_payment()), + } + } + + fn set_shielded_payment(&mut self, shielded_payment: Option) { + let current = std::mem::replace(self, TokenPaymentInfo::V0(TokenPaymentInfoV0::default())); + *self = match (current, shielded_payment) { + (TokenPaymentInfo::V0(v0), Some(payment)) => { + TokenPaymentInfo::V1(TokenPaymentInfoV1::from_v0(v0, payment)) + } + (TokenPaymentInfo::V1(v1), Some(payment)) => TokenPaymentInfo::V1(TokenPaymentInfoV1 { + shielded_payment: payment, + ..v1 + }), + (TokenPaymentInfo::V1(v1), None) => TokenPaymentInfo::V0(v1.into_v0()), + (v0 @ TokenPaymentInfo::V0(_), None) => v0, + }; + } +} + impl TokenPaymentInfoAccessorsV0 for TokenPaymentInfo { // Getters fn payment_token_contract_id(&self) -> Option { match self { TokenPaymentInfo::V0(v0) => v0.payment_token_contract_id(), + TokenPaymentInfo::V1(v1) => v1.payment_token_contract_id(), } } fn payment_token_contract_id_ref(&self) -> &Option { match self { TokenPaymentInfo::V0(v0) => v0.payment_token_contract_id_ref(), + TokenPaymentInfo::V1(v1) => v1.payment_token_contract_id_ref(), } } fn token_contract_position(&self) -> TokenContractPosition { match self { TokenPaymentInfo::V0(v0) => v0.token_contract_position(), + TokenPaymentInfo::V1(v1) => v1.token_contract_position(), } } fn minimum_token_cost(&self) -> Option { match self { TokenPaymentInfo::V0(v0) => v0.minimum_token_cost(), + TokenPaymentInfo::V1(v1) => v1.minimum_token_cost(), } } fn maximum_token_cost(&self) -> Option { match self { TokenPaymentInfo::V0(v0) => v0.maximum_token_cost(), + TokenPaymentInfo::V1(v1) => v1.maximum_token_cost(), } } fn gas_fees_paid_by(&self) -> GasFeesPaidBy { match self { TokenPaymentInfo::V0(v0) => v0.gas_fees_paid_by(), + TokenPaymentInfo::V1(v1) => v1.gas_fees_paid_by(), } } @@ -149,30 +190,35 @@ impl TokenPaymentInfoAccessorsV0 for TokenPaymentInfo { fn set_payment_token_contract_id(&mut self, id: Option) { match self { TokenPaymentInfo::V0(v0) => v0.set_payment_token_contract_id(id), + TokenPaymentInfo::V1(v1) => v1.set_payment_token_contract_id(id), } } fn set_token_contract_position(&mut self, position: TokenContractPosition) { match self { TokenPaymentInfo::V0(v0) => v0.set_token_contract_position(position), + TokenPaymentInfo::V1(v1) => v1.set_token_contract_position(position), } } fn set_minimum_token_cost(&mut self, cost: Option) { match self { TokenPaymentInfo::V0(v0) => v0.set_minimum_token_cost(cost), + TokenPaymentInfo::V1(v1) => v1.set_minimum_token_cost(cost), } } fn set_maximum_token_cost(&mut self, cost: Option) { match self { TokenPaymentInfo::V0(v0) => v0.set_maximum_token_cost(cost), + TokenPaymentInfo::V1(v1) => v1.set_maximum_token_cost(cost), } } fn set_gas_fees_paid_by(&mut self, payer: GasFeesPaidBy) { match self { TokenPaymentInfo::V0(v0) => v0.set_gas_fees_paid_by(payer), + TokenPaymentInfo::V1(v1) => v1.set_gas_fees_paid_by(payer), } } } @@ -191,9 +237,14 @@ impl TryFrom> for TokenPaymentInfo { Ok(token_payment_info.into()) } + "1" => { + let token_payment_info: TokenPaymentInfoV1 = map.try_into()?; + + Ok(token_payment_info.into()) + } version => Err(ProtocolError::UnknownVersionMismatch { method: "TokenPaymentInfo::from_value".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version .parse() .map_err(|_| ProtocolError::Generic("Conversion error".to_string()))?, @@ -262,6 +313,64 @@ mod json_convertible_tests { assert_eq!(original, recovered); } + fn v1_fixture() -> TokenPaymentInfo { + use crate::shielded::SerializedAction; + use crate::tokens::token_payment_info::v1::{TokenPaymentInfoV1, TokenShieldedPayment}; + TokenPaymentInfo::V1(TokenPaymentInfoV1 { + payment_token_contract_id: None, + token_contract_position: 1, + minimum_token_cost: None, + maximum_token_cost: Some(10), + gas_fees_paid_by: GasFeesPaidBy::DocumentOwner, + shielded_payment: TokenShieldedPayment { + amount: 10, + actions: vec![SerializedAction { + nullifier: [1u8; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + }], + anchor: [7u8; 32], + proof: vec![8u8; 10], + binding_signature: [9u8; 64], + }, + }) + } + + #[test] + fn v1_json_and_value_round_trip() { + use crate::serialization::{JsonConvertible, ValueConvertible}; + let original = v1_fixture(); + let json = original.to_json().expect("to_json"); + assert_eq!(json["$formatVersion"], "1"); + assert_eq!(json["shieldedPayment"]["amount"], 10); + let recovered = TokenPaymentInfo::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + let value = original.to_object().expect("to_object"); + let recovered = TokenPaymentInfo::from_object(value.clone()).expect("from_object"); + assert_eq!(original, recovered); + // the manual map path (`$tokenPaymentInfo` inside a document map) parses V1 too + let map = value.into_btree_string_map().expect("map"); + let recovered: TokenPaymentInfo = map.try_into().expect("try_into"); + assert_eq!(original, recovered); + } + + #[test] + fn setting_and_clearing_the_shielded_payment_switches_the_format_version() { + let mut info = fixture(); + let TokenPaymentInfo::V1(v1) = v1_fixture() else { + unreachable!() + }; + info.set_shielded_payment(Some(v1.shielded_payment.clone())); + assert!(matches!(info, TokenPaymentInfo::V1(_))); + assert_eq!(info.shielded_payment(), Some(&v1.shielded_payment)); + assert_eq!(info.maximum_token_cost(), Some(1_000)); + info.set_shielded_payment(None); + assert_eq!(info, fixture()); + } + #[test] fn value_round_trip_with_full_wire_shape() { use crate::serialization::ValueConvertible; diff --git a/packages/rs-dpp/src/tokens/token_payment_info/v1/mod.rs b/packages/rs-dpp/src/tokens/token_payment_info/v1/mod.rs new file mode 100644 index 00000000000..a15c4c4873c --- /dev/null +++ b/packages/rs-dpp/src/tokens/token_payment_info/v1/mod.rs @@ -0,0 +1,237 @@ +pub mod v1_accessors; + +use crate::balances::credits::TokenAmount; +use crate::data_contract::TokenContractPosition; +use crate::shielded::SerializedAction; +use crate::tokens::gas_fees_paid_by::GasFeesPaidBy; +use crate::tokens::token_payment_info::v0::v0_accessors::TokenPaymentInfoAccessorsV0; +use crate::tokens::token_payment_info::v0::TokenPaymentInfoV0; +use crate::ProtocolError; +use bincode::{Decode, DecodeUntrusted, Encode}; +use platform_value::btreemap_extensions::{ + BTreeValueRemoveFromMapHelper, BTreeValueRemoveInnerValueFromMapHelper, +}; +use platform_value::{Identifier, Value}; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::fmt; + +/// A spend bundle in the payment token's shielded pool that pays a document action's token +/// cost. The notes it spends leave the pool: `amount` of them go where the document type's +/// token cost effect sends them (the contract owner's balance, or out of the supply) and the +/// change returns to the pool as new notes. The note owner authorizes the spend; the batch +/// owner still signs the batch and pays its fee in credits. +#[derive(Debug, Clone, Encode, Decode, PartialEq, DecodeUntrusted)] +// Auto-injects `json_safe_u64` on `amount` and `serde_bytes` on the fixed byte arrays / +// `serde_bytes_var` on `proof` (base64 strings in JSON, raw bytes in Value). +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +pub struct TokenShieldedPayment { + /// The tokens the bundle pays: its value balance. Must equal the token cost of the document + /// action it pays for. + pub amount: TokenAmount, + /// Orchard actions (spend-output pairs). + pub actions: Vec, + /// Sinsemilla root of the token pool's note commitment tree the bundle was built against. + pub anchor: [u8; 32], + /// Halo 2 proof bytes. + pub proof: Vec, + /// RedPallas binding signature. + pub binding_signature: [u8; 64], +} + +impl Default for TokenShieldedPayment { + fn default() -> Self { + Self { + amount: 0, + actions: vec![], + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + } + } +} + +impl fmt::Display for TokenShieldedPayment { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Shielded Payment: amount {}, actions {}", + self.amount, + self.actions.len() + ) + } +} + +impl TryFrom> for TokenShieldedPayment { + type Error = ProtocolError; + + fn try_from(mut map: BTreeMap) -> Result { + let actions = map + .remove_inner_value_array::>("actions")? + .into_iter() + .map(|value| { + let mut action = value.into_btree_string_map()?; + Ok(SerializedAction { + nullifier: action.remove_hash256_bytes("nullifier")?, + rk: action.remove_hash256_bytes("rk")?, + cmx: action.remove_hash256_bytes("cmx")?, + encrypted_note: action.remove_bytes("encryptedNote")?, + cv_net: action.remove_hash256_bytes("cvNet")?, + spend_auth_sig: fixed_64(action.remove_bytes("spendAuthSig")?, "spendAuthSig")?, + }) + }) + .collect::, ProtocolError>>()?; + Ok(TokenShieldedPayment { + amount: map.remove_integer("amount")?, + actions, + anchor: map.remove_hash256_bytes("anchor")?, + proof: map.remove_bytes("proof")?, + binding_signature: fixed_64(map.remove_bytes("bindingSignature")?, "bindingSignature")?, + }) + } +} + +fn fixed_64(bytes: Vec, field: &str) -> Result<[u8; 64], ProtocolError> { + let len = bytes.len(); + bytes + .try_into() + .map_err(|_| ProtocolError::DecodingError(format!("{field} must be 64 bytes, got {len}"))) +} + +/// Format version 1 of the token payment info: the `V0` fields plus a shielded payment. The +/// document action's token cost is paid out of the token's shielded pool by `shielded_payment` +/// instead of the document owner's token balance; the identity signing the batch still pays +/// the credit fee and receives nothing. +#[derive(Debug, Clone, Encode, Decode, Default, PartialEq, DecodeUntrusted)] +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +pub struct TokenPaymentInfoV1 { + /// By default, we use a token in the same contract, this field must be set if the document + /// requires payment using another contracts token. + pub payment_token_contract_id: Option, + /// Which token (by position) on the contract the payment uses. + pub token_contract_position: TokenContractPosition, + /// Minimum token cost, this most often should not be set. + pub minimum_token_cost: Option, + /// Maximum token cost, this most often should be set. + pub maximum_token_cost: Option, + /// Who pays the gas fees, this needs to match what the contract allows. + pub gas_fees_paid_by: GasFeesPaidBy, + /// The spend bundle paying the token cost from the token's shielded pool. + pub shielded_payment: TokenShieldedPayment, +} + +impl fmt::Display for TokenPaymentInfoV1 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Contract ID: {:?}, Token Position: {:?}, Min Cost: {:?}, Max Cost: {:?}, Gas Fees Paid By: {}, {}", + self.payment_token_contract_id, + self.token_contract_position, + self.minimum_token_cost, + self.maximum_token_cost, + self.gas_fees_paid_by, + self.shielded_payment + ) + } +} + +impl TokenPaymentInfoV1 { + /// Builds a `V1` from a `V0` and the shielded payment that pays its cost. + pub fn from_v0(v0: TokenPaymentInfoV0, shielded_payment: TokenShieldedPayment) -> Self { + Self { + payment_token_contract_id: v0.payment_token_contract_id, + token_contract_position: v0.token_contract_position, + minimum_token_cost: v0.minimum_token_cost, + maximum_token_cost: v0.maximum_token_cost, + gas_fees_paid_by: v0.gas_fees_paid_by, + shielded_payment, + } + } + + /// Drops the shielded payment, leaving the `V0` fields. + pub fn into_v0(self) -> TokenPaymentInfoV0 { + TokenPaymentInfoV0 { + payment_token_contract_id: self.payment_token_contract_id, + token_contract_position: self.token_contract_position, + minimum_token_cost: self.minimum_token_cost, + maximum_token_cost: self.maximum_token_cost, + gas_fees_paid_by: self.gas_fees_paid_by, + } + } + + /// The spend bundle paying the token cost from the token's shielded pool. + pub fn shielded_payment(&self) -> &TokenShieldedPayment { + &self.shielded_payment + } +} + +impl TokenPaymentInfoAccessorsV0 for TokenPaymentInfoV1 { + fn payment_token_contract_id(&self) -> Option { + self.payment_token_contract_id + } + + fn payment_token_contract_id_ref(&self) -> &Option { + &self.payment_token_contract_id + } + + fn token_contract_position(&self) -> TokenContractPosition { + self.token_contract_position + } + + fn minimum_token_cost(&self) -> Option { + self.minimum_token_cost + } + + fn maximum_token_cost(&self) -> Option { + self.maximum_token_cost + } + + fn set_payment_token_contract_id(&mut self, id: Option) { + self.payment_token_contract_id = id; + } + + fn set_token_contract_position(&mut self, position: TokenContractPosition) { + self.token_contract_position = position; + } + + fn set_minimum_token_cost(&mut self, cost: Option) { + self.minimum_token_cost = cost; + } + + fn set_maximum_token_cost(&mut self, cost: Option) { + self.maximum_token_cost = cost; + } + + fn gas_fees_paid_by(&self) -> GasFeesPaidBy { + self.gas_fees_paid_by + } + + fn set_gas_fees_paid_by(&mut self, payer: GasFeesPaidBy) { + self.gas_fees_paid_by = payer; + } +} + +impl TryFrom> for TokenPaymentInfoV1 { + type Error = ProtocolError; + + fn try_from(mut map: BTreeMap) -> Result { + let shielded_map: BTreeMap = map + .remove_map_as_btree_map_keep_values_as_platform_value::( + "shieldedPayment", + )?; + let shielded_payment: TokenShieldedPayment = shielded_map.try_into()?; + let v0: TokenPaymentInfoV0 = map.try_into()?; + Ok(TokenPaymentInfoV1::from_v0(v0, shielded_payment)) + } +} diff --git a/packages/rs-dpp/src/tokens/token_payment_info/v1/v1_accessors.rs b/packages/rs-dpp/src/tokens/token_payment_info/v1/v1_accessors.rs new file mode 100644 index 00000000000..741ee4ab982 --- /dev/null +++ b/packages/rs-dpp/src/tokens/token_payment_info/v1/v1_accessors.rs @@ -0,0 +1,13 @@ +use crate::tokens::token_payment_info::v1::TokenShieldedPayment; + +/// Accessors for the shielded payment a `TokenPaymentInfo` may carry (format version 1 and up). +pub trait TokenPaymentInfoAccessorsV1 { + /// The spend bundle paying the token cost out of the token's shielded pool, when the + /// payment is shielded (`V1`); `None` for a `V0` that pays from the identity's balance. + fn shielded_payment(&self) -> Option<&TokenShieldedPayment>; + + /// Sets or clears the shielded payment. Setting one on a `V0` upgrades it to `V1`; + /// clearing a `V1`'s downgrades it to `V0`, so a payment info without a pool payment + /// always has the `V0` shape. + fn set_shielded_payment(&mut self, shielded_payment: Option); +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/record_token_shielded_pool_anchors/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/record_token_shielded_pool_anchors/v0/mod.rs index 6b986ddd2b3..387bee5adbf 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/record_token_shielded_pool_anchors/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/record_token_shielded_pool_anchors/v0/mod.rs @@ -35,7 +35,12 @@ where // does not exist must never turn the block end into a storage error. if !self .drive - .has_token_shielded_pool(*token_id, Some(transaction), platform_version) + .has_token_shielded_pool( + *token_id, + Some(transaction), + &mut vec![], + platform_version, + ) .map_err(Error::Drive)? { continue; diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index 7c5badddf26..e5e27ff6428 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -119,6 +119,10 @@ impl Platform { self.transition_to_version_14(block_info, transaction, platform_version)?; } + if previous_protocol_version < 15 && platform_version.protocol_version >= 15 { + self.transition_to_version_15(transaction, platform_version)?; + } + Ok(()) } @@ -811,10 +815,18 @@ impl Platform { // exists, so both node populations build the same prefunded balances Merk. self.drive .insert_contract_fee_pot_trees(Some(transaction), platform_version)?; - // Token shielded pools root: the BigSumTree under the Tokens tree that holds one Orchard - // pool per token opting in (`TokenConfigurationV1::has_shielded_pool`). CONSENSUS-CRITICAL: - // the genesis-v14 path (`Drive::create_initial_state_structure_v4`) calls the same helper, - // so a chain born at v14 and one upgraded to it build a byte-identical `[Tokens]` subtree. + Ok(()) + } + /// When transitioning to version 15 we insert the token shielded pools root: the BigSumTree + /// under the Tokens tree that holds one Orchard pool per token opting in + /// (`TokenConfigurationV1::has_shielded_pool`). CONSENSUS-CRITICAL: the genesis-v15 path + /// (`Drive::create_initial_state_structure_v4`) calls the same helper, so a chain born at v15 + /// and one upgraded to it build a byte-identical `[Tokens]` subtree. + fn transition_to_version_15( + &self, + transaction: &Transaction, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { self.drive .insert_token_shielded_pools_root_tree(Some(transaction), platform_version)?; @@ -834,6 +846,7 @@ mod tests { shielded_credit_pool_path, MAIN_SHIELDED_CREDIT_POOL_KEY_U8, SHIELDED_ANCHORS_IN_POOL_KEY, SHIELDED_NOTES_KEY, SHIELDED_NULLIFIERS_KEY, }; + use drive::drive::tokens::paths::TOKEN_SHIELDED_POOLS_KEY; /// Recursively compares the GroveDB subtree rooted at `root_path` between /// two platforms and returns a list of human-readable differences (empty ⇒ @@ -3098,6 +3111,59 @@ mod tests { /// plus a cascade of differing child hashes), GREEN after (both `[128]`, /// because both paths now call the shared /// `Drive::insert_shielded_pool_structure`). + /// The token shielded pools root is built by two paths that must coincide byte for byte: + /// `Drive::create_initial_state_structure_v4` at a v15 genesis and + /// `transition_to_version_15` on a chain upgraded from v14. + #[test] + fn test_genesis_v15_and_upgrade_to_v15_build_identical_token_shielded_pools_root() { + let platform_version_15 = PlatformVersion::get(15).expect("expected v15"); + + let platform_a = TestPlatformBuilder::new() + .with_initial_protocol_version(15) + .build_with_mock_rpc() + .set_genesis_state(); + + let platform_b = TestPlatformBuilder::new() + .with_initial_protocol_version(14) + .build_with_mock_rpc() + .set_genesis_state(); + + // A genuine v14 genesis must not contain the pools root yet. + { + let txn = platform_b.drive.grove.start_transaction(); + let tokens_path: [&[u8]; 1] = [&[RootTree::Tokens as u8]]; + let pools_root_pre = platform_b.drive.grove.get( + SubtreePath::from(&tokens_path[..]), + &[TOKEN_SHIELDED_POOLS_KEY], + Some(&txn), + &platform_version_15.drive.grove_version, + ); + assert!( + pools_root_pre.value.is_err(), + "v14 genesis must not contain the token shielded pools root before the upgrade; got {:?}", + pools_root_pre.value + ); + } + + let txn_b = platform_b.drive.grove.start_transaction(); + platform_b + .transition_to_version_15(&txn_b, platform_version_15) + .expect("upgrade: transition_to_version_15 should succeed"); + + let diffs = collect_subtree_diffs( + &platform_a, + &platform_b, + &txn_b, + vec![vec![RootTree::Tokens as u8]], + ); + assert!( + diffs.is_empty(), + "CONSENSUS FORK: the [Tokens] subtree differs between a fresh genesis-v15 node and an \ + in-place-upgraded v15 node.\n{}", + diffs.join("\n"), + ); + } + #[test] fn test_genesis_v12_and_upgrade_to_v12_build_identical_shielded_pool() { let platform_version_12 = PlatformVersion::get(12).expect("expected v12"); diff --git a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs index df72e540818..38dd99ef392 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs @@ -6,11 +6,16 @@ use dpp::block::block_info::BlockInfo; use dpp::consensus::codes::ErrorWithCode; use dpp::fee::Credits; use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; +use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; use dpp::state_transition::batch_transition::batched_transition::token_transition::{ TokenTransition, TokenTransitionV0Methods, }; use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRef; +use dpp::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; +use dpp::state_transition::batch_transition::document_base_transition::v1::v1_methods::DocumentBaseTransitionV1Methods; use dpp::state_transition::StateTransition; +use dpp::tokens::token_payment_info::methods::v0::TokenPaymentInfoMethodsV0; +use dpp::tokens::token_payment_info::v1::v1_accessors::TokenPaymentInfoAccessorsV1; use crate::execution::types::state_transition_container::v0::{ DecodedStateTransition, InvalidStateTransition, InvalidWithProtocolErrorStateTransition, @@ -392,8 +397,8 @@ fn error_to_internal_error_execution_result( StateTransitionExecutionResult::InternalError(error_with_st.error.to_string()) } -/// The token shielded pools a state transition writes to: the token ids of every -/// `TokenShield`, `TokenUnshield` and `TokenShieldedTransfer` in a batch. +/// The token shielded pools a state transition writes to: the token ids of every token pool +/// transition in a batch and of every document whose token cost is paid from a pool. fn token_shielded_pools_touched(state_transition: &StateTransition) -> Vec<[u8; 32]> { match state_transition { StateTransition::Batch(batch) => batch @@ -408,6 +413,13 @@ fn token_shielded_pools_touched(state_transition: &StateTransition) -> Vec<[u8; | TokenTransition::ClaimToPool(_) | TokenTransition::DirectPurchaseToPool(_)), ) => Some(token_transition.token_id().to_buffer()), + BatchedTransitionRef::Document(document_transition) => { + let base = document_transition.base(); + base.token_payment_info_ref() + .as_ref() + .filter(|info| info.shielded_payment().is_some()) + .map(|info| info.token_id(base.data_contract_id()).to_buffer()) + } _ => None, }) .collect(), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs index a7361c2e9c1..d4c9deb9643 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs @@ -5,9 +5,12 @@ use crate::rpc::core::CoreRPCLike; use dpp::consensus::basic::state_transition::StateTransitionNotActiveError; use dpp::prelude::ConsensusValidationResult; use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; +use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransition; use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRef; +use dpp::state_transition::batch_transition::document_base_transition::v1::v1_methods::DocumentBaseTransitionV1Methods; use dpp::state_transition::StateTransition; +use dpp::tokens::token_payment_info::v1::v1_accessors::TokenPaymentInfoAccessorsV1; use dpp::version::feature_initial_protocol_versions::{ ADDRESS_FUNDS_INITIAL_PROTOCOL_VERSION, CONTRACT_FEE_CLAIM_INITIAL_PROTOCOL_VERSION, CONTRACT_USER_MODERATION_INITIAL_PROTOCOL_VERSION, @@ -69,7 +72,8 @@ impl StateTransitionIsAllowedValidationV0 for StateTransition { ) -> Result, Error> { match self { StateTransition::Batch(st) => { - // Token shielded pools (and the three batch transitions that use them) are a + // Token shielded pools (the batch transitions that use them and a document token + // cost paid from one) are a // protocol-version feature, not a table-versioned validator, so the gate is // applied to the batch as a whole before its own `is_allowed` runs. if platform_version.protocol_version < TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION @@ -98,6 +102,15 @@ impl StateTransitionIsAllowedValidationV0 for StateTransition { BatchedTransitionRef::Token( TokenTransition::DirectPurchaseToPool(_), ) => Some("TokenDirectPurchaseToPool"), + BatchedTransitionRef::Document(document_transition) + if document_transition + .base() + .token_payment_info_ref() + .as_ref() + .is_some_and(|info| info.shielded_payment().is_some()) => + { + Some("DocumentShieldedTokenPayment") + } _ => None, }) { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs index c3d181e2d21..52d814eb17d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs @@ -36,6 +36,11 @@ use dpp::state_transition::batch_transition::token_burn_from_pool_transition::v0 use dpp::state_transition::batch_transition::token_claim_to_pool_transition::v0::v0_methods::TokenClaimToPoolTransitionV0Methods; use dpp::state_transition::batch_transition::token_direct_purchase_to_pool_transition::v0::v0_methods::TokenDirectPurchaseToPoolTransitionV0Methods; use dpp::state_transition::batch_transition::BatchTransition; +use dpp::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; +use dpp::state_transition::batch_transition::document_base_transition::v1::v1_methods::DocumentBaseTransitionV1Methods; +use dpp::tokens::token_payment_info::methods::v0::TokenPaymentInfoMethodsV0; +use dpp::tokens::token_payment_info::v1::v1_accessors::TokenPaymentInfoAccessorsV1; +use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; use dpp::state_transition::{StateTransition, StateTransitionOwned}; use dpp::util::hash::hash_single; use dpp::validation::SimpleConsensusValidationResult; @@ -200,6 +205,13 @@ impl StateTransitionHasShieldedProofValidationV0 for StateTransition { BatchedTransitionRef::Token(TokenTransition::DirectPurchaseToPool(t)) => { t.actions().len() } + BatchedTransitionRef::Document(document_transition) => document_transition + .base() + .token_payment_info_ref() + .as_ref() + .and_then(|info| info.shielded_payment()) + .map(|payment| payment.actions.len()) + .unwrap_or(0), _ => 0, }) .sum(), @@ -233,6 +245,22 @@ impl StateTransitionHasShieldedProofValidationV0 for StateTransition { contract_id: token_transition.data_contract_id().to_buffer(), nonce: token_transition.identity_contract_nonce(), }), + BatchedTransitionRef::Document(document_transition) + if document_transition + .base() + .token_payment_info_ref() + .as_ref() + .is_some_and(|info| info.shielded_payment().is_some()) => + { + Some(ShieldedProofAdmissionKey::IdentityContract { + identity_id: owner_id, + contract_id: document_transition + .base() + .data_contract_id() + .to_buffer(), + nonce: document_transition.base().identity_contract_nonce(), + }) + } _ => None, }) } @@ -1409,6 +1437,37 @@ fn validate_batch_token_shielded_proofs( &[], ) } + // A document whose token cost is paid from the token's shielded pool: the payment + // states the amount its bundle proves, and state validation rejects the document + // when that is not the document type's cost, so the bundle can be checked here. + BatchedTransitionRef::Document(document_transition) => { + let base = document_transition.base(); + let Some(token_payment_info) = base.token_payment_info_ref() else { + continue; + }; + let Some(payment) = token_payment_info.shielded_payment() else { + continue; + }; + let extra_sighash_data = dpp::shielded::document_token_payment_extra_sighash_data( + &token_payment_info + .token_id(base.data_contract_id()) + .to_buffer(), + &owner_id, + &base.data_contract_id().to_buffer(), + &base.id().to_buffer(), + payment.amount, + platform_version, + )?; + reconstruct_and_verify_bundle( + &payment.actions, + FLAGS_SPENDS_AND_OUTPUTS, + payment.amount as i64, + &payment.anchor, + &payment.proof, + &payment.binding_signature, + &extra_sighash_data, + ) + } _ => continue, }; if let Err(e) = result { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_base_transaction_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_base_transaction_action/mod.rs index 7e1c176e5bf..b230b1c4a82 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_base_transaction_action/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_base_transaction_action/mod.rs @@ -8,9 +8,11 @@ use crate::error::Error; use crate::error::execution::ExecutionError; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; use crate::execution::validation::state_transition::batch::action_validation::document::document_base_transaction_action::state_v0::DocumentBaseTransitionActionStateValidationV0; +use crate::execution::validation::state_transition::batch::action_validation::document::document_base_transaction_action::state_v1::DocumentBaseTransitionActionStateValidationV1; use crate::platform_types::platform::PlatformStateRef; mod state_v0; +mod state_v1; pub trait DocumentBaseTransitionActionValidation { #[allow(clippy::too_many_arguments)] @@ -53,9 +55,18 @@ impl DocumentBaseTransitionActionValidation for DocumentBaseTransitionAction { transaction, platform_version, ), + 1 => self.validate_state_v1( + platform, + owner_id, + block_info, + transition_type, + execution_context, + transaction, + platform_version, + ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "DocumentBaseTransitionAction::validate_state".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_base_transaction_action/state_v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_base_transaction_action/state_v1/mod.rs new file mode 100644 index 00000000000..4d58f4d7438 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_base_transaction_action/state_v1/mod.rs @@ -0,0 +1,56 @@ +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::document::document_base_transaction_action::state_v0::DocumentBaseTransitionActionStateValidationV0; +use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; +use dpp::identifier::Identifier; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::{DocumentBaseTransitionAction, DocumentBaseTransitionActionAccessorsV0}; + +pub(in crate::execution::validation::state_transition::state_transitions::batch::action_validation) trait DocumentBaseTransitionActionStateValidationV1 { + #[allow(clippy::too_many_arguments)] + fn validate_state_v1( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + transition_type: &str, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl DocumentBaseTransitionActionStateValidationV1 for DocumentBaseTransitionAction { + /// Version 1 (protocol version 15): a token cost paid out of the token's shielded pool + /// (`TokenPaymentInfo::V1`) does not touch the owner's token balance, so the balance and + /// frozen-account checks of v0 do not apply; the pool side (pool exists, token not paused, + /// anchor, unspent nullifiers, pool balance and the proof) is validated by + /// `validate_document_shielded_token_payment` once the document action itself is valid. + /// A cost paid from the balance is validated exactly as in v0. + fn validate_state_v1( + &self, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + transition_type: &str, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + if self.shielded_token_payment().is_some() { + return Ok(SimpleConsensusValidationResult::new()); + } + self.validate_state_v0( + platform, + owner_id, + block_info, + transition_type, + execution_context, + transaction, + platform_version, + ) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/advanced_structure_v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/advanced_structure_v1/mod.rs index f7bb13051c1..db731166140 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/advanced_structure_v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/advanced_structure_v1/mod.rs @@ -254,6 +254,7 @@ mod tests { gas_fees_paid_by: GasFeesPaidBy::default(), contract_gas_fees_paid_by: GasFeesPaidBy::default(), declared_action_fee: None, + shielded_token_payment: None, }), block_info: BlockInfo::default(), data, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_shielded_token_payment/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_shielded_token_payment/mod.rs new file mode 100644 index 00000000000..d255e76de1b --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_shielded_token_payment/mod.rs @@ -0,0 +1,201 @@ +//! State validation of a document action's token cost paid out of the token's shielded pool +//! (`TokenPaymentInfo::V1`). +//! +//! The document action itself is validated first by its own validator; this runs afterwards, +//! from the batch state validation, and mirrors the pool side of `TokenUnshield` (the notes +//! leave the pool; the cost lands in the contract owner's balance or leaves the supply): the +//! pool must exist, the token must not be paused, the anchor must be recorded, the nullifiers +//! unspent, the pool must hold the amount, and the spend bundle must verify with the token id, +//! the batch owner, the document's contract and id and the amount bound into its sighash. + +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_pool_common::{ + charge_drive_operations, validate_minimum_token_pool_notes, validate_token_not_paused, + validate_token_pool_anchor_exists, validate_token_pool_nullifiers, verify_token_pool_bundle, +}; +use crate::execution::validation::state_transition::state_transitions::shielded_common::FLAGS_SPENDS_AND_OUTPUTS; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformStateRef; +use dpp::block::block_info::BlockInfo; +use dpp::consensus::state::shielded::invalid_shielded_proof_error::InvalidShieldedProofError; +use dpp::consensus::state::state_error::StateError; +use dpp::consensus::state::token::TokenShieldedPoolNotEnabledError; +use dpp::prelude::Identifier; +use dpp::shielded::document_token_payment_extra_sighash_data; +use dpp::tokens::token_payment_info::v1::TokenShieldedPayment; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; +use drive::query::TransactionArg; +use drive::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::{ + DocumentBaseTransitionAction, DocumentBaseTransitionActionAccessorsV0, +}; + +/// Validates the pool side of a document action's shielded token payment and verifies its +/// bundle (in block processing; CheckTx verifies it under the nonce-aware admission instead). +#[allow(clippy::too_many_arguments)] +pub(in crate::execution::validation::state_transition::state_transitions::batch) fn validate_document_shielded_token_payment( + base: &DocumentBaseTransitionAction, + payment: &TokenShieldedPayment, + platform: &PlatformStateRef, + owner_id: Identifier, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + validation_mode: ValidationMode, + transaction: TransactionArg, + platform_version: &PlatformVersion, +) -> Result { + // The transformer only keeps a shielded payment when the document type has a token cost + // and the payment's amount equals it. + let Some((token_id, _effect, cost)) = base.token_cost() else { + return Ok(SimpleConsensusValidationResult::new()); + }; + let token_id_bytes = token_id.to_buffer(); + + let mut drive_operations = vec![]; + + // The pool trees exist exactly for the tokens whose configuration opted in. + let pool_exists = platform.drive.has_token_shielded_pool( + token_id_bytes, + transaction, + &mut drive_operations, + platform_version, + )?; + if !pool_exists { + charge_drive_operations( + platform.drive, + block_info, + execution_context, + drive_operations, + platform_version, + )?; + return Ok(SimpleConsensusValidationResult::new_with_error( + TokenShieldedPoolNotEnabledError::new(token_id).into(), + )); + } + + let validation_result = validate_token_not_paused( + platform, + token_id, + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + charge_drive_operations( + platform.drive, + block_info, + execution_context, + drive_operations, + platform_version, + )?; + return Ok(validation_result); + } + + let validation_result = validate_minimum_token_pool_notes( + platform.drive, + &token_id_bytes, + transaction, + &mut drive_operations, + platform_version, + )?; + if !validation_result.is_valid() { + charge_drive_operations( + platform.drive, + block_info, + execution_context, + drive_operations, + platform_version, + )?; + return Ok(validation_result); + } + + let validation_result = validate_token_pool_anchor_exists( + platform.drive, + &token_id_bytes, + &payment.anchor, + transaction, + &mut drive_operations, + platform_version, + )?; + if !validation_result.is_valid() { + charge_drive_operations( + platform.drive, + block_info, + execution_context, + drive_operations, + platform_version, + )?; + return Ok(validation_result); + } + + let nullifiers: Vec<[u8; 32]> = payment + .actions + .iter() + .map(|action| action.nullifier) + .collect(); + let validation_result = validate_token_pool_nullifiers( + platform.drive, + &token_id_bytes, + &nullifiers, + transaction, + &mut drive_operations, + platform_version, + )?; + if !validation_result.is_valid() { + charge_drive_operations( + platform.drive, + block_info, + execution_context, + drive_operations, + platform_version, + )?; + return Ok(validation_result); + } + + let pool_balance = platform.drive.read_token_shielded_pool_total_balance( + &token_id_bytes, + transaction, + &mut drive_operations, + platform_version, + )?; + + charge_drive_operations( + platform.drive, + block_info, + execution_context, + drive_operations, + platform_version, + )?; + + if pool_balance < cost { + return Ok(SimpleConsensusValidationResult::new_with_error( + StateError::InvalidShieldedProofError(InvalidShieldedProofError::new(format!( + "token shielded pool has insufficient balance: pool has {} but the document payment requires {}", + pool_balance, cost + ))) + .into(), + )); + } + + let extra_sighash_data = document_token_payment_extra_sighash_data( + &token_id_bytes, + &owner_id.to_buffer(), + &base.data_contract_id().to_buffer(), + &base.id().to_buffer(), + cost, + platform_version, + )?; + + verify_token_pool_bundle( + validation_mode, + &payment.actions, + FLAGS_SPENDS_AND_OUTPUTS, + cost as i64, + &payment.anchor, + &payment.proof, + &payment.binding_signature, + &extra_sighash_data, + ) +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/mod.rs index 3a4130219e7..e3a5712fb14 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/mod.rs @@ -5,5 +5,6 @@ pub(crate) mod document_index_only_delete_transition_action; pub(crate) mod document_purchase_transition_action; pub(crate) mod document_reference_validation; pub(crate) mod document_replace_transition_action; +pub(crate) mod document_shielded_token_payment; pub(crate) mod document_transfer_transition_action; pub(crate) mod document_update_price_transition_action; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs index 6e343589982..bb04ed192c7 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs @@ -42,7 +42,7 @@ use drive::state_transition_action::batch::batched_transition::token_transition: use std::collections::HashSet; /// The token's configuration must opt into a shielded pool. -pub(super) fn validate_token_shielded_pool_enabled( +pub(crate) fn validate_token_shielded_pool_enabled( base: &TokenBaseTransitionAction, ) -> Result { if !base.token_configuration()?.has_shielded_pool() { @@ -55,7 +55,7 @@ pub(super) fn validate_token_shielded_pool_enabled( /// A paused token allows no shielded operation either: shielding, unshielding and pool /// transfers all move the token. -pub(super) fn validate_token_not_paused( +pub(crate) fn validate_token_not_paused( platform: &PlatformStateRef, token_id: Identifier, block_info: &BlockInfo, @@ -89,7 +89,7 @@ pub(super) fn validate_token_not_paused( /// A frozen identity token account can neither shield out of nor (unless the token allows /// transfers to frozen balances) receive an unshield into its balance. #[allow(clippy::too_many_arguments)] -pub(super) fn validate_identity_token_account_not_frozen( +pub(crate) fn validate_identity_token_account_not_frozen( platform: &PlatformStateRef, token_id: Identifier, identity_id: Identifier, @@ -129,7 +129,7 @@ pub(super) fn validate_identity_token_account_not_frozen( /// Meters the GroveDB reads collected in `drive_operations` into the execution context so /// the batch owner pays for them, as every other identity-paid read does. -pub(super) fn charge_drive_operations( +pub(crate) fn charge_drive_operations( drive: &Drive, block_info: &BlockInfo, execution_context: &mut StateTransitionExecutionContext, @@ -152,7 +152,7 @@ pub(super) fn charge_drive_operations( } /// The bundle's anchor must be one of the token pool's recorded anchors (O(1) lookup). -pub(super) fn validate_token_pool_anchor_exists( +pub(crate) fn validate_token_pool_anchor_exists( drive: &Drive, token_id: &[u8; 32], anchor: &[u8; 32], @@ -176,7 +176,7 @@ pub(super) fn validate_token_pool_anchor_exists( } /// No nullifier may repeat within the bundle or already be spent in the token pool. -pub(super) fn validate_token_pool_nullifiers( +pub(crate) fn validate_token_pool_nullifiers( drive: &Drive, token_id: &[u8; 32], nullifiers: &[[u8; 32]], @@ -212,7 +212,7 @@ pub(super) fn validate_token_pool_nullifiers( /// The anonymity-set floor for outflows with an observable destination /// (`minimum_token_pool_notes_for_outgoing`, 0 at introduction). -pub(super) fn validate_minimum_token_pool_notes( +pub(crate) fn validate_minimum_token_pool_notes( drive: &Drive, token_id: &[u8; 32], transaction: TransactionArg, @@ -252,7 +252,7 @@ pub(super) fn validate_minimum_token_pool_notes( /// skipped here: `check_tx` verifies it afterwards under the nonce-aware admission limiter, /// once the fee estimate has shown the identity can pay for it. #[allow(clippy::too_many_arguments)] -pub(super) fn verify_token_pool_bundle( +pub(crate) fn verify_token_pool_bundle( validation_mode: ValidationMode, actions: &[SerializedAction], flags: u8, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs index 2f4a5072eb6..e258d784b53 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v0/mod.rs @@ -181,6 +181,7 @@ mod tests { gas_fees_paid_by: GasFeesPaidBy::DocumentOwner, contract_gas_fees_paid_by: GasFeesPaidBy::default(), declared_action_fee: None, + shielded_token_payment: None, } .into(); @@ -329,6 +330,7 @@ mod tests { gas_fees_paid_by: GasFeesPaidBy::DocumentOwner, contract_gas_fees_paid_by: GasFeesPaidBy::default(), declared_action_fee: None, + shielded_token_payment: None, }), }), ); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v2/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v2/mod.rs index a1eabc33db5..59e918548ca 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v2/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/data_triggers/triggers/withdrawals/v2/mod.rs @@ -231,6 +231,7 @@ mod tests { gas_fees_paid_by: GasFeesPaidBy::DocumentOwner, contract_gas_fees_paid_by: GasFeesPaidBy::default(), declared_action_fee: None, + shielded_token_payment: None, }), }), ); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs index f888d46411c..5f251313bc9 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs @@ -15,6 +15,8 @@ use drive::state_transition_action::system::bump_identity_data_contract_nonce_ac use crate::error::Error; use crate::error::execution::ExecutionError; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::batch::action_validation::document::document_shielded_token_payment::validate_document_shielded_token_payment; +use drive::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0; use crate::execution::validation::state_transition::batch::action_validation::document::document_create_transition_action::DocumentCreateTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::document::document_delete_transition_action::DocumentDeleteTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::document::document_index_only_delete_transition_action::DocumentIndexOnlyDeleteTransitionActionValidation; @@ -107,7 +109,7 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { // Next we need to validate the structure of all actions (this means with the data contract) for transition in state_transition_action.transitions_take() { - let transition_validation_result = match &transition { + let mut transition_validation_result = match &transition { BatchedTransitionAction::DocumentAction(document_action) => match document_action { DocumentTransitionAction::CreateAction(create_action) => create_action .validate_state( @@ -361,6 +363,26 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { } }; + // A document whose token cost is paid out of the token's shielded pool: the pool + // side and the bundle are validated once the document action itself is valid. + if transition_validation_result.is_valid() { + if let BatchedTransitionAction::DocumentAction(document_action) = &transition { + if let Some(payment) = document_action.base().shielded_token_payment() { + transition_validation_result = validate_document_shielded_token_payment( + document_action.base(), + payment, + platform, + owner_id, + block_info, + execution_context, + validation_mode, + transaction, + platform_version, + )?; + } + } + } + if !transition_validation_result.is_valid() { // If a state transition isn't valid we still need to bump the identity data contract nonce validation_result.add_errors(transition_validation_result.errors); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 27bd87bef59..54fdada4701 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -864,7 +864,7 @@ mod token_shielded_pool_tests { } #[tokio::test] - async fn test_token_shield_rejected_before_protocol_version_14() { + async fn test_token_shield_rejected_before_protocol_version_15() { let platform_version = PlatformVersion::get(TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION - 1) .expect("previous protocol version"); @@ -982,7 +982,7 @@ mod token_shielded_pool_tests { } #[tokio::test] - async fn test_contract_create_with_shielded_pool_token_rejected_before_protocol_version_14() { + async fn test_contract_create_with_shielded_pool_token_rejected_before_protocol_version_15() { let platform_version = PlatformVersion::get(TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION - 1) .expect("previous protocol version"); @@ -1727,3 +1727,499 @@ mod token_pool_mint_burn_claim_purchase_tests { assert_eq!(pool_balance(&platform, token_id), 3); } } + +/// Documents whose token cost is paid out of the token's shielded pool (`TokenPaymentInfo::V1`). +/// +/// The card game contract charges 10 of its token 0 to create a `card`, burned or paid to the +/// contract owner depending on the fixture. The buyer shields the tokens first, then pays the +/// cost with a spend bundle bound to the token, the buyer, the contract and the document id. +mod document_shielded_token_payment_tests { + use super::token_shielded_pool_tests::{ + assert_tokens_conserved, build_shield_bundle, build_spend_bundle, dummy_bundle, + identity_token_balance, insert_token_pool_anchor, nullifier_is_spent, + platform_with_latest_version, pool_balance, process, spendable_note, + }; + use super::*; + use crate::execution::validation::state_transition::state_transitions::tests::add_tokens_to_identity; + use crate::rpc::core::MockCoreRPCLike; + use crate::test::helpers::setup::TempPlatform; + use dpp::data_contract::accessors::v0::DataContractV0Setters; + use dpp::data_contract::accessors::v1::DataContractV1Setters; + use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters; + use dpp::data_contract::document_type::DocumentTypeRef; + use dpp::data_contract::DataContract; + use dpp::document::{Document, DocumentV0Getters}; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::{Identity, IdentityPublicKey}; + use dpp::shielded::{document_token_payment_extra_sighash_data_v0, OrchardBundleParams}; + use dpp::tokens::calculate_token_id; + use dpp::tokens::gas_fees_paid_by::GasFeesPaidBy; + use dpp::tokens::token_payment_info::v1::{TokenPaymentInfoV1, TokenShieldedPayment}; + use dpp::tokens::token_payment_info::TokenPaymentInfo; + use dpp::version::feature_initial_protocol_versions::TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION; + use drive::util::test_helpers::setup_contract; + use platform_version::version::PlatformVersion; + use simple_signer::signer::SimpleSigner; + + const CARD_COST: u64 = 10; + const SHIELDED: u64 = 15; + + /// The card game contract with an in-game currency: creating a `card` costs `CARD_COST` of + /// token 0, burned (`burn`) or paid to the contract owner. `pool` gives token 0 a shielded pool. + fn card_game_contract( + platform: &TempPlatform, + owner_id: Identifier, + burn: bool, + pool: bool, + platform_version: &PlatformVersion, + ) -> (DataContract, Identifier) { + let data_contract_id = DataContract::generate_data_contract_id_v0(owner_id, 1); + let path = if burn { + "tests/supporting_files/contract/crypto-card-game/crypto-card-game-in-game-currency-burn-tokens.json" + } else { + "tests/supporting_files/contract/crypto-card-game/crypto-card-game-in-game-currency.json" + }; + let contract = setup_contract( + &platform.drive, + path, + Some(data_contract_id.to_buffer()), + Some(owner_id.to_buffer()), + Some(|data_contract: &mut DataContract| { + data_contract.set_created_at_epoch(Some(0)); + data_contract.set_created_at(Some(0)); + data_contract.set_created_at_block_height(Some(0)); + if pool { + let configuration = data_contract + .tokens_mut() + .and_then(|tokens| tokens.get_mut(&0)) + .expect("token 0"); + configuration.set_has_shielded_pool(true); + } + }), + None, + Some(platform_version), + ); + ( + contract, + calculate_token_id(data_contract_id.as_bytes(), 0).into(), + ) + } + + fn random_card( + rng: &mut StdRng, + card_document_type: DocumentTypeRef, + owner_id: Identifier, + platform_version: &PlatformVersion, + ) -> (Document, Bytes32) { + let entropy = Bytes32::random_with_rng(rng); + let mut document = card_document_type + .random_document_with_identifier_and_entropy( + rng, + owner_id, + entropy, + DocumentFieldFillType::DoNotFillIfNotRequired, + DocumentFieldFillSize::AnyDocumentFillSize, + platform_version, + ) + .expect("expected a random document"); + document.set("attack", 4.into()); + document.set("defense", 7.into()); + (document, entropy) + } + + fn shielded_payment(bundle: OrchardBundleParams, amount: u64) -> TokenShieldedPayment { + TokenShieldedPayment { + amount, + actions: bundle.actions, + anchor: bundle.anchor, + proof: bundle.proof, + binding_signature: bundle.binding_signature, + } + } + + fn payment_info(shielded_payment: TokenShieldedPayment) -> TokenPaymentInfo { + TokenPaymentInfo::V1(TokenPaymentInfoV1 { + payment_token_contract_id: None, + token_contract_position: 0, + minimum_token_cost: None, + maximum_token_cost: Some(CARD_COST), + gas_fees_paid_by: GasFeesPaidBy::DocumentOwner, + shielded_payment, + }) + } + + fn total_supply(platform: &TempPlatform, token_id: Identifier) -> u64 { + platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, PlatformVersion::latest()) + .expect("total supply") + .expect("supply exists") + } + + /// Funds the buyer with `SHIELDED` tokens and shields all of them (identity contract nonce 1). + async fn fund_and_shield( + platform: &mut TempPlatform, + contract: &DataContract, + token_id: Identifier, + buyer: &Identity, + key: &IdentityPublicKey, + signer: &SimpleSigner, + seed: u64, + platform_version: &PlatformVersion, + ) { + add_tokens_to_identity(platform, token_id, buyer.id(), SHIELDED); + let shield = BatchTransition::new_token_shield_transition( + token_id, + buyer.id(), + contract.id(), + 0, + SHIELDED, + build_shield_bundle(SHIELDED, seed), + key, + 1, + 0, + signer, + platform_version, + None, + ) + .await + .expect("token shield transition"); + let result = process(platform, &shield); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(platform, token_id), SHIELDED); + } + + #[tokio::test] + async fn test_document_creation_paid_from_the_shielded_pool_with_a_burn() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9301); + + let (contract_owner, _, _) = setup_identity(&mut platform, 958, dash_to_credits!(0.1)); + let (buyer, signer, key) = setup_identity(&mut platform, 234, dash_to_credits!(0.5)); + let (contract, token_id) = + card_game_contract(&platform, contract_owner.id(), true, true, platform_version); + fund_and_shield( + &mut platform, + &contract, + token_id, + &buyer, + &key, + &signer, + 41, + platform_version, + ) + .await; + + let card_document_type = contract + .document_type_for_name("card") + .expect("card document type"); + let (document, entropy) = + random_card(&mut rng, card_document_type, buyer.id(), platform_version); + + // A 15 note the wallet holds pays the 10 cost; 5 return to the pool as change. + let (note, anchor, merkle_path) = spendable_note(SHIELDED, 4); + insert_token_pool_anchor(&platform, token_id, &anchor); + let extra = document_token_payment_extra_sighash_data_v0( + &token_id.to_buffer(), + &buyer.id().to_buffer(), + &contract.id().to_buffer(), + &document.id().to_buffer(), + CARD_COST, + ); + let (bundle, value_balance) = + build_spend_bundle(note, merkle_path, anchor, CARD_COST, &extra, 42); + assert_eq!(value_balance, CARD_COST as i64); + let payment = shielded_payment(bundle, CARD_COST); + + let transition = BatchTransition::new_document_creation_transition_from_document( + document, + card_document_type, + entropy.0, + &key, + 2, + 0, + Some(payment_info(payment.clone())), + &signer, + platform_version, + None, + ) + .await + .expect("document create transition"); + + let result = process(&platform, &transition); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + // The cost left the pool and the supply; the buyer's balance was never touched. + assert_eq!(pool_balance(&platform, token_id), SHIELDED - CARD_COST); + assert_eq!(total_supply(&platform, token_id), SHIELDED - CARD_COST); + assert_eq!( + identity_token_balance(&platform, token_id, buyer.id()).unwrap_or_default(), + 0 + ); + assert_eq!( + identity_token_balance(&platform, token_id, contract_owner.id()), + None + ); + assert!(nullifier_is_spent( + &platform, + token_id, + &payment.actions[0].nullifier + )); + assert_tokens_conserved(&platform); + + // The spent notes cannot pay for another document. + let (replay_document, replay_entropy) = + random_card(&mut rng, card_document_type, buyer.id(), platform_version); + let replay = BatchTransition::new_document_creation_transition_from_document( + replay_document, + card_document_type, + replay_entropy.0, + &key, + 3, + 0, + Some(payment_info(payment)), + &signer, + platform_version, + None, + ) + .await + .expect("document create transition"); + let result = process(&platform, &replay); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::NullifierAlreadySpentError(_)), + .. + }] + ); + assert_eq!(pool_balance(&platform, token_id), SHIELDED - CARD_COST); + assert_tokens_conserved(&platform); + } + + #[tokio::test] + async fn test_document_creation_paid_from_the_shielded_pool_to_the_contract_owner() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9302); + + let (contract_owner, _, _) = setup_identity(&mut platform, 959, dash_to_credits!(0.1)); + let (buyer, signer, key) = setup_identity(&mut platform, 235, dash_to_credits!(0.5)); + let (contract, token_id) = card_game_contract( + &platform, + contract_owner.id(), + false, + true, + platform_version, + ); + fund_and_shield( + &mut platform, + &contract, + token_id, + &buyer, + &key, + &signer, + 43, + platform_version, + ) + .await; + + let card_document_type = contract + .document_type_for_name("card") + .expect("card document type"); + let (document, entropy) = + random_card(&mut rng, card_document_type, buyer.id(), platform_version); + + let (note, anchor, merkle_path) = spendable_note(SHIELDED, 5); + insert_token_pool_anchor(&platform, token_id, &anchor); + let extra = document_token_payment_extra_sighash_data_v0( + &token_id.to_buffer(), + &buyer.id().to_buffer(), + &contract.id().to_buffer(), + &document.id().to_buffer(), + CARD_COST, + ); + let (bundle, _) = build_spend_bundle(note, merkle_path, anchor, CARD_COST, &extra, 44); + + let transition = BatchTransition::new_document_creation_transition_from_document( + document, + card_document_type, + entropy.0, + &key, + 2, + 0, + Some(payment_info(shielded_payment(bundle, CARD_COST))), + &signer, + platform_version, + None, + ) + .await + .expect("document create transition"); + + let result = process(&platform, &transition); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + // The cost moved from the pool into the contract owner's balance; the supply is unchanged. + assert_eq!(pool_balance(&platform, token_id), SHIELDED - CARD_COST); + assert_eq!(total_supply(&platform, token_id), SHIELDED); + assert_eq!( + identity_token_balance(&platform, token_id, contract_owner.id()), + Some(CARD_COST) + ); + assert_tokens_conserved(&platform); + } + + #[tokio::test] + async fn test_document_creation_shielded_payment_must_match_the_token_cost() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9303); + + let (contract_owner, _, _) = setup_identity(&mut platform, 960, dash_to_credits!(0.1)); + let (buyer, signer, key) = setup_identity(&mut platform, 236, dash_to_credits!(0.5)); + let (contract, token_id) = + card_game_contract(&platform, contract_owner.id(), true, true, platform_version); + fund_and_shield( + &mut platform, + &contract, + token_id, + &buyer, + &key, + &signer, + 45, + platform_version, + ) + .await; + + let card_document_type = contract + .document_type_for_name("card") + .expect("card document type"); + let (document, entropy) = + random_card(&mut rng, card_document_type, buyer.id(), platform_version); + + // A bundle proving 9 where the card costs 10. + let underpaid = CARD_COST - 1; + let (note, anchor, merkle_path) = spendable_note(SHIELDED, 6); + insert_token_pool_anchor(&platform, token_id, &anchor); + let extra = document_token_payment_extra_sighash_data_v0( + &token_id.to_buffer(), + &buyer.id().to_buffer(), + &contract.id().to_buffer(), + &document.id().to_buffer(), + underpaid, + ); + let (bundle, _) = build_spend_bundle(note, merkle_path, anchor, underpaid, &extra, 46); + let payment = shielded_payment(bundle, underpaid); + + let transition = BatchTransition::new_document_creation_transition_from_document( + document, + card_document_type, + entropy.0, + &key, + 2, + 0, + Some(payment_info(payment.clone())), + &signer, + platform_version, + None, + ) + .await + .expect("document create transition"); + + let result = process(&platform, &transition); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError( + StateError::TokenShieldedPaymentAmountMismatchError(_) + ), + .. + }] + ); + // Nothing left the pool and the notes are still unspent. + assert_eq!(pool_balance(&platform, token_id), SHIELDED); + assert_eq!(total_supply(&platform, token_id), SHIELDED); + assert!(!nullifier_is_spent( + &platform, + token_id, + &payment.actions[0].nullifier + )); + assert_tokens_conserved(&platform); + } + + #[tokio::test] + async fn test_document_shielded_payment_rejected_before_protocol_version_15() { + let platform_version = + PlatformVersion::get(TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION - 1) + .expect("previous protocol version"); + let mut platform = TestPlatformBuilder::new() + .with_initial_protocol_version(platform_version.protocol_version) + .build_with_mock_rpc() + .set_genesis_state(); + let mut rng = StdRng::seed_from_u64(9304); + + let (contract_owner, _, _) = setup_identity(&mut platform, 961, dash_to_credits!(0.1)); + let (buyer, signer, key) = setup_identity(&mut platform, 237, dash_to_credits!(0.5)); + // No pool: a pooled token cannot exist before the pools root does. + let (contract, _token_id) = card_game_contract( + &platform, + contract_owner.id(), + true, + false, + platform_version, + ); + + let card_document_type = contract + .document_type_for_name("card") + .expect("card document type"); + let (document, entropy) = + random_card(&mut rng, card_document_type, buyer.id(), platform_version); + + let transition = BatchTransition::new_document_creation_transition_from_document( + document, + card_document_type, + entropy.0, + &key, + 1, + 0, + Some(payment_info(shielded_payment(dummy_bundle(), CARD_COST))), + &signer, + platform_version, + None, + ) + .await + .expect("document create transition"); + + let platform_state = platform.state.load(); + let serialized = transition.serialize_to_bytes().expect("serialize"); + let transaction = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("process state transition"); + + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::BasicError(BasicError::StateTransitionNotActiveError(_)) + )] + ); + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs index cfd6d27fdfb..774f62bb90d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs @@ -49,7 +49,11 @@ use dpp::consensus::state::document::document_incorrect_purchase_price_error::Do use dpp::consensus::state::document::document_not_for_sale_error::DocumentNotForSaleError; use dpp::document::property_names::PRICE; use dpp::document::{Document, DocumentV0Getters}; +use dpp::fee::fee_result::FeeResult; use dpp::fee::Credits; +use dpp::shielded::compute_shielded_verification_fee; +use dpp::tokens::token_payment_info::v1::v1_accessors::TokenPaymentInfoAccessorsV1; +use dpp::state_transition::batch_transition::document_base_transition::v1::v1_methods::DocumentBaseTransitionV1Methods; use dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; use dpp::prelude::{Revision, UserFeeIncrease}; use dpp::validation::SimpleConsensusValidationResult; @@ -865,6 +869,27 @@ impl BatchTransitionInternalTransformerV0 for BatchTransition { execution_context: &mut StateTransitionExecutionContext, platform_version: &PlatformVersion, ) -> Result, Error> { + // A token cost paid out of the token's shielded pool carries an Orchard bundle whose + // Halo 2 verification and per-action work GroveDB cannot meter: charged here, before + // anything can fail, so CheckTx admission and block execution price it identically and + // a rejected document still pays for the proof it made the validators check. + if let Some(payment) = transition + .base() + .token_payment_info_ref() + .as_ref() + .and_then(|token_payment_info| token_payment_info.shielded_payment()) + { + execution_context.add_operation(ValidationOperation::PrecalculatedOperation( + FeeResult { + processing_fee: compute_shielded_verification_fee( + payment.actions.len(), + platform_version, + )?, + ..Default::default() + }, + )); + } + if let Some(max_depth) = platform_version.system_limits.max_document_value_depth { if let Some(actual_depth) = transition.first_data_depth_exceeding(max_depth as usize) { return Self::failed_per_transition_action( diff --git a/packages/rs-drive-abci/src/query/shielded/mod.rs b/packages/rs-drive-abci/src/query/shielded/mod.rs index ab01fb9ff4b..5b22cbc3429 100644 --- a/packages/rs-drive-abci/src/query/shielded/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/mod.rs @@ -29,7 +29,7 @@ pub(super) enum ShieldedPoolSelector { impl ShieldedPoolSelector { /// Resolves the optional `token_id` a request carries. A token pool is only addressable - /// once token shielded pools exist (protocol version 14), and the id must be 32 bytes. + /// once token shielded pools exist (protocol version 15), and the id must be 32 bytes. pub(super) fn from_request( token_id: Option>, platform_version: &PlatformVersion, diff --git a/packages/rs-drive-proof-verifier/src/types.rs b/packages/rs-drive-proof-verifier/src/types.rs index 165accd54f7..8d0f29ad702 100644 --- a/packages/rs-drive-proof-verifier/src/types.rs +++ b/packages/rs-drive-proof-verifier/src/types.rs @@ -939,7 +939,7 @@ pub struct ShieldedEncryptedNotesQuery { )] pub struct ShieldedNullifiersQuery(pub Vec<[u8; 32]>); -/// Targets one token's shielded pool (protocol version 14+) for the parameterless shielded +/// Targets one token's shielded pool (protocol version 15+) for the parameterless shielded /// queries: pool state, notes count, anchors and most recent anchor. #[derive(Debug, Clone)] #[cfg_attr( diff --git a/packages/rs-drive/src/drive/document/index_uniqueness/mod.rs b/packages/rs-drive/src/drive/document/index_uniqueness/mod.rs index 064f1dde49f..cf69327dc69 100644 --- a/packages/rs-drive/src/drive/document/index_uniqueness/mod.rs +++ b/packages/rs-drive/src/drive/document/index_uniqueness/mod.rs @@ -130,6 +130,7 @@ mod tests { gas_fees_paid_by: GasFeesPaidBy::default(), contract_gas_fees_paid_by: GasFeesPaidBy::default(), declared_action_fee: None, + shielded_token_payment: None, }); let data = BTreeMap::from([( diff --git a/packages/rs-drive/src/drive/initialization/v4/mod.rs b/packages/rs-drive/src/drive/initialization/v4/mod.rs index 4faef58650e..a28d3cf9dc2 100644 --- a/packages/rs-drive/src/drive/initialization/v4/mod.rs +++ b/packages/rs-drive/src/drive/initialization/v4/mod.rs @@ -103,9 +103,9 @@ impl Drive { // through the same helper: the prefunded balances Merk is then built by the same // sequence of inserts on both node populations. self.insert_contract_fee_pot_trees(transaction, platform_version)?; - // Token shielded pools root (v4, protocol version 14): an empty BigSumTree under the + // Token shielded pools root (v4, protocol version 15): an empty BigSumTree under the // Tokens tree. Inserted after the batch so `[Tokens]` already exists, through the - // helper the in-place upgrade (`Platform::transition_to_version_14`) also calls. + // helper the in-place upgrade (`Platform::transition_to_version_15`) also calls. self.insert_token_shielded_pools_root_tree(transaction, platform_version)?; Ok(()) diff --git a/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v0/mod.rs b/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v0/mod.rs index 6e723e81c4e..60b0d7c9b3a 100644 --- a/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v0/mod.rs @@ -42,7 +42,7 @@ impl Drive { Ok(TotalTokensBalance { total_tokens_in_platform, total_identity_token_balances, - // Token shielded pools do not exist before protocol version 14. + // Token shielded pools do not exist before protocol version 15. total_token_shielded_pool_balances: 0, }) } diff --git a/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v1/mod.rs b/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v1/mod.rs index ddd56681587..c75a60edb1e 100644 --- a/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v1/mod.rs +++ b/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v1/mod.rs @@ -9,7 +9,7 @@ use grovedb::TransactionArg; use platform_version::version::PlatformVersion; impl Drive { - /// Version 1 (protocol version 14): the token shielded pools BigSumTree joins the identity + /// Version 1 (protocol version 15): the token shielded pools BigSumTree joins the identity /// balances on the balance side, so `identity balances + shielded pool balances` must equal /// the total supplies. Shielding moves tokens between the two terms without touching supply. #[inline(always)] diff --git a/packages/rs-drive/src/drive/tokens/paths.rs b/packages/rs-drive/src/drive/tokens/paths.rs index 69fc5945300..7e8a3bf8d3f 100644 --- a/packages/rs-drive/src/drive/tokens/paths.rs +++ b/packages/rs-drive/src/drive/tokens/paths.rs @@ -24,7 +24,7 @@ pub const TOKEN_DIRECT_SELL_PRICE_KEY: u8 = 92; pub const TOKEN_DISTRIBUTIONS_KEY: u8 = 32; /// Key for the token shielded pools tree (a BigSumTree of per-token pool SumTrees). /// -/// Introduced in protocol version 14. Each token whose configuration has `has_shielded_pool` +/// Introduced in protocol version 15. Each token whose configuration has `has_shielded_pool` /// owns one child SumTree keyed by its token id, laid out exactly like the credit shielded pool /// (`crate::drive::shielded::paths`): notes, nullifiers, anchors, anchors-by-height and a total /// balance SumItem. The BigSumTree total is the amount of every token currently shielded and diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/insert_root_tree.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/insert_root_tree.rs index 0e50083c219..e9b87405731 100644 --- a/packages/rs-drive/src/drive/tokens/shielded_pool/insert_root_tree.rs +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/insert_root_tree.rs @@ -10,8 +10,8 @@ impl Drive { /// BigSumTree) if it does not exist yet. /// /// CONSENSUS-CRITICAL: both the genesis path (`Drive::create_initial_state_structure_v4`) - /// and the in-place upgrade path (`Platform::transition_to_version_14`) call this one helper - /// so a chain born at protocol version 14 and a chain upgraded to it build a byte-identical + /// and the in-place upgrade path (`Platform::transition_to_version_15`) call this one helper + /// so a chain born at protocol version 15 and a chain upgraded to it build a byte-identical /// `[Tokens]` subtree. The tree stays empty until a token with a shielded pool is registered. pub fn insert_token_shielded_pools_root_tree( &self, diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs index 722e79a2a46..76909af14e7 100644 --- a/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs @@ -46,11 +46,13 @@ pub(in crate::drive::tokens) enum TokenPoolBalanceChange { } impl Drive { - /// Whether the token owns a shielded pool subtree, without touching it. + /// Whether the token owns a shielded pool subtree, without touching it. The read is + /// collected into `drive_operations` so a caller validating a state transition can charge it. pub fn has_token_shielded_pool( &self, token_id: [u8; 32], transaction: TransactionArg, + drive_operations: &mut Vec, platform_version: &PlatformVersion, ) -> Result { self.grove_has_raw( @@ -58,7 +60,7 @@ impl Drive { &token_id, DirectQueryType::StatefulDirectQuery, transaction, - &mut vec![], + drive_operations, &platform_version.drive, ) } @@ -540,10 +542,10 @@ mod tests { // The minter's own balance is not involved. assert_eq!(token_balance(&drive, identity_id), Some(1_000)); assert!(drive - .has_token_shielded_pool(TOKEN_ID, None, platform_version) + .has_token_shielded_pool(TOKEN_ID, None, &mut vec![], platform_version) .expect("pool lookup")); assert!(!drive - .has_token_shielded_pool([9u8; 32], None, platform_version) + .has_token_shielded_pool([9u8; 32], None, &mut vec![], platform_version) .expect("pool lookup")); } diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_create_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_create_transition.rs index 16859a60aed..75073dde515 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_create_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_create_transition.rs @@ -1,7 +1,7 @@ use crate::error::Error; use crate::state_transition_action::action_convert_to_operations::batch::DriveHighLevelBatchOperationConverter; use crate::util::batch::DriveOperation::{ - DocumentOperation, IdentityOperation, PrefundedSpecializedBalanceOperation, TokenOperation, + DocumentOperation, IdentityOperation, PrefundedSpecializedBalanceOperation, }; use crate::util::batch::{DocumentOperationType, DriveOperation, IdentityOperationType}; use crate::util::object_size_info::DocumentInfo::DocumentOwnedInfo; @@ -12,12 +12,10 @@ use dpp::block::epoch::Epoch; use dpp::document::Document; use dpp::prelude::Identifier; use std::borrow::Cow; -use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::tokens::token_amount_on_contract_token::DocumentActionTokenEffect; use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::{DocumentCreateTransitionAction, DocumentCreateTransitionActionAccessorsV0, DocumentFromCreateTransitionAction}; use dpp::version::PlatformVersion; -use crate::util::batch::drive_op_batch::{PrefundedSpecializedBalanceOperationType, TokenOperationType}; +use crate::util::batch::drive_op_batch::{PrefundedSpecializedBalanceOperationType}; use crate::util::object_size_info::DataContractInfo::DataContractFetchInfo; use crate::error::drive::DriveError; @@ -48,7 +46,8 @@ impl DriveHighLevelBatchOperationConverter for DocumentCreateTransitionAction { let also_insert_vote_poll_stored_info = self.take_should_store_contest_info(); - let document_creation_token_cost = self.base().token_cost(); + let document_creation_token_cost_operations = + self.base().token_cost_operations(owner_id); let document = Document::try_from_owned_create_transition_action( self, @@ -67,28 +66,7 @@ impl DriveHighLevelBatchOperationConverter for DocumentCreateTransitionAction { }, )]; - if let Some((token_id, effect, cost)) = document_creation_token_cost { - match effect { - DocumentActionTokenEffect::TransferTokenToContractOwner => { - // If we are the owner, no need to send anything - if owner_id != contract_fetch_info.contract.owner_id() { - ops.push(TokenOperation(TokenOperationType::TokenTransfer { - token_id, - sender_id: owner_id, - recipient_id: contract_fetch_info.contract.owner_id(), - amount: cost, - })); - } - } - DocumentActionTokenEffect::BurnToken => { - ops.push(TokenOperation(TokenOperationType::TokenBurn { - token_id, - identity_balance_holder_id: owner_id, - burn_amount: cost, - })); - } - } - } + ops.extend(document_creation_token_cost_operations); if let Some((contested_document_resource_vote_poll, credits)) = maybe_prefunded_voting_balance diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_delete_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_delete_transition.rs index e0bef2d3979..b30d322f416 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_delete_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_delete_transition.rs @@ -1,20 +1,17 @@ use crate::state_transition_action::action_convert_to_operations::batch::DriveHighLevelBatchOperationConverter; -use crate::util::batch::DriveOperation::{DocumentOperation, IdentityOperation, TokenOperation}; +use crate::util::batch::DriveOperation::{DocumentOperation, IdentityOperation}; use crate::util::batch::{DocumentOperationType, DriveOperation, IdentityOperationType}; use crate::error::Error; use dpp::block::epoch::Epoch; -use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::identifier::Identifier; -use dpp::tokens::token_amount_on_contract_token::DocumentActionTokenEffect; use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::DocumentDeleteTransitionAction; use crate::state_transition_action::batch::batched_transition::document_transition::document_delete_transition_action::v0::DocumentDeleteTransitionActionAccessorsV0; use dpp::version::PlatformVersion; use crate::util::object_size_info::{DataContractInfo, DocumentTypeInfo}; use crate::error::drive::DriveError; -use crate::util::batch::drive_op_batch::TokenOperationType; impl DriveHighLevelBatchOperationConverter for DocumentDeleteTransitionAction { fn into_high_level_batch_drive_operations<'b>( @@ -33,13 +30,11 @@ impl DriveHighLevelBatchOperationConverter for DocumentDeleteTransitionAction { 0 => { let base = self.base_owned(); - let contract_fetch_info = base.data_contract_fetch_info(); - let data_contract_id = base.data_contract_id(); let identity_contract_nonce = base.identity_contract_nonce(); - let document_deletion_token_cost = base.token_cost(); + let document_deletion_token_cost_operations = base.token_cost_operations(owner_id); let mut ops = vec![ IdentityOperation(IdentityOperationType::UpdateIdentityContractNonce { @@ -58,28 +53,7 @@ impl DriveHighLevelBatchOperationConverter for DocumentDeleteTransitionAction { }), ]; - if let Some((token_id, effect, cost)) = document_deletion_token_cost { - match effect { - DocumentActionTokenEffect::TransferTokenToContractOwner => { - // If we are the owner, no need to send anything - if owner_id != contract_fetch_info.contract.owner_id() { - ops.push(TokenOperation(TokenOperationType::TokenTransfer { - token_id, - sender_id: owner_id, - recipient_id: contract_fetch_info.contract.owner_id(), - amount: cost, - })); - } - } - DocumentActionTokenEffect::BurnToken => { - ops.push(TokenOperation(TokenOperationType::TokenBurn { - token_id, - identity_balance_holder_id: owner_id, - burn_amount: cost, - })); - } - } - } + ops.extend(document_deletion_token_cost_operations); Ok(ops) } diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_index_only_delete_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_index_only_delete_transition.rs index a128321c1c2..222a19c0dd5 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_index_only_delete_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_index_only_delete_transition.rs @@ -1,19 +1,16 @@ use crate::state_transition_action::action_convert_to_operations::batch::DriveHighLevelBatchOperationConverter; -use crate::util::batch::DriveOperation::{DocumentOperation, IdentityOperation, TokenOperation}; +use crate::util::batch::DriveOperation::{DocumentOperation, IdentityOperation}; use crate::util::batch::{DocumentOperationType, DriveOperation, IdentityOperationType}; use crate::error::Error; use dpp::block::epoch::Epoch; -use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::identifier::Identifier; -use dpp::tokens::token_amount_on_contract_token::DocumentActionTokenEffect; use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::document_transition::document_index_only_delete_transition_action::DocumentIndexOnlyDeleteTransitionAction; use dpp::version::PlatformVersion; use crate::util::object_size_info::{DataContractInfo, DocumentTypeInfo}; use crate::error::drive::DriveError; -use crate::util::batch::drive_op_batch::TokenOperationType; impl DriveHighLevelBatchOperationConverter for DocumentIndexOnlyDeleteTransitionAction { fn into_high_level_batch_drive_operations<'b>( @@ -32,13 +29,12 @@ impl DriveHighLevelBatchOperationConverter for DocumentIndexOnlyDeleteTransition 0 => { let (base, data) = self.base_and_data_owned(); - let contract_fetch_info = base.data_contract_fetch_info(); let data_contract_id = base.data_contract_id(); let identity_contract_nonce = base.identity_contract_nonce(); - let document_deletion_token_cost = base.token_cost(); + let document_deletion_token_cost_operations = base.token_cost_operations(owner_id); let mut ops = vec![ IdentityOperation(IdentityOperationType::UpdateIdentityContractNonce { @@ -62,28 +58,7 @@ impl DriveHighLevelBatchOperationConverter for DocumentIndexOnlyDeleteTransition }), ]; - if let Some((token_id, effect, cost)) = document_deletion_token_cost { - match effect { - DocumentActionTokenEffect::TransferTokenToContractOwner => { - // If we are the owner, no need to send anything - if owner_id != contract_fetch_info.contract.owner_id() { - ops.push(TokenOperation(TokenOperationType::TokenTransfer { - token_id, - sender_id: owner_id, - recipient_id: contract_fetch_info.contract.owner_id(), - amount: cost, - })); - } - } - DocumentActionTokenEffect::BurnToken => { - ops.push(TokenOperation(TokenOperationType::TokenBurn { - token_id, - identity_balance_holder_id: owner_id, - burn_amount: cost, - })); - } - } - } + ops.extend(document_deletion_token_cost_operations); Ok(ops) } diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_purchase_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_purchase_transition.rs index 68830b0c8e6..66e2d14331c 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_purchase_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_purchase_transition.rs @@ -1,6 +1,6 @@ use crate::error::Error; use crate::state_transition_action::action_convert_to_operations::batch::DriveHighLevelBatchOperationConverter; -use crate::util::batch::DriveOperation::{DocumentOperation, IdentityOperation, TokenOperation}; +use crate::util::batch::DriveOperation::{DocumentOperation, IdentityOperation}; use crate::util::batch::{DocumentOperationType, DriveOperation, IdentityOperationType}; use crate::util::object_size_info::DocumentInfo::DocumentOwnedInfo; use crate::util::object_size_info::{DataContractInfo, DocumentTypeInfo, OwnedDocumentInfo}; @@ -13,12 +13,10 @@ use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::document::document_event::DocumentEvent; use dpp::document::DocumentV0Getters; -use dpp::tokens::token_amount_on_contract_token::DocumentActionTokenEffect; use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::document_transition::document_purchase_transition_action::{DocumentPurchaseTransitionAction, DocumentPurchaseTransitionActionAccessorsV0}; use dpp::version::PlatformVersion; use crate::error::drive::DriveError; -use crate::util::batch::drive_op_batch::TokenOperationType; impl DriveHighLevelBatchOperationConverter for DocumentPurchaseTransitionAction { fn into_high_level_batch_drive_operations<'b>( @@ -42,9 +40,8 @@ impl DriveHighLevelBatchOperationConverter for DocumentPurchaseTransitionAction let purchase_amount = self.price(); let contract_fetch_info = self.base().data_contract_fetch_info(); - let contract_owner_id = contract_fetch_info.contract.owner_id(); - - let document_purchase_token_cost = self.base().token_cost(); + let document_purchase_token_cost_operations = + self.base().token_cost_operations(owner_id); let document = self.document_owned(); @@ -84,28 +81,7 @@ impl DriveHighLevelBatchOperationConverter for DocumentPurchaseTransitionAction }), ]; - if let Some((token_id, effect, cost)) = document_purchase_token_cost { - match effect { - DocumentActionTokenEffect::TransferTokenToContractOwner => { - // If we are the owner, no need to send anything - if owner_id != contract_owner_id { - ops.push(TokenOperation(TokenOperationType::TokenTransfer { - token_id, - sender_id: owner_id, - recipient_id: contract_owner_id, - amount: cost, - })); - } - } - DocumentActionTokenEffect::BurnToken => { - ops.push(TokenOperation(TokenOperationType::TokenBurn { - token_id, - identity_balance_holder_id: owner_id, - burn_amount: cost, - })); - } - } - } + ops.extend(document_purchase_token_cost_operations); Ok(ops) } @@ -120,9 +96,8 @@ impl DriveHighLevelBatchOperationConverter for DocumentPurchaseTransitionAction let purchase_amount = self.price(); let contract_fetch_info = self.base().data_contract_fetch_info(); - let contract_owner_id = contract_fetch_info.contract.owner_id(); - - let document_purchase_token_cost = self.base().token_cost(); + let document_purchase_token_cost_operations = + self.base().token_cost_operations(owner_id); let mut document = self.document_owned(); @@ -195,28 +170,7 @@ impl DriveHighLevelBatchOperationConverter for DocumentPurchaseTransitionAction ops.push(document_history_operation); } - if let Some((token_id, effect, cost)) = document_purchase_token_cost { - match effect { - DocumentActionTokenEffect::TransferTokenToContractOwner => { - // If we are the owner, no need to send anything - if owner_id != contract_owner_id { - ops.push(TokenOperation(TokenOperationType::TokenTransfer { - token_id, - sender_id: owner_id, - recipient_id: contract_owner_id, - amount: cost, - })); - } - } - DocumentActionTokenEffect::BurnToken => { - ops.push(TokenOperation(TokenOperationType::TokenBurn { - token_id, - identity_balance_holder_id: owner_id, - burn_amount: cost, - })); - } - } - } + ops.extend(document_purchase_token_cost_operations); Ok(ops) } diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_replace_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_replace_transition.rs index 3bfe76d3940..3a29ecc142a 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_replace_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_replace_transition.rs @@ -1,6 +1,6 @@ use crate::error::Error; use crate::state_transition_action::action_convert_to_operations::batch::DriveHighLevelBatchOperationConverter; -use crate::util::batch::DriveOperation::{DocumentOperation, IdentityOperation, TokenOperation}; +use crate::util::batch::DriveOperation::{DocumentOperation, IdentityOperation}; use crate::util::batch::{DocumentOperationType, DriveOperation, IdentityOperationType}; use crate::util::object_size_info::DocumentInfo::DocumentOwnedInfo; use crate::util::object_size_info::{DataContractInfo, DocumentTypeInfo, OwnedDocumentInfo}; @@ -10,13 +10,10 @@ use dpp::block::epoch::Epoch; use dpp::document::Document; use dpp::prelude::Identifier; use std::borrow::Cow; -use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::tokens::token_amount_on_contract_token::DocumentActionTokenEffect; use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::document_transition::document_replace_transition_action::{DocumentFromReplaceTransitionAction, DocumentReplaceTransitionAction, DocumentReplaceTransitionActionAccessorsV0}; use dpp::version::PlatformVersion; use crate::error::drive::DriveError; -use crate::util::batch::drive_op_batch::TokenOperationType; impl DriveHighLevelBatchOperationConverter for DocumentReplaceTransitionAction { fn into_high_level_batch_drive_operations<'b>( @@ -37,9 +34,9 @@ impl DriveHighLevelBatchOperationConverter for DocumentReplaceTransitionAction { let document_type_name = self.base().document_type_name().clone(); let identity_contract_nonce = self.base().identity_contract_nonce(); let contract_fetch_info = self.base().data_contract_fetch_info(); - let contract_owner_id = contract_fetch_info.contract.owner_id(); - let document_replacement_token_cost = self.base().token_cost(); + let document_replacement_token_cost_operations = + self.base().token_cost_operations(owner_id); let document = Document::try_from_owned_replace_transition_action( self, owner_id, @@ -68,28 +65,7 @@ impl DriveHighLevelBatchOperationConverter for DocumentReplaceTransitionAction { }), ]; - if let Some((token_id, effect, cost)) = document_replacement_token_cost { - match effect { - DocumentActionTokenEffect::TransferTokenToContractOwner => { - // If we are the owner, no need to send anything - if owner_id != contract_owner_id { - ops.push(TokenOperation(TokenOperationType::TokenTransfer { - token_id, - sender_id: owner_id, - recipient_id: contract_owner_id, - amount: cost, - })); - } - } - DocumentActionTokenEffect::BurnToken => { - ops.push(TokenOperation(TokenOperationType::TokenBurn { - token_id, - identity_balance_holder_id: owner_id, - burn_amount: cost, - })); - } - } - } + ops.extend(document_replacement_token_cost_operations); Ok(ops) } diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_transfer_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_transfer_transition.rs index 6d62a8fb6fb..5162e4afc50 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_transfer_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_transfer_transition.rs @@ -1,6 +1,6 @@ use crate::error::Error; use crate::state_transition_action::action_convert_to_operations::batch::DriveHighLevelBatchOperationConverter; -use crate::util::batch::DriveOperation::{DocumentOperation, IdentityOperation, TokenOperation}; +use crate::util::batch::DriveOperation::{DocumentOperation, IdentityOperation}; use crate::util::batch::{DocumentOperationType, DriveOperation, IdentityOperationType}; use crate::util::object_size_info::DocumentInfo::DocumentOwnedInfo; use crate::util::object_size_info::{DataContractInfo, DocumentTypeInfo, OwnedDocumentInfo}; @@ -13,12 +13,10 @@ use std::borrow::Cow; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::document::document_event::DocumentEvent; -use dpp::tokens::token_amount_on_contract_token::DocumentActionTokenEffect; use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::document_transition::document_transfer_transition_action::{DocumentTransferTransitionAction, DocumentTransferTransitionActionAccessorsV0}; use dpp::version::PlatformVersion; use crate::error::drive::DriveError; -use crate::util::batch::drive_op_batch::TokenOperationType; impl DriveHighLevelBatchOperationConverter for DocumentTransferTransitionAction { fn into_high_level_batch_drive_operations<'b>( @@ -39,9 +37,9 @@ impl DriveHighLevelBatchOperationConverter for DocumentTransferTransitionAction let document_type_name = self.base().document_type_name().clone(); let identity_contract_nonce = self.base().identity_contract_nonce(); let contract_fetch_info = self.base().data_contract_fetch_info(); - let contract_owner_id = contract_fetch_info.contract.owner_id(); - let document_transfer_token_cost = self.base().token_cost(); + let document_transfer_token_cost_operations = + self.base().token_cost_operations(owner_id); let document = self.document_owned(); // we are transferring the document so the new storage flags should be on the new owner @@ -72,28 +70,7 @@ impl DriveHighLevelBatchOperationConverter for DocumentTransferTransitionAction }), ]; - if let Some((token_id, effect, cost)) = document_transfer_token_cost { - match effect { - DocumentActionTokenEffect::TransferTokenToContractOwner => { - // If we are the owner, no need to send anything - if owner_id != contract_owner_id { - ops.push(TokenOperation(TokenOperationType::TokenTransfer { - token_id, - sender_id: owner_id, - recipient_id: contract_owner_id, - amount: cost, - })); - } - } - DocumentActionTokenEffect::BurnToken => { - ops.push(TokenOperation(TokenOperationType::TokenBurn { - token_id, - identity_balance_holder_id: owner_id, - burn_amount: cost, - })); - } - } - } + ops.extend(document_transfer_token_cost_operations); Ok(ops) } @@ -105,9 +82,9 @@ impl DriveHighLevelBatchOperationConverter for DocumentTransferTransitionAction let document_type_name = self.base().document_type_name().clone(); let identity_contract_nonce = self.base().identity_contract_nonce(); let contract_fetch_info = self.base().data_contract_fetch_info(); - let contract_owner_id = contract_fetch_info.contract.owner_id(); - let document_transfer_token_cost = self.base().token_cost(); + let document_transfer_token_cost_operations = + self.base().token_cost_operations(owner_id); let mut document = self.document_owned(); // we are transferring the document so the new storage flags should be on the new owner @@ -170,28 +147,7 @@ impl DriveHighLevelBatchOperationConverter for DocumentTransferTransitionAction ops.push(document_history_operation); } - if let Some((token_id, effect, cost)) = document_transfer_token_cost { - match effect { - DocumentActionTokenEffect::TransferTokenToContractOwner => { - // If we are the owner, no need to send anything - if owner_id != contract_owner_id { - ops.push(TokenOperation(TokenOperationType::TokenTransfer { - token_id, - sender_id: owner_id, - recipient_id: contract_owner_id, - amount: cost, - })); - } - } - DocumentActionTokenEffect::BurnToken => { - ops.push(TokenOperation(TokenOperationType::TokenBurn { - token_id, - identity_balance_holder_id: owner_id, - burn_amount: cost, - })); - } - } - } + ops.extend(document_transfer_token_cost_operations); Ok(ops) } diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_update_price_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_update_price_transition.rs index ee3120dbc88..38e3c799888 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_update_price_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/document/document_update_price_transition.rs @@ -1,6 +1,6 @@ use crate::error::Error; use crate::state_transition_action::action_convert_to_operations::batch::DriveHighLevelBatchOperationConverter; -use crate::util::batch::DriveOperation::{DocumentOperation, IdentityOperation, TokenOperation}; +use crate::util::batch::DriveOperation::{DocumentOperation, IdentityOperation}; use crate::util::batch::{DocumentOperationType, DriveOperation, IdentityOperationType}; use crate::util::object_size_info::DocumentInfo::DocumentOwnedInfo; use crate::util::object_size_info::{DataContractInfo, DocumentTypeInfo, OwnedDocumentInfo}; @@ -16,12 +16,10 @@ use dpp::document::property_names::PRICE; use dpp::platform_value::btreemap_extensions::BTreeValueMapHelper; use dpp::document::DocumentV0Getters; use dpp::fee::Credits; -use dpp::tokens::token_amount_on_contract_token::DocumentActionTokenEffect; use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::DocumentBaseTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::document_transition::document_update_price_transition_action::{DocumentUpdatePriceTransitionAction, DocumentUpdatePriceTransitionActionAccessorsV0}; use dpp::version::PlatformVersion; use crate::error::drive::DriveError; -use crate::util::batch::drive_op_batch::TokenOperationType; impl DriveHighLevelBatchOperationConverter for DocumentUpdatePriceTransitionAction { fn into_high_level_batch_drive_operations<'b>( @@ -42,8 +40,8 @@ impl DriveHighLevelBatchOperationConverter for DocumentUpdatePriceTransitionActi let document_type_name = self.base().document_type_name().clone(); let identity_contract_nonce = self.base().identity_contract_nonce(); let fetch_info = self.base().data_contract_fetch_info(); - let document_update_price_token_cost = self.base().token_cost(); - let contract_owner_id = fetch_info.contract.owner_id(); + let document_update_price_token_cost_operations = + self.base().token_cost_operations(owner_id); let document = self.document_owned(); @@ -69,28 +67,7 @@ impl DriveHighLevelBatchOperationConverter for DocumentUpdatePriceTransitionActi }), ]; - if let Some((token_id, effect, cost)) = document_update_price_token_cost { - match effect { - DocumentActionTokenEffect::TransferTokenToContractOwner => { - // If we are the owner, no need to send anything - if owner_id != contract_owner_id { - ops.push(TokenOperation(TokenOperationType::TokenTransfer { - token_id, - sender_id: owner_id, - recipient_id: contract_owner_id, - amount: cost, - })); - } - } - DocumentActionTokenEffect::BurnToken => { - ops.push(TokenOperation(TokenOperationType::TokenBurn { - token_id, - identity_balance_holder_id: owner_id, - burn_amount: cost, - })); - } - } - } + ops.extend(document_update_price_token_cost_operations); Ok(ops) } @@ -102,8 +79,8 @@ impl DriveHighLevelBatchOperationConverter for DocumentUpdatePriceTransitionActi let document_type_name = self.base().document_type_name().clone(); let identity_contract_nonce = self.base().identity_contract_nonce(); let fetch_info = self.base().data_contract_fetch_info(); - let document_update_price_token_cost = self.base().token_cost(); - let contract_owner_id = fetch_info.contract.owner_id(); + let document_update_price_token_cost_operations = + self.base().token_cost_operations(owner_id); let document = self.document_owned(); @@ -160,28 +137,7 @@ impl DriveHighLevelBatchOperationConverter for DocumentUpdatePriceTransitionActi ops.push(document_history_operation); } - if let Some((token_id, effect, cost)) = document_update_price_token_cost { - match effect { - DocumentActionTokenEffect::TransferTokenToContractOwner => { - // If we are the owner, no need to send anything - if owner_id != contract_owner_id { - ops.push(TokenOperation(TokenOperationType::TokenTransfer { - token_id, - sender_id: owner_id, - recipient_id: contract_owner_id, - amount: cost, - })); - } - } - DocumentActionTokenEffect::BurnToken => { - ops.push(TokenOperation(TokenOperationType::TokenBurn { - token_id, - identity_balance_holder_id: owner_id, - burn_amount: cost, - })); - } - } - } + ops.extend(document_update_price_token_cost_operations); Ok(ops) } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/mod.rs index c4adc673cad..b9650b1307e 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/mod.rs @@ -2,6 +2,10 @@ use derive_more::From; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::platform_value::Identifier; +use crate::state_transition_action::shielded::ShieldedActionNote; +use crate::util::batch::drive_op_batch::TokenOperationType; +use crate::util::batch::DriveOperation; +use crate::util::batch::DriveOperation::TokenOperation; use dpp::balances::credits::TokenAmount; use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; use dpp::data_contract::document_type::action_fees::{ActionFeePricing, DocumentActionFee}; @@ -9,6 +13,7 @@ use dpp::data_contract::document_type::DocumentTypeRef; use dpp::prelude::IdentityNonce; use dpp::tokens::gas_fees_paid_by::GasFeesPaidBy; use dpp::tokens::token_amount_on_contract_token::DocumentActionTokenEffect; +use dpp::tokens::token_payment_info::v1::TokenShieldedPayment; use dpp::ProtocolError; use std::sync::Arc; @@ -112,4 +117,78 @@ impl DocumentBaseTransitionActionAccessorsV0 for DocumentBaseTransitionAction { DocumentBaseTransitionAction::V0(v0) => v0.declared_action_fee.as_deref().copied(), } } + + fn shielded_token_payment(&self) -> Option<&TokenShieldedPayment> { + match self { + DocumentBaseTransitionAction::V0(v0) => v0.shielded_token_payment.as_ref(), + } + } +} + +impl DocumentBaseTransitionAction { + /// The token operations that pay the document action's token cost, if it has one. + /// + /// Paid from the owner's balance: a transfer to the contract owner (skipped when the owner + /// is the contract owner) or a burn. Paid from the token's shielded pool (`TokenPaymentInfo::V1`): + /// the bundle's spent notes leave the pool and the cost goes into the contract owner's balance + /// or out of the supply; the change notes join the pool. + pub fn token_cost_operations<'b>(&self, owner_id: Identifier) -> Vec> { + let Some((token_id, effect, cost)) = self.token_cost() else { + return vec![]; + }; + let contract_owner_id = self.data_contract_fetch_info_ref().contract.owner_id(); + if let Some(payment) = self.shielded_token_payment() { + let nullifiers = payment + .actions + .iter() + .map(|action| action.nullifier) + .collect(); + let notes = payment + .actions + .iter() + .map(ShieldedActionNote::from) + .collect(); + return match effect { + DocumentActionTokenEffect::TransferTokenToContractOwner => { + vec![TokenOperation(TokenOperationType::TokenUnshield { + token_id, + recipient_id: contract_owner_id, + amount: cost, + nullifiers, + notes, + })] + } + DocumentActionTokenEffect::BurnToken => { + vec![TokenOperation(TokenOperationType::TokenBurnFromPool { + token_id, + amount: cost, + nullifiers, + notes, + })] + } + }; + } + match effect { + DocumentActionTokenEffect::TransferTokenToContractOwner => { + // If we are the owner, no need to send anything + if owner_id != contract_owner_id { + vec![TokenOperation(TokenOperationType::TokenTransfer { + token_id, + sender_id: owner_id, + recipient_id: contract_owner_id, + amount: cost, + })] + } else { + vec![] + } + } + DocumentActionTokenEffect::BurnToken => { + vec![TokenOperation(TokenOperationType::TokenBurn { + token_id, + identity_balance_holder_id: owner_id, + burn_amount: cost, + })] + } + } + } } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/mod.rs index a4233ab166a..7d8ee575b08 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/mod.rs @@ -10,6 +10,7 @@ use dpp::identifier::Identifier; use dpp::prelude::IdentityNonce; use dpp::tokens::gas_fees_paid_by::GasFeesPaidBy; use dpp::tokens::token_amount_on_contract_token::DocumentActionTokenEffect; +use dpp::tokens::token_payment_info::v1::TokenShieldedPayment; use dpp::ProtocolError; use std::sync::Arc; @@ -49,6 +50,9 @@ pub struct DocumentBaseTransitionActionV0 { /// most actions declare none, and the action sits in the largest variant of the batched /// transition enum. pub declared_action_fee: Option>, + /// The spend bundle paying `token_cost` out of the token's shielded pool instead of the + /// owner's token balance (`TokenPaymentInfo::V1`). Only set when there is a token cost. + pub shielded_token_payment: Option, } /// document base transition action accessors v0 @@ -90,4 +94,6 @@ pub trait DocumentBaseTransitionActionAccessorsV0 { /// The fee the document type declares for this action, with what the transition agreed /// to pay fn declared_action_fee_with_agreement(&self) -> Option; + /// The shielded payment of the token cost, when the cost is paid out of the token's pool + fn shielded_token_payment(&self) -> Option<&TokenShieldedPayment>; } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/transformer.rs index e6bbb2dc887..2c9edd42c13 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/transformer.rs @@ -6,7 +6,7 @@ use dpp::platform_value::Identifier; use std::sync::Arc; use dpp::consensus::ConsensusError; use dpp::consensus::state::state_error::StateError; -use dpp::consensus::state::token::{IdentityHasNotAgreedToPayRequiredTokenAmountError, IdentityTryingToPayWithWrongTokenError, RequiredTokenPaymentInfoNotSetError}; +use dpp::consensus::state::token::{IdentityHasNotAgreedToPayRequiredTokenAmountError, IdentityTryingToPayWithWrongTokenError, RequiredTokenPaymentInfoNotSetError, TokenShieldedPaymentAmountMismatchError, TokenShieldedPaymentNotRequiredError}; use dpp::prelude::ConsensusValidationResult; use dpp::ProtocolError; use dpp::state_transition::batch_transition::document_base_transition::DocumentBaseTransition; @@ -18,6 +18,7 @@ use dpp::tokens::gas_fees_paid_by::GasFeesPaidBy; use dpp::tokens::token_amount_on_contract_token::DocumentActionTokenCost; use dpp::tokens::token_payment_info::v0::v0_accessors::TokenPaymentInfoAccessorsV0; use dpp::tokens::token_payment_info::methods::v0::TokenPaymentInfoMethodsV0; +use dpp::tokens::token_payment_info::v1::v1_accessors::TokenPaymentInfoAccessorsV1; use crate::drive::contract::DataContractFetchInfo; use crate::error::Error; use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::{DeclaredDocumentActionFee, DocumentBaseTransitionActionV0}; @@ -60,6 +61,7 @@ impl DocumentBaseTransitionActionV0 { ) }, ); + let mut shielded_token_payment = None; if let Some(document_action_token_cost) = document_action_token_cost { let Some(token_payment_info) = value.token_payment_info_ref() else { return Ok(ConsensusValidationResult::new_with_error( @@ -107,6 +109,37 @@ impl DocumentBaseTransitionActionV0 { ), )); } + // A shielded payment pays exactly what its bundle proves: the document type's cost, + // or the document is rejected before the proof is verified. + if let Some(payment) = token_payment_info.shielded_payment() { + if payment.amount != document_action_token_cost.token_amount { + return Ok(ConsensusValidationResult::new_with_error( + ConsensusError::StateError( + StateError::TokenShieldedPaymentAmountMismatchError( + TokenShieldedPaymentAmountMismatchError::new( + token_cost.expect("expected token cost").0, + document_action_token_cost.token_amount, + payment.amount, + action.to_string(), + ), + ), + ), + )); + } + shielded_token_payment = Some(payment.clone()); + } + } else if let Some(token_payment_info) = value.token_payment_info_ref() { + // A bundle with nothing to pay would be verified for free and never applied. + if token_payment_info.shielded_payment().is_some() { + return Ok(ConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::TokenShieldedPaymentNotRequiredError( + TokenShieldedPaymentNotRequiredError::new( + token_payment_info.token_id(data_contract_id), + action.to_string(), + ), + )), + )); + } } let gas_fees_paid_by = value .token_payment_info_ref() @@ -143,6 +176,7 @@ impl DocumentBaseTransitionActionV0 { gas_fees_paid_by, contract_gas_fees_paid_by, declared_action_fee, + shielded_token_payment, } .into()) } diff --git a/packages/rs-drive/src/state_transition_action/batch/tests.rs b/packages/rs-drive/src/state_transition_action/batch/tests.rs index cf44f276ab7..3171c4aced9 100644 --- a/packages/rs-drive/src/state_transition_action/batch/tests.rs +++ b/packages/rs-drive/src/state_transition_action/batch/tests.rs @@ -134,6 +134,7 @@ fn test_document_base_v0() -> DocumentBaseTransitionActionV0 { gas_fees_paid_by: GasFeesPaidBy::default(), contract_gas_fees_paid_by: GasFeesPaidBy::default(), declared_action_fee: None, + shielded_token_payment: None, } } @@ -2939,6 +2940,7 @@ fn stamp_test_create_action(protocol_version: u32) -> DocumentCreateTransitionAc gas_fees_paid_by: GasFeesPaidBy::default(), contract_gas_fees_paid_by: GasFeesPaidBy::default(), declared_action_fee: None, + shielded_token_payment: None, }); DocumentCreateTransitionAction::V0(DocumentCreateTransitionActionV0 { base, @@ -2960,6 +2962,7 @@ fn stamp_test_replace_action(protocol_version: u32) -> DocumentReplaceTransition gas_fees_paid_by: GasFeesPaidBy::default(), contract_gas_fees_paid_by: GasFeesPaidBy::default(), declared_action_fee: None, + shielded_token_payment: None, }); DocumentReplaceTransitionAction::V0(DocumentReplaceTransitionActionV0 { base, diff --git a/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/transformer.rs b/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/transformer.rs index b37446ef6e2..469498ba70a 100644 --- a/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/transformer.rs @@ -385,6 +385,7 @@ mod tests { gas_fees_paid_by: GasFeesPaidBy::DocumentOwner, contract_gas_fees_paid_by: GasFeesPaidBy::default(), declared_action_fee: None, + shielded_token_payment: None, }); let action = BumpIdentityDataContractNonceAction::from_document_base_transition_action( base_action, @@ -408,6 +409,7 @@ mod tests { gas_fees_paid_by: GasFeesPaidBy::DocumentOwner, contract_gas_fees_paid_by: GasFeesPaidBy::default(), declared_action_fee: None, + shielded_token_payment: None, }); let action = BumpIdentityDataContractNonceAction::from_borrowed_document_base_transition_action( diff --git a/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/v0/transformer.rs index 813d52b1182..edc62789a80 100644 --- a/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/system/bump_identity_data_contract_nonce_action/v0/transformer.rs @@ -328,6 +328,7 @@ mod tests { gas_fees_paid_by: GasFeesPaidBy::DocumentOwner, contract_gas_fees_paid_by: GasFeesPaidBy::default(), declared_action_fee: None, + shielded_token_payment: None, }); let action = BumpIdentityDataContractNonceActionV0::from_document_base_transition_action( base_action, @@ -354,6 +355,7 @@ mod tests { gas_fees_paid_by: GasFeesPaidBy::DocumentOwner, contract_gas_fees_paid_by: GasFeesPaidBy::default(), declared_action_fee: None, + shielded_token_payment: None, }); let action = BumpIdentityDataContractNonceActionV0::from_borrowed_document_base_transition_action( diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs index 14fc0e7e10e..202aad14309 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs @@ -5,6 +5,7 @@ pub mod v3; pub mod v4; pub mod v5; pub mod v6; +pub mod v7; #[derive(Clone, Debug, Default)] pub struct DPPContractVersions { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v6.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v6.rs index 38972bb758e..56aaed612d4 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v6.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v6.rs @@ -116,11 +116,9 @@ pub const CONTRACT_VERSIONS_V6: DPPContractVersions = DPPContractVersions { // 1: an epoch-based perpetual distribution needs an interval of at least one epoch. validate_structure_interval: 1, validate_pre_programmed_distribution_amounts: 0, - // changed: `TokenConfigurationV1` (the per-token shielded pool flag) is admitted from - // protocol version 14; v5 keeps max 0 so a pre-activation contract cannot carry it. token_configuration_format: FeatureVersionBounds { min_version: 0, - max_version: 1, + max_version: 0, default_current_version: 0, }, }, diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v7.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v7.rs new file mode 100644 index 00000000000..b38c7d0193a --- /dev/null +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v7.rs @@ -0,0 +1,96 @@ +use crate::version::dpp_versions::dpp_contract_versions::{ + DPPContractVersions, DataContractMethodVersions, DocumentTypeClassMethodVersions, + DocumentTypeIndexVersions, DocumentTypeMethodVersions, DocumentTypeSchemaVersions, + DocumentTypeVersions, RecursiveSchemaValidatorVersions, TokenVersions, +}; +use versioned_feature_core::FeatureVersionBounds; + +// Introduced in protocol version 15 with token shielded pools. Identical to v6 except +// `token_configuration_format` admits format 1 (`TokenConfiguration::V1`, the per-token +// shielded pool flag). v6 keeps max 0 for protocol version 14 chain replay, so a +// pre-activation contract cannot carry the flag. +pub const CONTRACT_VERSIONS_V7: DPPContractVersions = DPPContractVersions { + max_serialized_size: 65000, + contract_serialization_version: FeatureVersionBounds { + min_version: 0, + max_version: 1, + default_current_version: 1, + }, + contract_structure_version: 1, + created_data_contract_structure: 0, + config: FeatureVersionBounds { + min_version: 1, + max_version: 1, + default_current_version: 1, + }, + methods: DataContractMethodVersions { + validate_document: 0, + // Generation 1 (requiredSince): feeds the new contract version into + // per-document-type update validation and validates requiredSince + // annotations on document types introduced by the update, which the + // per-type pass never sees. Generation 0 stays byte-identical for + // replay of pre-v14 blocks. + validate_update: 1, + schema: 0, + validate_groups: 0, + equal_ignoring_time_fields: 0, + registration_cost: 1, + }, + document_type_versions: DocumentTypeVersions { + index_versions: DocumentTypeIndexVersions { + index_levels_from_indices: 0, + }, + class_method_versions: DocumentTypeClassMethodVersions { + try_from_schema: 3, // changed: parser generation 3 — generation 2 plus the ranked index keywords + create_document_types_from_document_schemas: 1, + }, + structure_version: 0, + schema: DocumentTypeSchemaVersions { + document_type_schema: 3, // changed: v3 document meta-schema — v2 plus the ranked index keywords, and the gate the index parser reads + should_add_creator_id: 1, + enrich_with_base_schema: 1, + find_identifier_and_binary_paths: 0, + apply_property_reference: Some(0), // changed: the meta-schema v3 `refersTo` keyword is folded into the parsed property type; None before this version means the keyword is ignored, as it was before it existed + apply_required_since: Some(0), // changed: the meta-schema v3 `requiredSince` keyword (contract version a property is required from) is parsed onto the property; None before this version means the keyword is ignored, as it was before it existed + validate_max_depth: 0, + max_depth: 256, + recursive_schema_validator_versions: RecursiveSchemaValidatorVersions { + traversal_validator: 0, + }, + validate_schema_compatibility: 1, // changed: strips `indices` before diffing — index changes are validated by `validate_update` v1, so an index-order-only diff no longer hard-errors + }, + methods: DocumentTypeMethodVersions { + create_document_from_data: 0, + create_document_with_prevalidated_properties: 0, + prefunded_voting_balance_for_document: 0, + contested_vote_poll_for_document: 0, + estimated_size: 1, // changed: adds the document serialization format 3 contract-version stamp varint (worst case 5 bytes) to the estimate + // Changed: v1 requires a query's bound fields to cover a + // contiguous prefix of the candidate index (equalities exactly + // covering the leading properties, range/in immediately after, + // no unused property before an order-by field). v0 matched by + // positionless set membership, so a query binding only later + // index properties selected an index the positional path + // lowering misaligns on — returning cryptographically proven + // wrong or empty results. Gapped candidates are now skipped + // per-candidate, letting a well-shaped index win or the query + // fail with WhereClauseOnNonIndexedProperty. v0 stays frozen + // for replay at protocol versions <= 13. + index_for_types: 1, + max_size: 0, + serialize_value_for_key: 0, + deserialize_value_for_key: 0, + }, + }, + token_versions: TokenVersions { + // 1: an epoch-based perpetual distribution needs an interval of at least one epoch. + validate_structure_interval: 1, + // changed: `TokenConfigurationV1` (the per-token shielded pool flag) is admitted from + // protocol version 15; v6 keeps max 0 so a pre-activation contract cannot carry it. + token_configuration_format: FeatureVersionBounds { + min_version: 0, + max_version: 1, + default_current_version: 0, + }, + }, +}; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/mod.rs index c69dd339fe1..10edd8041c5 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/mod.rs @@ -2,6 +2,7 @@ use versioned_feature_core::{FeatureVersion, OptionalFeatureVersion}; pub mod v1; pub mod v10; +pub mod v11; pub mod v2; pub mod v3; pub mod v4; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs index f8580e95fcf..73f85871a6d 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs @@ -139,7 +139,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V10: DriveAbciMethodVersions = DriveAbciMet update_checkpoints: Some(0), record_shielded_pool_anchor: Some(0), prune_shielded_pool_anchors: Some(0), - record_token_shielded_pool_anchors: Some(0), // changed: per-token shielded pool anchors + record_token_shielded_pool_anchors: None, }, platform_state_storage: DriveAbciPlatformStateStorageMethodVersions { fetch_platform_state: 1, // changed: reads structure 1 records together with their masternode and validator set entries diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs new file mode 100644 index 00000000000..c5ac3e7f50d --- /dev/null +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs @@ -0,0 +1,143 @@ +use crate::version::drive_abci_versions::drive_abci_method_versions::{ + DriveAbciBlockEndMethodVersions, DriveAbciBlockFeeProcessingMethodVersions, + DriveAbciBlockStartMethodVersions, DriveAbciCoreBasedUpdatesMethodVersions, + DriveAbciCoreChainLockMethodVersionsAndConstants, DriveAbciCoreInstantSendLockMethodVersions, + DriveAbciEngineMethodVersions, DriveAbciEpochMethodVersions, + DriveAbciFeePoolInwardsDistributionMethodVersions, + DriveAbciFeePoolOutwardsDistributionMethodVersions, + DriveAbciIdentityCreditWithdrawalMethodVersions, DriveAbciInitializationMethodVersions, + DriveAbciMasternodeIdentitiesUpdatesMethodVersions, DriveAbciMethodVersions, + DriveAbciPlatformStateStorageMethodVersions, DriveAbciProtocolUpgradeMethodVersions, + DriveAbciStateTransitionProcessingMethodVersions, DriveAbciTokensProcessingMethodVersions, + DriveAbciVotingMethodVersions, +}; + +/// Drive ABCI method versions 11. Introduced in protocol v15 with token shielded pools: turns +/// on `record_token_shielded_pool_anchors` (`Some(0)`), the block-end step that records and +/// prunes the anchors of every token pool the block touched. +/// Everything else matches `DRIVE_ABCI_METHOD_VERSIONS_V10`. +pub const DRIVE_ABCI_METHOD_VERSIONS_V11: DriveAbciMethodVersions = DriveAbciMethodVersions { + engine: DriveAbciEngineMethodVersions { + init_chain: 0, + check_tx: 0, + run_block_proposal: 0, + finalize_block_proposal: 0, + consensus_params_update: 1, + }, + initialization: DriveAbciInitializationMethodVersions { + initial_core_height_and_time: 0, + create_genesis_state: 1, + }, + core_based_updates: DriveAbciCoreBasedUpdatesMethodVersions { + update_core_info: 0, + update_masternode_list: 0, + update_quorum_info: 0, + masternode_updates: DriveAbciMasternodeIdentitiesUpdatesMethodVersions { + get_voter_identity_key: 0, + get_operator_identity_keys: 0, + get_owner_identity_withdrawal_key: 0, + get_owner_identity_owner_key: 0, + get_voter_identifier_from_masternode_list_item: 0, + get_operator_identifier_from_masternode_list_item: 0, + create_operator_identity: 0, + create_owner_identity: 1, + create_voter_identity: 0, + disable_identity_keys: 0, + update_masternode_identities: 0, + update_operator_identity: 0, + update_owner_withdrawal_address: 1, + update_voter_identity: 0, + }, + }, + protocol_upgrade: DriveAbciProtocolUpgradeMethodVersions { + check_for_desired_protocol_upgrade: 1, + upgrade_protocol_version_on_epoch_change: 0, + perform_events_on_first_block_of_protocol_change: Some(1), + protocol_version_upgrade_percentage_needed: 67, + }, + block_fee_processing: DriveAbciBlockFeeProcessingMethodVersions { + add_process_epoch_change_operations: 0, + process_block_fees_and_validate_sum_trees: 1, + }, + tokens_processing: DriveAbciTokensProcessingMethodVersions { + validate_token_aggregated_balance: 0, + }, + core_chain_lock: DriveAbciCoreChainLockMethodVersionsAndConstants { + choose_quorum: 0, + verify_chain_lock: 0, + verify_chain_lock_locally: 0, + verify_chain_lock_through_core: 0, + make_sure_core_is_synced_to_chain_lock: 0, + recent_block_count_amount: 2, + }, + core_instant_send_lock: DriveAbciCoreInstantSendLockMethodVersions { + verify_recent_signature_locally: 0, + }, + fee_pool_inwards_distribution: DriveAbciFeePoolInwardsDistributionMethodVersions { + add_distribute_block_fees_into_pools_operations: 0, + add_distribute_storage_fee_to_epochs_operations: 0, + }, + fee_pool_outwards_distribution: DriveAbciFeePoolOutwardsDistributionMethodVersions { + add_distribute_fees_from_oldest_unpaid_epoch_pool_to_proposers_operations: 1, + add_epoch_pool_to_proposers_payout_operations: 0, + find_oldest_epoch_needing_payment: 0, + fetch_reward_shares_list_for_masternode: 0, + }, + withdrawals: DriveAbciIdentityCreditWithdrawalMethodVersions { + build_untied_withdrawal_transactions_from_documents: 0, + dequeue_and_build_unsigned_withdrawal_transactions: 0, + fetch_transactions_block_inclusion_status: 0, + pool_withdrawals_into_transactions_queue: 1, + update_broadcasted_withdrawal_statuses: 0, + rebroadcast_expired_withdrawal_documents: 2, // changed in v14: an expired withdrawal whose payout is Core dust is marked FAILED instead of re-signed + append_signatures_and_broadcast_withdrawal_transactions: 0, + cleanup_expired_locks_of_withdrawal_amounts: 1, // changed in v14: also prunes expired entries of the credit inflows sum tree + record_credit_inflows_for_withdrawals: Some(0), // new in v14: the block's credit mints recorded as an inflow for the net daily withdrawal limit + record_total_credits_history_for_withdrawals: Some(0), // changed in v14: per-block total credits history for the day-lagged daily withdrawal limit + }, + voting: DriveAbciVotingMethodVersions { + keep_record_of_finished_contested_resource_vote_poll: 0, + clean_up_after_vote_poll_end: 0, + clean_up_after_contested_resources_vote_poll_end: 1, + check_for_ended_vote_polls: 0, + tally_votes_for_contested_document_resource_vote_poll: 0, + award_document_to_winner: 0, + delay_vote_poll: 0, + run_dao_platform_events: 0, + remove_votes_for_removed_masternodes: 0, + }, + state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { + execute_event: 0, + process_raw_state_transitions: 0, + // unchanged from V9: v1 since v13 (records the balance effects of paid-INVALID / + // unsuccessful-paid transitions) + process_validation_result: 1, + decode_raw_state_transitions: 0, + validate_fees_of_event: 0, + store_address_balances_to_recent_block_storage: Some(0), + cleanup_recent_block_storage_address_balances: Some(0), + // unchanged from V9: v1 since v13 (records shielded-spend transparent credits) + record_added_balance_outputs: 1, + }, + epoch: DriveAbciEpochMethodVersions { + gather_epoch_info: 0, + get_genesis_time: 0, + }, + block_start: DriveAbciBlockStartMethodVersions { + clear_drive_block_cache: 0, + }, + block_end: DriveAbciBlockEndMethodVersions { + update_state_cache: 0, + update_drive_cache: 0, + validator_set_update: 2, + should_checkpoint: Some(0), + update_checkpoints: Some(0), + record_shielded_pool_anchor: Some(0), + prune_shielded_pool_anchors: Some(0), + record_token_shielded_pool_anchors: Some(0), // changed: per-token shielded pool anchors + }, + platform_state_storage: DriveAbciPlatformStateStorageMethodVersions { + fetch_platform_state: 0, + store_platform_state: 0, + }, +}; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs index 2fbda88a474..40010b9fc51 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs @@ -1,5 +1,6 @@ pub mod v1; pub mod v10; +pub mod v11; pub mod v2; pub mod v3; pub mod v4; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs new file mode 100644 index 00000000000..31b62693fc9 --- /dev/null +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs @@ -0,0 +1,405 @@ +use crate::version::drive_abci_versions::drive_abci_validation_versions::{ + DriveAbciAssetLockValidationVersions, DriveAbciDocumentsStateTransitionValidationVersions, + DriveAbciStateTransitionCommonValidationVersions, DriveAbciStateTransitionValidationVersion, + DriveAbciStateTransitionValidationVersions, DriveAbciValidationConstants, + DriveAbciValidationDataTriggerAndBindingVersions, DriveAbciValidationDataTriggerVersions, + DriveAbciValidationVersions, PenaltyAmounts, +}; + +// PROTOCOL_VERSION_15: token shielded pools. The batch validators of the token pool +// transitions (`token_shield`, `token_unshield`, `token_shielded_transfer`, +// `token_mint_to_pool`, `token_burn_from_pool`, `token_claim_to_pool` and +// `token_direct_purchase_to_pool`) are reachable from this version on; the transitions +// themselves are gated by `TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION` in +// `is_allowed`. v10 remains unchanged for PROTOCOL_VERSION_14 chain replay. +pub const DRIVE_ABCI_VALIDATION_VERSIONS_V11: DriveAbciValidationVersions = + DriveAbciValidationVersions { + state_transitions: DriveAbciStateTransitionValidationVersions { + common_validation_methods: DriveAbciStateTransitionCommonValidationVersions { + asset_locks: DriveAbciAssetLockValidationVersions { + fetch_asset_lock_transaction_output_sync: 0, + verify_asset_lock_is_not_spent_and_has_enough_balance: 0, + }, + validate_identity_public_key_contract_bounds: 1, + validate_identity_public_key_ids_dont_exist_in_state: 0, + validate_identity_public_key_ids_exist_in_state: 0, + validate_state_transition_identity_signed: 0, + validate_unique_identity_public_key_hashes_in_state: 1, + validate_master_key_uniqueness: 0, + validate_non_masternode_identity_exists: 0, + validate_identity_exists: 0, + }, + max_asset_lock_usage_attempts: 16, + identity_create_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: Some(0), + identity_signatures: Some(0), + nonce: None, + state: 0, + transform_into_action: 0, + }, + identity_update_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: Some(0), + identity_signatures: Some(0), + nonce: Some(0), + state: 0, + transform_into_action: 0, + }, + identity_top_up_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + identity_credit_withdrawal_state_transition: + DriveAbciStateTransitionValidationVersion { + // v1 adds config min_version enforcement: since protocol version 12, V0 config is no longer + // accepted because it lacks sized_integer_types support. + basic_structure: Some(1), + advanced_structure: None, + identity_signatures: None, + nonce: Some(0), + state: 0, + transform_into_action: 0, + }, + identity_credit_withdrawal_state_transition_purpose_matches_requirements: 0, + identity_credit_transfer_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: Some(0), + state: 0, + transform_into_action: 0, + }, + identity_credit_transfer_to_addresses_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: Some(0), + state: 0, + transform_into_action: 0, + }, + masternode_vote_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: Some(0), + identity_signatures: None, + nonce: Some(1), + state: 0, + transform_into_action: 0, + }, + masternode_vote_state_transition_balance_pre_check: 0, + contract_create_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(2), // changed: rejects `requiredSince` other than 1 on a newly created contract — the annotation must name the version the change arrives with, and a fresh contract is version 1 + advanced_structure: Some(1), + identity_signatures: None, + nonce: Some(0), + state: 1, // changed: runs data_contract_reference_validation on the created contract's refersTo declarations + transform_into_action: 0, + }, + contract_update_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(1), + advanced_structure: None, + identity_signatures: None, + nonce: Some(0), + state: 1, // changed: runs data_contract_reference_validation on the updated contract's refersTo declarations + transform_into_action: 0, + }, + data_contract_reference_validation: 0, + batch_state_transition: DriveAbciDocumentsStateTransitionValidationVersions { + basic_structure: 0, + advanced_structure: 0, + state: 0, + revision: 0, + // PROTOCOL_VERSION_12 (v3.1 hard fork): batch state transition + // fee accounting fixes. This single field gates multiple + // related billing changes so they all activate together at + // the same hard fork. On v0 every behavior below is the + // legacy under-billing, preserved verbatim for + // PROTOCOL_VERSION_11 chain replay. + // + // Gated by `transform_into_action: 1`: + // * B7 — outer `execution_context` is threaded through the + // batch transformer (was a dropped local) so per- + // transition fee_results in `try_into_action_v0` are + // billed. + // * B4 — `query_documents` cost in + // `fetch_documents_for_transitions_knowing_contract_and_document_type` + // is added to `execution_context`. + // * B5 — `query_documents` cost in `fetch_document_with_id` + // is added to `execution_context`. + // * T1 — DPNS data trigger parent-domain + // `query_documents` cost. + // * T2 — DPNS data trigger preorder `query_documents` cost. + // * T3 — DashPay data trigger recipient identity-balance + // fetch cost (switched to `fetch_identity_balance_with_costs`). + // * T4 — withdrawals data trigger `query_documents` cost. + transform_into_action: 1, + // PROTOCOL_VERSION_12 (v3.1 hard fork): per-transition + // failure paths in `transform_document_transition` now emit + // a `BumpIdentityDataContractNonce` action so the user pays + // for the validation work that already ran (fetch + + // ownership/revision check). v0 stays for chain + // reproducibility on PROTOCOL_VERSION_11 and below. + failed_per_transition_action: 1, + // PROTOCOL_VERSION_12 (v3.1 hard fork): fetch_documents + // helpers bumped to v1 which bill the grovedb cost of + // their query_documents calls. v0 stays for PV11 chain + // replay (the v0 helpers pass epoch=None and never call + // add_operation — byte-identical to pre-PR behavior). + fetch_documents_for_transitions_knowing_contract_and_document_type: 1, + fetch_document_with_id: 1, + data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { + // PROTOCOL_VERSION_14: v2 adds DashPay `profile` + // Create/Replace triggers enforcing the DIP-33 + // payment-address type byte (0x00 P2PKH / 0x01 P2SH). + // It keeps v1's PROTOCOL_VERSION_13 change: no reject + // bindings for Transfer, Purchase and UpdatePrice on + // DPNS `domain` documents (username transfers/sales). + bindings: 2, + triggers: DriveAbciValidationDataTriggerVersions { + // PROTOCOL_VERSION_12 (v3.1 hard fork): triggers + // that perform drive reads now have `_v1` versions + // that bill the cost via add_operation on the + // outer execution_context. v0 versions remain + // byte-identical to PV11 (don't bill). + create_contact_request_data_trigger: 1, + validate_profile_payment_addresses_data_trigger: 0, + create_domain_data_trigger: 1, + create_identity_data_trigger: 0, + create_feature_flag_data_trigger: 0, + create_masternode_reward_shares_data_trigger: 0, + delete_withdrawal_data_trigger: 2, // changed in v14: FAILED withdrawals may be deleted like COMPLETE ones + // Reject does no drive reads — stays at v0. + reject_data_trigger: 0, + }, + }, + is_allowed: 0, + document_create_transition_structure_validation: 1, + // Reject deletes on legacy keep-history types as paid consensus errors. + // Protocols through 13 retain the original internal-error outcome. + document_delete_transition_structure_validation: 1, + document_index_only_delete_transition_structure_validation: 0, + document_replace_transition_structure_validation: 0, + document_transfer_transition_structure_validation: 0, + document_purchase_transition_structure_validation: 0, + document_update_price_transition_structure_validation: 0, + document_base_transition_state_validation: 1, // changed: a token cost paid from the token's shielded pool skips the owner's balance checks + document_create_transition_state_validation: 2, + document_delete_transition_state_validation: 0, + document_index_only_delete_transition_state_validation: 0, + document_replace_transition_state_validation: 1, + document_transfer_transition_state_validation: 0, + document_purchase_transition_state_validation: 0, + document_update_price_transition_state_validation: 0, + document_reference_validation: 0, + token_mint_transition_structure_validation: 0, + token_burn_transition_structure_validation: 0, + token_transfer_transition_structure_validation: 0, + token_mint_transition_state_validation: 0, + token_burn_transition_state_validation: 0, + token_transfer_transition_state_validation: 0, + token_base_transition_structure_validation: 0, + token_base_transition_state_validation: 1, + token_freeze_transition_structure_validation: 0, + token_unfreeze_transition_structure_validation: 0, + token_freeze_transition_state_validation: 0, + token_unfreeze_transition_state_validation: 0, + token_destroy_frozen_funds_transition_structure_validation: 0, + token_destroy_frozen_funds_transition_state_validation: 0, + token_emergency_action_transition_structure_validation: 0, + token_emergency_action_transition_state_validation: 0, + token_config_update_transition_structure_validation: 0, + token_config_update_transition_state_validation: 1, + token_base_transition_group_action_validation: 1, + token_claim_transition_structure_validation: 0, + token_claim_transition_state_validation: 0, + token_direct_purchase_transition_structure_validation: 0, + token_direct_purchase_transition_state_validation: 0, + token_set_price_for_direct_purchase_transition_structure_validation: 0, + token_set_price_for_direct_purchase_transition_state_validation: 0, + token_shield_transition_structure_validation: 0, + token_shield_transition_state_validation: 0, + token_mint_to_pool_transition_structure_validation: 0, + token_mint_to_pool_transition_state_validation: 0, + token_burn_from_pool_transition_structure_validation: 0, + token_burn_from_pool_transition_state_validation: 0, + token_claim_to_pool_transition_structure_validation: 0, + token_claim_to_pool_transition_state_validation: 0, + token_direct_purchase_to_pool_transition_structure_validation: 0, + token_direct_purchase_to_pool_transition_state_validation: 0, + token_unshield_transition_structure_validation: 0, + token_unshield_transition_state_validation: 0, + token_shielded_transfer_transition_structure_validation: 0, + token_shielded_transfer_transition_state_validation: 0, + }, + identity_create_from_addresses_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: Some(0), + identity_signatures: Some(0), + nonce: Some(0), + state: 0, + transform_into_action: 0, + }, + identity_top_up_from_addresses_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: Some(0), + state: 0, + transform_into_action: 0, + }, + address_credit_withdrawal: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: Some(0), + state: 0, + transform_into_action: 0, + }, + address_funds_from_asset_lock: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: Some(0), + identity_signatures: None, + nonce: Some(0), + state: 0, + transform_into_action: 0, + }, + address_funds_transfer: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: Some(0), + state: 0, + transform_into_action: 0, + }, + shield_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + shielded_transfer_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + unshield_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + shield_from_asset_lock_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + shielded_withdrawal_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + identity_create_from_shielded_pool_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + shield_from_identity_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: Some(0), + nonce: Some(0), + state: 0, + transform_into_action: 0, + }, + identity_top_up_from_shielded_pool_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + }, + has_nonce_validation: 1, + has_address_witness_validation: 0, + validate_address_witnesses: 0, + validate_shielded_proof: 0, + validate_minimum_shielded_fee: 0, + process_state_transition: 0, + state_transition_to_execution_event_for_check_tx: 0, + penalties: PenaltyAmounts { + identity_id_not_correct: 50000000, + unique_key_already_present: 10000000, + validation_of_added_keys_structure_failure: 10000000, + validation_of_added_keys_proof_of_possession_failure: 50000000, + address_funds_insufficient_balance: 10000000, + shielded_proof_verification_failure: 50000000, + }, + event_constants: DriveAbciValidationConstants { + maximum_vote_polls_to_process: 2, + maximum_contenders_to_consider: 100, + minimum_pool_notes_for_outgoing: 250, + minimum_token_pool_notes_for_outgoing: 0, + shielded_anchor_retention_blocks: 1000, + shielded_anchor_pruning_interval: 100, + // Rebalanced for protocol 14: one Halo 2 bundle verification is + // ~5 ms; at the fee model's ~8M credits/ms of CPU this prices it + // at ~27x a BLS signature verification — reserved for compute + // alone, never for database work (the storage allowance below + // covers that independently). + shielded_proof_verification_fee: 40_000_000, + // Retained from protocol 13, versioned independently of the + // rebalanced bundle proof fee above: it prices the per-action + // work — the marginal Halo 2 verification CPU (~1.1 ms/action), + // the RedPallas spend-auth check, the nullifier duplicate check + // and the tree-insertion processing — and its ~18M headroom over + // the ~3.5M credits of metered per-append GroveDB processing is + // deliberate, not a shared calibration rate with the 40M bundle + // fee. + shielded_per_action_processing_fee: 22_000_000, + // Rebalanced for protocol 14 alongside the proof fee: the + // GROVE_V4 fixed per-append model meters a 1-action transfer at + // 17,882,707 credits total, 14,337,000 of it storage — 523 + // bytes at the full 27,400 credits/byte rate, the declared + // 344-byte payload plus Merk framing, dense path records and + // the amortized chunk framing. 550 covers that with headroom, + // so the storage component alone pays for the database work and + // the compute fees above stay reserved for compute. + shielded_storage_bytes_per_action: 550, + shielded_implicit_fee_cap: 20_000_000_000, + // 0.1, 0.3, 0.5, 1.0 DASH in credits (1 DASH = 10^8 duffs, CREDITS_PER_DUFF = 1000). + // v13 revises the v8 set: adds 0.03 and 0.25 DASH, retires 0.3 DASH. + shielded_identity_create_denominations: &[ + 3_000_000_000, // 0.03 DASH + 10_000_000_000, // 0.1 DASH + 25_000_000_000, // 0.25 DASH + 50_000_000_000, // 0.5 DASH + 100_000_000_000, // 1 DASH + ], + }, + }; diff --git a/packages/rs-platform-version/src/version/drive_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/mod.rs index 1d7c32caed6..889baec9f92 100644 --- a/packages/rs-platform-version/src/version/drive_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/mod.rs @@ -30,6 +30,7 @@ pub mod drive_token_method_versions; pub mod drive_verify_method_versions; pub mod drive_vote_method_versions; pub mod v1; +pub mod v10; pub mod v2; pub mod v3; pub mod v4; diff --git a/packages/rs-platform-version/src/version/drive_versions/v10.rs b/packages/rs-platform-version/src/version/drive_versions/v10.rs new file mode 100644 index 00000000000..6f6bfbce3e5 --- /dev/null +++ b/packages/rs-platform-version/src/version/drive_versions/v10.rs @@ -0,0 +1,144 @@ +use crate::version::drive_versions::drive_address_funds_method_versions::v2::DRIVE_ADDRESS_FUNDS_METHOD_VERSIONS_V2; +use crate::version::drive_versions::drive_contract_method_versions::v4::DRIVE_CONTRACT_METHOD_VERSIONS_V4; +use crate::version::drive_versions::drive_credit_pool_method_versions::v1::CREDIT_POOL_METHOD_VERSIONS_V1; +use crate::version::drive_versions::drive_document_method_versions::v4::DRIVE_DOCUMENT_METHOD_VERSIONS_V4; +use crate::version::drive_versions::drive_group_method_versions::v1::DRIVE_GROUP_METHOD_VERSIONS_V1; +use crate::version::drive_versions::drive_group_method_versions::DriveShieldedMethodVersions; +use crate::version::drive_versions::drive_grove_method_versions::v1::DRIVE_GROVE_METHOD_VERSIONS_V1; +use crate::version::drive_versions::drive_identity_method_versions::v2::DRIVE_IDENTITY_METHOD_VERSIONS_V2; +use crate::version::drive_versions::drive_state_transition_method_versions::v4::DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4; +use crate::version::drive_versions::drive_structure_version::v1::DRIVE_STRUCTURE_V1; +use crate::version::drive_versions::drive_token_method_versions::v2::DRIVE_TOKEN_METHOD_VERSIONS_V2; +use crate::version::drive_versions::drive_verify_method_versions::v2::DRIVE_VERIFY_METHOD_VERSIONS_V2; +use crate::version::drive_versions::drive_vote_method_versions::v2::DRIVE_VOTE_METHOD_VERSIONS_V2; +use crate::version::drive_versions::{ + DriveAssetLockMethodVersions, DriveBalancesMethodVersions, DriveBatchOperationsMethodVersion, + DriveEstimatedCostsMethodVersions, DriveFeesMethodVersions, DriveFetchMethodVersions, + DriveInitializationMethodVersions, DriveMethodVersions, DriveOperationsMethodVersion, + DrivePlatformStateMethodVersions, DrivePlatformSystemMethodVersions, + DrivePrefundedSpecializedMethodVersions, DriveProtocolUpgradeVersions, + DriveProveMethodVersions, DriveSavedBlockTransactionsMethodVersions, + DriveSystemEstimationCostsMethodVersions, DriveVersion, +}; +use grovedb_version::version::v4::GROVE_V4; + +/// Drive version 10. Introduced in protocol version 15 with token shielded pools: +/// `create_initial_state_structure` 4 adds the token shielded pools BigSumTree under the +/// Tokens root at genesis, and `DRIVE_TOKEN_METHOD_VERSIONS_V2` carries the pool operations +/// (pool creation, shield, unshield, shielded transfer, mint to and burn from the pool) plus +/// `calculate_total_tokens_balance` v1, which adds the pool balances to the conservation +/// check. +/// +/// Everything else matches `DRIVE_VERSION_V9`. +pub const DRIVE_VERSION_V10: DriveVersion = DriveVersion { + structure: DRIVE_STRUCTURE_V1, + methods: DriveMethodVersions { + initialization: DriveInitializationMethodVersions { + create_initial_state_structure: 4, // changed in v10: adds the token shielded pools BigSumTree under the Tokens root (v8 added the credit shielded pool trees) + }, + credit_pools: CREDIT_POOL_METHOD_VERSIONS_V1, + protocol_upgrade: DriveProtocolUpgradeVersions { + clear_version_information: 0, + fetch_versions_with_counter: 0, + fetch_proved_versions_with_counter: 0, + fetch_validator_version_votes: 0, + fetch_proved_validator_version_votes: 0, + remove_validators_proposed_app_versions: 0, + update_validator_proposed_app_version: 0, + }, + prove: DriveProveMethodVersions { + prove_elements: 0, + prove_multiple_state_transition_results: 0, + prove_state_transition: 0, + }, + balances: DriveBalancesMethodVersions { + add_to_system_credits: 0, + add_to_system_credits_operations: 0, + remove_from_system_credits: 0, + remove_from_system_credits_operations: 0, + calculate_total_credits_balance: 2, // ShieldedBalances root tree adds a fifth term to the equation + }, + document: DRIVE_DOCUMENT_METHOD_VERSIONS_V4, // unchanged since v9: v2 index walkers + v1 update walker (shared-prefix aggregate indexes become insertable) and the detect_ranked_mode slot + vote: DRIVE_VOTE_METHOD_VERSIONS_V2, + contract: DRIVE_CONTRACT_METHOD_VERSIONS_V4, // unchanged since v9: add_contract_to_storage v1 writes the contract version item beside the contract + fees: DriveFeesMethodVersions { calculate_fee: 0 }, + estimated_costs: DriveEstimatedCostsMethodVersions { + add_estimation_costs_for_levels_up_to_contract: 0, + add_estimation_costs_for_levels_up_to_contract_document_type_excluded: 0, + add_estimation_costs_for_contested_document_tree_levels_up_to_contract: 0, + add_estimation_costs_for_contested_document_tree_levels_up_to_contract_document_type_excluded: 0, + }, + asset_lock: DriveAssetLockMethodVersions { + add_asset_lock_outpoint: 0, + add_estimation_costs_for_adding_asset_lock: 0, + fetch_asset_lock_outpoint_info: 0, + }, + verify: DRIVE_VERIFY_METHOD_VERSIONS_V2, // changed in v8: compacted address-balance proof envelope (verify v1) + identity: DRIVE_IDENTITY_METHOD_VERSIONS_V2, // unchanged since v9: v1 withdrawal-by-transaction-index query builder (structural, identical lowering) + token: DRIVE_TOKEN_METHOD_VERSIONS_V2, // changed in v10: token shielded pools (pool creation + shield / unshield / shielded transfer / mint to / burn from ops, conservation v1) + platform_system: DrivePlatformSystemMethodVersions { + estimation_costs: DriveSystemEstimationCostsMethodVersions { + for_total_system_credits_update: 0, + }, + }, + operations: DriveOperationsMethodVersion { + rollback_transaction: 0, + drop_cache: 0, + commit_transaction: 0, + apply_partial_batch_low_level_drive_operations: 0, + apply_partial_batch_grovedb_operations: 0, + apply_batch_low_level_drive_operations: 0, + apply_batch_grovedb_operations: 0, + }, + state_transitions: DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4, // changed: document_from_action generation 1 stamps built documents with the contract version (create assigns, replace re-assigns; paired with document serialization format 3) + batch_operations: DriveBatchOperationsMethodVersion { + convert_drive_operations_to_grove_operations: 0, + apply_drive_operations: 0, + }, + platform_state: DrivePlatformStateMethodVersions { + fetch_platform_state_bytes: 0, + store_platform_state_bytes: 0, + }, + fetch: DriveFetchMethodVersions { fetch_elements: 0 }, + prefunded_specialized_balances: DrivePrefundedSpecializedMethodVersions { + fetch_single: 0, + prove_single: 0, + add_prefunded_specialized_balance: 0, + add_prefunded_specialized_balance_operations: 1, + deduct_from_prefunded_specialized_balance: 1, + deduct_from_prefunded_specialized_balance_operations: 0, + estimated_cost_for_prefunded_specialized_balance_update: 0, + empty_prefunded_specialized_balance: 0, + }, + group: DRIVE_GROUP_METHOD_VERSIONS_V1, + address_funds: DRIVE_ADDRESS_FUNDS_METHOD_VERSIONS_V2, + shielded: DriveShieldedMethodVersions { + insert_note: 0, + insert_nullifiers: 0, + update_total_balance: 0, + record_anchor_if_changed: 0, + prune_anchors: 0, + has_anchor: 0, + has_nullifier: 0, + read_total_balance: 0, + notes_count: 0, + }, + saved_block_transactions: DriveSavedBlockTransactionsMethodVersions { + store_address_balances: 0, + fetch_address_balances: 0, + prove_compacted_address_balance_changes: 1, + compact_address_balances: 0, + cleanup_expired_address_balances: 0, + max_blocks_before_compaction: 64, + max_addresses_before_compaction: 2048, + }, + }, + grove_methods: DRIVE_GROVE_METHOD_VERSIONS_V1, + // unchanged since v9: GROVE_V4 activates the indexed-tree batch cleanup + // gates (overwrite inspection + delete-tree actual-type cleanup). + // Indexed trees only exist from protocol v14, so activating the + // stricter cleanup with them costs older versions nothing; staying + // on V3 would let a batch overwrite of a ranked index orphan its + // per-axis secondary storage. + grove_version: GROVE_V4, +}; diff --git a/packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs b/packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs index 17da6c6f124..8d8dfea39a8 100644 --- a/packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs +++ b/packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs @@ -23,6 +23,6 @@ pub const CONTRACT_FEE_CLAIM_INITIAL_PROTOCOL_VERSION: ProtocolVersion = 14; /// not exist and lookups must report it absent. pub const APP_CONNECT_CONTRACT_INITIAL_PROTOCOL_VERSION: ProtocolVersion = 14; /// Token shielded pools (per-token Orchard pools behind `TokenConfigurationV1::has_shielded_pool`, -/// with the `TokenShield`, `TokenUnshield` and `TokenShieldedTransfer` batch transitions) activate -/// with protocol version 14. -pub const TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION: ProtocolVersion = 14; +/// with the batch token transitions that shield, unshield, transfer, mint, burn, claim and +/// purchase into or out of them) activate with protocol version 15. +pub const TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION: ProtocolVersion = 15; diff --git a/packages/rs-platform-version/src/version/mod.rs b/packages/rs-platform-version/src/version/mod.rs index 1b1635efb42..ae5fd0887e1 100644 --- a/packages/rs-platform-version/src/version/mod.rs +++ b/packages/rs-platform-version/src/version/mod.rs @@ -1,6 +1,6 @@ mod protocol_version; -use crate::version::v14::PROTOCOL_VERSION_14; +use crate::version::v15::PROTOCOL_VERSION_15; pub use protocol_version::*; use std::ops::RangeInclusive; @@ -20,6 +20,7 @@ pub mod v11; pub mod v12; pub mod v13; pub mod v14; +pub mod v15; pub mod v2; pub mod v3; pub mod v4; @@ -33,5 +34,5 @@ pub type ProtocolVersion = u32; pub const ALL_VERSIONS: RangeInclusive = 1..=LATEST_VERSION; -pub const LATEST_VERSION: ProtocolVersion = PROTOCOL_VERSION_14; +pub const LATEST_VERSION: ProtocolVersion = PROTOCOL_VERSION_15; pub const INITIAL_PROTOCOL_VERSION: ProtocolVersion = 1; diff --git a/packages/rs-platform-version/src/version/protocol_version.rs b/packages/rs-platform-version/src/version/protocol_version.rs index 00cc470bbc7..d6273f0e766 100644 --- a/packages/rs-platform-version/src/version/protocol_version.rs +++ b/packages/rs-platform-version/src/version/protocol_version.rs @@ -22,6 +22,7 @@ use crate::version::v11::PLATFORM_V11; use crate::version::v12::PLATFORM_V12; use crate::version::v13::PLATFORM_V13; use crate::version::v14::PLATFORM_V14; +use crate::version::v15::PLATFORM_V15; use crate::version::v2::PLATFORM_V2; use crate::version::v3::PLATFORM_V3; use crate::version::v4::PLATFORM_V4; @@ -61,6 +62,7 @@ pub const PLATFORM_VERSIONS: &[PlatformVersion] = &[ PLATFORM_V12, PLATFORM_V13, PLATFORM_V14, + PLATFORM_V15, ]; #[cfg(feature = "mock-versions")] @@ -69,7 +71,7 @@ pub static PLATFORM_TEST_VERSIONS: OnceLock> = OnceLock::ne #[cfg(feature = "mock-versions")] const DEFAULT_PLATFORM_TEST_VERSIONS: &[PlatformVersion] = &[TEST_PLATFORM_V2, TEST_PLATFORM_V3]; -pub const LATEST_PLATFORM_VERSION: &PlatformVersion = &PLATFORM_V14; +pub const LATEST_PLATFORM_VERSION: &PlatformVersion = &PLATFORM_V15; pub const DESIRED_PLATFORM_VERSION: &PlatformVersion = LATEST_PLATFORM_VERSION; diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index 6cf76a00872..2dcdc60b4e3 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -30,7 +30,7 @@ use crate::version::ProtocolVersion; pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; -/// v14 hosts seven consensus changes: +/// v14 hosts six consensus changes: /// /// 1. **Contract-level ranked aggregates**: an index can /// declare that its groups are rankable by an aggregate, so a query like @@ -145,23 +145,6 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// is determinism, not correct rounding: on a boundary tuple the host /// libm (glibc, macOS) can still be 1 ulp away, so anything predicting /// rewards with host math may differ from consensus by one unit. -/// 7. **Token shielded pools**: a token configuration in format version 1 -/// (`TokenConfiguration::V1`, admitted by `CONTRACT_VERSIONS_V6`'s -/// `token_configuration_format` bounds) can set `hasShieldedPool`, which -/// gives the token its own Orchard pool under -/// `[Tokens, TOKEN_SHIELDED_POOLS_KEY, token_id]` laid out like the credit -/// pool. Three batch token transitions (`TokenShield`, `TokenUnshield`, -/// `TokenShieldedTransfer`, `DRIVE_ABCI_VALIDATION_V10` / -/// `DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4`) move tokens between an -/// identity balance and the pool or inside it; the identity signs and pays -/// the fee in credits, the token id and owner id (and recipient and amount -/// for an unshield) are bound into the Orchard sighash, and the pool -/// balances are a term of the token conservation check -/// (`calculate_total_tokens_balance` v1 in `DRIVE_TOKEN_METHOD_VERSIONS_V2`). -/// `record_token_shielded_pool_anchors` records and prunes the anchors of -/// the pools a block touched. The pools root tree is inserted by the -/// upgrade transition and by `create_initial_state_structure` v4; the six -/// shielded queries accept an optional `token_id` to target a token pool. /// /// The first two are orthogonal by construction: the ranked upgrade decides the /// *property-name* tree type, the demotion decides the *value* tree type diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs new file mode 100644 index 00000000000..6c0c7bfbabe --- /dev/null +++ b/packages/rs-platform-version/src/version/v15.rs @@ -0,0 +1,93 @@ +use crate::version::consensus_versions::ConsensusVersions; +use crate::version::dpp_versions::dpp_asset_lock_versions::v1::DPP_ASSET_LOCK_VERSIONS_V1; +use crate::version::dpp_versions::dpp_contract_versions::v7::CONTRACT_VERSIONS_V7; +use crate::version::dpp_versions::dpp_costs_versions::v1::DPP_COSTS_VERSIONS_V1; +use crate::version::dpp_versions::dpp_document_versions::v4::DOCUMENT_VERSIONS_V4; +use crate::version::dpp_versions::dpp_factory_versions::v1::DPP_FACTORY_VERSIONS_V1; +use crate::version::dpp_versions::dpp_identity_versions::v1::IDENTITY_VERSIONS_V1; +use crate::version::dpp_versions::dpp_method_versions::v3::DPP_METHOD_VERSIONS_V3; +use crate::version::dpp_versions::dpp_state_transition_conversion_versions::v2::STATE_TRANSITION_CONVERSION_VERSIONS_V2; +use crate::version::dpp_versions::dpp_state_transition_method_versions::v1::STATE_TRANSITION_METHOD_VERSIONS_V1; +use crate::version::dpp_versions::dpp_state_transition_serialization_versions::v3::STATE_TRANSITION_SERIALIZATION_VERSIONS_V3; +use crate::version::dpp_versions::dpp_state_transition_versions::v3::STATE_TRANSITION_VERSIONS_V3; +use crate::version::dpp_versions::dpp_token_versions::v3::TOKEN_VERSIONS_V3; +use crate::version::dpp_versions::dpp_validation_versions::v5::DPP_VALIDATION_VERSIONS_V5; +use crate::version::dpp_versions::dpp_voting_versions::v2::VOTING_VERSION_V2; +use crate::version::dpp_versions::DPPVersion; +use crate::version::drive_abci_versions::drive_abci_checkpoint_parameters::v1::DRIVE_ABCI_CHECKPOINT_PARAMETERS_V1; +use crate::version::drive_abci_versions::drive_abci_method_versions::v11::DRIVE_ABCI_METHOD_VERSIONS_V11; +use crate::version::drive_abci_versions::drive_abci_query_versions::v3::DRIVE_ABCI_QUERY_VERSIONS_V3; +use crate::version::drive_abci_versions::drive_abci_structure_versions::v1::DRIVE_ABCI_STRUCTURE_VERSIONS_V1; +use crate::version::drive_abci_versions::drive_abci_validation_versions::v11::DRIVE_ABCI_VALIDATION_VERSIONS_V11; +use crate::version::drive_abci_versions::drive_abci_withdrawal_constants::v3::DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V3; +use crate::version::drive_abci_versions::DriveAbciVersion; +use crate::version::drive_versions::v10::DRIVE_VERSION_V10; +use crate::version::fee::v3::FEE_VERSION3; +use crate::version::protocol_version::PlatformVersion; +use crate::version::system_data_contract_versions::v3::SYSTEM_DATA_CONTRACT_VERSIONS_V3; +use crate::version::system_limits::v4::SYSTEM_LIMITS_V4; +use crate::version::ProtocolVersion; + +pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; + +/// v15 hosts one consensus change: +/// +/// 1. **Token shielded pools**: a token configuration in format version 1 +/// (`TokenConfiguration::V1`, admitted by `CONTRACT_VERSIONS_V7`'s +/// `token_configuration_format` bounds) can set `hasShieldedPool`, which +/// gives the token its own Orchard pool under +/// `[Tokens, TOKEN_SHIELDED_POOLS_KEY, token_id]` laid out like the credit +/// pool. A pooled token must leave its freeze, unfreeze and destroy-frozen- +/// funds rules unassigned, since notes have no owner to freeze. Seven batch +/// token transitions (`TokenShield`, `TokenUnshield`, +/// `TokenShieldedTransfer`, `TokenMintToPool`, `TokenBurnFromPool`, +/// `TokenClaimToPool` and `TokenDirectPurchaseToPool`, validated through +/// `DRIVE_ABCI_VALIDATION_VERSIONS_V11` and gated by +/// `TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION`) move tokens between an +/// identity balance, the supply and the pool or inside it; the identity +/// signs and pays the fee in credits, and every spend bundle binds the +/// token id and owner id (plus the recipient and amount where tokens leave +/// the pool) into the Orchard sighash. The pool balances are a term of the +/// token conservation check (`calculate_total_tokens_balance` v1 in +/// `DRIVE_TOKEN_METHOD_VERSIONS_V2`, reached through `DRIVE_VERSION_V10`). +/// `record_token_shielded_pool_anchors` (`DRIVE_ABCI_METHOD_VERSIONS_V11`) +/// records and prunes the anchors of the pools a block touched. The pools +/// root tree is inserted by the version 15 upgrade transition and by +/// `create_initial_state_structure` v4; the six shielded queries accept an +/// optional `token_id` to target a token pool. +/// +/// Everything else matches v14. +pub const PLATFORM_V15: PlatformVersion = PlatformVersion { + protocol_version: PROTOCOL_VERSION_15, + drive: DRIVE_VERSION_V10, // changed: token shielded pools (genesis structure v4 + token method versions v2) + drive_abci: DriveAbciVersion { + structs: DRIVE_ABCI_STRUCTURE_VERSIONS_V1, + methods: DRIVE_ABCI_METHOD_VERSIONS_V11, // changed: records and prunes the token shielded pool anchors at block end + validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V11, // changed: token shielded pool batch validators + withdrawal_constants: DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V3, + query: DRIVE_ABCI_QUERY_VERSIONS_V3, + checkpoints: DRIVE_ABCI_CHECKPOINT_PARAMETERS_V1, + }, + dpp: DPPVersion { + costs: DPP_COSTS_VERSIONS_V1, + validation: DPP_VALIDATION_VERSIONS_V5, + state_transition_serialization_versions: STATE_TRANSITION_SERIALIZATION_VERSIONS_V3, + state_transition_conversion_versions: STATE_TRANSITION_CONVERSION_VERSIONS_V2, + state_transition_method_versions: STATE_TRANSITION_METHOD_VERSIONS_V1, + state_transitions: STATE_TRANSITION_VERSIONS_V3, + contract_versions: CONTRACT_VERSIONS_V7, // changed: token configuration format 1 (the shielded pool flag) is admitted + document_versions: DOCUMENT_VERSIONS_V4, + identity_versions: IDENTITY_VERSIONS_V1, + voting_versions: VOTING_VERSION_V2, + token_versions: TOKEN_VERSIONS_V3, + asset_lock_versions: DPP_ASSET_LOCK_VERSIONS_V1, + methods: DPP_METHOD_VERSIONS_V3, + factory_versions: DPP_FACTORY_VERSIONS_V1, + }, + system_data_contracts: SYSTEM_DATA_CONTRACT_VERSIONS_V3, + fee_version: FEE_VERSION3, + system_limits: SYSTEM_LIMITS_V4, + consensus: ConsensusVersions { + tenderdash_consensus_version: 1, + }, +}; diff --git a/packages/rs-sdk/src/platform/query.rs b/packages/rs-sdk/src/platform/query.rs index f64b8dfa488..0e1cf0bb4dc 100644 --- a/packages/rs-sdk/src/platform/query.rs +++ b/packages/rs-sdk/src/platform/query.rs @@ -1438,7 +1438,7 @@ impl Query for ShieldedNullifiersQuery { } } -// --- Token Shielded Pool Queries (protocol version 14+) --- +// --- Token Shielded Pool Queries (protocol version 15+) --- impl Query for TokenShieldedPoolQuery { fn query( diff --git a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs index 6e2da204469..f66191cd11b 100644 --- a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs +++ b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs @@ -136,7 +136,7 @@ use dpp::consensus::state::identity::no_transfer_key_for_core_withdrawal_availab use dpp::consensus::state::identity::RecipientIdentityDoesNotExistError; use dpp::consensus::state::prefunded_specialized_balances::prefunded_specialized_balance_insufficient_error::PrefundedSpecializedBalanceInsufficientError; use dpp::consensus::state::prefunded_specialized_balances::prefunded_specialized_balance_not_found_error::PrefundedSpecializedBalanceNotFoundError; -use dpp::consensus::state::token::{IdentityDoesNotHaveEnoughTokenBalanceError, IdentityTokenAccountNotFrozenError, IdentityTokenAccountFrozenError, TokenIsPausedError, IdentityTokenAccountAlreadyFrozenError, UnauthorizedTokenActionError, TokenSettingMaxSupplyToLessThanCurrentSupplyError, TokenMintPastMaxSupplyError, NewTokensDestinationIdentityDoesNotExistError, NewAuthorizedActionTakerIdentityDoesNotExistError, NewAuthorizedActionTakerGroupDoesNotExistError, NewAuthorizedActionTakerMainGroupNotSetError, InvalidGroupPositionError, TokenAlreadyPausedError, TokenNotPausedError, InvalidTokenClaimPropertyMismatch, InvalidTokenClaimNoCurrentRewards, InvalidTokenClaimWrongClaimant, TokenTransferRecipientIdentityNotExistError, PreProgrammedDistributionTimestampInPastError, IdentityHasNotAgreedToPayRequiredTokenAmountError, RequiredTokenPaymentInfoNotSetError, IdentityTryingToPayWithWrongTokenError, TokenDirectPurchaseUserPriceTooLow, TokenAmountUnderMinimumSaleAmount, TokenNotForDirectSale, InvalidTokenPositionStateError, TokenOncePerIdentityDistributionAlreadyClaimedError, TokenShieldedPoolNotEnabledError}; +use dpp::consensus::state::token::{IdentityDoesNotHaveEnoughTokenBalanceError, IdentityTokenAccountNotFrozenError, IdentityTokenAccountFrozenError, TokenIsPausedError, IdentityTokenAccountAlreadyFrozenError, UnauthorizedTokenActionError, TokenSettingMaxSupplyToLessThanCurrentSupplyError, TokenMintPastMaxSupplyError, NewTokensDestinationIdentityDoesNotExistError, NewAuthorizedActionTakerIdentityDoesNotExistError, NewAuthorizedActionTakerGroupDoesNotExistError, NewAuthorizedActionTakerMainGroupNotSetError, InvalidGroupPositionError, TokenAlreadyPausedError, TokenNotPausedError, InvalidTokenClaimPropertyMismatch, InvalidTokenClaimNoCurrentRewards, InvalidTokenClaimWrongClaimant, TokenTransferRecipientIdentityNotExistError, PreProgrammedDistributionTimestampInPastError, IdentityHasNotAgreedToPayRequiredTokenAmountError, RequiredTokenPaymentInfoNotSetError, IdentityTryingToPayWithWrongTokenError, TokenDirectPurchaseUserPriceTooLow, TokenAmountUnderMinimumSaleAmount, TokenNotForDirectSale, InvalidTokenPositionStateError, TokenOncePerIdentityDistributionAlreadyClaimedError, TokenShieldedPoolNotEnabledError, TokenShieldedPaymentAmountMismatchError, TokenShieldedPaymentNotRequiredError}; use dpp::consensus::state::address_funds::{AddressDoesNotExistError, AddressInvalidNonceError, AddressNotEnoughFundsError, AddressesNotEnoughFundsError}; use dpp::consensus::state::document::referenced_document_type_deletable_error::ReferencedDocumentTypeDeletableError; use dpp::consensus::state::document::referenced_document_type_not_deletable_error::ReferencedDocumentTypeNotDeletableError; @@ -493,6 +493,12 @@ pub fn from_state_error(state_error: &StateError) -> JsValue { StateError::TokenShieldedPoolNotEnabledError(e) => { generic_consensus_error!(TokenShieldedPoolNotEnabledError, e).into() } + StateError::TokenShieldedPaymentAmountMismatchError(e) => { + generic_consensus_error!(TokenShieldedPaymentAmountMismatchError, e).into() + } + StateError::TokenShieldedPaymentNotRequiredError(e) => { + generic_consensus_error!(TokenShieldedPaymentNotRequiredError, e).into() + } StateError::IdentityInTokenConfigurationNotFoundError(e) => { generic_consensus_error!(IdentityInTokenConfigurationNotFoundError, e).into() } diff --git a/packages/wasm-dpp2/src/state_transitions/batch/mod.rs b/packages/wasm-dpp2/src/state_transitions/batch/mod.rs index 4d375d84c61..c7e44f82ead 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/mod.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/mod.rs @@ -9,6 +9,7 @@ pub mod prefunded_voting_balance; pub mod token_base_transition; pub mod token_payment_info; pub mod token_pricing_schedule; +pub mod token_shielded_payment; pub mod token_transition; pub mod token_transitions; diff --git a/packages/wasm-dpp2/src/state_transitions/batch/token_payment_info.rs b/packages/wasm-dpp2/src/state_transitions/batch/token_payment_info.rs index 070c6a22c7d..e014659e61a 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/token_payment_info.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/token_payment_info.rs @@ -4,6 +4,7 @@ use crate::identifier::{IdentifierLikeOrUndefinedJs, IdentifierWasm}; use crate::impl_try_from_js_value; use crate::impl_wasm_conversions_inner; use crate::impl_wasm_type_info; +use crate::state_transitions::batch::token_shielded_payment::TokenShieldedPaymentWasm; use crate::utils::try_from_options_optional; use dpp::balances::credits::TokenAmount; use dpp::data_contract::TokenContractPosition; @@ -11,6 +12,8 @@ use dpp::tokens::gas_fees_paid_by::GasFeesPaidBy; use dpp::tokens::token_payment_info::TokenPaymentInfo; use dpp::tokens::token_payment_info::v0::TokenPaymentInfoV0; use dpp::tokens::token_payment_info::v0::v0_accessors::TokenPaymentInfoAccessorsV0; +use dpp::tokens::token_payment_info::v1::TokenPaymentInfoV1; +use dpp::tokens::token_payment_info::v1::v1_accessors::TokenPaymentInfoAccessorsV1; use serde::Deserialize; use wasm_bindgen::prelude::wasm_bindgen; @@ -32,6 +35,11 @@ export interface TokenPaymentInfoOptions { minimumTokenCost?: bigint; maximumTokenCost?: bigint; gasFeesPaidBy?: GasFeesPaidByLike; + /** + * Pay the token cost out of the token's shielded pool with this spend bundle instead of + * the document owner's token balance (format version 1, protocol version 15+). + */ + shieldedPayment?: TokenShieldedPayment; } /** @@ -44,6 +52,7 @@ export interface TokenPaymentInfoObject { minimumTokenCost: bigint | null; maximumTokenCost: bigint | null; gasFeesPaidBy: string; + shieldedPayment?: TokenShieldedPaymentObject; } /** @@ -56,6 +65,7 @@ export interface TokenPaymentInfoJSON { minimumTokenCost: number | string | null; maximumTokenCost: number | string | null; gasFeesPaidBy: string; + shieldedPayment?: TokenShieldedPaymentJSON; } "#; @@ -101,19 +111,25 @@ impl TokenPaymentInfoWasm { .map(Into::into) .unwrap_or_default(); + let shielded_payment: Option = + try_from_options_optional(&options, "shieldedPayment")?; + // Deserialize primitive fields via serde last (consumes options) let opts: TokenPaymentInfoOptions = serde_wasm_bindgen::from_value(options.into()) .map_err(|e| WasmDppError::invalid_argument(e.to_string()))?; - Ok(TokenPaymentInfoWasm(TokenPaymentInfo::V0( - TokenPaymentInfoV0 { - payment_token_contract_id: payment_token_contract_id.map(Into::into), - token_contract_position: opts.token_contract_position, - minimum_token_cost: opts.minimum_token_cost, - maximum_token_cost: opts.maximum_token_cost, - gas_fees_paid_by, - }, - ))) + let v0 = TokenPaymentInfoV0 { + payment_token_contract_id: payment_token_contract_id.map(Into::into), + token_contract_position: opts.token_contract_position, + minimum_token_cost: opts.minimum_token_cost, + maximum_token_cost: opts.maximum_token_cost, + gas_fees_paid_by, + }; + + Ok(TokenPaymentInfoWasm(match shielded_payment { + None => TokenPaymentInfo::V0(v0), + Some(payment) => TokenPaymentInfo::V1(TokenPaymentInfoV1::from_v0(v0, payment.into())), + })) } #[wasm_bindgen(getter = "paymentTokenContractId")] @@ -141,6 +157,20 @@ impl TokenPaymentInfoWasm { GasFeesPaidByWasm::from(self.0.gas_fees_paid_by()).into() } + /// The spend bundle paying the token cost from the token's shielded pool, when the + /// payment is shielded (format version 1). + #[wasm_bindgen(getter = "shieldedPayment")] + pub fn shielded_payment(&self) -> Option { + self.0.shielded_payment().cloned().map(Into::into) + } + + /// Sets (format version 1) or clears (format version 0) the shielded payment. + #[wasm_bindgen(setter = "shieldedPayment")] + pub fn set_shielded_payment(&mut self, shielded_payment: Option) { + self.0 + .set_shielded_payment(shielded_payment.map(Into::into)); + } + #[wasm_bindgen(setter = "paymentTokenContractId")] pub fn set_payment_token_contract_id( &mut self, diff --git a/packages/wasm-dpp2/src/state_transitions/batch/token_shielded_payment.rs b/packages/wasm-dpp2/src/state_transitions/batch/token_shielded_payment.rs new file mode 100644 index 00000000000..7974034cda8 --- /dev/null +++ b/packages/wasm-dpp2/src/state_transitions/batch/token_shielded_payment.rs @@ -0,0 +1,165 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::impl_try_from_js_value; +use crate::impl_wasm_conversions_inner; +use crate::impl_wasm_type_info; +use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; +use crate::utils::{try_to_u64, try_vec_to_fixed_bytes}; +use dpp::tokens::token_payment_info::v1::TokenShieldedPayment; +use serde::Deserialize; +use wasm_bindgen::JsValue; +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen(typescript_custom_section)] +const TOKEN_SHIELDED_PAYMENT_TS: &str = r#" +/** + * Options for constructing a TokenShieldedPayment: an Orchard spend bundle in the payment + * token's shielded pool whose value balance (`amount`) pays a document action's token cost. + */ +export interface TokenShieldedPaymentOptions { + amount: bigint; + actions: SerializedOrchardAction[]; + anchor: Uint8Array; + proof: Uint8Array; + bindingSignature: Uint8Array; +} + +/** + * TokenShieldedPayment serialized as a plain object. + */ +export interface TokenShieldedPaymentObject { + amount: bigint; + actions: SerializedOrchardActionObject[]; + anchor: Uint8Array; + proof: Uint8Array; + bindingSignature: Uint8Array; +} + +/** + * TokenShieldedPayment serialized as JSON. + */ +export interface TokenShieldedPaymentJSON { + amount: number | string; + actions: SerializedOrchardActionJSON[]; + anchor: string; + proof: string; + bindingSignature: string; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "TokenShieldedPaymentOptions")] + pub type TokenShieldedPaymentOptionsJs; + + #[wasm_bindgen(typescript_type = "TokenShieldedPaymentObject")] + pub type TokenShieldedPaymentObjectJs; + + #[wasm_bindgen(typescript_type = "TokenShieldedPaymentJSON")] + pub type TokenShieldedPaymentJSONJs; +} + +/// Non-WASM-instance fields extracted from the constructor options via serde. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct TokenShieldedPaymentSimpleFields { + amount: u64, + anchor: Vec, + proof: Vec, + binding_signature: Vec, +} + +/// A spend bundle in a token's shielded pool paying a document action's token cost. +#[derive(Clone, serde::Serialize, serde::Deserialize)] +#[serde(transparent)] +#[wasm_bindgen(js_name = "TokenShieldedPayment")] +pub struct TokenShieldedPaymentWasm(TokenShieldedPayment); + +impl From for TokenShieldedPaymentWasm { + fn from(payment: TokenShieldedPayment) -> Self { + Self(payment) + } +} + +impl From for TokenShieldedPayment { + fn from(payment: TokenShieldedPaymentWasm) -> Self { + payment.0 + } +} + +#[wasm_bindgen(js_class = TokenShieldedPayment)] +impl TokenShieldedPaymentWasm { + #[wasm_bindgen(constructor)] + pub fn constructor( + options: TokenShieldedPaymentOptionsJs, + ) -> WasmDppResult { + let js_opts: &JsValue = options.as_ref(); + let actions = actions_from_js_options(js_opts, "actions")?; + + let fields: TokenShieldedPaymentSimpleFields = + serde_wasm_bindgen::from_value(options.into()) + .map_err(|e| WasmDppError::invalid_argument(e.to_string()))?; + + let anchor: [u8; 32] = try_vec_to_fixed_bytes(fields.anchor, "anchor")?; + let binding_signature: [u8; 64] = + try_vec_to_fixed_bytes(fields.binding_signature, "bindingSignature")?; + + Ok(TokenShieldedPaymentWasm(TokenShieldedPayment { + amount: fields.amount, + actions: actions.into_iter().map(Into::into).collect(), + anchor, + proof: fields.proof, + binding_signature, + })) + } + + /// The tokens the bundle pays (its value balance): the document action's token cost. + #[wasm_bindgen(getter = "amount")] + pub fn amount(&self) -> u64 { + self.0.amount + } + + #[wasm_bindgen(setter = "amount")] + pub fn set_amount(&mut self, amount: &js_sys::BigInt) -> WasmDppResult<()> { + self.0.amount = try_to_u64(amount, "amount")?; + Ok(()) + } + + /// The serialized Orchard actions. + #[wasm_bindgen(getter = "actions")] + pub fn actions(&self) -> Vec { + self.0 + .actions + .iter() + .cloned() + .map(SerializedOrchardActionWasm::from) + .collect() + } + + /// The 32-byte Orchard anchor the bundle was built against. + #[wasm_bindgen(getter = "anchor")] + pub fn anchor(&self) -> Vec { + self.0.anchor.to_vec() + } + + /// The Halo 2 proof bytes. + #[wasm_bindgen(getter = "proof")] + pub fn proof(&self) -> Vec { + self.0.proof.clone() + } + + /// The 64-byte RedPallas binding signature. + #[wasm_bindgen(getter = "bindingSignature")] + pub fn binding_signature(&self) -> Vec { + self.0.binding_signature.to_vec() + } +} + +impl_try_from_js_value!(TokenShieldedPaymentWasm, "TokenShieldedPayment"); +impl_wasm_type_info!(TokenShieldedPaymentWasm, TokenShieldedPayment); +impl_wasm_conversions_inner!( + TokenShieldedPaymentWasm, + TokenShieldedPayment, + TokenShieldedPayment, + TokenShieldedPaymentObjectJs, + TokenShieldedPaymentJSONJs +); diff --git a/packages/wasm-dpp2/src/tokens/configuration/token_configuration.rs b/packages/wasm-dpp2/src/tokens/configuration/token_configuration.rs index 211a3fbbba7..5d97918f694 100644 --- a/packages/wasm-dpp2/src/tokens/configuration/token_configuration.rs +++ b/packages/wasm-dpp2/src/tokens/configuration/token_configuration.rs @@ -65,7 +65,7 @@ export interface TokenConfigurationOptions { mainControlGroupCanBeModified: AuthorizedActionTakers; description?: string; /** - * Give the token its own shielded pool (protocol version 14+). Produces a + * Give the token its own shielded pool (protocol version 15+). Produces a * format-version-1 token configuration; the flag cannot change after creation, and * freezeRules, unfreezeRules and destroyFrozenFundsRules must then authorize no one * (no action takers and no admins): shielded notes cannot be frozen or destroyed. From 6de6ff393b45ef937001c6b45dc1e62092ec9c6f Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 16 Sep 2026 19:02:59 +0700 Subject: [PATCH 06/94] fix(platform): box the shielded token payment and clone the payment info in the SDK TokenPaymentInfo lost Copy when format version 1 gained a bundle: the SDK document builders now clone it, TokenShieldedPayment gets the JSON and Value conversions the wasm wrapper macro expects, and the payment is boxed inside the payment info and the document base action so the enums holding them keep their size (clippy large_enum_variant). Co-Authored-By: Claude Fable 5.1 --- .../validate_structure/v0/mod.rs | 2 +- .../validate_structure/v0/mod.rs | 2 +- .../src/tokens/token_payment_info/mod.rs | 10 ++++---- .../src/tokens/token_payment_info/v1/mod.rs | 13 +++++++--- .../batch/tests/token/shielded/mod.rs | 2 +- packages/rs-drive/src/drive/shielded/paths.rs | 24 +++++++++---------- .../document_base_transition_action/mod.rs | 2 +- .../document_base_transition_action/v0/mod.rs | 2 +- .../v0/transformer.rs | 2 +- .../platform/documents/transitions/create.rs | 2 +- .../platform/documents/transitions/delete.rs | 2 +- .../documents/transitions/purchase.rs | 2 +- .../platform/documents/transitions/replace.rs | 2 +- .../documents/transitions/set_price.rs | 2 +- .../documents/transitions/transfer.rs | 2 +- 15 files changed, 39 insertions(+), 32 deletions(-) diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/validate_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/validate_structure/v0/mod.rs index 524f8748ccd..19b1a0c9ae8 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/validate_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/validate_structure/v0/mod.rs @@ -117,7 +117,7 @@ mod tests { token_id: Identifier::default(), using_group_info: None, }), - amount: amount, + amount, actions: vec![action()], anchor: [9u8; 32], proof: vec![1u8; 10], diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/validate_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/validate_structure/v0/mod.rs index 92ab368d1e2..0d6780f4f3e 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/validate_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/validate_structure/v0/mod.rs @@ -117,7 +117,7 @@ mod tests { token_id: Identifier::default(), using_group_info: None, }), - amount: amount, + amount, actions: vec![action()], anchor: [9u8; 32], proof: vec![1u8; 10], diff --git a/packages/rs-dpp/src/tokens/token_payment_info/mod.rs b/packages/rs-dpp/src/tokens/token_payment_info/mod.rs index 806d774ce56..69e86642a61 100644 --- a/packages/rs-dpp/src/tokens/token_payment_info/mod.rs +++ b/packages/rs-dpp/src/tokens/token_payment_info/mod.rs @@ -133,7 +133,7 @@ impl TokenPaymentInfoAccessorsV1 for TokenPaymentInfo { TokenPaymentInfo::V1(TokenPaymentInfoV1::from_v0(v0, payment)) } (TokenPaymentInfo::V1(v1), Some(payment)) => TokenPaymentInfo::V1(TokenPaymentInfoV1 { - shielded_payment: payment, + shielded_payment: Box::new(payment), ..v1 }), (TokenPaymentInfo::V1(v1), None) => TokenPaymentInfo::V0(v1.into_v0()), @@ -322,7 +322,7 @@ mod json_convertible_tests { minimum_token_cost: None, maximum_token_cost: Some(10), gas_fees_paid_by: GasFeesPaidBy::DocumentOwner, - shielded_payment: TokenShieldedPayment { + shielded_payment: Box::new(TokenShieldedPayment { amount: 10, actions: vec![SerializedAction { nullifier: [1u8; 32], @@ -335,7 +335,7 @@ mod json_convertible_tests { anchor: [7u8; 32], proof: vec![8u8; 10], binding_signature: [9u8; 64], - }, + }), }) } @@ -363,9 +363,9 @@ mod json_convertible_tests { let TokenPaymentInfo::V1(v1) = v1_fixture() else { unreachable!() }; - info.set_shielded_payment(Some(v1.shielded_payment.clone())); + info.set_shielded_payment(Some((*v1.shielded_payment).clone())); assert!(matches!(info, TokenPaymentInfo::V1(_))); - assert_eq!(info.shielded_payment(), Some(&v1.shielded_payment)); + assert_eq!(info.shielded_payment(), Some(&*v1.shielded_payment)); assert_eq!(info.maximum_token_cost(), Some(1_000)); info.set_shielded_payment(None); assert_eq!(info, fixture()); diff --git a/packages/rs-dpp/src/tokens/token_payment_info/v1/mod.rs b/packages/rs-dpp/src/tokens/token_payment_info/v1/mod.rs index a15c4c4873c..1bbcf7f0875 100644 --- a/packages/rs-dpp/src/tokens/token_payment_info/v1/mod.rs +++ b/packages/rs-dpp/src/tokens/token_payment_info/v1/mod.rs @@ -45,6 +45,12 @@ pub struct TokenShieldedPayment { pub binding_signature: [u8; 64], } +#[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] +impl crate::serialization::JsonConvertible for TokenShieldedPayment {} + +#[cfg(all(feature = "value-conversion", feature = "serde-conversion"))] +impl crate::serialization::ValueConvertible for TokenShieldedPayment {} + impl Default for TokenShieldedPayment { fn default() -> Self { Self { @@ -127,8 +133,9 @@ pub struct TokenPaymentInfoV1 { pub maximum_token_cost: Option, /// Who pays the gas fees, this needs to match what the contract allows. pub gas_fees_paid_by: GasFeesPaidBy, - /// The spend bundle paying the token cost from the token's shielded pool. - pub shielded_payment: TokenShieldedPayment, + /// The spend bundle paying the token cost from the token's shielded pool. Boxed so a + /// payment info without one stays small. + pub shielded_payment: Box, } impl fmt::Display for TokenPaymentInfoV1 { @@ -155,7 +162,7 @@ impl TokenPaymentInfoV1 { minimum_token_cost: v0.minimum_token_cost, maximum_token_cost: v0.maximum_token_cost, gas_fees_paid_by: v0.gas_fees_paid_by, - shielded_payment, + shielded_payment: Box::new(shielded_payment), } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 54fdada4701..7149289c49e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -1844,7 +1844,7 @@ mod document_shielded_token_payment_tests { minimum_token_cost: None, maximum_token_cost: Some(CARD_COST), gas_fees_paid_by: GasFeesPaidBy::DocumentOwner, - shielded_payment, + shielded_payment: Box::new(shielded_payment), }) } diff --git a/packages/rs-drive/src/drive/shielded/paths.rs b/packages/rs-drive/src/drive/shielded/paths.rs index 2b09c7cf051..4c1c88d6863 100644 --- a/packages/rs-drive/src/drive/shielded/paths.rs +++ b/packages/rs-drive/src/drive/shielded/paths.rs @@ -377,6 +377,18 @@ pub fn nullifiers_path_for_pool( } } +/// The proof query for a token pool's total balance: the `TOTAL_BALANCE` item of the pool. +pub fn token_shielded_pool_state_path_query(token_id: [u8; 32]) -> PathQuery { + PathQuery { + path: token_shielded_pool_path_vec(token_id), + query: SizedQuery { + query: Query::new_single_key(vec![SHIELDED_TOTAL_BALANCE_KEY]), + limit: Some(1), + offset: None, + }, + } +} + #[cfg(test)] mod tests { use super::*; @@ -503,15 +515,3 @@ mod tests { assert_ne!(pool_path[2], by_height[2]); } } - -/// The proof query for a token pool's total balance: the `TOTAL_BALANCE` item of the pool. -pub fn token_shielded_pool_state_path_query(token_id: [u8; 32]) -> PathQuery { - PathQuery { - path: token_shielded_pool_path_vec(token_id), - query: SizedQuery { - query: Query::new_single_key(vec![SHIELDED_TOTAL_BALANCE_KEY]), - limit: Some(1), - offset: None, - }, - } -} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/mod.rs index b9650b1307e..a0180c585ab 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/mod.rs @@ -120,7 +120,7 @@ impl DocumentBaseTransitionActionAccessorsV0 for DocumentBaseTransitionAction { fn shielded_token_payment(&self) -> Option<&TokenShieldedPayment> { match self { - DocumentBaseTransitionAction::V0(v0) => v0.shielded_token_payment.as_ref(), + DocumentBaseTransitionAction::V0(v0) => v0.shielded_token_payment.as_deref(), } } } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/mod.rs index 7d8ee575b08..5c8d0e9a4ed 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/mod.rs @@ -52,7 +52,7 @@ pub struct DocumentBaseTransitionActionV0 { pub declared_action_fee: Option>, /// The spend bundle paying `token_cost` out of the token's shielded pool instead of the /// owner's token balance (`TokenPaymentInfo::V1`). Only set when there is a token cost. - pub shielded_token_payment: Option, + pub shielded_token_payment: Option>, } /// document base transition action accessors v0 diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/transformer.rs index 2c9edd42c13..449c703dbe8 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/transformer.rs @@ -126,7 +126,7 @@ impl DocumentBaseTransitionActionV0 { ), )); } - shielded_token_payment = Some(payment.clone()); + shielded_token_payment = Some(Box::new(payment.clone())); } } else if let Some(token_payment_info) = value.token_payment_info_ref() { // A bundle with nothing to pay would be verified for free and never applied. diff --git a/packages/rs-sdk/src/platform/documents/transitions/create.rs b/packages/rs-sdk/src/platform/documents/transitions/create.rs index 438f2dd4a63..d1b73a34d33 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/create.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/create.rs @@ -192,7 +192,7 @@ impl DocumentCreateTransitionBuilder { identity_public_key, identity_contract_nonce, self.user_fee_increase.unwrap_or_default(), - self.token_payment_info, + self.token_payment_info.clone(), signer, platform_version, self.state_transition_creation_options, diff --git a/packages/rs-sdk/src/platform/documents/transitions/delete.rs b/packages/rs-sdk/src/platform/documents/transitions/delete.rs index 51f7dcf6d20..6a777bcb5ab 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/delete.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/delete.rs @@ -298,7 +298,7 @@ impl DocumentDeleteTransitionBuilder { identity_public_key, identity_contract_nonce, self.user_fee_increase.unwrap_or_default(), - self.token_payment_info, + self.token_payment_info.clone(), signer, platform_version, self.state_transition_creation_options, diff --git a/packages/rs-sdk/src/platform/documents/transitions/purchase.rs b/packages/rs-sdk/src/platform/documents/transitions/purchase.rs index 4e04b4d33cb..3c5278ba85d 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/purchase.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/purchase.rs @@ -248,7 +248,7 @@ impl DocumentPurchaseTransitionBuilder { identity_public_key, identity_contract_nonce, self.user_fee_increase.unwrap_or_default(), - self.token_payment_info, + self.token_payment_info.clone(), signer, platform_version, self.state_transition_creation_options, diff --git a/packages/rs-sdk/src/platform/documents/transitions/replace.rs b/packages/rs-sdk/src/platform/documents/transitions/replace.rs index a768ef1260c..ffc633b6e21 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/replace.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/replace.rs @@ -186,7 +186,7 @@ impl DocumentReplaceTransitionBuilder { identity_public_key, identity_contract_nonce, self.user_fee_increase.unwrap_or_default(), - self.token_payment_info, + self.token_payment_info.clone(), signer, platform_version, self.state_transition_creation_options, diff --git a/packages/rs-sdk/src/platform/documents/transitions/set_price.rs b/packages/rs-sdk/src/platform/documents/transitions/set_price.rs index eb95fd6f023..00223d73050 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/set_price.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/set_price.rs @@ -235,7 +235,7 @@ impl DocumentSetPriceTransitionBuilder { identity_public_key, identity_contract_nonce, self.user_fee_increase.unwrap_or_default(), - self.token_payment_info, + self.token_payment_info.clone(), signer, platform_version, self.state_transition_creation_options, diff --git a/packages/rs-sdk/src/platform/documents/transitions/transfer.rs b/packages/rs-sdk/src/platform/documents/transitions/transfer.rs index d7c51cf96be..e528b027001 100644 --- a/packages/rs-sdk/src/platform/documents/transitions/transfer.rs +++ b/packages/rs-sdk/src/platform/documents/transitions/transfer.rs @@ -234,7 +234,7 @@ impl DocumentTransferTransitionBuilder { identity_public_key, identity_contract_nonce, self.user_fee_increase.unwrap_or_default(), - self.token_payment_info, + self.token_payment_info.clone(), signer, platform_version, self.state_transition_creation_options, From 9f636ba1034182b7ab7c49f4a7e1c348700a9b9b Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 16 Sep 2026 19:28:20 +0700 Subject: [PATCH 07/94] feat(platform)!: identity-less token pool transitions paid from the credit shielded pool Three top-level state transitions move tokens through a token's shielded pool with no identity anywhere: each carries an Orchard bundle in the token pool and a second spend bundle in the credit shielded pool that pays the fee, both authorized only by spend keys. TokenShieldedTransferWithShieldedFee (23) transfers inside the token pool (value balance zero), TokenUnshieldWithShieldedFee (24) moves an amount into an identity's token balance, and TokenPurchaseFromShieldedPool (25) buys tokens at the direct purchase price out of the credit pool and mints them into the token pool, crediting the contract owner. The token bundle's sighash binds the state transition type, the token id and the transparent fields; the fee bundle's sighash binds the type, the token id and a digest of the token bundle's actions, so the two cannot be re-paired. The minimum-fee validation pins the fee bundle's value balance to the two-bundle fee (compute_token_pool_paid_shielded_fee), execution is a pool-paid event, uniqueness is by the nullifiers of both bundles, and the transitions are gated on the token shielded pool protocol version. The wiring mirrors IdentityTopUpFromShieldedPool across dpp, drive, drive-abci and the wasm bindings; dpp gains builders for the three transitions, and the book documents the batch transitions, documents paid from the pool and the identity-less transitions. Co-Authored-By: Claude Fable 5.1 --- book/src/data-model/token-shielded-pools.md | 128 +++- book/src/fees/shielded-fees.md | 20 + packages/rs-dpp/src/shielded/builder/mod.rs | 6 + .../src/shielded/builder/token_pool_paid.rs | 547 ++++++++++++++++++ .../compute_minimum_shielded_fee/mod.rs | 72 +++ .../compute_minimum_shielded_fee/v0/mod.rs | 31 + packages/rs-dpp/src/shielded/mod.rs | 21 +- packages/rs-dpp/src/shielded/sighash.rs | 176 ++++++ packages/rs-dpp/src/state_transition/mod.rs | 130 +++++ .../state_transition_types.rs | 24 + .../state_transitions/shielded/mod.rs | 3 + .../accessors/mod.rs | 84 +++ .../accessors/v0/mod.rs | 53 ++ .../methods/mod.rs | 75 +++ .../methods/v0/mod.rs | 45 ++ .../mod.rs | 145 +++++ ...ate_transition_estimated_fee_validation.rs | 16 + .../state_transition_like.rs | 36 ++ .../state_transition_validation.rs | 17 + .../v0/mod.rs | 75 +++ .../v0/state_transition_like.rs | 39 ++ .../v0/state_transition_validation.rs | 219 +++++++ .../v0/types.rs | 16 + .../v0/v0_methods.rs | 58 ++ .../v0/version.rs | 9 + .../version.rs | 11 + .../accessors/mod.rs | 73 +++ .../accessors/v0/mod.rs | 48 ++ .../methods/mod.rs | 72 +++ .../methods/v0/mod.rs | 41 ++ .../mod.rs | 146 +++++ ...ate_transition_estimated_fee_validation.rs | 16 + .../state_transition_like.rs | 36 ++ .../state_transition_validation.rs | 17 + .../v0/mod.rs | 70 +++ .../v0/state_transition_like.rs | 39 ++ .../v0/state_transition_validation.rs | 183 ++++++ .../v0/types.rs | 16 + .../v0/v0_methods.rs | 52 ++ .../v0/version.rs | 9 + .../version.rs | 11 + .../accessors/mod.rs | 82 +++ .../accessors/v0/mod.rs | 53 ++ .../methods/mod.rs | 75 +++ .../methods/v0/mod.rs | 45 ++ .../mod.rs | 144 +++++ ...ate_transition_estimated_fee_validation.rs | 16 + .../state_transition_like.rs | 36 ++ .../state_transition_validation.rs | 17 + .../v0/mod.rs | 75 +++ .../v0/state_transition_like.rs | 39 ++ .../v0/state_transition_validation.rs | 209 +++++++ .../v0/types.rs | 16 + .../v0/v0_methods.rs | 56 ++ .../v0/version.rs | 9 + .../version.rs | 11 + packages/rs-drive-abci/Cargo.toml | 1 + .../process_raw_state_transitions/v0/mod.rs | 11 +- .../execution/types/execution_event/mod.rs | 36 ++ .../traits/address_balances_and_nonces.rs | 6 + .../processor/traits/address_witnesses.rs | 6 + .../traits/addresses_minimum_balance.rs | 6 + .../processor/traits/basic_structure.rs | 90 +++ .../processor/traits/identity_balance.rs | 3 + .../traits/identity_based_signature.rs | 9 + .../processor/traits/identity_nonces.rs | 6 + .../processor/traits/is_allowed.rs | 20 + .../processor/traits/shielded_proof.rs | 402 ++++++++++++- .../processor/traits/state.rs | 18 + .../batch/tests/token/shielded/mod.rs | 494 ++++++++++++++++ .../state_transition/state_transitions/mod.rs | 8 + .../token_pool_paid_common/mod.rs | 208 +++++++ .../token_purchase_from_shielded_pool/mod.rs | 50 ++ .../transform_into_action/mod.rs | 1 + .../transform_into_action/v0/mod.rs | 115 ++++ .../mod.rs | 55 ++ .../transform_into_action/mod.rs | 1 + .../transform_into_action/v0/mod.rs | 80 +++ .../token_unshield_with_shielded_fee/mod.rs | 50 ++ .../transform_into_action/mod.rs | 1 + .../transform_into_action/v0/mod.rs | 120 ++++ .../state_transition/transformer/mod.rs | 15 + .../verify_state_transitions.rs | 5 +- .../prove/prove_state_transition/v0/mod.rs | 25 + .../action_convert_to_operations/mod.rs | 9 + .../shielded/mod.rs | 3 + ..._purchase_from_shielded_pool_transition.rs | 79 +++ ...d_transfer_with_shielded_fee_transition.rs | 61 ++ ...n_unshield_with_shielded_fee_transition.rs | 65 +++ .../mod.rs | 1 + .../v0/mod.rs | 1 + .../v0/transformer.rs | 32 +- .../src/state_transition_action/mod.rs | 16 + .../state_transition_action/shielded/mod.rs | 6 + .../token_purchase_from_shielded_pool/mod.rs | 105 ++++ .../transformer.rs | 30 + .../v0/mod.rs | 33 ++ .../v0/transformer.rs | 41 ++ .../mod.rs | 78 +++ .../transformer.rs | 26 + .../v0/mod.rs | 24 + .../v0/transformer.rs | 37 ++ .../token_unshield_with_shielded_fee/mod.rs | 91 +++ .../transformer.rs | 26 + .../v0/mod.rs | 29 + .../v0/transformer.rs | 37 ++ .../v0/mod.rs | 65 +++ .../mod.rs | 5 + .../v1.rs | 15 + .../v2.rs | 15 + .../v3.rs | 15 + .../drive_abci_validation_versions/mod.rs | 6 + .../drive_abci_validation_versions/v1.rs | 27 + .../drive_abci_validation_versions/v10.rs | 27 + .../drive_abci_validation_versions/v11.rs | 27 + .../drive_abci_validation_versions/v2.rs | 27 + .../drive_abci_validation_versions/v3.rs | 27 + .../drive_abci_validation_versions/v4.rs | 27 + .../drive_abci_validation_versions/v5.rs | 27 + .../drive_abci_validation_versions/v6.rs | 27 + .../drive_abci_validation_versions/v7.rs | 27 + .../drive_abci_validation_versions/v8.rs | 27 + .../drive_abci_validation_versions/v9.rs | 27 + .../mod.rs | 3 + .../v1.rs | 3 + .../v2.rs | 3 + .../v3.rs | 3 + .../v4.rs | 3 + .../state_transition_factory.rs | 3 + packages/wasm-dpp2/src/lib.rs | 5 +- packages/wasm-dpp2/src/shielded/mod.rs | 6 + ..._purchase_from_shielded_pool_transition.rs | 331 +++++++++++ ...d_transfer_with_shielded_fee_transition.rs | 319 ++++++++++ ...n_unshield_with_shielded_fee_transition.rs | 327 +++++++++++ .../base/state_transition.rs | 18 + 135 files changed, 7841 insertions(+), 41 deletions(-) create mode 100644 packages/rs-dpp/src/shielded/builder/token_pool_paid.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/accessors/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/accessors/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/methods/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/methods/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/state_transition_estimated_fee_validation.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/state_transition_like.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/state_transition_validation.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/state_transition_like.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/state_transition_validation.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/types.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/v0_methods.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/version.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/version.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/accessors/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/accessors/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/methods/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/methods/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/state_transition_estimated_fee_validation.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/state_transition_like.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/state_transition_validation.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/state_transition_like.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/state_transition_validation.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/types.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/v0_methods.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/version.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/version.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/accessors/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/accessors/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/methods/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/methods/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/state_transition_estimated_fee_validation.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/state_transition_like.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/state_transition_validation.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/state_transition_like.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/state_transition_validation.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/types.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/v0_methods.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/version.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/version.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_pool_paid_common/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_shielded_transfer_with_shielded_fee/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_shielded_transfer_with_shielded_fee/transform_into_action/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_shielded_transfer_with_shielded_fee/transform_into_action/v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_unshield_with_shielded_fee/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_unshield_with_shielded_fee/transform_into_action/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_unshield_with_shielded_fee/transform_into_action/v0/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_purchase_from_shielded_pool_transition.rs create mode 100644 packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_shielded_transfer_with_shielded_fee_transition.rs create mode 100644 packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_unshield_with_shielded_fee_transition.rs create mode 100644 packages/rs-drive/src/state_transition_action/shielded/token_purchase_from_shielded_pool/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/shielded/token_purchase_from_shielded_pool/transformer.rs create mode 100644 packages/rs-drive/src/state_transition_action/shielded/token_purchase_from_shielded_pool/v0/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/shielded/token_purchase_from_shielded_pool/v0/transformer.rs create mode 100644 packages/rs-drive/src/state_transition_action/shielded/token_shielded_transfer_with_shielded_fee/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/shielded/token_shielded_transfer_with_shielded_fee/transformer.rs create mode 100644 packages/rs-drive/src/state_transition_action/shielded/token_shielded_transfer_with_shielded_fee/v0/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/shielded/token_shielded_transfer_with_shielded_fee/v0/transformer.rs create mode 100644 packages/rs-drive/src/state_transition_action/shielded/token_unshield_with_shielded_fee/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/shielded/token_unshield_with_shielded_fee/transformer.rs create mode 100644 packages/rs-drive/src/state_transition_action/shielded/token_unshield_with_shielded_fee/v0/mod.rs create mode 100644 packages/rs-drive/src/state_transition_action/shielded/token_unshield_with_shielded_fee/v0/transformer.rs create mode 100644 packages/wasm-dpp2/src/shielded/token_purchase_from_shielded_pool_transition.rs create mode 100644 packages/wasm-dpp2/src/shielded/token_shielded_transfer_with_shielded_fee_transition.rs create mode 100644 packages/wasm-dpp2/src/shielded/token_unshield_with_shielded_fee_transition.rs diff --git a/book/src/data-model/token-shielded-pools.md b/book/src/data-model/token-shielded-pools.md index 5346ab4f77e..ae9c586324b 100644 --- a/book/src/data-model/token-shielded-pools.md +++ b/book/src/data-model/token-shielded-pools.md @@ -1,10 +1,12 @@ # Token Shielded Pools From protocol version 15 a token can own a shielded pool: an Orchard pool that holds that token -instead of credits. Holders move tokens between their identity balance and the pool with three -token transitions inside a batch, and transfer inside the pool without revealing amounts or -counterparties. This chapter describes the storage, the configuration flag, the transitions, the -validation rules, the block end bookkeeping, the queries and the client builders. +instead of credits. Holders move tokens between their identity balance and the pool with token +transitions inside a batch, issuers mint, burn, release and sell straight into or out of it, +document costs can be paid out of it, and three identity-less transitions move tokens with the +fee paid from the credit shielded pool, so no identity appears at all. This chapter describes the +storage, the configuration flag, the transitions, the validation rules, the block end +bookkeeping, the queries and the client builders. ## Why one pool per token @@ -65,17 +67,21 @@ Pausing still works: shield, unshield and shielded transfer are all rejected whi paused. The frozen-account checks in the shield and unshield validators remain as defence in depth for state written before this rule. -## Transitions +## Batch transitions -The three operations are `TokenTransition` variants inside a `Batch` transition, like every -other token operation. The identity signs the batch and pays the fee in credits. Tokens cannot -pay fees, so unlike the credit pool nothing is carved from the bundle's value balance. +Seven operations are `TokenTransition` variants inside a `Batch` transition, like every other +token operation. The identity signs the batch and pays the fee in credits. Tokens cannot pay +fees, so unlike the credit pool nothing is carved from the bundle's value balance. | Transition | Flags | Value balance | Extra sighash data | Effect | |---|---|---|---|---| | `TokenShield` | outputs only | `-amount` | none | `amount` leaves the owner's balance and enters the pool as new notes. | | `TokenUnshield` | spends and outputs | `+amount` | `token_id, owner_id, recipient_id, amount` | Notes are spent; `amount` is credited to `recipient_id`; change comes back as new notes. | | `TokenShieldedTransfer` | spends and outputs | `0` | `token_id, owner_id` | Notes are spent and recreated; the pool balance is unchanged. | +| `TokenMintToPool` | outputs only | `-amount` | none | An authorized minter (manual minting rules, group actions supported) mints `amount` into new notes; the supply and the pool balance grow. | +| `TokenBurnFromPool` | spends and outputs | `+amount` | `token_id, owner_id, amount` | An authorized burner (manual burning rules) spends notes and destroys `amount`; the supply and the pool balance shrink. | +| `TokenClaimToPool` | outputs only | `-amount` | none | A distribution claim released into new notes instead of the claimant's balance; a perpetual claim names the cycle-aligned moment it claims up to so the amount is predictable. | +| `TokenDirectPurchaseToPool` | outputs only | `-token_count` | none | The buyer pays credits at the direct purchase price and the tokens are minted into new notes. | Each transition carries the Orchard bundle (`actions`, `anchor`, `proof`, `binding_signature`) next to the token base transition (`token_id`, contract id, contract position, identity @@ -85,8 +91,71 @@ owner, recipient or amount cannot be replayed with another. The layouts are in `dpp::shielded::sighash` (`token_unshield_extra_sighash_data`, `token_shielded_transfer_extra_sighash_data`). -The shield bundle has no spends, so its anchor is not checked against the pool; the client -builds it against the empty tree. Spending bundles must name an anchor the pool has recorded. +Outputs-only bundles (shield, mint, claim, purchase) have no spends, so their anchor is not +checked against the pool; the client builds them against the empty tree. Spending bundles must +name an anchor the pool has recorded. + +A mint or burn into the pool that goes through a group action stores +`TokenEvent::MintToPool` / `TokenEvent::BurnFromPool` with a digest of the serialized actions +(`serialized_actions_digest`), so every signer commits to exactly the same notes. No token +history document is written for pool operations: shielded activity is not recorded publicly. + +## Documents paid from the pool + +A document type whose action has a token cost can be paid out of the token's pool. +`TokenPaymentInfo` gains a format version 1 that carries a `TokenShieldedPayment`: a spend +bundle in the payment token's pool (`amount`, `actions`, `anchor`, `proof`, +`binding_signature`) whose value balance is the document action's token cost. The identity still +signs the batch and pays the credit fee; its token balance is never touched. The bundle's sighash +binds the token id, the batch owner, the document's contract and id and the amount +(`document_token_payment_extra_sighash_data`), so it cannot pay for another document. + +The transformer rejects a payment whose amount is not the document type's cost +(`TokenShieldedPaymentAmountMismatchError`, 40723) and a shielded payment on an action with no +token cost (`TokenShieldedPaymentNotRequiredError`, 40724). Document base state validation +version 1 skips the owner's balance and frozen-account checks for a shielded payment; the pool +side (pool exists, token not paused, anchor, unspent nullifiers, pool balance, proof) is +validated once the document action itself is valid. The lowering pays the cost from the pool: a +`TransferTokenToContractOwner` effect is a token unshield into the contract owner's balance, a +`BurnToken` effect a burn from the pool. The verification fee is charged like the batch pool +transitions, and CheckTx admits the bundle under the identity contract nonce. + +## Identity-less transitions + +Every batch transition above is signed by an identity that pays credits, so the chain sees +which identity moved token `T` at height `H`. Three top-level state transitions remove the +identity: each carries a bundle in the token's pool and a second spend bundle in the credit +shielded pool that pays the fee, both authorized only by Orchard spend keys. + +| Type | Transition | Token bundle | Fee bundle | Effect | +|---|---|---|---|---| +| 23 | `TokenShieldedTransferWithShieldedFee` | spends, value `0` | spends, value = fee | Notes spent and recreated in the token pool; the fee leaves the credit pool. | +| 24 | `TokenUnshieldWithShieldedFee` | spends, value `+amount` | spends, value = fee | `amount` leaves the token pool into `recipient_id`'s token balance. | +| 25 | `TokenPurchaseFromShieldedPool` | outputs only, value `-token_count` | spends, value = price + fee | `token_count` is minted into the token pool at the direct purchase price; the price is credited to the contract owner. | + +Every transition names the contract, the token position and the token id (which must derive +from the two), the two bundles and `credit_amount`, the fee bundle's value balance. The token +bundle's sighash binds the state transition type byte, the token id and the transparent fields +(recipient and amount, or count and price); the fee bundle's sighash binds the type byte, the +token id and a digest of the token bundle's actions (`token_pool_fee_bundle_extra_sighash_data`), +so a fee bundle can only ever pay for that exact token bundle. + +The processor treats them like the credit pool's own pool-paid transitions: structure +validation checks both bundles, the minimum fee validation pins `credit_amount` to exactly the +two-bundle fee (`compute_token_pool_paid_shielded_fee`: the base fee of each bundle plus the +flat storage of what the transition writes outside the pools; a purchase adds the agreed price +on top), both proofs are verified statelessly, and the transform validates the pools: the +token owns a pool and is not paused, the token bundle's anchor is recorded and its nullifiers +unspent in the token pool, the credit pool holds what leaves it and the fee bundle's anchor and +nullifiers check out there, plus the transition's own rules (recipient exists and is not frozen +for an unshield, the pricing schedule and the max supply for a purchase). Execution is a +`PaidFromShieldedPool` event: the fee goes to the fee pools, the token side is the matching +token operation, and a purchase credits the contract owner. Uniqueness is by the spent +nullifiers of both bundles; a replay is an unpaid rejection. CheckTx admits their proofs under +the generic pool-paid limiter. All three are gated on `TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION`. + +A transfer proves its execution with the token pool nullifiers it spent, an unshield with the +recipient's token balance, and a purchase with the token pool's total balance. ## Validation @@ -101,7 +170,11 @@ State validation runs in this order, and the first failure is returned: 3. Shield: the owner holds `amount`, the owner's account is not frozen, the token is not paused. Unshield: the token is not paused, the recipient identity exists, the recipient's account is not frozen unless the token allows transfers to frozen balances. Shielded - transfer: the token is not paused. + transfer: the token is not paused. Mint to pool: the minting rules authorize the identity + (or group) and the max supply is not exceeded. Burn from pool: the burning rules authorize + the identity and the token is not paused. Claim to pool: the claim resolves exactly as a + claim into a balance does (the shared `resolve_token_claim`). Purchase to pool: the pricing + schedule and the max supply. 4. Spending bundles: the anchor is recorded in the pool (`InvalidAnchorError`), no nullifier repeats within the bundle or is already spent (`NullifierAlreadySpentError`), and for an unshield the pool holds `amount`. @@ -126,15 +199,20 @@ The drive operations are composites of the credit pool primitives re-rooted unde the pool's `TOTAL_BALANCE`; - unshield: insert the nullifiers, append the notes, subtract `amount` from the pool balance, add `amount` to the recipient's token balance; -- shielded transfer: insert the nullifiers, append the notes. +- shielded transfer: insert the nullifiers, append the notes; +- mint, claim and purchase to pool: append the notes, add `amount` to the pool balance and to + the total supply (`TokenMintToPool`); +- burn from pool: insert the nullifiers, append the change notes, subtract `amount` from the + pool balance and from the total supply (`TokenBurnFromPool`). -The total supply of a token never changes. `calculate_total_tokens_balance` version 1 reads +Only a mint, claim, purchase or burn changes a token's total supply. `calculate_total_tokens_balance` version 1 reads the token pools BigSumTree and the block end conservation check requires `identity balances + pool balances == total supply`. ## Block end -Every successful or paid token pool transition records its pool in +Every successful or paid token pool transition, document paid from a pool and identity-less +token pool transition records its pool in `StateTransitionsProcessingResult::token_shielded_pools_touched`. At block end `record_token_shielded_pool_anchors` (enabled by `DRIVE_ABCI_METHOD_VERSIONS_V10`) records each touched pool's current anchor if the commitment tree changed and prunes that pool's @@ -160,15 +238,23 @@ proves the spent nullifiers in the token's pool. ## Client builders `dpp::shielded::builder` provides `build_token_shield_transition`, -`build_token_unshield_transition` and `build_token_shielded_transfer_transition`. They prove -the bundle, bind the extra sighash data, and call the batch constructors -(`new_token_shield_transition` and siblings) which sign with the identity key. The wasm -bindings expose `TokenShieldTransition`, `TokenUnshieldTransition` and -`TokenShieldedTransferTransition`, and `TokenConfiguration` accepts `hasShieldedPool` and -reports `formatVersion`. +`build_token_unshield_transition`, `build_token_shielded_transfer_transition`, +`build_token_mint_to_pool_transition`, `build_token_burn_from_pool_transition`, +`build_token_claim_to_pool_transition` and `build_token_direct_purchase_to_pool_transition`. +They prove the bundle, bind the extra sighash data, and call the batch constructors +(`new_token_shield_transition` and siblings) which sign with the identity key. +`build_document_shielded_token_payment` builds the bundle of a `TokenPaymentInfo::V1`, and +`build_token_shielded_transfer_with_shielded_fee_transition`, +`build_token_unshield_with_shielded_fee_transition` and +`build_token_purchase_from_shielded_pool_transition` build the identity-less transitions from a +`TokenPoolSpender` (the token pool notes and keys) and a `ShieldedFeePayer` (the credit pool +notes and keys). The wasm bindings expose a wrapper per transition, `TokenPaymentInfo` accepts +a `shieldedPayment`, and `TokenConfiguration` accepts `hasShieldedPool` and reports +`formatVersion`. ## Fees See [Shielded Transaction Fees](../fees/shielded-fees.md#token-shielded-pool-fees). In short: the identity pays the metered cost of the writes plus the proof verification fee, exactly like -`ShieldFromIdentity`, for all three transitions. +`ShieldFromIdentity`, for every batch transition and for a document paid from the pool; the +identity-less transitions carve the two-bundle fee from the credit pool bundle. diff --git a/book/src/fees/shielded-fees.md b/book/src/fees/shielded-fees.md index 551814f571b..c5096c6a740 100644 --- a/book/src/fees/shielded-fees.md +++ b/book/src/fees/shielded-fees.md @@ -58,10 +58,30 @@ signing identity pays in credits through the standard identity-paid path. | **TokenShield** | `fee = metered(storage + processing) + shielded_verification_fee`, paid by the signing identity | Same model as `ShieldFromIdentity`: the note appends, the identity token balance write and the pool balance write are metered, and `compute_shielded_verification_fee(num_actions)` is added as a precalculated operation before the proof is verified. `value_balance` is `-amount` in tokens and carries no fee. | | **TokenUnshield** | `fee = metered(storage + processing) + shielded_verification_fee`, paid by the signing identity | `value_balance` equals the unshielded token amount exactly; the recipient receives the full amount. Nullifier inserts, note appends and the two balance writes are metered. | | **TokenShieldedTransfer** | `fee = metered(storage + processing) + shielded_verification_fee`, paid by the signing identity | `value_balance` is exactly zero; consensus rejects any other value. Only the nullifier inserts and note appends are metered. | +| **TokenMintToPool**, **TokenClaimToPool**, **TokenDirectPurchaseToPool** | same model | Outputs-only bundles; the note appends and the supply and pool balance writes are metered, the verification fee is charged by the action transformer so CheckTx and block execution price the bundle identically. | +| **TokenBurnFromPool** | same model | Nullifier inserts, change note appends and the supply and pool balance writes are metered. | +| Document with a **TokenPaymentInfo::V1** | same model, on top of the document's own fee | The shielded payment bundle's verification fee is added by the batch transformer before anything can fail; the pool writes it causes are metered. | Because the verification fee is charged before the proof is checked, an invalid proof is a paid failure: the identity is charged, its identity contract nonce advances, and no token moves. +### Identity-less token pool transitions + +`TokenShieldedTransferWithShieldedFee` (23), `TokenUnshieldWithShieldedFee` (24) and +`TokenPurchaseFromShieldedPool` (25) have no identity: the fee is carved from a second bundle +spent in the credit shielded pool, exactly as `ShieldedTransfer` carves its own fee. + +| Transition | Fee Formula | Explanation | +|---|---|---| +| **TokenShieldedTransferWithShieldedFee** | `credit_amount == base(fee_actions) + base(token_actions)` | Two bundles are verified and stored, so the fee is the base shielded fee of each (`compute_token_pool_paid_shielded_fee`). Pure fee: overpayment is rejected. | +| **TokenUnshieldWithShieldedFee** | `credit_amount == base(fee_actions) + base(token_actions) + identity balance bytes` | Adds the flat storage of the recipient's token balance item. Pure fee. | +| **TokenPurchaseFromShieldedPool** | `credit_amount == total_agreed_price + base(fee_actions) + base(token_actions) + balance write + supply bytes` | The agreed price rides on top of the fee and is credited to the contract owner; `credit_amount - total_agreed_price` must equal the fee exactly. | + +The fee bundle's value balance is `credit_amount`; the minimum-fee validation, the SDK builders +and the transformer all use the same `compute_token_*` function so the threshold never drifts +from what is carved. An invalid proof or a spent nullifier is an unpaid rejection: nothing is +committed and no fee is charged, the same as for the credit pool's pool-paid transitions. + For `ShieldedTransfer`, the client constructs the bundle so that `total_spent − total_output = desired_fee`. The Orchard circuit proves that value is conserved (inputs = outputs + value_balance), and the binding signature cryptographically diff --git a/packages/rs-dpp/src/shielded/builder/mod.rs b/packages/rs-dpp/src/shielded/builder/mod.rs index 2dbce0f8d2f..dce3bc432fe 100644 --- a/packages/rs-dpp/src/shielded/builder/mod.rs +++ b/packages/rs-dpp/src/shielded/builder/mod.rs @@ -41,6 +41,7 @@ mod token_burn_from_pool; mod token_claim_to_pool; mod token_direct_purchase_to_pool; mod token_mint_to_pool; +mod token_pool_paid; mod token_shield; mod token_shielded_transfer; mod token_unshield; @@ -62,6 +63,11 @@ pub use token_burn_from_pool::build_token_burn_from_pool_transition; pub use token_claim_to_pool::build_token_claim_to_pool_transition; pub use token_direct_purchase_to_pool::build_token_direct_purchase_to_pool_transition; pub use token_mint_to_pool::build_token_mint_to_pool_transition; +pub use token_pool_paid::{ + build_token_purchase_from_shielded_pool_transition, + build_token_shielded_transfer_with_shielded_fee_transition, + build_token_unshield_with_shielded_fee_transition, ShieldedFeePayer, TokenPoolSpender, +}; pub use token_shield::build_token_shield_transition; pub use token_shielded_transfer::build_token_shielded_transfer_transition; pub use token_unshield::build_token_unshield_transition; diff --git a/packages/rs-dpp/src/shielded/builder/token_pool_paid.rs b/packages/rs-dpp/src/shielded/builder/token_pool_paid.rs new file mode 100644 index 00000000000..3909ae82a0c --- /dev/null +++ b/packages/rs-dpp/src/shielded/builder/token_pool_paid.rs @@ -0,0 +1,547 @@ +use grovedb_commitment_tree::{ + Anchor, Builder, BundleType, DashMemo, FullViewingKey, NoteValue, PaymentAddress, Scope, + SpendAuthorizingKey, +}; + +use crate::address_funds::OrchardAddress; +use crate::balances::credits::TokenAmount; +use crate::data_contract::TokenContractPosition; +use crate::fee::Credits; +use crate::shielded::{ + compute_token_purchase_from_shielded_pool_fee, + compute_token_shielded_transfer_with_shielded_fee_fee, + compute_token_unshield_with_shielded_fee_fee, token_pool_fee_bundle_extra_sighash_data, + token_purchase_from_shielded_pool_extra_sighash_data, + token_shielded_transfer_with_shielded_fee_extra_sighash_data, + token_unshield_with_shielded_fee_extra_sighash_data, SerializedAction, + TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE, + TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE, +}; +use crate::state_transition::token_purchase_from_shielded_pool_transition::methods::TokenPurchaseFromShieldedPoolTransitionMethodsV0; +use crate::state_transition::token_purchase_from_shielded_pool_transition::TokenPurchaseFromShieldedPoolTransition; +use crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::methods::TokenShieldedTransferWithShieldedFeeTransitionMethodsV0; +use crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::TokenShieldedTransferWithShieldedFeeTransition; +use crate::state_transition::token_unshield_with_shielded_fee_transition::methods::TokenUnshieldWithShieldedFeeTransitionMethodsV0; +use crate::state_transition::token_unshield_with_shielded_fee_transition::TokenUnshieldWithShieldedFeeTransition; +use crate::state_transition::StateTransition; +use crate::ProtocolError; +use platform_value::Identifier; +use platform_version::version::PlatformVersion; + +use super::{ + build_output_only_bundle, build_spend_bundle, prove_and_sign_bundle, + serialize_authorized_bundle, OrchardProver, SerializedBundle, SpendableNote, +}; + +/// The credit pool side of an identity-less token pool transition: the wallet's credit pool +/// notes to spend, where the change goes, and the keys that authorize the spend. +pub struct ShieldedFeePayer<'a> { + /// Credit pool notes to spend, with their Merkle paths. + pub spends: Vec, + /// Orchard address for the change note in the credit pool. + pub change_address: &'a OrchardAddress, + /// Full viewing key of the credit pool notes. + pub fvk: &'a FullViewingKey, + /// Spend authorizing key of the credit pool notes. + pub ask: &'a SpendAuthorizingKey, + /// Sinsemilla root of the credit pool's note commitment tree. + pub anchor: Anchor, +} + +/// The wallet's notes in a token's shielded pool and the keys that spend them. +pub struct TokenPoolSpender<'a> { + /// Token pool notes to spend, with their Merkle paths. + pub spends: Vec, + /// Orchard address for the change note in the token pool. + pub change_address: &'a OrchardAddress, + /// Full viewing key of the token pool notes. + pub fvk: &'a FullViewingKey, + /// Spend authorizing key of the token pool notes. + pub ask: &'a SpendAuthorizingKey, + /// Sinsemilla root of the token pool's note commitment tree. + pub anchor: Anchor, +} + +fn total_value(spends: &[SpendableNote]) -> Result { + spends + .iter() + .try_fold(0u64, |total, spend| { + total.checked_add(spend.note.value().inner()) + }) + .ok_or_else(|| { + ProtocolError::ShieldedBuildError("total spendable value overflows u64".to_string()) + }) +} + +/// Builds the credit pool fee bundle: spends `payer.spends`, carves `credits_leaving` (the fee, +/// plus the price for a purchase) and returns the remainder as change, bound to the token +/// bundle through `token_actions`. +#[allow(clippy::too_many_arguments)] +fn build_fee_bundle( + state_transition_type: u8, + token_id: &Identifier, + token_actions: &[SerializedAction], + payer: ShieldedFeePayer<'_>, + credits_leaving: Credits, + expected_actions: usize, + prover: &P, + platform_version: &PlatformVersion, +) -> Result { + let total_spent = total_value(&payer.spends)?; + if credits_leaving > total_spent { + return Err(ProtocolError::ShieldedBuildError(format!( + "credits leaving the pool {} exceed the total spendable value {}", + credits_leaving, total_spent + ))); + } + let change_amount = total_spent - credits_leaving; + let extra_sighash_data = token_pool_fee_bundle_extra_sighash_data( + state_transition_type, + &token_id.to_buffer(), + token_actions, + platform_version, + )?; + let bundle = build_spend_bundle( + payer.spends, + payer.change_address, + change_amount, + [0u8; 36], + payer.fvk, + payer.ask, + payer.anchor, + prover, + &extra_sighash_data, + )?; + let sb = serialize_authorized_bundle(&bundle); + if sb.value_balance != credits_leaving as i64 { + return Err(ProtocolError::ShieldedBuildError(format!( + "fee bundle value balance {} does not equal the credits leaving the pool {}", + sb.value_balance, credits_leaving + ))); + } + if sb.actions.len() != expected_actions { + return Err(ProtocolError::ShieldedBuildError(format!( + "fee bundle has {} actions, the fee was computed for {}", + sb.actions.len(), + expected_actions + ))); + } + Ok(sb) +} + +/// Builds a `TokenShieldedTransferWithShieldedFee`: pays `transfer_amount` of the token to +/// `recipient` inside the token's shielded pool (value balance zero) and the fee out of the +/// credit shielded pool. No identity is involved. Returns the transition and the fee paid. +#[allow(clippy::too_many_arguments)] +pub fn build_token_shielded_transfer_with_shielded_fee_transition( + token_id: Identifier, + data_contract_id: Identifier, + token_contract_position: TokenContractPosition, + spender: TokenPoolSpender<'_>, + recipient: &OrchardAddress, + transfer_amount: TokenAmount, + memo: [u8; 36], + fee_payer: ShieldedFeePayer<'_>, + prover: &P, + platform_version: &PlatformVersion, +) -> Result<(StateTransition, Credits), ProtocolError> { + if transfer_amount == 0 { + return Err(ProtocolError::ShieldedBuildError( + "token shielded transfer amount must be greater than zero".to_string(), + )); + } + let total_spent = total_value(&spender.spends)?; + if transfer_amount > total_spent { + return Err(ProtocolError::ShieldedBuildError(format!( + "token shielded transfer amount {} exceeds total spendable value {}", + transfer_amount, total_spent + ))); + } + let change_amount = total_spent - transfer_amount; + let mut builder = Builder::::new(BundleType::DEFAULT, spender.anchor); + for spend in spender.spends { + builder + .add_spend(spender.fvk.clone(), spend.note, spend.merkle_path) + .map_err(|e| { + ProtocolError::ShieldedBuildError(format!("failed to add spend: {:?}", e)) + })?; + } + let sender_ovk = spender.fvk.to_ovk(Scope::External); + builder + .add_output( + Some(sender_ovk.clone()), + PaymentAddress::from(recipient), + NoteValue::from_raw(transfer_amount), + memo, + ) + .map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to add output: {:?}", e)))?; + if change_amount > 0 { + builder + .add_output( + Some(sender_ovk), + PaymentAddress::from(spender.change_address), + NoteValue::from_raw(change_amount), + [0u8; 36], + ) + .map_err(|e| { + ProtocolError::ShieldedBuildError(format!("failed to add change output: {:?}", e)) + })?; + } + let token_extra = token_shielded_transfer_with_shielded_fee_extra_sighash_data( + &token_id.to_buffer(), + platform_version, + )?; + let token_bundle = prove_and_sign_bundle( + builder, + prover, + std::slice::from_ref(spender.ask), + &token_extra, + )?; + let token_sb = serialize_authorized_bundle(&token_bundle); + if token_sb.value_balance != 0 { + return Err(ProtocolError::ShieldedBuildError(format!( + "token shielded transfer bundle value balance must be zero, got {}", + token_sb.value_balance + ))); + } + let fee_actions = fee_payer.spends.len().max(2); + let fee = compute_token_shielded_transfer_with_shielded_fee_fee( + token_sb.actions.len(), + fee_actions, + platform_version, + )?; + let fee_sb = build_fee_bundle( + TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE, + &token_id, + &token_sb.actions, + fee_payer, + fee, + fee_actions, + prover, + platform_version, + )?; + let transition = TokenShieldedTransferWithShieldedFeeTransition::try_from_bundles( + data_contract_id, + token_contract_position, + token_id, + token_sb.actions, + token_sb.anchor, + token_sb.proof, + token_sb.binding_signature, + fee_sb.actions, + fee_sb.anchor, + fee_sb.proof, + fee_sb.binding_signature, + fee, + platform_version, + )?; + Ok((transition, fee)) +} + +/// Builds a `TokenUnshieldWithShieldedFee`: `amount` of the token leaves the token's shielded +/// pool into `recipient_id`'s balance, the fee is paid out of the credit shielded pool. Returns +/// the transition and the fee paid. +#[allow(clippy::too_many_arguments)] +pub fn build_token_unshield_with_shielded_fee_transition( + token_id: Identifier, + data_contract_id: Identifier, + token_contract_position: TokenContractPosition, + spender: TokenPoolSpender<'_>, + recipient_id: Identifier, + amount: TokenAmount, + memo: [u8; 36], + fee_payer: ShieldedFeePayer<'_>, + prover: &P, + platform_version: &PlatformVersion, +) -> Result<(StateTransition, Credits), ProtocolError> { + if amount == 0 { + return Err(ProtocolError::ShieldedBuildError( + "token unshield amount must be greater than zero".to_string(), + )); + } + let total_spent = total_value(&spender.spends)?; + if amount > total_spent { + return Err(ProtocolError::ShieldedBuildError(format!( + "token unshield amount {} exceeds total spendable value {}", + amount, total_spent + ))); + } + let change_amount = total_spent - amount; + let token_extra = token_unshield_with_shielded_fee_extra_sighash_data( + &token_id.to_buffer(), + &recipient_id.to_buffer(), + amount, + platform_version, + )?; + let token_bundle = build_spend_bundle( + spender.spends, + spender.change_address, + change_amount, + memo, + spender.fvk, + spender.ask, + spender.anchor, + prover, + &token_extra, + )?; + let token_sb = serialize_authorized_bundle(&token_bundle); + if token_sb.value_balance != amount as i64 { + return Err(ProtocolError::ShieldedBuildError(format!( + "token unshield bundle value balance {} does not equal the amount {}", + token_sb.value_balance, amount + ))); + } + let fee_actions = fee_payer.spends.len().max(2); + let fee = compute_token_unshield_with_shielded_fee_fee( + token_sb.actions.len(), + fee_actions, + platform_version, + )?; + let fee_sb = build_fee_bundle( + TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE, + &token_id, + &token_sb.actions, + fee_payer, + fee, + fee_actions, + prover, + platform_version, + )?; + let transition = TokenUnshieldWithShieldedFeeTransition::try_from_bundles( + data_contract_id, + token_contract_position, + token_id, + recipient_id, + amount, + token_sb.actions, + token_sb.anchor, + token_sb.proof, + token_sb.binding_signature, + fee_sb.actions, + fee_sb.anchor, + fee_sb.proof, + fee_sb.binding_signature, + fee, + platform_version, + )?; + Ok((transition, fee)) +} + +/// Builds a `TokenPurchaseFromShieldedPool`: `token_count` tokens are bought at +/// `total_agreed_price` credits (which must match the token's direct purchase price) paid out +/// of the credit shielded pool together with the fee, and minted into a note for `recipient` +/// in the token's shielded pool. Returns the transition and the fee paid. +#[allow(clippy::too_many_arguments)] +pub fn build_token_purchase_from_shielded_pool_transition( + token_id: Identifier, + data_contract_id: Identifier, + token_contract_position: TokenContractPosition, + recipient: &OrchardAddress, + recipient_fvk: &FullViewingKey, + token_count: TokenAmount, + total_agreed_price: Credits, + memo: [u8; 36], + fee_payer: ShieldedFeePayer<'_>, + prover: &P, + platform_version: &PlatformVersion, +) -> Result<(StateTransition, Credits), ProtocolError> { + if token_count == 0 { + return Err(ProtocolError::ShieldedBuildError( + "token purchase count must be greater than zero".to_string(), + )); + } + let token_bundle = build_output_only_bundle( + recipient, + token_count, + memo, + Some(recipient_fvk.to_ovk(Scope::External)), + 0, + prover, + )?; + // An outputs-only bundle carries no spend-auth signatures; its binding signature must + // still commit to the token id, count and price, so re-sign it over that sighash. + let token_sb = { + let mut builder = Builder::::new( + BundleType::Transactional { + flags: grovedb_commitment_tree::Flags::SPENDS_DISABLED, + bundle_required: false, + }, + Anchor::empty_tree(), + ); + builder + .add_output( + Some(recipient_fvk.to_ovk(Scope::External)), + PaymentAddress::from(recipient), + NoteValue::from_raw(token_count), + memo, + ) + .map_err(|e| { + ProtocolError::ShieldedBuildError(format!("failed to add output: {:?}", e)) + })?; + let token_extra = token_purchase_from_shielded_pool_extra_sighash_data( + &token_id.to_buffer(), + token_count, + total_agreed_price, + platform_version, + )?; + drop(token_bundle); + serialize_authorized_bundle(&prove_and_sign_bundle(builder, prover, &[], &token_extra)?) + }; + if token_sb.value_balance != -(token_count as i64) { + return Err(ProtocolError::ShieldedBuildError(format!( + "token purchase bundle value balance {} does not equal minus the token count {}", + token_sb.value_balance, token_count + ))); + } + let fee_actions = fee_payer.spends.len().max(2); + let fee = compute_token_purchase_from_shielded_pool_fee( + token_sb.actions.len(), + fee_actions, + platform_version, + )?; + let credits_leaving = total_agreed_price.checked_add(fee).ok_or_else(|| { + ProtocolError::ShieldedBuildError("price + fee overflows u64".to_string()) + })?; + let fee_sb = build_fee_bundle( + TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, + &token_id, + &token_sb.actions, + fee_payer, + credits_leaving, + fee_actions, + prover, + platform_version, + )?; + let transition = TokenPurchaseFromShieldedPoolTransition::try_from_bundles( + data_contract_id, + token_contract_position, + token_id, + token_count, + total_agreed_price, + token_sb.actions, + token_sb.anchor, + token_sb.proof, + token_sb.binding_signature, + fee_sb.actions, + fee_sb.anchor, + fee_sb.proof, + fee_sb.binding_signature, + credits_leaving, + platform_version, + )?; + Ok((transition, fee)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::shielded::builder::test_helpers::{ + test_orchard_address, test_spendable_note, TestProver, + }; + use grovedb_commitment_tree::SpendingKey; + + fn keys() -> (FullViewingKey, SpendAuthorizingKey) { + let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes"); + (FullViewingKey::from(&sk), SpendAuthorizingKey::from(&sk)) + } + + #[test] + fn rejects_a_transfer_above_the_spendable_token_value() { + let (fvk, ask) = keys(); + let address = test_orchard_address(); + let err = build_token_shielded_transfer_with_shielded_fee_transition( + Identifier::from([1u8; 32]), + Identifier::from([2u8; 32]), + 0, + TokenPoolSpender { + spends: vec![test_spendable_note(100)], + change_address: &address, + fvk: &fvk, + ask: &ask, + anchor: Anchor::empty_tree(), + }, + &address, + 1_000, + [0u8; 36], + ShieldedFeePayer { + spends: vec![test_spendable_note(1_000_000_000)], + change_address: &address, + fvk: &fvk, + ask: &ask, + anchor: Anchor::empty_tree(), + }, + &TestProver, + PlatformVersion::latest(), + ) + .expect_err("overspend must be rejected") + .to_string(); + assert!( + err.contains("exceeds total spendable value"), + "unexpected error: {err}" + ); + } + + #[test] + fn rejects_a_zero_unshield_amount() { + let (fvk, ask) = keys(); + let address = test_orchard_address(); + let err = build_token_unshield_with_shielded_fee_transition( + Identifier::from([1u8; 32]), + Identifier::from([2u8; 32]), + 0, + TokenPoolSpender { + spends: vec![test_spendable_note(100)], + change_address: &address, + fvk: &fvk, + ask: &ask, + anchor: Anchor::empty_tree(), + }, + Identifier::from([3u8; 32]), + 0, + [0u8; 36], + ShieldedFeePayer { + spends: vec![test_spendable_note(1_000_000_000)], + change_address: &address, + fvk: &fvk, + ask: &ask, + anchor: Anchor::empty_tree(), + }, + &TestProver, + PlatformVersion::latest(), + ) + .expect_err("zero amount must be rejected") + .to_string(); + assert!(err.contains("greater than zero"), "unexpected error: {err}"); + } + + #[test] + fn rejects_a_purchase_the_fee_notes_cannot_pay() { + let (fvk, ask) = keys(); + let address = test_orchard_address(); + let err = build_token_purchase_from_shielded_pool_transition( + Identifier::from([1u8; 32]), + Identifier::from([2u8; 32]), + 0, + &address, + &fvk, + 5, + 1_000, + [0u8; 36], + ShieldedFeePayer { + spends: vec![test_spendable_note(10)], + change_address: &address, + fvk: &fvk, + ask: &ask, + anchor: Anchor::empty_tree(), + }, + &TestProver, + PlatformVersion::latest(), + ) + .expect_err("the fee notes cannot cover price plus fee") + .to_string(); + assert!( + err.contains("exceed the total spendable value"), + "unexpected error: {err}" + ); + } +} diff --git a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/mod.rs b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/mod.rs index 614dd9e5c40..eac5481ba47 100644 --- a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/mod.rs +++ b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/mod.rs @@ -1,6 +1,9 @@ mod v0; use crate::fee::Credits; +use crate::shielded::{ + SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES, SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES, +}; use crate::ProtocolError; use platform_version::version::PlatformVersion; use v0::compute_minimum_shielded_fee_v0; @@ -226,3 +229,72 @@ pub fn compute_shielded_identity_create_fee( }), } } + +/// Computes the fee of an identity-less token pool transition (in credits): two bundles are +/// verified and stored, so it is [`compute_minimum_shielded_fee`] of the fee bundle PLUS the +/// same base for the token bundle, plus `extra_storage_bytes` of flat per-transition storage +/// priced at the storage rate (the balance items the transition writes outside the pools). +/// +/// The fee bundle's value balance must equal this exactly (plus the agreed price for a +/// purchase); the SDK builders and the minimum-fee validation both use it. +pub fn compute_token_pool_paid_shielded_fee( + token_actions: usize, + fee_actions: usize, + extra_storage_bytes: u64, + platform_version: &PlatformVersion, +) -> Result { + match platform_version.dpp.methods.compute_minimum_shielded_fee { + 0 => v0::compute_token_pool_paid_shielded_fee_v0( + token_actions, + fee_actions, + extra_storage_bytes, + platform_version, + ), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "compute_token_pool_paid_shielded_fee".to_string(), + known_versions: vec![0], + received: version, + }), + } +} + +/// The fee of a `TokenShieldedTransferWithShieldedFee`: both bundles, nothing written outside +/// the pools. +pub fn compute_token_shielded_transfer_with_shielded_fee_fee( + token_actions: usize, + fee_actions: usize, + platform_version: &PlatformVersion, +) -> Result { + compute_token_pool_paid_shielded_fee(token_actions, fee_actions, 0, platform_version) +} + +/// The fee of a `TokenUnshieldWithShieldedFee`: both bundles plus the recipient's token +/// balance item. +pub fn compute_token_unshield_with_shielded_fee_fee( + token_actions: usize, + fee_actions: usize, + platform_version: &PlatformVersion, +) -> Result { + compute_token_pool_paid_shielded_fee( + token_actions, + fee_actions, + SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES, + platform_version, + ) +} + +/// The fee of a `TokenPurchaseFromShieldedPool`: both bundles plus the contract owner's credit +/// balance write and the token supply item. +pub fn compute_token_purchase_from_shielded_pool_fee( + token_actions: usize, + fee_actions: usize, + platform_version: &PlatformVersion, +) -> Result { + compute_token_pool_paid_shielded_fee( + token_actions, + fee_actions, + SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES + .saturating_add(SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES), + platform_version, + ) +} diff --git a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs index 3f23ef06605..e56e41389be 100644 --- a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs +++ b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs @@ -324,6 +324,37 @@ pub fn compute_shielded_identity_create_fee_v0( )) } +/// Version 0 of the identity-less token pool transition fee: the base for each of the two +/// bundles plus `extra_storage_bytes` at the storage rate. +pub fn compute_token_pool_paid_shielded_fee_v0( + token_actions: usize, + fee_actions: usize, + extra_storage_bytes: u64, + platform_version: &PlatformVersion, +) -> Result { + let storage = &platform_version.fee_version.storage; + let fee_bundle = compute_minimum_shielded_fee_v0(fee_actions, platform_version)?; + let token_bundle = compute_minimum_shielded_fee_v0(token_actions, platform_version)?; + let per_byte_rate = storage + .storage_disk_usage_credit_per_byte + .checked_add(storage.storage_processing_credit_per_byte) + .ok_or(ProtocolError::Overflow( + "shielded storage per-byte rate overflow", + ))?; + let extra_storage_fee = + extra_storage_bytes + .checked_mul(per_byte_rate) + .ok_or(ProtocolError::Overflow( + "token pool paid transition storage fee overflow", + ))?; + fee_bundle + .checked_add(token_bundle) + .and_then(|fee| fee.checked_add(extra_storage_fee)) + .ok_or(ProtocolError::Overflow( + "token pool paid transition fee overflow", + )) +} + #[cfg(test)] mod tests { use super::*; diff --git a/packages/rs-dpp/src/shielded/mod.rs b/packages/rs-dpp/src/shielded/mod.rs index 7fe3f737d82..b820130fc8f 100644 --- a/packages/rs-dpp/src/shielded/mod.rs +++ b/packages/rs-dpp/src/shielded/mod.rs @@ -18,7 +18,10 @@ pub use compute_minimum_shielded_fee::{ compute_minimum_shielded_fee, compute_shielded_identity_balance_write_fee, compute_shielded_identity_create_fee, compute_shielded_identity_top_up_fee, compute_shielded_unshield_fee, compute_shielded_verification_fee, - compute_shielded_withdrawal_fee, + compute_shielded_withdrawal_fee, compute_token_pool_paid_shielded_fee, + compute_token_purchase_from_shielded_pool_fee, + compute_token_shielded_transfer_with_shielded_fee_fee, + compute_token_unshield_with_shielded_fee_fee, }; // Re-exported so the public paths stay `dpp::shielded::` after moving the sighash preimage @@ -47,10 +50,18 @@ pub use sighash::{ identity_top_up_from_shielded_extra_sighash_data, identity_top_up_from_shielded_extra_sighash_data_v0, shielded_withdrawal_extra_sighash_data, shielded_withdrawal_extra_sighash_data_v0, token_burn_from_pool_extra_sighash_data, - token_burn_from_pool_extra_sighash_data_v0, token_shielded_transfer_extra_sighash_data, - token_shielded_transfer_extra_sighash_data_v0, token_unshield_extra_sighash_data, - token_unshield_extra_sighash_data_v0, unshield_extra_sighash_data, - unshield_extra_sighash_data_v0, + token_burn_from_pool_extra_sighash_data_v0, token_pool_fee_bundle_extra_sighash_data, + token_pool_fee_bundle_extra_sighash_data_v0, + token_purchase_from_shielded_pool_extra_sighash_data, + token_purchase_from_shielded_pool_extra_sighash_data_v0, + token_shielded_transfer_extra_sighash_data, token_shielded_transfer_extra_sighash_data_v0, + token_shielded_transfer_with_shielded_fee_extra_sighash_data, + token_shielded_transfer_with_shielded_fee_extra_sighash_data_v0, + token_unshield_extra_sighash_data, token_unshield_extra_sighash_data_v0, + token_unshield_with_shielded_fee_extra_sighash_data, + token_unshield_with_shielded_fee_extra_sighash_data_v0, unshield_extra_sighash_data, + unshield_extra_sighash_data_v0, TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, + TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE, TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE, }; /// Calibrated effective storage-byte cost of the Core withdrawal document a diff --git a/packages/rs-dpp/src/shielded/sighash.rs b/packages/rs-dpp/src/shielded/sighash.rs index 544f332de81..8ec533aab46 100644 --- a/packages/rs-dpp/src/shielded/sighash.rs +++ b/packages/rs-dpp/src/shielded/sighash.rs @@ -8,6 +8,7 @@ use crate::address_funds::PlatformAddress; use crate::identity::identity_public_key::contract_bounds::ContractBounds; +use crate::shielded::{serialized_actions_digest, SerializedAction}; use crate::state_transition::public_key_in_creation::accessors::IdentityPublicKeyInCreationV0Getters; use crate::state_transition::public_key_in_creation::IdentityPublicKeyInCreation; use crate::withdrawal::Pooling; @@ -18,6 +19,14 @@ use sha2::{Digest, Sha256}; /// Domain separator for Platform sighash computation. const SIGHASH_DOMAIN: &[u8] = b"DashPlatformSighash"; +/// The state transition type byte the token bundle of a `TokenShieldedTransferWithShieldedFee` +/// commits to (`StateTransitionType::TokenShieldedTransferWithShieldedFee`). +pub const TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE: u8 = 23; +/// The state transition type byte the token bundle of a `TokenUnshieldWithShieldedFee` commits to. +pub const TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE: u8 = 24; +/// The state transition type byte the token bundle of a `TokenPurchaseFromShieldedPool` commits to. +pub const TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE: u8 = 25; + /// Computes the platform sighash from an Orchard bundle commitment and optional /// transparent field data. /// @@ -427,6 +436,145 @@ pub fn document_token_payment_extra_sighash_data_v0( data } +/// Extra sighash data of the token bundle of a `TokenShieldedTransferWithShieldedFee`: the +/// state transition type and the token id, so the bundle is pinned to one token pool and one +/// transition kind. +pub fn token_shielded_transfer_with_shielded_fee_extra_sighash_data( + token_id: &[u8; 32], + platform_version: &PlatformVersion, +) -> Result, ProtocolError> { + match platform_version.dpp.methods.shielded_extra_sighash_data { + 0 => Ok(token_shielded_transfer_with_shielded_fee_extra_sighash_data_v0(token_id)), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "token_shielded_transfer_with_shielded_fee_extra_sighash_data".to_string(), + known_versions: vec![0], + received: version, + }), + } +} + +/// Version 0 layout: `state transition type (1, = 23) || token_id (32)`. Frozen. +pub fn token_shielded_transfer_with_shielded_fee_extra_sighash_data_v0( + token_id: &[u8; 32], +) -> Vec { + let mut data = Vec::with_capacity(1 + 32); + data.push(TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE); + data.extend_from_slice(token_id); + data +} + +/// Extra sighash data of the token bundle of a `TokenUnshieldWithShieldedFee`: the state +/// transition type, the token id, the recipient and the amount, so the bundle cannot be +/// replayed against another token, recipient or amount. +pub fn token_unshield_with_shielded_fee_extra_sighash_data( + token_id: &[u8; 32], + recipient_id: &[u8; 32], + amount: u64, + platform_version: &PlatformVersion, +) -> Result, ProtocolError> { + match platform_version.dpp.methods.shielded_extra_sighash_data { + 0 => Ok(token_unshield_with_shielded_fee_extra_sighash_data_v0( + token_id, + recipient_id, + amount, + )), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "token_unshield_with_shielded_fee_extra_sighash_data".to_string(), + known_versions: vec![0], + received: version, + }), + } +} + +/// Version 0 layout: `state transition type (1, = 24) || token_id (32) || recipient_id (32) +/// || amount (8, little endian)`. Frozen. +pub fn token_unshield_with_shielded_fee_extra_sighash_data_v0( + token_id: &[u8; 32], + recipient_id: &[u8; 32], + amount: u64, +) -> Vec { + let mut data = Vec::with_capacity(1 + 32 + 32 + 8); + data.push(TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE); + data.extend_from_slice(token_id); + data.extend_from_slice(recipient_id); + data.extend_from_slice(&amount.to_le_bytes()); + data +} + +/// Extra sighash data of the token bundle of a `TokenPurchaseFromShieldedPool`: the state +/// transition type, the token id, the token count and the agreed price. +pub fn token_purchase_from_shielded_pool_extra_sighash_data( + token_id: &[u8; 32], + token_count: u64, + total_agreed_price: u64, + platform_version: &PlatformVersion, +) -> Result, ProtocolError> { + match platform_version.dpp.methods.shielded_extra_sighash_data { + 0 => Ok(token_purchase_from_shielded_pool_extra_sighash_data_v0( + token_id, + token_count, + total_agreed_price, + )), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "token_purchase_from_shielded_pool_extra_sighash_data".to_string(), + known_versions: vec![0], + received: version, + }), + } +} + +/// Version 0 layout: `state transition type (1, = 25) || token_id (32) || token_count (8, LE) +/// || total_agreed_price (8, LE)`. Frozen. +pub fn token_purchase_from_shielded_pool_extra_sighash_data_v0( + token_id: &[u8; 32], + token_count: u64, + total_agreed_price: u64, +) -> Vec { + let mut data = Vec::with_capacity(1 + 32 + 8 + 8); + data.push(TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE); + data.extend_from_slice(token_id); + data.extend_from_slice(&token_count.to_le_bytes()); + data.extend_from_slice(&total_agreed_price.to_le_bytes()); + data +} + +/// Extra sighash data of the credit pool fee bundle of an identity-less token pool transition: +/// the state transition type, the token id and a digest of the token bundle's actions, so the +/// fee bundle can only ever pay for that exact token bundle. +pub fn token_pool_fee_bundle_extra_sighash_data( + state_transition_type: u8, + token_id: &[u8; 32], + token_actions: &[SerializedAction], + platform_version: &PlatformVersion, +) -> Result, ProtocolError> { + match platform_version.dpp.methods.shielded_extra_sighash_data { + 0 => Ok(token_pool_fee_bundle_extra_sighash_data_v0( + state_transition_type, + token_id, + &serialized_actions_digest(token_actions), + )), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "token_pool_fee_bundle_extra_sighash_data".to_string(), + known_versions: vec![0], + received: version, + }), + } +} + +/// Version 0 layout: `state transition type (1) || token_id (32) || token actions digest (32)`. +/// Frozen. +pub fn token_pool_fee_bundle_extra_sighash_data_v0( + state_transition_type: u8, + token_id: &[u8; 32], + token_actions_digest: &[u8; 32], +) -> Vec { + let mut data = Vec::with_capacity(1 + 32 + 32); + data.push(state_transition_type); + data.extend_from_slice(token_id); + data.extend_from_slice(token_actions_digest); + data +} + /// Builds the transparent `extra_data` bound into a `TokenShieldedTransfer`'s platform sighash, /// with the byte layout `token_id (32) || owner_id (32)`. /// @@ -783,6 +931,34 @@ mod tests { assert_eq!(&data[128..], &10u64.to_le_bytes()); } + #[test] + fn token_pool_paid_layouts_start_with_the_state_transition_type() { + let transfer = token_shielded_transfer_with_shielded_fee_extra_sighash_data_v0(&[1u8; 32]); + assert_eq!(transfer.len(), 33); + assert_eq!(transfer[0], 23); + assert_eq!(&transfer[1..], &[1u8; 32]); + + let unshield = + token_unshield_with_shielded_fee_extra_sighash_data_v0(&[1u8; 32], &[2u8; 32], 300); + assert_eq!(unshield.len(), 73); + assert_eq!(unshield[0], 24); + assert_eq!(&unshield[1..33], &[1u8; 32]); + assert_eq!(&unshield[33..65], &[2u8; 32]); + assert_eq!(&unshield[65..], &300u64.to_le_bytes()); + + let purchase = token_purchase_from_shielded_pool_extra_sighash_data_v0(&[1u8; 32], 5, 900); + assert_eq!(purchase.len(), 49); + assert_eq!(purchase[0], 25); + assert_eq!(&purchase[33..41], &5u64.to_le_bytes()); + assert_eq!(&purchase[41..], &900u64.to_le_bytes()); + + let fee = token_pool_fee_bundle_extra_sighash_data_v0(24, &[1u8; 32], &[3u8; 32]); + assert_eq!(fee.len(), 65); + assert_eq!(fee[0], 24); + assert_eq!(&fee[1..33], &[1u8; 32]); + assert_eq!(&fee[33..], &[3u8; 32]); + } + #[test] fn token_burn_from_pool_layout_is_token_owner_amount() { let data = token_burn_from_pool_extra_sighash_data_v0(&[1u8; 32], &[2u8; 32], 300); diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index e40a76a63da..01d05e58996 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -178,6 +178,16 @@ use crate::state_transition::shielded_withdrawal_transition::{ }; #[cfg(feature = "state-transition-signing")] use crate::state_transition::state_transitions::document::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; +use crate::state_transition::token_purchase_from_shielded_pool_transition::{ + TokenPurchaseFromShieldedPoolTransition, TokenPurchaseFromShieldedPoolTransitionSignable, +}; +use crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::{ + TokenShieldedTransferWithShieldedFeeTransition, + TokenShieldedTransferWithShieldedFeeTransitionSignable, +}; +use crate::state_transition::token_unshield_with_shielded_fee_transition::{ + TokenUnshieldWithShieldedFeeTransition, TokenUnshieldWithShieldedFeeTransitionSignable, +}; use crate::state_transition::unshield_transition::{ UnshieldTransition, UnshieldTransitionSignable, }; @@ -212,6 +222,9 @@ macro_rules! call_method { StateTransition::ShieldedTransfer(st) => st.$method($args), StateTransition::Unshield(st) => st.$method($args), StateTransition::IdentityTopUpFromShieldedPool(st) => st.$method($args), + StateTransition::TokenShieldedTransferWithShieldedFee(st) => st.$method($args), + StateTransition::TokenUnshieldWithShieldedFee(st) => st.$method($args), + StateTransition::TokenPurchaseFromShieldedPool(st) => st.$method($args), StateTransition::ShieldFromAssetLock(st) => st.$method($args), StateTransition::ShieldedWithdrawal(st) => st.$method($args), StateTransition::IdentityCreateFromShieldedPool(st) => st.$method($args), @@ -242,6 +255,9 @@ macro_rules! call_method { StateTransition::ShieldedTransfer(st) => st.$method(), StateTransition::Unshield(st) => st.$method(), StateTransition::IdentityTopUpFromShieldedPool(st) => st.$method(), + StateTransition::TokenShieldedTransferWithShieldedFee(st) => st.$method(), + StateTransition::TokenUnshieldWithShieldedFee(st) => st.$method(), + StateTransition::TokenPurchaseFromShieldedPool(st) => st.$method(), StateTransition::ShieldFromAssetLock(st) => st.$method(), StateTransition::ShieldedWithdrawal(st) => st.$method(), StateTransition::IdentityCreateFromShieldedPool(st) => st.$method(), @@ -275,6 +291,9 @@ macro_rules! call_getter_method_identity_signed { StateTransition::ShieldedTransfer(_) => None, StateTransition::Unshield(_) => None, StateTransition::IdentityTopUpFromShieldedPool(_) => None, + StateTransition::TokenShieldedTransferWithShieldedFee(_) => None, + StateTransition::TokenUnshieldWithShieldedFee(_) => None, + StateTransition::TokenPurchaseFromShieldedPool(_) => None, StateTransition::ShieldFromAssetLock(_) => None, StateTransition::ShieldedWithdrawal(_) => None, StateTransition::IdentityCreateFromShieldedPool(_) => None, @@ -305,6 +324,9 @@ macro_rules! call_getter_method_identity_signed { StateTransition::ShieldedTransfer(_) => None, StateTransition::Unshield(_) => None, StateTransition::IdentityTopUpFromShieldedPool(_) => None, + StateTransition::TokenShieldedTransferWithShieldedFee(_) => None, + StateTransition::TokenUnshieldWithShieldedFee(_) => None, + StateTransition::TokenPurchaseFromShieldedPool(_) => None, StateTransition::ShieldFromAssetLock(_) => None, StateTransition::ShieldedWithdrawal(_) => None, StateTransition::IdentityCreateFromShieldedPool(_) => None, @@ -338,6 +360,9 @@ macro_rules! call_method_identity_signed { StateTransition::ShieldedTransfer(_) => {} StateTransition::Unshield(_) => {} StateTransition::IdentityTopUpFromShieldedPool(_) => {} + StateTransition::TokenShieldedTransferWithShieldedFee(_) => {} + StateTransition::TokenUnshieldWithShieldedFee(_) => {} + StateTransition::TokenPurchaseFromShieldedPool(_) => {} StateTransition::ShieldFromAssetLock(_) => {} StateTransition::ShieldedWithdrawal(_) => {} StateTransition::IdentityCreateFromShieldedPool(_) => {} @@ -368,6 +393,9 @@ macro_rules! call_method_identity_signed { StateTransition::ShieldedTransfer(_) => {} StateTransition::Unshield(_) => {} StateTransition::IdentityTopUpFromShieldedPool(_) => {} + StateTransition::TokenShieldedTransferWithShieldedFee(_) => {} + StateTransition::TokenUnshieldWithShieldedFee(_) => {} + StateTransition::TokenPurchaseFromShieldedPool(_) => {} StateTransition::ShieldFromAssetLock(_) => {} StateTransition::ShieldedWithdrawal(_) => {} StateTransition::IdentityCreateFromShieldedPool(_) => {} @@ -424,6 +452,15 @@ macro_rules! call_errorable_method_identity_signed { StateTransition::IdentityTopUpFromShieldedPool(_) => Err(ProtocolError::CorruptedCodeExecution( "identity top up from shielded pool transition can not be called for identity signing".to_string(), )), + StateTransition::TokenShieldedTransferWithShieldedFee(_) => Err(ProtocolError::CorruptedCodeExecution( + "token shielded transfer with shielded fee transition can not be called for identity signing".to_string(), + )), + StateTransition::TokenUnshieldWithShieldedFee(_) => Err(ProtocolError::CorruptedCodeExecution( + "token unshield with shielded fee transition can not be called for identity signing".to_string(), + )), + StateTransition::TokenPurchaseFromShieldedPool(_) => Err(ProtocolError::CorruptedCodeExecution( + "token purchase from shielded pool transition can not be called for identity signing".to_string(), + )), StateTransition::ShieldFromAssetLock(_) => Err(ProtocolError::CorruptedCodeExecution( "shield from asset lock transition can not be called for identity signing".to_string(), )), @@ -482,6 +519,15 @@ macro_rules! call_errorable_method_identity_signed { StateTransition::IdentityTopUpFromShieldedPool(_) => Err(ProtocolError::CorruptedCodeExecution( "identity top up from shielded pool transition can not be called for identity signing".to_string(), )), + StateTransition::TokenShieldedTransferWithShieldedFee(_) => Err(ProtocolError::CorruptedCodeExecution( + "token shielded transfer with shielded fee transition can not be called for identity signing".to_string(), + )), + StateTransition::TokenUnshieldWithShieldedFee(_) => Err(ProtocolError::CorruptedCodeExecution( + "token unshield with shielded fee transition can not be called for identity signing".to_string(), + )), + StateTransition::TokenPurchaseFromShieldedPool(_) => Err(ProtocolError::CorruptedCodeExecution( + "token purchase from shielded pool transition can not be called for identity signing".to_string(), + )), StateTransition::ShieldFromAssetLock(_) => Err(ProtocolError::CorruptedCodeExecution( "shield from asset lock transition can not be called for identity signing".to_string(), )), @@ -562,6 +608,9 @@ pub enum StateTransition { IdentityKeyLimitsUpdate(IdentityKeyLimitsUpdateTransition), ContractUserModeration(ContractUserModerationTransition), ContractFeeClaim(ContractFeeClaimTransition), + TokenShieldedTransferWithShieldedFee(TokenShieldedTransferWithShieldedFeeTransition), + TokenUnshieldWithShieldedFee(TokenUnshieldWithShieldedFeeTransition), + TokenPurchaseFromShieldedPool(TokenPurchaseFromShieldedPoolTransition), } #[cfg(all(feature = "json-conversion", feature = "serde-conversion"))] @@ -885,6 +934,30 @@ mod json_convertible_tests { "identityTopUpFromShieldedPool", ); } + #[test] + fn umbrella_token_shielded_transfer_with_shielded_fee() { + let inner = crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip( + StateTransition::TokenShieldedTransferWithShieldedFee(inner), + "tokenShieldedTransferWithShieldedFee", + ); + } + #[test] + fn umbrella_token_unshield_with_shielded_fee() { + let inner = crate::state_transition::token_unshield_with_shielded_fee_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip( + StateTransition::TokenUnshieldWithShieldedFee(inner), + "tokenUnshieldWithShieldedFee", + ); + } + #[test] + fn umbrella_token_purchase_from_shielded_pool() { + let inner = crate::state_transition::token_purchase_from_shielded_pool_transition::json_convertible_tests::fixture(); + assert_umbrella_round_trip( + StateTransition::TokenPurchaseFromShieldedPool(inner), + "tokenPurchaseFromShieldedPool", + ); + } #[test] fn umbrella_shield_from_identity() { @@ -1063,6 +1136,10 @@ impl StateTransition { | StateTransition::IdentityKeyLimitsUpdate(_) | StateTransition::ContractUserModeration(_) | StateTransition::ContractFeeClaim(_) => 14..=LATEST_VERSION, + + StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) => 15..=LATEST_VERSION, } } @@ -1078,6 +1155,9 @@ impl StateTransition { | StateTransition::ShieldedWithdrawal(_) | StateTransition::IdentityCreateFromShieldedPool(_) | StateTransition::IdentityTopUpFromShieldedPool(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) ) } @@ -1206,6 +1286,11 @@ impl StateTransition { Self::ShieldedTransfer(_) => "ShieldedTransfer".to_string(), Self::Unshield(_) => "Unshield".to_string(), Self::IdentityTopUpFromShieldedPool(_) => "IdentityTopUpFromShieldedPool".to_string(), + Self::TokenShieldedTransferWithShieldedFee(_) => { + "TokenShieldedTransferWithShieldedFee".to_string() + } + Self::TokenUnshieldWithShieldedFee(_) => "TokenUnshieldWithShieldedFee".to_string(), + Self::TokenPurchaseFromShieldedPool(_) => "TokenPurchaseFromShieldedPool".to_string(), Self::ShieldFromAssetLock(_) => "ShieldFromAssetLock".to_string(), Self::ShieldedWithdrawal(_) => "ShieldedWithdrawal".to_string(), Self::IdentityCreateFromShieldedPool(_) => "IdentityCreateFromShieldedPool".to_string(), @@ -1238,6 +1323,9 @@ impl StateTransition { StateTransition::ShieldedTransfer(_) => None, StateTransition::Unshield(_) => None, StateTransition::IdentityTopUpFromShieldedPool(_) => None, + StateTransition::TokenShieldedTransferWithShieldedFee(_) => None, + StateTransition::TokenUnshieldWithShieldedFee(_) => None, + StateTransition::TokenPurchaseFromShieldedPool(_) => None, StateTransition::ShieldFromAssetLock(st) => Some(st.signature()), StateTransition::ShieldedWithdrawal(_) => None, StateTransition::IdentityCreateFromShieldedPool(_) => None, @@ -1256,6 +1344,9 @@ impl StateTransition { StateTransition::ShieldedTransfer(_) => 0, StateTransition::Unshield(_) => 0, StateTransition::IdentityTopUpFromShieldedPool(_) => 0, + StateTransition::TokenShieldedTransferWithShieldedFee(_) => 0, + StateTransition::TokenUnshieldWithShieldedFee(_) => 0, + StateTransition::TokenPurchaseFromShieldedPool(_) => 0, StateTransition::ShieldFromAssetLock(_) => 0, StateTransition::ShieldedWithdrawal(_) => 0, StateTransition::IdentityCreateFromShieldedPool(_) => 0, @@ -1290,6 +1381,9 @@ impl StateTransition { StateTransition::ShieldedTransfer(_) => 0, StateTransition::Unshield(_) => 0, StateTransition::IdentityTopUpFromShieldedPool(_) => 0, + StateTransition::TokenShieldedTransferWithShieldedFee(_) => 0, + StateTransition::TokenUnshieldWithShieldedFee(_) => 0, + StateTransition::TokenPurchaseFromShieldedPool(_) => 0, StateTransition::ShieldedWithdrawal(_) => 0, StateTransition::IdentityCreateFromShieldedPool(_) => 0, StateTransition::ShieldFromIdentity(st) => st.user_fee_increase(), @@ -1362,6 +1456,9 @@ impl StateTransition { StateTransition::ShieldedTransfer(_) => None, StateTransition::Unshield(_) => None, StateTransition::IdentityTopUpFromShieldedPool(_) => None, + StateTransition::TokenShieldedTransferWithShieldedFee(_) => None, + StateTransition::TokenUnshieldWithShieldedFee(_) => None, + StateTransition::TokenPurchaseFromShieldedPool(_) => None, StateTransition::ShieldFromAssetLock(_) => None, StateTransition::ShieldedWithdrawal(_) => None, StateTransition::IdentityCreateFromShieldedPool(_) => None, @@ -1394,6 +1491,9 @@ impl StateTransition { StateTransition::ShieldedTransfer(_) => None, StateTransition::Unshield(_) => None, StateTransition::IdentityTopUpFromShieldedPool(_) => None, + StateTransition::TokenShieldedTransferWithShieldedFee(_) => None, + StateTransition::TokenUnshieldWithShieldedFee(_) => None, + StateTransition::TokenPurchaseFromShieldedPool(_) => None, StateTransition::ShieldFromAssetLock(_) => None, StateTransition::ShieldedWithdrawal(_) => None, StateTransition::IdentityCreateFromShieldedPool(_) => None, @@ -1474,6 +1574,9 @@ impl StateTransition { | StateTransition::Unshield(_) | StateTransition::ShieldedWithdrawal(_) | StateTransition::IdentityCreateFromShieldedPool(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) | StateTransition::IdentityTopUpFromShieldedPool(_) => false, StateTransition::ShieldFromIdentity(st) => { st.set_signature(signature); @@ -1538,6 +1641,9 @@ impl StateTransition { StateTransition::ShieldedTransfer(_) => {} StateTransition::Unshield(_) => {} StateTransition::IdentityTopUpFromShieldedPool(_) => {} + StateTransition::TokenShieldedTransferWithShieldedFee(_) => {} + StateTransition::TokenUnshieldWithShieldedFee(_) => {} + StateTransition::TokenPurchaseFromShieldedPool(_) => {} StateTransition::ShieldedWithdrawal(_) => {} StateTransition::IdentityCreateFromShieldedPool(_) => {} StateTransition::ShieldFromIdentity(st) => st.set_user_fee_increase(user_fee_increase), @@ -1722,6 +1828,21 @@ impl StateTransition { "identity top up from shielded pool transition can not be called for identity signing".to_string(), )) } + StateTransition::TokenShieldedTransferWithShieldedFee(_) => { + return Err(ProtocolError::CorruptedCodeExecution( + "token shielded transfer with shielded fee transition can not be called for identity signing".to_string(), + )) + } + StateTransition::TokenUnshieldWithShieldedFee(_) => { + return Err(ProtocolError::CorruptedCodeExecution( + "token unshield with shielded fee transition can not be called for identity signing".to_string(), + )) + } + StateTransition::TokenPurchaseFromShieldedPool(_) => { + return Err(ProtocolError::CorruptedCodeExecution( + "token purchase from shielded pool transition can not be called for identity signing".to_string(), + )) + } StateTransition::ShieldFromAssetLock(_) => { return Err(ProtocolError::CorruptedCodeExecution( "shield from asset lock transition can not be called for identity signing" @@ -2285,6 +2406,15 @@ impl StateTransitionStructureValidation for StateTransition { StateTransition::IdentityTopUpFromShieldedPool(transition) => { transition.validate_structure(platform_version) } + StateTransition::TokenShieldedTransferWithShieldedFee(transition) => { + transition.validate_structure(platform_version) + } + StateTransition::TokenUnshieldWithShieldedFee(transition) => { + transition.validate_structure(platform_version) + } + StateTransition::TokenPurchaseFromShieldedPool(transition) => { + transition.validate_structure(platform_version) + } StateTransition::ShieldFromAssetLock(transition) => { transition.validate_structure(platform_version) } diff --git a/packages/rs-dpp/src/state_transition/state_transition_types.rs b/packages/rs-dpp/src/state_transition/state_transition_types.rs index 04347f1e667..8efec0a6386 100644 --- a/packages/rs-dpp/src/state_transition/state_transition_types.rs +++ b/packages/rs-dpp/src/state_transition/state_transition_types.rs @@ -47,6 +47,9 @@ pub enum StateTransitionType { IdentityKeyLimitsUpdate = 23, ContractUserModeration = 24, ContractFeeClaim = 25, + TokenShieldedTransferWithShieldedFee = 26, + TokenUnshieldWithShieldedFee = 27, + TokenPurchaseFromShieldedPool = 28, } impl std::fmt::Display for StateTransitionType { @@ -146,6 +149,18 @@ mod tests { "ContractUserModeration", ), (StateTransitionType::ContractFeeClaim, "ContractFeeClaim"), + ( + StateTransitionType::TokenShieldedTransferWithShieldedFee, + "TokenShieldedTransferWithShieldedFee", + ), + ( + StateTransitionType::TokenUnshieldWithShieldedFee, + "TokenUnshieldWithShieldedFee", + ), + ( + StateTransitionType::TokenPurchaseFromShieldedPool, + "TokenPurchaseFromShieldedPool", + ), ]; for (variant, expected) in cases { assert_eq!( @@ -186,6 +201,12 @@ mod tests { (23, StateTransitionType::IdentityKeyLimitsUpdate), (24, StateTransitionType::ContractUserModeration), (25, StateTransitionType::ContractFeeClaim), + ( + 26, + StateTransitionType::TokenShieldedTransferWithShieldedFee, + ), + (27, StateTransitionType::TokenUnshieldWithShieldedFee), + (28, StateTransitionType::TokenPurchaseFromShieldedPool), ]; for (val, expected) in pairs { let result = StateTransitionType::try_from(val).unwrap(); @@ -228,6 +249,9 @@ mod tests { StateTransitionType::IdentityKeyLimitsUpdate, StateTransitionType::ContractUserModeration, StateTransitionType::ContractFeeClaim, + StateTransitionType::TokenShieldedTransferWithShieldedFee, + StateTransitionType::TokenUnshieldWithShieldedFee, + StateTransitionType::TokenPurchaseFromShieldedPool, ]; for variant in all_variants { let val: u8 = variant.into(); diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/mod.rs index 1f73ff3f01f..fbeb5e3b420 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/mod.rs @@ -6,4 +6,7 @@ pub mod shield_from_identity_transition; pub mod shield_transition; pub mod shielded_transfer_transition; pub mod shielded_withdrawal_transition; +pub mod token_purchase_from_shielded_pool_transition; +pub mod token_shielded_transfer_with_shielded_fee_transition; +pub mod token_unshield_with_shielded_fee_transition; pub mod unshield_transition; diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/accessors/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/accessors/mod.rs new file mode 100644 index 00000000000..da521012966 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/accessors/mod.rs @@ -0,0 +1,84 @@ +mod v0; +pub use v0::*; + +use crate::balances::credits::TokenAmount; +use crate::data_contract::TokenContractPosition; +use crate::fee::Credits; +use crate::shielded::SerializedAction; +use crate::state_transition::token_purchase_from_shielded_pool_transition::TokenPurchaseFromShieldedPoolTransition; +use platform_value::Identifier; + +impl TokenPurchaseFromShieldedPoolTransitionAccessorsV0 + for TokenPurchaseFromShieldedPoolTransition +{ + fn data_contract_id(&self) -> Identifier { + match self { + TokenPurchaseFromShieldedPoolTransition::V0(v0) => v0.data_contract_id, + } + } + fn token_contract_position(&self) -> TokenContractPosition { + match self { + TokenPurchaseFromShieldedPoolTransition::V0(v0) => v0.token_contract_position, + } + } + fn token_id(&self) -> Identifier { + match self { + TokenPurchaseFromShieldedPoolTransition::V0(v0) => v0.token_id, + } + } + fn token_count(&self) -> TokenAmount { + match self { + TokenPurchaseFromShieldedPoolTransition::V0(v0) => v0.token_count, + } + } + fn total_agreed_price(&self) -> Credits { + match self { + TokenPurchaseFromShieldedPoolTransition::V0(v0) => v0.total_agreed_price, + } + } + fn token_actions(&self) -> &[SerializedAction] { + match self { + TokenPurchaseFromShieldedPoolTransition::V0(v0) => &v0.token_actions, + } + } + fn token_anchor(&self) -> [u8; 32] { + match self { + TokenPurchaseFromShieldedPoolTransition::V0(v0) => v0.token_anchor, + } + } + fn token_proof(&self) -> &[u8] { + match self { + TokenPurchaseFromShieldedPoolTransition::V0(v0) => &v0.token_proof, + } + } + fn token_binding_signature(&self) -> [u8; 64] { + match self { + TokenPurchaseFromShieldedPoolTransition::V0(v0) => v0.token_binding_signature, + } + } + fn fee_actions(&self) -> &[SerializedAction] { + match self { + TokenPurchaseFromShieldedPoolTransition::V0(v0) => &v0.fee_actions, + } + } + fn fee_anchor(&self) -> [u8; 32] { + match self { + TokenPurchaseFromShieldedPoolTransition::V0(v0) => v0.fee_anchor, + } + } + fn fee_proof(&self) -> &[u8] { + match self { + TokenPurchaseFromShieldedPoolTransition::V0(v0) => &v0.fee_proof, + } + } + fn fee_binding_signature(&self) -> [u8; 64] { + match self { + TokenPurchaseFromShieldedPoolTransition::V0(v0) => v0.fee_binding_signature, + } + } + fn credit_amount(&self) -> Credits { + match self { + TokenPurchaseFromShieldedPoolTransition::V0(v0) => v0.credit_amount, + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/accessors/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/accessors/v0/mod.rs new file mode 100644 index 00000000000..1c47ea7359a --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/accessors/v0/mod.rs @@ -0,0 +1,53 @@ +use crate::balances::credits::TokenAmount; +use crate::data_contract::TokenContractPosition; +use crate::fee::Credits; +use crate::shielded::SerializedAction; +use platform_value::Identifier; + +/// Accessors for the fields of a `TokenPurchaseFromShieldedPoolTransition`. +pub trait TokenPurchaseFromShieldedPoolTransitionAccessorsV0 { + /// The contract defining the token. + fn data_contract_id(&self) -> Identifier; + /// The token's position in the contract. + fn token_contract_position(&self) -> TokenContractPosition; + /// The token id, `calculate_token_id(data_contract_id, token_contract_position)`. + fn token_id(&self) -> Identifier; + /// Tokens bought and minted into the pool; the token bundle's value balance is its negation. + fn token_count(&self) -> TokenAmount; + /// The credits the buyer agrees to pay for `token_count`, paid out of the credit bundle to the contract owner; `credit_amount` minus this is the fee. + fn total_agreed_price(&self) -> Credits; + /// Orchard actions of the bundle in the token's shielded pool. + fn token_actions(&self) -> &[SerializedAction]; + /// Sinsemilla root of the token pool's note commitment tree the token bundle was built against. + fn token_anchor(&self) -> [u8; 32]; + /// Halo 2 proof of the token bundle. + fn token_proof(&self) -> &[u8]; + /// RedPallas binding signature of the token bundle. + fn token_binding_signature(&self) -> [u8; 64]; + /// Orchard actions of the spend bundle in the credit shielded pool that pays the fee. + fn fee_actions(&self) -> &[SerializedAction]; + /// Sinsemilla root of the credit pool's note commitment tree the fee bundle was built against. + fn fee_anchor(&self) -> [u8; 32]; + /// Halo 2 proof of the fee bundle. + fn fee_proof(&self) -> &[u8]; + /// RedPallas binding signature of the fee bundle. + fn fee_binding_signature(&self) -> [u8; 64]; + /// Credits leaving the credit pool: the fee bundle's value balance. + fn credit_amount(&self) -> Credits; + + /// The nullifiers the token bundle spends. + fn token_nullifiers>(&self) -> Vec { + self.token_actions() + .iter() + .map(|action| T::from(action.nullifier)) + .collect() + } + + /// The nullifiers the fee bundle spends. + fn fee_nullifiers>(&self) -> Vec { + self.fee_actions() + .iter() + .map(|action| T::from(action.nullifier)) + .collect() + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/methods/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/methods/mod.rs new file mode 100644 index 00000000000..f3a44afa5ac --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/methods/mod.rs @@ -0,0 +1,75 @@ +mod v0; +pub use v0::*; + +#[cfg(feature = "state-transition-signing")] +use crate::balances::credits::TokenAmount; +#[cfg(feature = "state-transition-signing")] +use crate::data_contract::TokenContractPosition; +#[cfg(feature = "state-transition-signing")] +use crate::fee::Credits; +#[cfg(feature = "state-transition-signing")] +use crate::shielded::SerializedAction; +use crate::state_transition::token_purchase_from_shielded_pool_transition::TokenPurchaseFromShieldedPoolTransition; +#[cfg(feature = "state-transition-signing")] +use crate::{ + state_transition::{ + token_purchase_from_shielded_pool_transition::v0::TokenPurchaseFromShieldedPoolTransitionV0, + StateTransition, + }, + ProtocolError, +}; +#[cfg(feature = "state-transition-signing")] +use platform_value::Identifier; +#[cfg(feature = "state-transition-signing")] +use platform_version::version::PlatformVersion; + +impl TokenPurchaseFromShieldedPoolTransitionMethodsV0 for TokenPurchaseFromShieldedPoolTransition { + #[cfg(feature = "state-transition-signing")] + fn try_from_bundles( + data_contract_id: Identifier, + token_contract_position: TokenContractPosition, + token_id: Identifier, + token_count: TokenAmount, + total_agreed_price: Credits, + token_actions: Vec, + token_anchor: [u8; 32], + token_proof: Vec, + token_binding_signature: [u8; 64], + fee_actions: Vec, + fee_anchor: [u8; 32], + fee_proof: Vec, + fee_binding_signature: [u8; 64], + credit_amount: Credits, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .dpp + .state_transition_serialization_versions + .token_purchase_from_shielded_pool_state_transition + .default_current_version + { + 0 => TokenPurchaseFromShieldedPoolTransitionV0::try_from_bundles( + data_contract_id, + token_contract_position, + token_id, + token_count, + total_agreed_price, + token_actions, + token_anchor, + token_proof, + token_binding_signature, + fee_actions, + fee_anchor, + fee_proof, + fee_binding_signature, + credit_amount, + platform_version, + ), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "TokenPurchaseFromShieldedPoolTransition::try_from_bundles".to_string(), + known_versions: vec![0], + received: version, + }), + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/methods/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/methods/v0/mod.rs new file mode 100644 index 00000000000..69793172077 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/methods/v0/mod.rs @@ -0,0 +1,45 @@ +#[cfg(feature = "state-transition-signing")] +use crate::balances::credits::TokenAmount; +#[cfg(feature = "state-transition-signing")] +use crate::data_contract::TokenContractPosition; +#[cfg(feature = "state-transition-signing")] +use crate::fee::Credits; +#[cfg(feature = "state-transition-signing")] +use crate::shielded::SerializedAction; +use crate::state_transition::StateTransitionType; +#[cfg(feature = "state-transition-signing")] +use crate::{state_transition::StateTransition, ProtocolError}; +#[cfg(feature = "state-transition-signing")] +use platform_value::Identifier; +#[cfg(feature = "state-transition-signing")] +use platform_version::version::PlatformVersion; + +pub trait TokenPurchaseFromShieldedPoolTransitionMethodsV0 { + /// Build the transition from two already proven and signed Orchard bundles: the token pool + /// bundle, whose sighash binds the token id and the transparent fields, and the credit pool + /// fee bundle, whose sighash binds the token id and a digest of the token bundle's actions. + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + fn try_from_bundles( + data_contract_id: Identifier, + token_contract_position: TokenContractPosition, + token_id: Identifier, + token_count: TokenAmount, + total_agreed_price: Credits, + token_actions: Vec, + token_anchor: [u8; 32], + token_proof: Vec, + token_binding_signature: [u8; 64], + fee_actions: Vec, + fee_anchor: [u8; 32], + fee_proof: Vec, + fee_binding_signature: [u8; 64], + credit_amount: Credits, + platform_version: &PlatformVersion, + ) -> Result; + + /// Get State Transition Type + fn get_type() -> StateTransitionType { + StateTransitionType::TokenPurchaseFromShieldedPool + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/mod.rs new file mode 100644 index 00000000000..7a9cf587a78 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/mod.rs @@ -0,0 +1,145 @@ +pub mod accessors; +pub mod methods; +mod state_transition_estimated_fee_validation; +mod state_transition_like; +mod state_transition_validation; +pub mod v0; +mod version; + +use crate::state_transition::token_purchase_from_shielded_pool_transition::v0::TokenPurchaseFromShieldedPoolTransitionV0; +use crate::state_transition::token_purchase_from_shielded_pool_transition::v0::TokenPurchaseFromShieldedPoolTransitionV0Signable; +use crate::state_transition::StateTransitionFieldTypes; + +pub type TokenPurchaseFromShieldedPoolTransitionLatest = TokenPurchaseFromShieldedPoolTransitionV0; + +use crate::identity::state_transition::OptionallyAssetLockProved; +#[cfg(feature = "json-conversion")] +use crate::serialization::JsonConvertible; +#[cfg(feature = "value-conversion")] +use crate::serialization::ValueConvertible; +use crate::ProtocolError; +use bincode::{Decode, DecodeUntrusted, Encode}; +use derive_more::From; +use platform_serialization_derive::{ + PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, PlatformSignable, +}; +use platform_versioning::PlatformVersioned; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; + +/// Token purchase paid from the credit pool. +/// +/// Tokens bought at the token's direct purchase price and minted straight into the token's shielded pool, paid out of the credit shielded pool: an outputs-only Orchard bundle in the token pool (value balance minus `token_count`) and a spend bundle in the credit pool whose value balance is the price plus the fee. The price goes to the contract owner; no identity signs. +#[derive( + Debug, + Clone, + Encode, + Decode, + PlatformDeserializeTrusted, + PlatformDeserializeUntrusted, + PlatformSerialize, + PlatformSignable, + PlatformVersioned, + From, + PartialEq, + DecodeUntrusted, +)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] +#[cfg_attr( + all(feature = "json-conversion", feature = "serde-conversion"), + derive(JsonConvertible) +)] +#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] +#[platform_serialize(unversioned)] //versioned directly, no need to use platform_version +#[platform_version_path_bounds( + "dpp.state_transition_serialization_versions.token_purchase_from_shielded_pool_state_transition" +)] +pub enum TokenPurchaseFromShieldedPoolTransition { + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] + V0(TokenPurchaseFromShieldedPoolTransitionV0), +} + +impl OptionallyAssetLockProved for TokenPurchaseFromShieldedPoolTransition {} + +impl StateTransitionFieldTypes for TokenPurchaseFromShieldedPoolTransition { + fn signature_property_paths() -> Vec<&'static str> { + vec![] + } + + fn identifiers_property_paths() -> Vec<&'static str> { + vec![] + } + + fn binary_property_paths() -> Vec<&'static str> { + vec![] + } +} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::shielded::SerializedAction; + use platform_value::Identifier; + + fn fixture_action() -> SerializedAction { + SerializedAction { + nullifier: [0x11; 32], + rk: [0x22; 32], + cmx: [0x33; 32], + encrypted_note: vec![0x44; 216], + cv_net: [0x55; 32], + spend_auth_sig: [0x66; 64], + } + } + + pub(crate) fn fixture() -> TokenPurchaseFromShieldedPoolTransition { + TokenPurchaseFromShieldedPoolTransition::V0(TokenPurchaseFromShieldedPoolTransitionV0 { + data_contract_id: Identifier::new([0x10; 32]), + token_contract_position: 1, + token_id: Identifier::new([0x12; 32]), + token_count: 1003, + total_agreed_price: 250004, + token_actions: vec![fixture_action()], + token_anchor: [0x76; 32], + token_proof: vec![0x87; 192], + token_binding_signature: [0x98; 64], + fee_actions: vec![fixture_action()], + fee_anchor: [0x7a; 32], + fee_proof: vec![0x8b; 192], + fee_binding_signature: [0x9c; 64], + credit_amount: 250013, + }) + } + + #[test] + fn json_round_trip() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + assert_eq!(json["$formatVersion"], "0"); + assert!(json.get("tokenActions").is_some()); + assert!(json.get("feeActions").is_some()); + let recovered = + TokenPurchaseFromShieldedPoolTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + let recovered = + TokenPurchaseFromShieldedPoolTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/state_transition_estimated_fee_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/state_transition_estimated_fee_validation.rs new file mode 100644 index 00000000000..25d4d5a3138 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/state_transition_estimated_fee_validation.rs @@ -0,0 +1,16 @@ +use crate::fee::Credits; +use crate::state_transition::token_purchase_from_shielded_pool_transition::TokenPurchaseFromShieldedPoolTransition; +use crate::state_transition::StateTransitionEstimatedFeeValidation; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +impl StateTransitionEstimatedFeeValidation for TokenPurchaseFromShieldedPoolTransition { + /// Pool-paid: the fee is carved from the fee bundle's value balance and enforced by the + /// shielded minimum-fee validation, not by an identity balance floor. + fn calculate_min_required_fee( + &self, + _platform_version: &PlatformVersion, + ) -> Result { + Ok(0) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/state_transition_like.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/state_transition_like.rs new file mode 100644 index 00000000000..c5f2b6c68e4 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/state_transition_like.rs @@ -0,0 +1,36 @@ +use crate::state_transition::token_purchase_from_shielded_pool_transition::TokenPurchaseFromShieldedPoolTransition; +use crate::state_transition::{StateTransitionLike, StateTransitionType}; +use crate::version::FeatureVersion; +use platform_value::Identifier; + +impl StateTransitionLike for TokenPurchaseFromShieldedPoolTransition { + fn modified_data_ids(&self) -> Vec { + match self { + TokenPurchaseFromShieldedPoolTransition::V0(transition) => { + transition.modified_data_ids() + } + } + } + + fn state_transition_protocol_version(&self) -> FeatureVersion { + match self { + TokenPurchaseFromShieldedPoolTransition::V0(_) => 0, + } + } + + fn state_transition_type(&self) -> StateTransitionType { + match self { + TokenPurchaseFromShieldedPoolTransition::V0(transition) => { + transition.state_transition_type() + } + } + } + + fn unique_identifiers(&self) -> Vec { + match self { + TokenPurchaseFromShieldedPoolTransition::V0(transition) => { + transition.unique_identifiers() + } + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/state_transition_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/state_transition_validation.rs new file mode 100644 index 00000000000..a9bfaf31a9f --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/state_transition_validation.rs @@ -0,0 +1,17 @@ +use crate::state_transition::token_purchase_from_shielded_pool_transition::TokenPurchaseFromShieldedPoolTransition; +use crate::state_transition::StateTransitionStructureValidation; +use crate::validation::SimpleConsensusValidationResult; +use platform_version::version::PlatformVersion; + +impl StateTransitionStructureValidation for TokenPurchaseFromShieldedPoolTransition { + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> SimpleConsensusValidationResult { + match self { + TokenPurchaseFromShieldedPoolTransition::V0(v0) => { + v0.validate_structure(platform_version) + } + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/mod.rs new file mode 100644 index 00000000000..1ab6a510bcb --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/mod.rs @@ -0,0 +1,75 @@ +mod state_transition_like; +mod state_transition_validation; +mod types; +pub(super) mod v0_methods; +mod version; + +use crate::balances::credits::TokenAmount; +use crate::data_contract::TokenContractPosition; +use crate::fee::Credits; +use crate::prelude::Identifier; +use crate::shielded::SerializedAction; +use crate::ProtocolError; +use bincode::{Decode, DecodeUntrusted, Encode}; +use platform_serialization_derive::{ + PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, PlatformSignable, +}; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; + +/// Token purchase paid from the credit pool, version 0. +/// +/// Tokens bought at the token's direct purchase price and minted straight into the token's shielded pool, paid out of the credit shielded pool: an outputs-only Orchard bundle in the token pool (value balance minus `token_count`) and a spend bundle in the credit pool whose value balance is the price plus the fee. The price goes to the contract owner; no identity signs. +/// +/// The token bundle binds the token id and the transition's transparent fields into its +/// sighash; the fee bundle binds the token id and a digest of the token bundle's actions, so +/// neither bundle can be paired with another. +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] +#[derive( + Debug, + Clone, + Encode, + Decode, + PlatformSerialize, + PlatformDeserializeTrusted, + PlatformDeserializeUntrusted, + PlatformSignable, + PartialEq, + DecodeUntrusted, +)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +#[platform_serialize(unversioned)] +pub struct TokenPurchaseFromShieldedPoolTransitionV0 { + /// The contract defining the token. + pub data_contract_id: Identifier, + /// The token's position in the contract. + pub token_contract_position: TokenContractPosition, + /// The token id, `calculate_token_id(data_contract_id, token_contract_position)`. + pub token_id: Identifier, + /// Tokens bought and minted into the pool; the token bundle's value balance is its negation. + pub token_count: TokenAmount, + /// The credits the buyer agrees to pay for `token_count`, paid out of the credit bundle to the contract owner; `credit_amount` minus this is the fee. + pub total_agreed_price: Credits, + /// Orchard actions of the bundle in the token's shielded pool. + pub token_actions: Vec, + /// Sinsemilla root of the token pool's note commitment tree the token bundle was built against. + pub token_anchor: [u8; 32], + /// Halo 2 proof of the token bundle. + pub token_proof: Vec, + /// RedPallas binding signature of the token bundle. + pub token_binding_signature: [u8; 64], + /// Orchard actions of the spend bundle in the credit shielded pool that pays the fee. + pub fee_actions: Vec, + /// Sinsemilla root of the credit pool's note commitment tree the fee bundle was built against. + pub fee_anchor: [u8; 32], + /// Halo 2 proof of the fee bundle. + pub fee_proof: Vec, + /// RedPallas binding signature of the fee bundle. + pub fee_binding_signature: [u8; 64], + /// Credits leaving the credit pool: the fee bundle's value balance. + pub credit_amount: Credits, +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/state_transition_like.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/state_transition_like.rs new file mode 100644 index 00000000000..45fc00b108b --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/state_transition_like.rs @@ -0,0 +1,39 @@ +use crate::state_transition::token_purchase_from_shielded_pool_transition::v0::TokenPurchaseFromShieldedPoolTransitionV0; +use crate::state_transition::token_purchase_from_shielded_pool_transition::TokenPurchaseFromShieldedPoolTransition; +use crate::state_transition::StateTransition; +use crate::version::FeatureVersion; +use crate::{ + prelude::Identifier, + state_transition::{StateTransitionLike, StateTransitionType}, +}; + +impl From for StateTransition { + fn from(value: TokenPurchaseFromShieldedPoolTransitionV0) -> Self { + let transition: TokenPurchaseFromShieldedPoolTransition = value.into(); + transition.into() + } +} + +impl StateTransitionLike for TokenPurchaseFromShieldedPoolTransitionV0 { + fn state_transition_protocol_version(&self) -> FeatureVersion { + 0 + } + + fn state_transition_type(&self) -> StateTransitionType { + StateTransitionType::TokenPurchaseFromShieldedPool + } + + fn modified_data_ids(&self) -> Vec { + vec![] + } + + /// Unique by every spent nullifier of both bundles: two transitions spending the same note + /// in either pool can never both be valid. + fn unique_identifiers(&self) -> Vec { + self.token_actions + .iter() + .chain(self.fee_actions.iter()) + .map(|action| hex::encode(action.nullifier)) + .collect() + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/state_transition_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/state_transition_validation.rs new file mode 100644 index 00000000000..cee9c94d1c9 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/state_transition_validation.rs @@ -0,0 +1,219 @@ +use crate::consensus::basic::state_transition::ShieldedInvalidValueBalanceError; +use crate::consensus::basic::token::InvalidTokenAmountError; +use crate::consensus::basic::token::InvalidTokenIdError; +use crate::consensus::basic::BasicError; +use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::MAX_DISTRIBUTION_PARAM; +use crate::state_transition::state_transitions::shielded::common_validation::{ + validate_actions_count, validate_anchor_not_zero, validate_encrypted_note_sizes, + validate_proof_not_empty, +}; +use crate::state_transition::token_purchase_from_shielded_pool_transition::v0::TokenPurchaseFromShieldedPoolTransitionV0; +use crate::state_transition::StateTransitionStructureValidation; +use crate::tokens::calculate_token_id; +use crate::validation::SimpleConsensusValidationResult; +use platform_value::Identifier; +use platform_version::version::PlatformVersion; + +impl StateTransitionStructureValidation for TokenPurchaseFromShieldedPoolTransitionV0 { + /// The token id must derive from the contract and position, the amounts must be within + /// bounds and both bundles pass the stateless checks every shielded bundle passes (action + /// count, note sizes, non-empty proof, non-zero anchor). The proofs are verified by the + /// processor once the fee floor has been checked. + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> SimpleConsensusValidationResult { + let calculated_token_id: Identifier = calculate_token_id( + self.data_contract_id.as_bytes(), + self.token_contract_position, + ) + .into(); + if self.token_id != calculated_token_id { + return SimpleConsensusValidationResult::new_with_error( + BasicError::InvalidTokenIdError(InvalidTokenIdError::new( + calculated_token_id, + self.token_id, + )) + .into(), + ); + } + if self.token_count == 0 || self.token_count > MAX_DISTRIBUTION_PARAM { + return SimpleConsensusValidationResult::new_with_error( + BasicError::InvalidTokenAmountError(InvalidTokenAmountError::new( + MAX_DISTRIBUTION_PARAM, + self.token_count, + )) + .into(), + ); + } + if self.total_agreed_price > self.credit_amount { + return SimpleConsensusValidationResult::new_with_error( + BasicError::ShieldedInvalidValueBalanceError( + ShieldedInvalidValueBalanceError::new( + "the credits leaving the pool must cover the agreed price".to_string(), + ), + ) + .into(), + ); + } + if self.credit_amount == 0 { + return SimpleConsensusValidationResult::new_with_error( + BasicError::ShieldedInvalidValueBalanceError( + ShieldedInvalidValueBalanceError::new( + "the credits leaving the pool must be greater than zero".to_string(), + ), + ) + .into(), + ); + } + if self.credit_amount > i64::MAX as u64 { + return SimpleConsensusValidationResult::new_with_error( + BasicError::ShieldedInvalidValueBalanceError( + ShieldedInvalidValueBalanceError::new( + "the credits leaving the pool exceed the maximum allowed value".to_string(), + ), + ) + .into(), + ); + } + let max_actions = platform_version + .system_limits + .max_shielded_transition_actions; + for (actions, proof, anchor) in [ + (&self.token_actions, &self.token_proof, &self.token_anchor), + (&self.fee_actions, &self.fee_proof, &self.fee_anchor), + ] { + let result = validate_actions_count(actions, max_actions); + if !result.is_valid() { + return result; + } + let result = validate_encrypted_note_sizes(actions); + if !result.is_valid() { + return result; + } + let result = validate_proof_not_empty(proof); + if !result.is_valid() { + return result; + } + let result = validate_anchor_not_zero(anchor); + if !result.is_valid() { + return result; + } + } + SimpleConsensusValidationResult::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::consensus::ConsensusError; + use crate::shielded::SerializedAction; + use assert_matches::assert_matches; + + fn action() -> SerializedAction { + SerializedAction { + nullifier: [1u8; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + } + } + + fn valid() -> TokenPurchaseFromShieldedPoolTransitionV0 { + TokenPurchaseFromShieldedPoolTransitionV0 { + data_contract_id: Identifier::from([1u8; 32]), + token_contract_position: 0, + token_id: Identifier::from(calculate_token_id(&[1u8; 32], 0)), + token_count: 1_000, + total_agreed_price: 1_000, + token_actions: vec![action()], + token_anchor: [7u8; 32], + token_proof: vec![8u8; 100], + token_binding_signature: [9u8; 64], + fee_actions: vec![action()], + fee_anchor: [7u8; 32], + fee_proof: vec![8u8; 100], + fee_binding_signature: [9u8; 64], + credit_amount: 2_000, + } + } + + #[test] + fn should_accept_a_well_formed_transition() { + let result = valid().validate_structure(PlatformVersion::latest()); + assert!(result.is_valid(), "{:?}", result.errors); + } + + #[test] + fn should_reject_a_wrong_token_id() { + let mut t = valid(); + t.token_id = Identifier::from([3u8; 32]); + let result = t.validate_structure(PlatformVersion::latest()); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError(BasicError::InvalidTokenIdError( + _ + ))] + ); + } + + #[test] + fn should_reject_a_zero_token_amount() { + let mut t = valid(); + t.token_count = 0; + let result = t.validate_structure(PlatformVersion::latest()); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::InvalidTokenAmountError(_) + )] + ); + } + + #[test] + fn should_reject_a_zero_credit_amount() { + let mut t = valid(); + t.credit_amount = 0; + let result = t.validate_structure(PlatformVersion::latest()); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedInvalidValueBalanceError(_) + )] + ); + } + + #[test] + fn should_reject_a_fee_bundle_without_actions_or_proof() { + let mut t = valid(); + t.fee_actions = vec![]; + let result = t.validate_structure(PlatformVersion::latest()); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedNoActionsError(_) + )] + ); + let mut t = valid(); + t.fee_proof = vec![]; + let result = t.validate_structure(PlatformVersion::latest()); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedEmptyProofError(_) + )] + ); + let mut t = valid(); + t.token_anchor = [0u8; 32]; + let result = t.validate_structure(PlatformVersion::latest()); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedZeroAnchorError(_) + )] + ); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/types.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/types.rs new file mode 100644 index 00000000000..77991748804 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/types.rs @@ -0,0 +1,16 @@ +use crate::state_transition::token_purchase_from_shielded_pool_transition::v0::TokenPurchaseFromShieldedPoolTransitionV0; +use crate::state_transition::StateTransitionFieldTypes; + +impl StateTransitionFieldTypes for TokenPurchaseFromShieldedPoolTransitionV0 { + fn signature_property_paths() -> Vec<&'static str> { + vec![] + } + + fn identifiers_property_paths() -> Vec<&'static str> { + vec![] + } + + fn binary_property_paths() -> Vec<&'static str> { + vec![] + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/v0_methods.rs new file mode 100644 index 00000000000..cc3cce36d72 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/v0_methods.rs @@ -0,0 +1,58 @@ +#[cfg(feature = "state-transition-signing")] +use crate::balances::credits::TokenAmount; +#[cfg(feature = "state-transition-signing")] +use crate::data_contract::TokenContractPosition; +#[cfg(feature = "state-transition-signing")] +use crate::fee::Credits; +#[cfg(feature = "state-transition-signing")] +use crate::shielded::SerializedAction; +use crate::state_transition::token_purchase_from_shielded_pool_transition::methods::TokenPurchaseFromShieldedPoolTransitionMethodsV0; +use crate::state_transition::token_purchase_from_shielded_pool_transition::v0::TokenPurchaseFromShieldedPoolTransitionV0; +#[cfg(feature = "state-transition-signing")] +use crate::{state_transition::StateTransition, ProtocolError}; +#[cfg(feature = "state-transition-signing")] +use platform_value::Identifier; +#[cfg(feature = "state-transition-signing")] +use platform_version::version::PlatformVersion; + +impl TokenPurchaseFromShieldedPoolTransitionMethodsV0 + for TokenPurchaseFromShieldedPoolTransitionV0 +{ + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + fn try_from_bundles( + data_contract_id: Identifier, + token_contract_position: TokenContractPosition, + token_id: Identifier, + token_count: TokenAmount, + total_agreed_price: Credits, + token_actions: Vec, + token_anchor: [u8; 32], + token_proof: Vec, + token_binding_signature: [u8; 64], + fee_actions: Vec, + fee_anchor: [u8; 32], + fee_proof: Vec, + fee_binding_signature: [u8; 64], + credit_amount: Credits, + _platform_version: &PlatformVersion, + ) -> Result { + let transition = TokenPurchaseFromShieldedPoolTransitionV0 { + data_contract_id, + token_contract_position, + token_id, + token_count, + total_agreed_price, + token_actions, + token_anchor, + token_proof, + token_binding_signature, + fee_actions, + fee_anchor, + fee_proof, + fee_binding_signature, + credit_amount, + }; + Ok(transition.into()) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/version.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/version.rs new file mode 100644 index 00000000000..3b4055001ff --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/v0/version.rs @@ -0,0 +1,9 @@ +use crate::state_transition::token_purchase_from_shielded_pool_transition::v0::TokenPurchaseFromShieldedPoolTransitionV0; +use crate::state_transition::FeatureVersioned; +use crate::version::FeatureVersion; + +impl FeatureVersioned for TokenPurchaseFromShieldedPoolTransitionV0 { + fn feature_version(&self) -> FeatureVersion { + 0 + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/version.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/version.rs new file mode 100644 index 00000000000..29606f37f0a --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_purchase_from_shielded_pool_transition/version.rs @@ -0,0 +1,11 @@ +use crate::state_transition::token_purchase_from_shielded_pool_transition::TokenPurchaseFromShieldedPoolTransition; +use crate::state_transition::FeatureVersioned; +use crate::version::FeatureVersion; + +impl FeatureVersioned for TokenPurchaseFromShieldedPoolTransition { + fn feature_version(&self) -> FeatureVersion { + match self { + TokenPurchaseFromShieldedPoolTransition::V0(v0) => v0.feature_version(), + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/accessors/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/accessors/mod.rs new file mode 100644 index 00000000000..ffd6ef77d03 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/accessors/mod.rs @@ -0,0 +1,73 @@ +mod v0; +pub use v0::*; + +use crate::data_contract::TokenContractPosition; +use crate::fee::Credits; +use crate::shielded::SerializedAction; +use crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::TokenShieldedTransferWithShieldedFeeTransition; +use platform_value::Identifier; + +impl TokenShieldedTransferWithShieldedFeeTransitionAccessorsV0 + for TokenShieldedTransferWithShieldedFeeTransition +{ + fn data_contract_id(&self) -> Identifier { + match self { + TokenShieldedTransferWithShieldedFeeTransition::V0(v0) => v0.data_contract_id, + } + } + fn token_contract_position(&self) -> TokenContractPosition { + match self { + TokenShieldedTransferWithShieldedFeeTransition::V0(v0) => v0.token_contract_position, + } + } + fn token_id(&self) -> Identifier { + match self { + TokenShieldedTransferWithShieldedFeeTransition::V0(v0) => v0.token_id, + } + } + fn token_actions(&self) -> &[SerializedAction] { + match self { + TokenShieldedTransferWithShieldedFeeTransition::V0(v0) => &v0.token_actions, + } + } + fn token_anchor(&self) -> [u8; 32] { + match self { + TokenShieldedTransferWithShieldedFeeTransition::V0(v0) => v0.token_anchor, + } + } + fn token_proof(&self) -> &[u8] { + match self { + TokenShieldedTransferWithShieldedFeeTransition::V0(v0) => &v0.token_proof, + } + } + fn token_binding_signature(&self) -> [u8; 64] { + match self { + TokenShieldedTransferWithShieldedFeeTransition::V0(v0) => v0.token_binding_signature, + } + } + fn fee_actions(&self) -> &[SerializedAction] { + match self { + TokenShieldedTransferWithShieldedFeeTransition::V0(v0) => &v0.fee_actions, + } + } + fn fee_anchor(&self) -> [u8; 32] { + match self { + TokenShieldedTransferWithShieldedFeeTransition::V0(v0) => v0.fee_anchor, + } + } + fn fee_proof(&self) -> &[u8] { + match self { + TokenShieldedTransferWithShieldedFeeTransition::V0(v0) => &v0.fee_proof, + } + } + fn fee_binding_signature(&self) -> [u8; 64] { + match self { + TokenShieldedTransferWithShieldedFeeTransition::V0(v0) => v0.fee_binding_signature, + } + } + fn credit_amount(&self) -> Credits { + match self { + TokenShieldedTransferWithShieldedFeeTransition::V0(v0) => v0.credit_amount, + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/accessors/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/accessors/v0/mod.rs new file mode 100644 index 00000000000..aecce19b823 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/accessors/v0/mod.rs @@ -0,0 +1,48 @@ +use crate::data_contract::TokenContractPosition; +use crate::fee::Credits; +use crate::shielded::SerializedAction; +use platform_value::Identifier; + +/// Accessors for the fields of a `TokenShieldedTransferWithShieldedFeeTransition`. +pub trait TokenShieldedTransferWithShieldedFeeTransitionAccessorsV0 { + /// The contract defining the token. + fn data_contract_id(&self) -> Identifier; + /// The token's position in the contract. + fn token_contract_position(&self) -> TokenContractPosition; + /// The token id, `calculate_token_id(data_contract_id, token_contract_position)`. + fn token_id(&self) -> Identifier; + /// Orchard actions of the bundle in the token's shielded pool. + fn token_actions(&self) -> &[SerializedAction]; + /// Sinsemilla root of the token pool's note commitment tree the token bundle was built against. + fn token_anchor(&self) -> [u8; 32]; + /// Halo 2 proof of the token bundle. + fn token_proof(&self) -> &[u8]; + /// RedPallas binding signature of the token bundle. + fn token_binding_signature(&self) -> [u8; 64]; + /// Orchard actions of the spend bundle in the credit shielded pool that pays the fee. + fn fee_actions(&self) -> &[SerializedAction]; + /// Sinsemilla root of the credit pool's note commitment tree the fee bundle was built against. + fn fee_anchor(&self) -> [u8; 32]; + /// Halo 2 proof of the fee bundle. + fn fee_proof(&self) -> &[u8]; + /// RedPallas binding signature of the fee bundle. + fn fee_binding_signature(&self) -> [u8; 64]; + /// Credits leaving the credit pool: the fee bundle's value balance. + fn credit_amount(&self) -> Credits; + + /// The nullifiers the token bundle spends. + fn token_nullifiers>(&self) -> Vec { + self.token_actions() + .iter() + .map(|action| T::from(action.nullifier)) + .collect() + } + + /// The nullifiers the fee bundle spends. + fn fee_nullifiers>(&self) -> Vec { + self.fee_actions() + .iter() + .map(|action| T::from(action.nullifier)) + .collect() + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/methods/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/methods/mod.rs new file mode 100644 index 00000000000..78ac591f5bf --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/methods/mod.rs @@ -0,0 +1,72 @@ +mod v0; +pub use v0::*; + +#[cfg(feature = "state-transition-signing")] +use crate::data_contract::TokenContractPosition; +#[cfg(feature = "state-transition-signing")] +use crate::fee::Credits; +#[cfg(feature = "state-transition-signing")] +use crate::shielded::SerializedAction; +use crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::TokenShieldedTransferWithShieldedFeeTransition; +#[cfg(feature = "state-transition-signing")] +use crate::{ + state_transition::{ + token_shielded_transfer_with_shielded_fee_transition::v0::TokenShieldedTransferWithShieldedFeeTransitionV0, + StateTransition, + }, + ProtocolError, +}; +#[cfg(feature = "state-transition-signing")] +use platform_value::Identifier; +#[cfg(feature = "state-transition-signing")] +use platform_version::version::PlatformVersion; + +impl TokenShieldedTransferWithShieldedFeeTransitionMethodsV0 + for TokenShieldedTransferWithShieldedFeeTransition +{ + #[cfg(feature = "state-transition-signing")] + fn try_from_bundles( + data_contract_id: Identifier, + token_contract_position: TokenContractPosition, + token_id: Identifier, + token_actions: Vec, + token_anchor: [u8; 32], + token_proof: Vec, + token_binding_signature: [u8; 64], + fee_actions: Vec, + fee_anchor: [u8; 32], + fee_proof: Vec, + fee_binding_signature: [u8; 64], + credit_amount: Credits, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .dpp + .state_transition_serialization_versions + .token_shielded_transfer_with_shielded_fee_state_transition + .default_current_version + { + 0 => TokenShieldedTransferWithShieldedFeeTransitionV0::try_from_bundles( + data_contract_id, + token_contract_position, + token_id, + token_actions, + token_anchor, + token_proof, + token_binding_signature, + fee_actions, + fee_anchor, + fee_proof, + fee_binding_signature, + credit_amount, + platform_version, + ), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "TokenShieldedTransferWithShieldedFeeTransition::try_from_bundles" + .to_string(), + known_versions: vec![0], + received: version, + }), + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/methods/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/methods/v0/mod.rs new file mode 100644 index 00000000000..6af06c52055 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/methods/v0/mod.rs @@ -0,0 +1,41 @@ +#[cfg(feature = "state-transition-signing")] +use crate::data_contract::TokenContractPosition; +#[cfg(feature = "state-transition-signing")] +use crate::fee::Credits; +#[cfg(feature = "state-transition-signing")] +use crate::shielded::SerializedAction; +use crate::state_transition::StateTransitionType; +#[cfg(feature = "state-transition-signing")] +use crate::{state_transition::StateTransition, ProtocolError}; +#[cfg(feature = "state-transition-signing")] +use platform_value::Identifier; +#[cfg(feature = "state-transition-signing")] +use platform_version::version::PlatformVersion; + +pub trait TokenShieldedTransferWithShieldedFeeTransitionMethodsV0 { + /// Build the transition from two already proven and signed Orchard bundles: the token pool + /// bundle, whose sighash binds the token id and the transparent fields, and the credit pool + /// fee bundle, whose sighash binds the token id and a digest of the token bundle's actions. + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + fn try_from_bundles( + data_contract_id: Identifier, + token_contract_position: TokenContractPosition, + token_id: Identifier, + token_actions: Vec, + token_anchor: [u8; 32], + token_proof: Vec, + token_binding_signature: [u8; 64], + fee_actions: Vec, + fee_anchor: [u8; 32], + fee_proof: Vec, + fee_binding_signature: [u8; 64], + credit_amount: Credits, + platform_version: &PlatformVersion, + ) -> Result; + + /// Get State Transition Type + fn get_type() -> StateTransitionType { + StateTransitionType::TokenShieldedTransferWithShieldedFee + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/mod.rs new file mode 100644 index 00000000000..d25fe01db78 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/mod.rs @@ -0,0 +1,146 @@ +pub mod accessors; +pub mod methods; +mod state_transition_estimated_fee_validation; +mod state_transition_like; +mod state_transition_validation; +pub mod v0; +mod version; + +use crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::v0::TokenShieldedTransferWithShieldedFeeTransitionV0; +use crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::v0::TokenShieldedTransferWithShieldedFeeTransitionV0Signable; +use crate::state_transition::StateTransitionFieldTypes; + +pub type TokenShieldedTransferWithShieldedFeeTransitionLatest = + TokenShieldedTransferWithShieldedFeeTransitionV0; + +use crate::identity::state_transition::OptionallyAssetLockProved; +#[cfg(feature = "json-conversion")] +use crate::serialization::JsonConvertible; +#[cfg(feature = "value-conversion")] +use crate::serialization::ValueConvertible; +use crate::ProtocolError; +use bincode::{Decode, DecodeUntrusted, Encode}; +use derive_more::From; +use platform_serialization_derive::{ + PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, PlatformSignable, +}; +use platform_versioning::PlatformVersioned; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; + +/// Token shielded transfer paid from the credit pool. +/// +/// A transfer inside a token's shielded pool whose fee is paid out of the credit shielded pool: an Orchard spend bundle in the token pool (value balance zero) and one in the credit pool (value balance the fee), both authorized by spend keys. No identity signs or is named anywhere. +#[derive( + Debug, + Clone, + Encode, + Decode, + PlatformDeserializeTrusted, + PlatformDeserializeUntrusted, + PlatformSerialize, + PlatformSignable, + PlatformVersioned, + From, + PartialEq, + DecodeUntrusted, +)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] +#[cfg_attr( + all(feature = "json-conversion", feature = "serde-conversion"), + derive(JsonConvertible) +)] +#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] +#[platform_serialize(unversioned)] //versioned directly, no need to use platform_version +#[platform_version_path_bounds( + "dpp.state_transition_serialization_versions.token_shielded_transfer_with_shielded_fee_state_transition" +)] +pub enum TokenShieldedTransferWithShieldedFeeTransition { + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] + V0(TokenShieldedTransferWithShieldedFeeTransitionV0), +} + +impl OptionallyAssetLockProved for TokenShieldedTransferWithShieldedFeeTransition {} + +impl StateTransitionFieldTypes for TokenShieldedTransferWithShieldedFeeTransition { + fn signature_property_paths() -> Vec<&'static str> { + vec![] + } + + fn identifiers_property_paths() -> Vec<&'static str> { + vec![] + } + + fn binary_property_paths() -> Vec<&'static str> { + vec![] + } +} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::shielded::SerializedAction; + use platform_value::Identifier; + + fn fixture_action() -> SerializedAction { + SerializedAction { + nullifier: [0x11; 32], + rk: [0x22; 32], + cmx: [0x33; 32], + encrypted_note: vec![0x44; 216], + cv_net: [0x55; 32], + spend_auth_sig: [0x66; 64], + } + } + + pub(crate) fn fixture() -> TokenShieldedTransferWithShieldedFeeTransition { + TokenShieldedTransferWithShieldedFeeTransition::V0( + TokenShieldedTransferWithShieldedFeeTransitionV0 { + data_contract_id: Identifier::new([0x10; 32]), + token_contract_position: 1, + token_id: Identifier::new([0x12; 32]), + token_actions: vec![fixture_action()], + token_anchor: [0x74; 32], + token_proof: vec![0x85; 192], + token_binding_signature: [0x96; 64], + fee_actions: vec![fixture_action()], + fee_anchor: [0x78; 32], + fee_proof: vec![0x89; 192], + fee_binding_signature: [0x9a; 64], + credit_amount: 250011, + }, + ) + } + + #[test] + fn json_round_trip() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + assert_eq!(json["$formatVersion"], "0"); + assert!(json.get("tokenActions").is_some()); + assert!(json.get("feeActions").is_some()); + let recovered = + TokenShieldedTransferWithShieldedFeeTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + let recovered = TokenShieldedTransferWithShieldedFeeTransition::from_object(value) + .expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/state_transition_estimated_fee_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/state_transition_estimated_fee_validation.rs new file mode 100644 index 00000000000..605feaeeb92 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/state_transition_estimated_fee_validation.rs @@ -0,0 +1,16 @@ +use crate::fee::Credits; +use crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::TokenShieldedTransferWithShieldedFeeTransition; +use crate::state_transition::StateTransitionEstimatedFeeValidation; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +impl StateTransitionEstimatedFeeValidation for TokenShieldedTransferWithShieldedFeeTransition { + /// Pool-paid: the fee is carved from the fee bundle's value balance and enforced by the + /// shielded minimum-fee validation, not by an identity balance floor. + fn calculate_min_required_fee( + &self, + _platform_version: &PlatformVersion, + ) -> Result { + Ok(0) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/state_transition_like.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/state_transition_like.rs new file mode 100644 index 00000000000..231afe9018a --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/state_transition_like.rs @@ -0,0 +1,36 @@ +use crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::TokenShieldedTransferWithShieldedFeeTransition; +use crate::state_transition::{StateTransitionLike, StateTransitionType}; +use crate::version::FeatureVersion; +use platform_value::Identifier; + +impl StateTransitionLike for TokenShieldedTransferWithShieldedFeeTransition { + fn modified_data_ids(&self) -> Vec { + match self { + TokenShieldedTransferWithShieldedFeeTransition::V0(transition) => { + transition.modified_data_ids() + } + } + } + + fn state_transition_protocol_version(&self) -> FeatureVersion { + match self { + TokenShieldedTransferWithShieldedFeeTransition::V0(_) => 0, + } + } + + fn state_transition_type(&self) -> StateTransitionType { + match self { + TokenShieldedTransferWithShieldedFeeTransition::V0(transition) => { + transition.state_transition_type() + } + } + } + + fn unique_identifiers(&self) -> Vec { + match self { + TokenShieldedTransferWithShieldedFeeTransition::V0(transition) => { + transition.unique_identifiers() + } + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/state_transition_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/state_transition_validation.rs new file mode 100644 index 00000000000..ceab1713a19 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/state_transition_validation.rs @@ -0,0 +1,17 @@ +use crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::TokenShieldedTransferWithShieldedFeeTransition; +use crate::state_transition::StateTransitionStructureValidation; +use crate::validation::SimpleConsensusValidationResult; +use platform_version::version::PlatformVersion; + +impl StateTransitionStructureValidation for TokenShieldedTransferWithShieldedFeeTransition { + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> SimpleConsensusValidationResult { + match self { + TokenShieldedTransferWithShieldedFeeTransition::V0(v0) => { + v0.validate_structure(platform_version) + } + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/mod.rs new file mode 100644 index 00000000000..bb273b6bbb7 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/mod.rs @@ -0,0 +1,70 @@ +mod state_transition_like; +mod state_transition_validation; +mod types; +pub(super) mod v0_methods; +mod version; + +use crate::data_contract::TokenContractPosition; +use crate::fee::Credits; +use crate::prelude::Identifier; +use crate::shielded::SerializedAction; +use crate::ProtocolError; +use bincode::{Decode, DecodeUntrusted, Encode}; +use platform_serialization_derive::{ + PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, PlatformSignable, +}; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; + +/// Token shielded transfer paid from the credit pool, version 0. +/// +/// A transfer inside a token's shielded pool whose fee is paid out of the credit shielded pool: an Orchard spend bundle in the token pool (value balance zero) and one in the credit pool (value balance the fee), both authorized by spend keys. No identity signs or is named anywhere. +/// +/// The token bundle binds the token id and the transition's transparent fields into its +/// sighash; the fee bundle binds the token id and a digest of the token bundle's actions, so +/// neither bundle can be paired with another. +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] +#[derive( + Debug, + Clone, + Encode, + Decode, + PlatformSerialize, + PlatformDeserializeTrusted, + PlatformDeserializeUntrusted, + PlatformSignable, + PartialEq, + DecodeUntrusted, +)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +#[platform_serialize(unversioned)] +pub struct TokenShieldedTransferWithShieldedFeeTransitionV0 { + /// The contract defining the token. + pub data_contract_id: Identifier, + /// The token's position in the contract. + pub token_contract_position: TokenContractPosition, + /// The token id, `calculate_token_id(data_contract_id, token_contract_position)`. + pub token_id: Identifier, + /// Orchard actions of the bundle in the token's shielded pool. + pub token_actions: Vec, + /// Sinsemilla root of the token pool's note commitment tree the token bundle was built against. + pub token_anchor: [u8; 32], + /// Halo 2 proof of the token bundle. + pub token_proof: Vec, + /// RedPallas binding signature of the token bundle. + pub token_binding_signature: [u8; 64], + /// Orchard actions of the spend bundle in the credit shielded pool that pays the fee. + pub fee_actions: Vec, + /// Sinsemilla root of the credit pool's note commitment tree the fee bundle was built against. + pub fee_anchor: [u8; 32], + /// Halo 2 proof of the fee bundle. + pub fee_proof: Vec, + /// RedPallas binding signature of the fee bundle. + pub fee_binding_signature: [u8; 64], + /// Credits leaving the credit pool: the fee bundle's value balance. + pub credit_amount: Credits, +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/state_transition_like.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/state_transition_like.rs new file mode 100644 index 00000000000..7cc3761bb7a --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/state_transition_like.rs @@ -0,0 +1,39 @@ +use crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::v0::TokenShieldedTransferWithShieldedFeeTransitionV0; +use crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::TokenShieldedTransferWithShieldedFeeTransition; +use crate::state_transition::StateTransition; +use crate::version::FeatureVersion; +use crate::{ + prelude::Identifier, + state_transition::{StateTransitionLike, StateTransitionType}, +}; + +impl From for StateTransition { + fn from(value: TokenShieldedTransferWithShieldedFeeTransitionV0) -> Self { + let transition: TokenShieldedTransferWithShieldedFeeTransition = value.into(); + transition.into() + } +} + +impl StateTransitionLike for TokenShieldedTransferWithShieldedFeeTransitionV0 { + fn state_transition_protocol_version(&self) -> FeatureVersion { + 0 + } + + fn state_transition_type(&self) -> StateTransitionType { + StateTransitionType::TokenShieldedTransferWithShieldedFee + } + + fn modified_data_ids(&self) -> Vec { + vec![] + } + + /// Unique by every spent nullifier of both bundles: two transitions spending the same note + /// in either pool can never both be valid. + fn unique_identifiers(&self) -> Vec { + self.token_actions + .iter() + .chain(self.fee_actions.iter()) + .map(|action| hex::encode(action.nullifier)) + .collect() + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/state_transition_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/state_transition_validation.rs new file mode 100644 index 00000000000..c486f506926 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/state_transition_validation.rs @@ -0,0 +1,183 @@ +use crate::consensus::basic::state_transition::ShieldedInvalidValueBalanceError; +use crate::consensus::basic::token::InvalidTokenIdError; +use crate::consensus::basic::BasicError; +use crate::state_transition::state_transitions::shielded::common_validation::{ + validate_actions_count, validate_anchor_not_zero, validate_encrypted_note_sizes, + validate_proof_not_empty, +}; +use crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::v0::TokenShieldedTransferWithShieldedFeeTransitionV0; +use crate::state_transition::StateTransitionStructureValidation; +use crate::tokens::calculate_token_id; +use crate::validation::SimpleConsensusValidationResult; +use platform_value::Identifier; +use platform_version::version::PlatformVersion; + +impl StateTransitionStructureValidation for TokenShieldedTransferWithShieldedFeeTransitionV0 { + /// The token id must derive from the contract and position, the amounts must be within + /// bounds and both bundles pass the stateless checks every shielded bundle passes (action + /// count, note sizes, non-empty proof, non-zero anchor). The proofs are verified by the + /// processor once the fee floor has been checked. + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> SimpleConsensusValidationResult { + let calculated_token_id: Identifier = calculate_token_id( + self.data_contract_id.as_bytes(), + self.token_contract_position, + ) + .into(); + if self.token_id != calculated_token_id { + return SimpleConsensusValidationResult::new_with_error( + BasicError::InvalidTokenIdError(InvalidTokenIdError::new( + calculated_token_id, + self.token_id, + )) + .into(), + ); + } + if self.credit_amount == 0 { + return SimpleConsensusValidationResult::new_with_error( + BasicError::ShieldedInvalidValueBalanceError( + ShieldedInvalidValueBalanceError::new( + "the credits leaving the pool must be greater than zero".to_string(), + ), + ) + .into(), + ); + } + if self.credit_amount > i64::MAX as u64 { + return SimpleConsensusValidationResult::new_with_error( + BasicError::ShieldedInvalidValueBalanceError( + ShieldedInvalidValueBalanceError::new( + "the credits leaving the pool exceed the maximum allowed value".to_string(), + ), + ) + .into(), + ); + } + let max_actions = platform_version + .system_limits + .max_shielded_transition_actions; + for (actions, proof, anchor) in [ + (&self.token_actions, &self.token_proof, &self.token_anchor), + (&self.fee_actions, &self.fee_proof, &self.fee_anchor), + ] { + let result = validate_actions_count(actions, max_actions); + if !result.is_valid() { + return result; + } + let result = validate_encrypted_note_sizes(actions); + if !result.is_valid() { + return result; + } + let result = validate_proof_not_empty(proof); + if !result.is_valid() { + return result; + } + let result = validate_anchor_not_zero(anchor); + if !result.is_valid() { + return result; + } + } + SimpleConsensusValidationResult::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::consensus::ConsensusError; + use crate::shielded::SerializedAction; + use assert_matches::assert_matches; + + fn action() -> SerializedAction { + SerializedAction { + nullifier: [1u8; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + } + } + + fn valid() -> TokenShieldedTransferWithShieldedFeeTransitionV0 { + TokenShieldedTransferWithShieldedFeeTransitionV0 { + data_contract_id: Identifier::from([1u8; 32]), + token_contract_position: 0, + token_id: Identifier::from(calculate_token_id(&[1u8; 32], 0)), + token_actions: vec![action()], + token_anchor: [7u8; 32], + token_proof: vec![8u8; 100], + token_binding_signature: [9u8; 64], + fee_actions: vec![action()], + fee_anchor: [7u8; 32], + fee_proof: vec![8u8; 100], + fee_binding_signature: [9u8; 64], + credit_amount: 2_000, + } + } + + #[test] + fn should_accept_a_well_formed_transition() { + let result = valid().validate_structure(PlatformVersion::latest()); + assert!(result.is_valid(), "{:?}", result.errors); + } + + #[test] + fn should_reject_a_wrong_token_id() { + let mut t = valid(); + t.token_id = Identifier::from([3u8; 32]); + let result = t.validate_structure(PlatformVersion::latest()); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError(BasicError::InvalidTokenIdError( + _ + ))] + ); + } + + #[test] + fn should_reject_a_zero_credit_amount() { + let mut t = valid(); + t.credit_amount = 0; + let result = t.validate_structure(PlatformVersion::latest()); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedInvalidValueBalanceError(_) + )] + ); + } + + #[test] + fn should_reject_a_fee_bundle_without_actions_or_proof() { + let mut t = valid(); + t.fee_actions = vec![]; + let result = t.validate_structure(PlatformVersion::latest()); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedNoActionsError(_) + )] + ); + let mut t = valid(); + t.fee_proof = vec![]; + let result = t.validate_structure(PlatformVersion::latest()); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedEmptyProofError(_) + )] + ); + let mut t = valid(); + t.token_anchor = [0u8; 32]; + let result = t.validate_structure(PlatformVersion::latest()); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedZeroAnchorError(_) + )] + ); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/types.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/types.rs new file mode 100644 index 00000000000..16e8c3e2868 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/types.rs @@ -0,0 +1,16 @@ +use crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::v0::TokenShieldedTransferWithShieldedFeeTransitionV0; +use crate::state_transition::StateTransitionFieldTypes; + +impl StateTransitionFieldTypes for TokenShieldedTransferWithShieldedFeeTransitionV0 { + fn signature_property_paths() -> Vec<&'static str> { + vec![] + } + + fn identifiers_property_paths() -> Vec<&'static str> { + vec![] + } + + fn binary_property_paths() -> Vec<&'static str> { + vec![] + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/v0_methods.rs new file mode 100644 index 00000000000..fa120efc610 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/v0_methods.rs @@ -0,0 +1,52 @@ +#[cfg(feature = "state-transition-signing")] +use crate::data_contract::TokenContractPosition; +#[cfg(feature = "state-transition-signing")] +use crate::fee::Credits; +#[cfg(feature = "state-transition-signing")] +use crate::shielded::SerializedAction; +use crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::methods::TokenShieldedTransferWithShieldedFeeTransitionMethodsV0; +use crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::v0::TokenShieldedTransferWithShieldedFeeTransitionV0; +#[cfg(feature = "state-transition-signing")] +use crate::{state_transition::StateTransition, ProtocolError}; +#[cfg(feature = "state-transition-signing")] +use platform_value::Identifier; +#[cfg(feature = "state-transition-signing")] +use platform_version::version::PlatformVersion; + +impl TokenShieldedTransferWithShieldedFeeTransitionMethodsV0 + for TokenShieldedTransferWithShieldedFeeTransitionV0 +{ + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + fn try_from_bundles( + data_contract_id: Identifier, + token_contract_position: TokenContractPosition, + token_id: Identifier, + token_actions: Vec, + token_anchor: [u8; 32], + token_proof: Vec, + token_binding_signature: [u8; 64], + fee_actions: Vec, + fee_anchor: [u8; 32], + fee_proof: Vec, + fee_binding_signature: [u8; 64], + credit_amount: Credits, + _platform_version: &PlatformVersion, + ) -> Result { + let transition = TokenShieldedTransferWithShieldedFeeTransitionV0 { + data_contract_id, + token_contract_position, + token_id, + token_actions, + token_anchor, + token_proof, + token_binding_signature, + fee_actions, + fee_anchor, + fee_proof, + fee_binding_signature, + credit_amount, + }; + Ok(transition.into()) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/version.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/version.rs new file mode 100644 index 00000000000..83f27a9fdaf --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/v0/version.rs @@ -0,0 +1,9 @@ +use crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::v0::TokenShieldedTransferWithShieldedFeeTransitionV0; +use crate::state_transition::FeatureVersioned; +use crate::version::FeatureVersion; + +impl FeatureVersioned for TokenShieldedTransferWithShieldedFeeTransitionV0 { + fn feature_version(&self) -> FeatureVersion { + 0 + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/version.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/version.rs new file mode 100644 index 00000000000..d469d96730b --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_shielded_transfer_with_shielded_fee_transition/version.rs @@ -0,0 +1,11 @@ +use crate::state_transition::token_shielded_transfer_with_shielded_fee_transition::TokenShieldedTransferWithShieldedFeeTransition; +use crate::state_transition::FeatureVersioned; +use crate::version::FeatureVersion; + +impl FeatureVersioned for TokenShieldedTransferWithShieldedFeeTransition { + fn feature_version(&self) -> FeatureVersion { + match self { + TokenShieldedTransferWithShieldedFeeTransition::V0(v0) => v0.feature_version(), + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/accessors/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/accessors/mod.rs new file mode 100644 index 00000000000..38b7e1e28eb --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/accessors/mod.rs @@ -0,0 +1,82 @@ +mod v0; +pub use v0::*; + +use crate::balances::credits::TokenAmount; +use crate::data_contract::TokenContractPosition; +use crate::fee::Credits; +use crate::shielded::SerializedAction; +use crate::state_transition::token_unshield_with_shielded_fee_transition::TokenUnshieldWithShieldedFeeTransition; +use platform_value::Identifier; + +impl TokenUnshieldWithShieldedFeeTransitionAccessorsV0 for TokenUnshieldWithShieldedFeeTransition { + fn data_contract_id(&self) -> Identifier { + match self { + TokenUnshieldWithShieldedFeeTransition::V0(v0) => v0.data_contract_id, + } + } + fn token_contract_position(&self) -> TokenContractPosition { + match self { + TokenUnshieldWithShieldedFeeTransition::V0(v0) => v0.token_contract_position, + } + } + fn token_id(&self) -> Identifier { + match self { + TokenUnshieldWithShieldedFeeTransition::V0(v0) => v0.token_id, + } + } + fn recipient_id(&self) -> Identifier { + match self { + TokenUnshieldWithShieldedFeeTransition::V0(v0) => v0.recipient_id, + } + } + fn amount(&self) -> TokenAmount { + match self { + TokenUnshieldWithShieldedFeeTransition::V0(v0) => v0.amount, + } + } + fn token_actions(&self) -> &[SerializedAction] { + match self { + TokenUnshieldWithShieldedFeeTransition::V0(v0) => &v0.token_actions, + } + } + fn token_anchor(&self) -> [u8; 32] { + match self { + TokenUnshieldWithShieldedFeeTransition::V0(v0) => v0.token_anchor, + } + } + fn token_proof(&self) -> &[u8] { + match self { + TokenUnshieldWithShieldedFeeTransition::V0(v0) => &v0.token_proof, + } + } + fn token_binding_signature(&self) -> [u8; 64] { + match self { + TokenUnshieldWithShieldedFeeTransition::V0(v0) => v0.token_binding_signature, + } + } + fn fee_actions(&self) -> &[SerializedAction] { + match self { + TokenUnshieldWithShieldedFeeTransition::V0(v0) => &v0.fee_actions, + } + } + fn fee_anchor(&self) -> [u8; 32] { + match self { + TokenUnshieldWithShieldedFeeTransition::V0(v0) => v0.fee_anchor, + } + } + fn fee_proof(&self) -> &[u8] { + match self { + TokenUnshieldWithShieldedFeeTransition::V0(v0) => &v0.fee_proof, + } + } + fn fee_binding_signature(&self) -> [u8; 64] { + match self { + TokenUnshieldWithShieldedFeeTransition::V0(v0) => v0.fee_binding_signature, + } + } + fn credit_amount(&self) -> Credits { + match self { + TokenUnshieldWithShieldedFeeTransition::V0(v0) => v0.credit_amount, + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/accessors/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/accessors/v0/mod.rs new file mode 100644 index 00000000000..4814ed08cb2 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/accessors/v0/mod.rs @@ -0,0 +1,53 @@ +use crate::balances::credits::TokenAmount; +use crate::data_contract::TokenContractPosition; +use crate::fee::Credits; +use crate::shielded::SerializedAction; +use platform_value::Identifier; + +/// Accessors for the fields of a `TokenUnshieldWithShieldedFeeTransition`. +pub trait TokenUnshieldWithShieldedFeeTransitionAccessorsV0 { + /// The contract defining the token. + fn data_contract_id(&self) -> Identifier; + /// The token's position in the contract. + fn token_contract_position(&self) -> TokenContractPosition; + /// The token id, `calculate_token_id(data_contract_id, token_contract_position)`. + fn token_id(&self) -> Identifier; + /// The identity whose token balance receives `amount`. + fn recipient_id(&self) -> Identifier; + /// Tokens leaving the pool into `recipient_id`'s balance: the token bundle's value balance. + fn amount(&self) -> TokenAmount; + /// Orchard actions of the bundle in the token's shielded pool. + fn token_actions(&self) -> &[SerializedAction]; + /// Sinsemilla root of the token pool's note commitment tree the token bundle was built against. + fn token_anchor(&self) -> [u8; 32]; + /// Halo 2 proof of the token bundle. + fn token_proof(&self) -> &[u8]; + /// RedPallas binding signature of the token bundle. + fn token_binding_signature(&self) -> [u8; 64]; + /// Orchard actions of the spend bundle in the credit shielded pool that pays the fee. + fn fee_actions(&self) -> &[SerializedAction]; + /// Sinsemilla root of the credit pool's note commitment tree the fee bundle was built against. + fn fee_anchor(&self) -> [u8; 32]; + /// Halo 2 proof of the fee bundle. + fn fee_proof(&self) -> &[u8]; + /// RedPallas binding signature of the fee bundle. + fn fee_binding_signature(&self) -> [u8; 64]; + /// Credits leaving the credit pool: the fee bundle's value balance. + fn credit_amount(&self) -> Credits; + + /// The nullifiers the token bundle spends. + fn token_nullifiers>(&self) -> Vec { + self.token_actions() + .iter() + .map(|action| T::from(action.nullifier)) + .collect() + } + + /// The nullifiers the fee bundle spends. + fn fee_nullifiers>(&self) -> Vec { + self.fee_actions() + .iter() + .map(|action| T::from(action.nullifier)) + .collect() + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/methods/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/methods/mod.rs new file mode 100644 index 00000000000..f7764ce93bf --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/methods/mod.rs @@ -0,0 +1,75 @@ +mod v0; +pub use v0::*; + +#[cfg(feature = "state-transition-signing")] +use crate::balances::credits::TokenAmount; +#[cfg(feature = "state-transition-signing")] +use crate::data_contract::TokenContractPosition; +#[cfg(feature = "state-transition-signing")] +use crate::fee::Credits; +#[cfg(feature = "state-transition-signing")] +use crate::shielded::SerializedAction; +use crate::state_transition::token_unshield_with_shielded_fee_transition::TokenUnshieldWithShieldedFeeTransition; +#[cfg(feature = "state-transition-signing")] +use crate::{ + state_transition::{ + token_unshield_with_shielded_fee_transition::v0::TokenUnshieldWithShieldedFeeTransitionV0, + StateTransition, + }, + ProtocolError, +}; +#[cfg(feature = "state-transition-signing")] +use platform_value::Identifier; +#[cfg(feature = "state-transition-signing")] +use platform_version::version::PlatformVersion; + +impl TokenUnshieldWithShieldedFeeTransitionMethodsV0 for TokenUnshieldWithShieldedFeeTransition { + #[cfg(feature = "state-transition-signing")] + fn try_from_bundles( + data_contract_id: Identifier, + token_contract_position: TokenContractPosition, + token_id: Identifier, + recipient_id: Identifier, + amount: TokenAmount, + token_actions: Vec, + token_anchor: [u8; 32], + token_proof: Vec, + token_binding_signature: [u8; 64], + fee_actions: Vec, + fee_anchor: [u8; 32], + fee_proof: Vec, + fee_binding_signature: [u8; 64], + credit_amount: Credits, + platform_version: &PlatformVersion, + ) -> Result { + match platform_version + .dpp + .state_transition_serialization_versions + .token_unshield_with_shielded_fee_state_transition + .default_current_version + { + 0 => TokenUnshieldWithShieldedFeeTransitionV0::try_from_bundles( + data_contract_id, + token_contract_position, + token_id, + recipient_id, + amount, + token_actions, + token_anchor, + token_proof, + token_binding_signature, + fee_actions, + fee_anchor, + fee_proof, + fee_binding_signature, + credit_amount, + platform_version, + ), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "TokenUnshieldWithShieldedFeeTransition::try_from_bundles".to_string(), + known_versions: vec![0], + received: version, + }), + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/methods/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/methods/v0/mod.rs new file mode 100644 index 00000000000..a9282586d4e --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/methods/v0/mod.rs @@ -0,0 +1,45 @@ +#[cfg(feature = "state-transition-signing")] +use crate::balances::credits::TokenAmount; +#[cfg(feature = "state-transition-signing")] +use crate::data_contract::TokenContractPosition; +#[cfg(feature = "state-transition-signing")] +use crate::fee::Credits; +#[cfg(feature = "state-transition-signing")] +use crate::shielded::SerializedAction; +use crate::state_transition::StateTransitionType; +#[cfg(feature = "state-transition-signing")] +use crate::{state_transition::StateTransition, ProtocolError}; +#[cfg(feature = "state-transition-signing")] +use platform_value::Identifier; +#[cfg(feature = "state-transition-signing")] +use platform_version::version::PlatformVersion; + +pub trait TokenUnshieldWithShieldedFeeTransitionMethodsV0 { + /// Build the transition from two already proven and signed Orchard bundles: the token pool + /// bundle, whose sighash binds the token id and the transparent fields, and the credit pool + /// fee bundle, whose sighash binds the token id and a digest of the token bundle's actions. + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + fn try_from_bundles( + data_contract_id: Identifier, + token_contract_position: TokenContractPosition, + token_id: Identifier, + recipient_id: Identifier, + amount: TokenAmount, + token_actions: Vec, + token_anchor: [u8; 32], + token_proof: Vec, + token_binding_signature: [u8; 64], + fee_actions: Vec, + fee_anchor: [u8; 32], + fee_proof: Vec, + fee_binding_signature: [u8; 64], + credit_amount: Credits, + platform_version: &PlatformVersion, + ) -> Result; + + /// Get State Transition Type + fn get_type() -> StateTransitionType { + StateTransitionType::TokenUnshieldWithShieldedFee + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/mod.rs new file mode 100644 index 00000000000..345aa917e79 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/mod.rs @@ -0,0 +1,144 @@ +pub mod accessors; +pub mod methods; +mod state_transition_estimated_fee_validation; +mod state_transition_like; +mod state_transition_validation; +pub mod v0; +mod version; + +use crate::state_transition::token_unshield_with_shielded_fee_transition::v0::TokenUnshieldWithShieldedFeeTransitionV0; +use crate::state_transition::token_unshield_with_shielded_fee_transition::v0::TokenUnshieldWithShieldedFeeTransitionV0Signable; +use crate::state_transition::StateTransitionFieldTypes; + +pub type TokenUnshieldWithShieldedFeeTransitionLatest = TokenUnshieldWithShieldedFeeTransitionV0; + +use crate::identity::state_transition::OptionallyAssetLockProved; +#[cfg(feature = "json-conversion")] +use crate::serialization::JsonConvertible; +#[cfg(feature = "value-conversion")] +use crate::serialization::ValueConvertible; +use crate::ProtocolError; +use bincode::{Decode, DecodeUntrusted, Encode}; +use derive_more::From; +use platform_serialization_derive::{ + PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, PlatformSignable, +}; +use platform_versioning::PlatformVersioned; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; + +/// Identity-less unshield. +/// +/// Tokens leaving a token's shielded pool into an identity's token balance, with the fee paid out of the credit shielded pool: an Orchard spend bundle in the token pool (value balance `amount`) and one in the credit pool (value balance the fee). No identity signs; the recipient and amount are bound into the token bundle's sighash. +#[derive( + Debug, + Clone, + Encode, + Decode, + PlatformDeserializeTrusted, + PlatformDeserializeUntrusted, + PlatformSerialize, + PlatformSignable, + PlatformVersioned, + From, + PartialEq, + DecodeUntrusted, +)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(tag = "$formatVersion") +)] +#[cfg_attr( + all(feature = "json-conversion", feature = "serde-conversion"), + derive(JsonConvertible) +)] +#[cfg_attr(feature = "value-conversion", derive(ValueConvertible))] +#[platform_serialize(unversioned)] //versioned directly, no need to use platform_version +#[platform_version_path_bounds( + "dpp.state_transition_serialization_versions.token_unshield_with_shielded_fee_state_transition" +)] +pub enum TokenUnshieldWithShieldedFeeTransition { + #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] + V0(TokenUnshieldWithShieldedFeeTransitionV0), +} + +impl OptionallyAssetLockProved for TokenUnshieldWithShieldedFeeTransition {} + +impl StateTransitionFieldTypes for TokenUnshieldWithShieldedFeeTransition { + fn signature_property_paths() -> Vec<&'static str> { + vec![] + } + + fn identifiers_property_paths() -> Vec<&'static str> { + vec![] + } + + fn binary_property_paths() -> Vec<&'static str> { + vec![] + } +} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +pub(crate) mod json_convertible_tests { + use super::*; + use crate::shielded::SerializedAction; + use platform_value::Identifier; + + fn fixture_action() -> SerializedAction { + SerializedAction { + nullifier: [0x11; 32], + rk: [0x22; 32], + cmx: [0x33; 32], + encrypted_note: vec![0x44; 216], + cv_net: [0x55; 32], + spend_auth_sig: [0x66; 64], + } + } + + pub(crate) fn fixture() -> TokenUnshieldWithShieldedFeeTransition { + TokenUnshieldWithShieldedFeeTransition::V0(TokenUnshieldWithShieldedFeeTransitionV0 { + data_contract_id: Identifier::new([0x10; 32]), + token_contract_position: 1, + token_id: Identifier::new([0x12; 32]), + recipient_id: Identifier::new([0x13; 32]), + amount: 1004, + token_actions: vec![fixture_action()], + token_anchor: [0x76; 32], + token_proof: vec![0x87; 192], + token_binding_signature: [0x98; 64], + fee_actions: vec![fixture_action()], + fee_anchor: [0x7a; 32], + fee_proof: vec![0x8b; 192], + fee_binding_signature: [0x9c; 64], + credit_amount: 250013, + }) + } + + #[test] + fn json_round_trip() { + use crate::serialization::JsonConvertible; + let original = fixture(); + let json = original.to_json().expect("to_json"); + assert_eq!(json["$formatVersion"], "0"); + assert!(json.get("tokenActions").is_some()); + assert!(json.get("feeActions").is_some()); + let recovered = TokenUnshieldWithShieldedFeeTransition::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + #[test] + fn value_round_trip() { + use crate::serialization::ValueConvertible; + let original = fixture(); + let value = original.to_object().expect("to_object"); + let recovered = + TokenUnshieldWithShieldedFeeTransition::from_object(value).expect("from_object"); + assert_eq!(original, recovered); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/state_transition_estimated_fee_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/state_transition_estimated_fee_validation.rs new file mode 100644 index 00000000000..27a0276a05b --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/state_transition_estimated_fee_validation.rs @@ -0,0 +1,16 @@ +use crate::fee::Credits; +use crate::state_transition::token_unshield_with_shielded_fee_transition::TokenUnshieldWithShieldedFeeTransition; +use crate::state_transition::StateTransitionEstimatedFeeValidation; +use crate::ProtocolError; +use platform_version::version::PlatformVersion; + +impl StateTransitionEstimatedFeeValidation for TokenUnshieldWithShieldedFeeTransition { + /// Pool-paid: the fee is carved from the fee bundle's value balance and enforced by the + /// shielded minimum-fee validation, not by an identity balance floor. + fn calculate_min_required_fee( + &self, + _platform_version: &PlatformVersion, + ) -> Result { + Ok(0) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/state_transition_like.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/state_transition_like.rs new file mode 100644 index 00000000000..e6d983db660 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/state_transition_like.rs @@ -0,0 +1,36 @@ +use crate::state_transition::token_unshield_with_shielded_fee_transition::TokenUnshieldWithShieldedFeeTransition; +use crate::state_transition::{StateTransitionLike, StateTransitionType}; +use crate::version::FeatureVersion; +use platform_value::Identifier; + +impl StateTransitionLike for TokenUnshieldWithShieldedFeeTransition { + fn modified_data_ids(&self) -> Vec { + match self { + TokenUnshieldWithShieldedFeeTransition::V0(transition) => { + transition.modified_data_ids() + } + } + } + + fn state_transition_protocol_version(&self) -> FeatureVersion { + match self { + TokenUnshieldWithShieldedFeeTransition::V0(_) => 0, + } + } + + fn state_transition_type(&self) -> StateTransitionType { + match self { + TokenUnshieldWithShieldedFeeTransition::V0(transition) => { + transition.state_transition_type() + } + } + } + + fn unique_identifiers(&self) -> Vec { + match self { + TokenUnshieldWithShieldedFeeTransition::V0(transition) => { + transition.unique_identifiers() + } + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/state_transition_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/state_transition_validation.rs new file mode 100644 index 00000000000..c9b9e2eb59e --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/state_transition_validation.rs @@ -0,0 +1,17 @@ +use crate::state_transition::token_unshield_with_shielded_fee_transition::TokenUnshieldWithShieldedFeeTransition; +use crate::state_transition::StateTransitionStructureValidation; +use crate::validation::SimpleConsensusValidationResult; +use platform_version::version::PlatformVersion; + +impl StateTransitionStructureValidation for TokenUnshieldWithShieldedFeeTransition { + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> SimpleConsensusValidationResult { + match self { + TokenUnshieldWithShieldedFeeTransition::V0(v0) => { + v0.validate_structure(platform_version) + } + } + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/mod.rs new file mode 100644 index 00000000000..e5dc77b69ba --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/mod.rs @@ -0,0 +1,75 @@ +mod state_transition_like; +mod state_transition_validation; +mod types; +pub(super) mod v0_methods; +mod version; + +use crate::balances::credits::TokenAmount; +use crate::data_contract::TokenContractPosition; +use crate::fee::Credits; +use crate::prelude::Identifier; +use crate::shielded::SerializedAction; +use crate::ProtocolError; +use bincode::{Decode, DecodeUntrusted, Encode}; +use platform_serialization_derive::{ + PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, PlatformSignable, +}; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; + +/// Identity-less unshield, version 0. +/// +/// Tokens leaving a token's shielded pool into an identity's token balance, with the fee paid out of the credit shielded pool: an Orchard spend bundle in the token pool (value balance `amount`) and one in the credit pool (value balance the fee). No identity signs; the recipient and amount are bound into the token bundle's sighash. +/// +/// The token bundle binds the token id and the transition's transparent fields into its +/// sighash; the fee bundle binds the token id and a digest of the token bundle's actions, so +/// neither bundle can be paired with another. +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] +#[derive( + Debug, + Clone, + Encode, + Decode, + PlatformSerialize, + PlatformDeserializeTrusted, + PlatformDeserializeUntrusted, + PlatformSignable, + PartialEq, + DecodeUntrusted, +)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +#[platform_serialize(unversioned)] +pub struct TokenUnshieldWithShieldedFeeTransitionV0 { + /// The contract defining the token. + pub data_contract_id: Identifier, + /// The token's position in the contract. + pub token_contract_position: TokenContractPosition, + /// The token id, `calculate_token_id(data_contract_id, token_contract_position)`. + pub token_id: Identifier, + /// The identity whose token balance receives `amount`. + pub recipient_id: Identifier, + /// Tokens leaving the pool into `recipient_id`'s balance: the token bundle's value balance. + pub amount: TokenAmount, + /// Orchard actions of the bundle in the token's shielded pool. + pub token_actions: Vec, + /// Sinsemilla root of the token pool's note commitment tree the token bundle was built against. + pub token_anchor: [u8; 32], + /// Halo 2 proof of the token bundle. + pub token_proof: Vec, + /// RedPallas binding signature of the token bundle. + pub token_binding_signature: [u8; 64], + /// Orchard actions of the spend bundle in the credit shielded pool that pays the fee. + pub fee_actions: Vec, + /// Sinsemilla root of the credit pool's note commitment tree the fee bundle was built against. + pub fee_anchor: [u8; 32], + /// Halo 2 proof of the fee bundle. + pub fee_proof: Vec, + /// RedPallas binding signature of the fee bundle. + pub fee_binding_signature: [u8; 64], + /// Credits leaving the credit pool: the fee bundle's value balance. + pub credit_amount: Credits, +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/state_transition_like.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/state_transition_like.rs new file mode 100644 index 00000000000..d9907759e40 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/state_transition_like.rs @@ -0,0 +1,39 @@ +use crate::state_transition::token_unshield_with_shielded_fee_transition::v0::TokenUnshieldWithShieldedFeeTransitionV0; +use crate::state_transition::token_unshield_with_shielded_fee_transition::TokenUnshieldWithShieldedFeeTransition; +use crate::state_transition::StateTransition; +use crate::version::FeatureVersion; +use crate::{ + prelude::Identifier, + state_transition::{StateTransitionLike, StateTransitionType}, +}; + +impl From for StateTransition { + fn from(value: TokenUnshieldWithShieldedFeeTransitionV0) -> Self { + let transition: TokenUnshieldWithShieldedFeeTransition = value.into(); + transition.into() + } +} + +impl StateTransitionLike for TokenUnshieldWithShieldedFeeTransitionV0 { + fn state_transition_protocol_version(&self) -> FeatureVersion { + 0 + } + + fn state_transition_type(&self) -> StateTransitionType { + StateTransitionType::TokenUnshieldWithShieldedFee + } + + fn modified_data_ids(&self) -> Vec { + vec![self.recipient_id] + } + + /// Unique by every spent nullifier of both bundles: two transitions spending the same note + /// in either pool can never both be valid. + fn unique_identifiers(&self) -> Vec { + self.token_actions + .iter() + .chain(self.fee_actions.iter()) + .map(|action| hex::encode(action.nullifier)) + .collect() + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/state_transition_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/state_transition_validation.rs new file mode 100644 index 00000000000..010849c5a1e --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/state_transition_validation.rs @@ -0,0 +1,209 @@ +use crate::consensus::basic::state_transition::ShieldedInvalidValueBalanceError; +use crate::consensus::basic::token::InvalidTokenAmountError; +use crate::consensus::basic::token::InvalidTokenIdError; +use crate::consensus::basic::BasicError; +use crate::data_contract::associated_token::token_perpetual_distribution::distribution_function::MAX_DISTRIBUTION_PARAM; +use crate::state_transition::state_transitions::shielded::common_validation::{ + validate_actions_count, validate_anchor_not_zero, validate_encrypted_note_sizes, + validate_proof_not_empty, +}; +use crate::state_transition::token_unshield_with_shielded_fee_transition::v0::TokenUnshieldWithShieldedFeeTransitionV0; +use crate::state_transition::StateTransitionStructureValidation; +use crate::tokens::calculate_token_id; +use crate::validation::SimpleConsensusValidationResult; +use platform_value::Identifier; +use platform_version::version::PlatformVersion; + +impl StateTransitionStructureValidation for TokenUnshieldWithShieldedFeeTransitionV0 { + /// The token id must derive from the contract and position, the amounts must be within + /// bounds and both bundles pass the stateless checks every shielded bundle passes (action + /// count, note sizes, non-empty proof, non-zero anchor). The proofs are verified by the + /// processor once the fee floor has been checked. + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> SimpleConsensusValidationResult { + let calculated_token_id: Identifier = calculate_token_id( + self.data_contract_id.as_bytes(), + self.token_contract_position, + ) + .into(); + if self.token_id != calculated_token_id { + return SimpleConsensusValidationResult::new_with_error( + BasicError::InvalidTokenIdError(InvalidTokenIdError::new( + calculated_token_id, + self.token_id, + )) + .into(), + ); + } + if self.amount == 0 || self.amount > MAX_DISTRIBUTION_PARAM { + return SimpleConsensusValidationResult::new_with_error( + BasicError::InvalidTokenAmountError(InvalidTokenAmountError::new( + MAX_DISTRIBUTION_PARAM, + self.amount, + )) + .into(), + ); + } + if self.credit_amount == 0 { + return SimpleConsensusValidationResult::new_with_error( + BasicError::ShieldedInvalidValueBalanceError( + ShieldedInvalidValueBalanceError::new( + "the credits leaving the pool must be greater than zero".to_string(), + ), + ) + .into(), + ); + } + if self.credit_amount > i64::MAX as u64 { + return SimpleConsensusValidationResult::new_with_error( + BasicError::ShieldedInvalidValueBalanceError( + ShieldedInvalidValueBalanceError::new( + "the credits leaving the pool exceed the maximum allowed value".to_string(), + ), + ) + .into(), + ); + } + let max_actions = platform_version + .system_limits + .max_shielded_transition_actions; + for (actions, proof, anchor) in [ + (&self.token_actions, &self.token_proof, &self.token_anchor), + (&self.fee_actions, &self.fee_proof, &self.fee_anchor), + ] { + let result = validate_actions_count(actions, max_actions); + if !result.is_valid() { + return result; + } + let result = validate_encrypted_note_sizes(actions); + if !result.is_valid() { + return result; + } + let result = validate_proof_not_empty(proof); + if !result.is_valid() { + return result; + } + let result = validate_anchor_not_zero(anchor); + if !result.is_valid() { + return result; + } + } + SimpleConsensusValidationResult::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::consensus::ConsensusError; + use crate::shielded::SerializedAction; + use assert_matches::assert_matches; + + fn action() -> SerializedAction { + SerializedAction { + nullifier: [1u8; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + } + } + + fn valid() -> TokenUnshieldWithShieldedFeeTransitionV0 { + TokenUnshieldWithShieldedFeeTransitionV0 { + data_contract_id: Identifier::from([1u8; 32]), + token_contract_position: 0, + token_id: Identifier::from(calculate_token_id(&[1u8; 32], 0)), + recipient_id: Identifier::from([2u8; 32]), + amount: 1_000, + token_actions: vec![action()], + token_anchor: [7u8; 32], + token_proof: vec![8u8; 100], + token_binding_signature: [9u8; 64], + fee_actions: vec![action()], + fee_anchor: [7u8; 32], + fee_proof: vec![8u8; 100], + fee_binding_signature: [9u8; 64], + credit_amount: 2_000, + } + } + + #[test] + fn should_accept_a_well_formed_transition() { + let result = valid().validate_structure(PlatformVersion::latest()); + assert!(result.is_valid(), "{:?}", result.errors); + } + + #[test] + fn should_reject_a_wrong_token_id() { + let mut t = valid(); + t.token_id = Identifier::from([3u8; 32]); + let result = t.validate_structure(PlatformVersion::latest()); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError(BasicError::InvalidTokenIdError( + _ + ))] + ); + } + + #[test] + fn should_reject_a_zero_token_amount() { + let mut t = valid(); + t.amount = 0; + let result = t.validate_structure(PlatformVersion::latest()); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::InvalidTokenAmountError(_) + )] + ); + } + + #[test] + fn should_reject_a_zero_credit_amount() { + let mut t = valid(); + t.credit_amount = 0; + let result = t.validate_structure(PlatformVersion::latest()); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedInvalidValueBalanceError(_) + )] + ); + } + + #[test] + fn should_reject_a_fee_bundle_without_actions_or_proof() { + let mut t = valid(); + t.fee_actions = vec![]; + let result = t.validate_structure(PlatformVersion::latest()); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedNoActionsError(_) + )] + ); + let mut t = valid(); + t.fee_proof = vec![]; + let result = t.validate_structure(PlatformVersion::latest()); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedEmptyProofError(_) + )] + ); + let mut t = valid(); + t.token_anchor = [0u8; 32]; + let result = t.validate_structure(PlatformVersion::latest()); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError( + BasicError::ShieldedZeroAnchorError(_) + )] + ); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/types.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/types.rs new file mode 100644 index 00000000000..3c760847309 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/types.rs @@ -0,0 +1,16 @@ +use crate::state_transition::token_unshield_with_shielded_fee_transition::v0::TokenUnshieldWithShieldedFeeTransitionV0; +use crate::state_transition::StateTransitionFieldTypes; + +impl StateTransitionFieldTypes for TokenUnshieldWithShieldedFeeTransitionV0 { + fn signature_property_paths() -> Vec<&'static str> { + vec![] + } + + fn identifiers_property_paths() -> Vec<&'static str> { + vec![] + } + + fn binary_property_paths() -> Vec<&'static str> { + vec![] + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/v0_methods.rs new file mode 100644 index 00000000000..07909ca8304 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/v0_methods.rs @@ -0,0 +1,56 @@ +#[cfg(feature = "state-transition-signing")] +use crate::balances::credits::TokenAmount; +#[cfg(feature = "state-transition-signing")] +use crate::data_contract::TokenContractPosition; +#[cfg(feature = "state-transition-signing")] +use crate::fee::Credits; +#[cfg(feature = "state-transition-signing")] +use crate::shielded::SerializedAction; +use crate::state_transition::token_unshield_with_shielded_fee_transition::methods::TokenUnshieldWithShieldedFeeTransitionMethodsV0; +use crate::state_transition::token_unshield_with_shielded_fee_transition::v0::TokenUnshieldWithShieldedFeeTransitionV0; +#[cfg(feature = "state-transition-signing")] +use crate::{state_transition::StateTransition, ProtocolError}; +#[cfg(feature = "state-transition-signing")] +use platform_value::Identifier; +#[cfg(feature = "state-transition-signing")] +use platform_version::version::PlatformVersion; + +impl TokenUnshieldWithShieldedFeeTransitionMethodsV0 for TokenUnshieldWithShieldedFeeTransitionV0 { + #[cfg(feature = "state-transition-signing")] + #[allow(clippy::too_many_arguments)] + fn try_from_bundles( + data_contract_id: Identifier, + token_contract_position: TokenContractPosition, + token_id: Identifier, + recipient_id: Identifier, + amount: TokenAmount, + token_actions: Vec, + token_anchor: [u8; 32], + token_proof: Vec, + token_binding_signature: [u8; 64], + fee_actions: Vec, + fee_anchor: [u8; 32], + fee_proof: Vec, + fee_binding_signature: [u8; 64], + credit_amount: Credits, + _platform_version: &PlatformVersion, + ) -> Result { + let transition = TokenUnshieldWithShieldedFeeTransitionV0 { + data_contract_id, + token_contract_position, + token_id, + recipient_id, + amount, + token_actions, + token_anchor, + token_proof, + token_binding_signature, + fee_actions, + fee_anchor, + fee_proof, + fee_binding_signature, + credit_amount, + }; + Ok(transition.into()) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/version.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/version.rs new file mode 100644 index 00000000000..3ad42bff5dd --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/v0/version.rs @@ -0,0 +1,9 @@ +use crate::state_transition::token_unshield_with_shielded_fee_transition::v0::TokenUnshieldWithShieldedFeeTransitionV0; +use crate::state_transition::FeatureVersioned; +use crate::version::FeatureVersion; + +impl FeatureVersioned for TokenUnshieldWithShieldedFeeTransitionV0 { + fn feature_version(&self) -> FeatureVersion { + 0 + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/version.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/version.rs new file mode 100644 index 00000000000..bbc4c85dc09 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/token_unshield_with_shielded_fee_transition/version.rs @@ -0,0 +1,11 @@ +use crate::state_transition::token_unshield_with_shielded_fee_transition::TokenUnshieldWithShieldedFeeTransition; +use crate::state_transition::FeatureVersioned; +use crate::version::FeatureVersion; + +impl FeatureVersioned for TokenUnshieldWithShieldedFeeTransition { + fn feature_version(&self) -> FeatureVersion { + match self { + TokenUnshieldWithShieldedFeeTransition::V0(v0) => v0.feature_version(), + } + } +} diff --git a/packages/rs-drive-abci/Cargo.toml b/packages/rs-drive-abci/Cargo.toml index c5bee1c9a55..1d194f2416d 100644 --- a/packages/rs-drive-abci/Cargo.toml +++ b/packages/rs-drive-abci/Cargo.toml @@ -98,6 +98,7 @@ dpp = { path = "../rs-dpp", default-features = false, features = [ "abci", "random-documents", "state-transition-signing", + "shielded-client", "random-identities", "random-public-keys", "random-document-types", diff --git a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs index 38dd99ef392..ddaec719bc5 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs @@ -13,6 +13,9 @@ use dpp::state_transition::batch_transition::batched_transition::token_transitio use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRef; use dpp::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; use dpp::state_transition::batch_transition::document_base_transition::v1::v1_methods::DocumentBaseTransitionV1Methods; +use dpp::state_transition::token_purchase_from_shielded_pool_transition::accessors::TokenPurchaseFromShieldedPoolTransitionAccessorsV0; +use dpp::state_transition::token_shielded_transfer_with_shielded_fee_transition::accessors::TokenShieldedTransferWithShieldedFeeTransitionAccessorsV0; +use dpp::state_transition::token_unshield_with_shielded_fee_transition::accessors::TokenUnshieldWithShieldedFeeTransitionAccessorsV0; use dpp::state_transition::StateTransition; use dpp::tokens::token_payment_info::methods::v0::TokenPaymentInfoMethodsV0; use dpp::tokens::token_payment_info::v1::v1_accessors::TokenPaymentInfoAccessorsV1; @@ -398,7 +401,8 @@ fn error_to_internal_error_execution_result( } /// The token shielded pools a state transition writes to: the token ids of every token pool -/// transition in a batch and of every document whose token cost is paid from a pool. +/// transition in a batch, of every document whose token cost is paid from a pool, and of the +/// identity-less token pool transitions. fn token_shielded_pools_touched(state_transition: &StateTransition) -> Vec<[u8; 32]> { match state_transition { StateTransition::Batch(batch) => batch @@ -423,6 +427,11 @@ fn token_shielded_pools_touched(state_transition: &StateTransition) -> Vec<[u8; _ => None, }) .collect(), + StateTransition::TokenShieldedTransferWithShieldedFee(st) => { + vec![st.token_id().to_buffer()] + } + StateTransition::TokenUnshieldWithShieldedFee(st) => vec![st.token_id().to_buffer()], + StateTransition::TokenPurchaseFromShieldedPool(st) => vec![st.token_id().to_buffer()], _ => vec![], } } diff --git a/packages/rs-drive-abci/src/execution/types/execution_event/mod.rs b/packages/rs-drive-abci/src/execution/types/execution_event/mod.rs index fe4e31846be..5e7ef1513cd 100644 --- a/packages/rs-drive-abci/src/execution/types/execution_event/mod.rs +++ b/packages/rs-drive-abci/src/execution/types/execution_event/mod.rs @@ -605,6 +605,42 @@ impl ExecutionEvent<'_> { user_fee_increase, }) } + // Pool-paid like the credit pool's own transfers: the flat fee carved from the + // credit bundle goes to the fee pools, the token side and the price (for a + // purchase) are ordinary operations, no transparent address is credited. + StateTransitionAction::TokenShieldedTransferWithShieldedFeeAction(ref pool_action) => { + let fee_amount = pool_action.fee_amount(); + let operations = + action.into_high_level_drive_operations(epoch, platform_version)?; + Ok(ExecutionEvent::PaidFromShieldedPool { + operations, + fees_to_add_to_pool: fee_amount, + added_to_balance_outputs: None, + chargeable_failure: false, + }) + } + StateTransitionAction::TokenUnshieldWithShieldedFeeAction(ref pool_action) => { + let fee_amount = pool_action.fee_amount(); + let operations = + action.into_high_level_drive_operations(epoch, platform_version)?; + Ok(ExecutionEvent::PaidFromShieldedPool { + operations, + fees_to_add_to_pool: fee_amount, + added_to_balance_outputs: None, + chargeable_failure: false, + }) + } + StateTransitionAction::TokenPurchaseFromShieldedPoolAction(ref pool_action) => { + let fee_amount = pool_action.fee_amount(); + let operations = + action.into_high_level_drive_operations(epoch, platform_version)?; + Ok(ExecutionEvent::PaidFromShieldedPool { + operations, + fees_to_add_to_pool: fee_amount, + added_to_balance_outputs: None, + chargeable_failure: false, + }) + } StateTransitionAction::IdentityTopUpFromShieldedPoolAction(ref top_up_action) => { // Pool-paid exactly like Unshield: the flat fee is carved from the value // balance and routed to the fee pools; the identity receives the rest. diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/address_balances_and_nonces.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/address_balances_and_nonces.rs index 7d1a1086cea..c4fb2b6e494 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/address_balances_and_nonces.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/address_balances_and_nonces.rs @@ -184,6 +184,9 @@ impl StateTransitionAddressBalancesAndNoncesValidation for StateTransition { | StateTransition::IdentityCreditTransferToAddresses(_) | StateTransition::ShieldedTransfer(_) | StateTransition::IdentityTopUpFromShieldedPool(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) | StateTransition::Unshield(_) | StateTransition::ShieldFromAssetLock(_) | StateTransition::ShieldedWithdrawal(_) @@ -258,6 +261,9 @@ impl StateTransitionAddressBalancesAndNoncesValidation for StateTransition { | StateTransition::IdentityCreditTransferToAddresses(_) | StateTransition::ShieldedTransfer(_) | StateTransition::IdentityTopUpFromShieldedPool(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) | StateTransition::Unshield(_) | StateTransition::ShieldFromAssetLock(_) | StateTransition::ShieldedWithdrawal(_) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/address_witnesses.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/address_witnesses.rs index 1cb6b0f09c0..927c55f5c3e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/address_witnesses.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/address_witnesses.rs @@ -86,6 +86,9 @@ impl StateTransitionAddressWitnessValidationV0 for StateTransition { | StateTransition::IdentityCreditTransferToAddresses(_) | StateTransition::ShieldedTransfer(_) | StateTransition::IdentityTopUpFromShieldedPool(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) | StateTransition::Unshield(_) | StateTransition::ShieldFromAssetLock(_) | StateTransition::ShieldedWithdrawal(_) @@ -209,6 +212,9 @@ impl StateTransitionHasAddressWitnessValidationV0 for StateTransition { | StateTransition::IdentityCreditTransferToAddresses(_) | StateTransition::ShieldedTransfer(_) | StateTransition::IdentityTopUpFromShieldedPool(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) | StateTransition::Unshield(_) | StateTransition::ShieldFromAssetLock(_) | StateTransition::ShieldedWithdrawal(_) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/addresses_minimum_balance.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/addresses_minimum_balance.rs index b4c93ff2739..9c6e1c819c9 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/addresses_minimum_balance.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/addresses_minimum_balance.rs @@ -78,6 +78,9 @@ impl StateTransitionAddressesMinimumBalanceValidationV0 for StateTransition { | StateTransition::Shield(_) | StateTransition::ShieldedTransfer(_) | StateTransition::IdentityTopUpFromShieldedPool(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) | StateTransition::Unshield(_) | StateTransition::ShieldFromAssetLock(_) | StateTransition::ShieldedWithdrawal(_) @@ -116,6 +119,9 @@ impl StateTransitionAddressesMinimumBalanceValidationV0 for StateTransition { | StateTransition::Shield(_) | StateTransition::ShieldedTransfer(_) | StateTransition::IdentityTopUpFromShieldedPool(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) | StateTransition::Unshield(_) | StateTransition::ShieldFromAssetLock(_) | StateTransition::ShieldedWithdrawal(_) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/basic_structure.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/basic_structure.rs index 0a86983222e..f66a4f1dcac 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/basic_structure.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/basic_structure.rs @@ -387,6 +387,72 @@ impl StateTransitionBasicStructureValidationV0 for StateTransition { })), } } + StateTransition::TokenShieldedTransferWithShieldedFee(st) => { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .token_shielded_transfer_with_shielded_fee_state_transition + .basic_structure + { + Some(0) => Ok(st.validate_structure(platform_version)), + Some(version) => { + Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "token shielded transfer with shielded fee transition: validate_basic_structure".to_string(), + known_versions: vec![0], + received: version, + })) + } + None => Err(Error::Execution(ExecutionError::VersionNotActive { + method: "token shielded transfer with shielded fee transition: validate_basic_structure".to_string(), + known_versions: vec![0], + })), + } + } + StateTransition::TokenUnshieldWithShieldedFee(st) => { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .token_unshield_with_shielded_fee_state_transition + .basic_structure + { + Some(0) => Ok(st.validate_structure(platform_version)), + Some(version) => { + Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "token unshield with shielded fee transition: validate_basic_structure".to_string(), + known_versions: vec![0], + received: version, + })) + } + None => Err(Error::Execution(ExecutionError::VersionNotActive { + method: "token unshield with shielded fee transition: validate_basic_structure".to_string(), + known_versions: vec![0], + })), + } + } + StateTransition::TokenPurchaseFromShieldedPool(st) => { + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .token_purchase_from_shielded_pool_state_transition + .basic_structure + { + Some(0) => Ok(st.validate_structure(platform_version)), + Some(version) => { + Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "token purchase from shielded pool transition: validate_basic_structure".to_string(), + known_versions: vec![0], + received: version, + })) + } + None => Err(Error::Execution(ExecutionError::VersionNotActive { + method: "token purchase from shielded pool transition: validate_basic_structure".to_string(), + known_versions: vec![0], + })), + } + } StateTransition::ShieldFromAssetLock(st) => { match platform_version .drive_abci @@ -544,6 +610,27 @@ impl StateTransitionBasicStructureValidationV0 for StateTransition { .identity_top_up_from_shielded_pool_state_transition .basic_structure .is_some(), + StateTransition::TokenShieldedTransferWithShieldedFee(_) => platform_version + .drive_abci + .validation_and_processing + .state_transitions + .token_shielded_transfer_with_shielded_fee_state_transition + .basic_structure + .is_some(), + StateTransition::TokenUnshieldWithShieldedFee(_) => platform_version + .drive_abci + .validation_and_processing + .state_transitions + .token_unshield_with_shielded_fee_state_transition + .basic_structure + .is_some(), + StateTransition::TokenPurchaseFromShieldedPool(_) => platform_version + .drive_abci + .validation_and_processing + .state_transitions + .token_purchase_from_shielded_pool_state_transition + .basic_structure + .is_some(), StateTransition::ShieldFromAssetLock(_) => platform_version .drive_abci .validation_and_processing @@ -930,6 +1017,9 @@ mod tests { | StateTransition::Shield(_) | StateTransition::ShieldedTransfer(_) | StateTransition::IdentityTopUpFromShieldedPool(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) | StateTransition::Unshield(_) | StateTransition::ShieldedWithdrawal(_) | StateTransition::IdentityCreateFromShieldedPool(_) => false, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/identity_balance.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/identity_balance.rs index dabc8c51b2d..7791d7473a9 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/identity_balance.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/identity_balance.rs @@ -127,6 +127,9 @@ impl StateTransitionIdentityBalanceValidationV0 for StateTransition { | StateTransition::Shield(_) | StateTransition::ShieldedTransfer(_) | StateTransition::IdentityTopUpFromShieldedPool(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) | StateTransition::Unshield(_) | StateTransition::ShieldFromAssetLock(_) | StateTransition::ShieldedWithdrawal(_) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/identity_based_signature.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/identity_based_signature.rs index 0d78c6b3825..6087ff3275d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/identity_based_signature.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/identity_based_signature.rs @@ -138,6 +138,9 @@ impl StateTransitionIdentityBasedSignatureValidationV0 for StateTransition { | StateTransition::Shield(_) | StateTransition::ShieldedTransfer(_) | StateTransition::IdentityTopUpFromShieldedPool(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) | StateTransition::Unshield(_) | StateTransition::ShieldFromAssetLock(_) | StateTransition::ShieldedWithdrawal(_) @@ -182,6 +185,9 @@ impl StateTransitionIdentityBasedSignatureValidationV0 for StateTransition { | StateTransition::Shield(_) | StateTransition::ShieldedTransfer(_) | StateTransition::IdentityTopUpFromShieldedPool(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) | StateTransition::Unshield(_) | StateTransition::ShieldFromAssetLock(_) | StateTransition::ShieldedWithdrawal(_) @@ -216,6 +222,9 @@ impl StateTransitionIdentityBasedSignatureValidationV0 for StateTransition { | StateTransition::Shield(_) | StateTransition::ShieldedTransfer(_) | StateTransition::IdentityTopUpFromShieldedPool(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) | StateTransition::Unshield(_) | StateTransition::ShieldFromAssetLock(_) | StateTransition::ShieldedWithdrawal(_) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/identity_nonces.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/identity_nonces.rs index 0130140638b..a23db96866c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/identity_nonces.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/identity_nonces.rs @@ -146,6 +146,9 @@ impl StateTransitionIdentityNonceValidationV0 for StateTransition { | StateTransition::Shield(_) | StateTransition::ShieldedTransfer(_) | StateTransition::IdentityTopUpFromShieldedPool(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) | StateTransition::Unshield(_) | StateTransition::ShieldFromAssetLock(_) | StateTransition::ShieldedWithdrawal(_) @@ -205,6 +208,9 @@ impl StateTransitionHasIdentityNonceValidationV0 for StateTransition { | StateTransition::Shield(_) | StateTransition::ShieldedTransfer(_) | StateTransition::IdentityTopUpFromShieldedPool(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) | StateTransition::Unshield(_) | StateTransition::ShieldFromAssetLock(_) | StateTransition::ShieldedWithdrawal(_) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs index d4c9deb9643..5ab7dd1d669 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs @@ -46,6 +46,9 @@ impl StateTransitionIsAllowedValidationV0 for StateTransition { | StateTransition::Shield(_) | StateTransition::ShieldedTransfer(_) | StateTransition::IdentityTopUpFromShieldedPool(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) | StateTransition::Unshield(_) | StateTransition::ShieldFromAssetLock(_) | StateTransition::ShieldedWithdrawal(_) @@ -180,6 +183,23 @@ impl StateTransitionIsAllowedValidationV0 for StateTransition { ])) } } + StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) => { + if platform_version.protocol_version >= TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION + { + Ok(ConsensusValidationResult::new()) + } else { + Ok(ConsensusValidationResult::new_with_errors(vec![ + StateTransitionNotActiveError::new( + self.state_transition_type().to_string(), + platform_version.protocol_version, + TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION, + ) + .into(), + ])) + } + } StateTransition::ShieldFromIdentity(_) => { if platform_version.protocol_version >= SHIELD_FROM_IDENTITY_INITIAL_PROTOCOL_VERSION diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs index 52d814eb17d..64625bfdab4 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs @@ -20,6 +20,9 @@ use dpp::state_transition::public_key_in_creation::accessors::IdentityPublicKeyI use dpp::state_transition::public_key_in_creation::IdentityPublicKeyInCreation; use dpp::state_transition::state_transitions::shielded::identity_create_from_shielded_pool_transition::IdentityCreateFromShieldedPoolTransition; use dpp::state_transition::identity_top_up_from_shielded_pool_transition::IdentityTopUpFromShieldedPoolTransition; +use dpp::state_transition::token_purchase_from_shielded_pool_transition::TokenPurchaseFromShieldedPoolTransition; +use dpp::state_transition::token_shielded_transfer_with_shielded_fee_transition::TokenShieldedTransferWithShieldedFeeTransition; +use dpp::state_transition::token_unshield_with_shielded_fee_transition::TokenUnshieldWithShieldedFeeTransition; use dpp::state_transition::shield_from_identity_transition::ShieldFromIdentityTransition; use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; use dpp::state_transition::batch_transition::batched_transition::token_transition::{ @@ -146,6 +149,9 @@ impl StateTransitionHasShieldedProofValidationV0 for StateTransition { | StateTransition::Unshield(_) | StateTransition::ShieldedWithdrawal(_) | StateTransition::IdentityCreateFromShieldedPool(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) ) } @@ -172,6 +178,22 @@ impl StateTransitionHasShieldedProofValidationV0 for StateTransition { StateTransition::IdentityTopUpFromShieldedPool(st) => match st { IdentityTopUpFromShieldedPoolTransition::V0(v0) => v0.actions.len(), }, + // Two bundles are verified: both counts are admitted. + StateTransition::TokenShieldedTransferWithShieldedFee(st) => match st { + TokenShieldedTransferWithShieldedFeeTransition::V0(v0) => { + v0.token_actions.len() + v0.fee_actions.len() + } + }, + StateTransition::TokenUnshieldWithShieldedFee(st) => match st { + TokenUnshieldWithShieldedFeeTransition::V0(v0) => { + v0.token_actions.len() + v0.fee_actions.len() + } + }, + StateTransition::TokenPurchaseFromShieldedPool(st) => match st { + TokenPurchaseFromShieldedPoolTransition::V0(v0) => { + v0.token_actions.len() + v0.fee_actions.len() + } + }, StateTransition::ShieldedWithdrawal(st) => match st { dpp::state_transition::shielded_withdrawal_transition::ShieldedWithdrawalTransition::V0(v0) => { v0.actions.len() @@ -278,6 +300,9 @@ impl StateTransitionHasShieldedProofValidationV0 for StateTransition { | StateTransition::Unshield(_) | StateTransition::ShieldedWithdrawal(_) | StateTransition::IdentityCreateFromShieldedPool(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) ) } } @@ -341,6 +366,22 @@ enum ShieldedMinFeeKind { num_keys: usize, }, IdentityTopUp, + /// `compute_token_shielded_transfer_with_shielded_fee_fee` — two bundles (the token pool + /// transfer and the credit pool fee), nothing written outside the pools. + TokenShieldedTransfer { + token_actions: usize, + }, + /// `compute_token_unshield_with_shielded_fee_fee` — two bundles plus the recipient's token + /// balance item. + TokenUnshield { + token_actions: usize, + }, + /// `compute_token_purchase_from_shielded_pool_fee` — two bundles plus the contract owner's + /// balance write and the supply item; `validated_amount` is the fee bundle's value balance + /// minus the agreed price. + TokenPurchase { + token_actions: usize, + }, } impl StateTransitionShieldedMinimumFeeValidationV0 for StateTransition { @@ -447,6 +488,40 @@ impl StateTransitionShieldedMinimumFeeValidationV0 for StateTransition { ) } }, + // The identity-less token pool transitions: `credit_amount` is the fee + // bundle's value balance and IS the fee (pure fee, exact), except for a + // purchase where the agreed price rides on top of it. + StateTransition::TokenShieldedTransferWithShieldedFee(st) => match st { + TokenShieldedTransferWithShieldedFeeTransition::V0(v0) => ( + v0.credit_amount as i64, + v0.fee_actions.len(), + 0, + u64::MAX, + true, + ShieldedMinFeeKind::TokenShieldedTransfer { token_actions: v0.token_actions.len() }, + ), + }, + StateTransition::TokenUnshieldWithShieldedFee(st) => match st { + TokenUnshieldWithShieldedFeeTransition::V0(v0) => ( + v0.credit_amount as i64, + v0.fee_actions.len(), + 0, + u64::MAX, + true, + ShieldedMinFeeKind::TokenUnshield { token_actions: v0.token_actions.len() }, + ), + }, + StateTransition::TokenPurchaseFromShieldedPool(st) => match st { + TokenPurchaseFromShieldedPoolTransition::V0(v0) => ( + // Structure validation guarantees credit_amount >= total_agreed_price. + v0.credit_amount.saturating_sub(v0.total_agreed_price) as i64, + v0.fee_actions.len(), + 0, + u64::MAX, + true, + ShieldedMinFeeKind::TokenPurchase { token_actions: v0.token_actions.len() }, + ), + }, // Other transitions don't go through shielded fee validation. _ => return Ok(SimpleConsensusValidationResult::new()), }; @@ -513,6 +588,27 @@ impl StateTransitionShieldedMinimumFeeValidationV0 for StateTransition { platform_version, )? } + ShieldedMinFeeKind::TokenShieldedTransfer { token_actions } => { + dpp::shielded::compute_token_shielded_transfer_with_shielded_fee_fee( + token_actions, + num_actions, + platform_version, + )? + } + ShieldedMinFeeKind::TokenUnshield { token_actions } => { + dpp::shielded::compute_token_unshield_with_shielded_fee_fee( + token_actions, + num_actions, + platform_version, + )? + } + ShieldedMinFeeKind::TokenPurchase { token_actions } => { + dpp::shielded::compute_token_purchase_from_shielded_pool_fee( + token_actions, + num_actions, + platform_version, + )? + } }; if (validated_amount as u64) < minimum_shielded_fee { @@ -610,9 +706,6 @@ impl StateTransitionShieldedProofValidationV0 for StateTransition { { 0 => validate_shielded_proof_v0(self, platform_version), 1 => { - if let StateTransition::Batch(batch) = self { - return validate_batch_token_shielded_proofs(batch, platform_version); - } // v1 refuses, in `IdentityCreateFromShieldedPool`, the keys the v0 Orchard sighash // preimage cannot bind, before that preimage is built: a key bound to a contract // group (the layout predates group bounds, and an error out of the preimage @@ -626,9 +719,10 @@ impl StateTransitionShieldedProofValidationV0 for StateTransition { } validate_shielded_proof_v0(self, platform_version) } + 2 => validate_shielded_proof_v2(self, platform_version), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "StateTransition::validate_shielded_proof".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0, 1, 2], received: version, })), } @@ -1478,3 +1572,303 @@ fn validate_batch_token_shielded_proofs( } Ok(SimpleConsensusValidationResult::new()) } + +/// Protocol 15 proof validation adds token pools while retaining the identity key restrictions. +fn validate_shielded_proof_v2( + state_transition: &StateTransition, + platform_version: &PlatformVersion, +) -> Result { + if let Some(error) = key_not_allowed_in_shielded_creation(state_transition) { + return Ok(SimpleConsensusValidationResult::new_with_error(error)); + } + // A batch carries no pool-paid bundle of its own; its token shielded + // transitions are verified one by one against the sighash data each binds. + // Block processing verifies them again inside state validation, where a + // failure is a paid nonce bump; this stateless pass is CheckTx's admission + // filter, run under the nonce-aware limiter. + if let StateTransition::Batch(batch) = state_transition { + return validate_batch_token_shielded_proofs(batch, platform_version); + } + + // `IdentityCreateFromShieldedPool` is the only shielded transition carrying separate + // per-key proof-of-possession signatures that are NOT covered by the Orchard proof + // (they sign the platform signable bytes, and only id+denomination+keys — not the PoP + // sigs — are bound into `extra_sighash_data`). Validate the CHEAP key structure + + // per-key PoP here, BEFORE the expensive Halo 2 bundle verification, so a relayer + // who flips a PoP byte on an observed transition is rejected without the node paying + // for proof verification (DoS hardening). Same `signable_bytes` the transformer uses. + if let StateTransition::IdentityCreateFromShieldedPool(st) = state_transition { + let IdentityCreateFromShieldedPoolTransition::V0(v0) = st; + + let key_structure_result = + IdentityPublicKeyInCreation::validate_identity_public_keys_structure( + &v0.public_keys, + true, + platform_version, + )?; + if !key_structure_result.is_valid() { + return Ok(key_structure_result); + } + + let signable_bytes = state_transition.signable_bytes()?; + for key in v0.public_keys.iter() { + let pop_result = signable_bytes.as_slice().verify_signature( + key.key_type(), + key.data().as_slice(), + key.signature().as_slice(), + ); + if !pop_result.is_valid() { + return Ok(pop_result); + } + } + } + + let result = match state_transition { + StateTransition::Shield(st) => match st { + dpp::state_transition::shield_transition::ShieldTransition::V0(v0) => { + reconstruct_and_verify_bundle( + &v0.actions, + FLAGS_OUTPUTS_ONLY, + -(v0.amount as i64), + &v0.anchor, + v0.proof.as_slice(), + &v0.binding_signature, + &[], // No transparent fields for shield + ) + } + }, + StateTransition::ShieldFromIdentity(st) => match st { + ShieldFromIdentityTransition::V0(v0) => reconstruct_and_verify_bundle( + &v0.actions, + FLAGS_OUTPUTS_ONLY, + -(v0.amount as i64), + &v0.anchor, + v0.proof.as_slice(), + &v0.binding_signature, + &[], + ), + }, + StateTransition::ShieldedTransfer(st) => match st { + dpp::state_transition::shielded_transfer_transition::ShieldedTransferTransition::V0(v0) => { + reconstruct_and_verify_bundle( + &v0.actions, + FLAGS_SPENDS_AND_OUTPUTS, + v0.value_balance as i64, + &v0.anchor, + v0.proof.as_slice(), + &v0.binding_signature, + &[], // No transparent fields for shielded transfer + ) + } + }, + StateTransition::Unshield(st) => match st { + dpp::state_transition::unshield_transition::UnshieldTransition::V0(v0) => { + let extra_sighash_data = dpp::shielded::unshield_extra_sighash_data( + &v0.output_address.to_bytes(), + v0.unshielding_amount, + platform_version, + )?; + reconstruct_and_verify_bundle( + &v0.actions, + FLAGS_SPENDS_AND_OUTPUTS, + v0.unshielding_amount as i64, + &v0.anchor, + v0.proof.as_slice(), + &v0.binding_signature, + &extra_sighash_data, + ) + } + }, + StateTransition::TokenShieldedTransferWithShieldedFee(st) => match st { + TokenShieldedTransferWithShieldedFeeTransition::V0(v0) => { + let token_extra_sighash_data = dpp::shielded::token_shielded_transfer_with_shielded_fee_extra_sighash_data( + &v0.token_id.to_buffer(), + platform_version, + )?; + let fee_extra_sighash_data = + dpp::shielded::token_pool_fee_bundle_extra_sighash_data( + dpp::shielded::TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE, + &v0.token_id.to_buffer(), + &v0.token_actions, + platform_version, + )?; + reconstruct_and_verify_bundle( + &v0.token_actions, + FLAGS_SPENDS_AND_OUTPUTS, + 0, + &v0.token_anchor, + v0.token_proof.as_slice(), + &v0.token_binding_signature, + &token_extra_sighash_data, + ) + .and_then(|_| { + reconstruct_and_verify_bundle( + &v0.fee_actions, + FLAGS_SPENDS_AND_OUTPUTS, + v0.credit_amount as i64, + &v0.fee_anchor, + v0.fee_proof.as_slice(), + &v0.fee_binding_signature, + &fee_extra_sighash_data, + ) + }) + } + }, + StateTransition::TokenUnshieldWithShieldedFee(st) => match st { + TokenUnshieldWithShieldedFeeTransition::V0(v0) => { + let token_extra_sighash_data = dpp::shielded::token_unshield_with_shielded_fee_extra_sighash_data( + &v0.token_id.to_buffer(), + &v0.recipient_id.to_buffer(), + v0.amount, + platform_version, + )?; + let fee_extra_sighash_data = + dpp::shielded::token_pool_fee_bundle_extra_sighash_data( + dpp::shielded::TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE, + &v0.token_id.to_buffer(), + &v0.token_actions, + platform_version, + )?; + reconstruct_and_verify_bundle( + &v0.token_actions, + FLAGS_SPENDS_AND_OUTPUTS, + v0.amount as i64, + &v0.token_anchor, + v0.token_proof.as_slice(), + &v0.token_binding_signature, + &token_extra_sighash_data, + ) + .and_then(|_| { + reconstruct_and_verify_bundle( + &v0.fee_actions, + FLAGS_SPENDS_AND_OUTPUTS, + v0.credit_amount as i64, + &v0.fee_anchor, + v0.fee_proof.as_slice(), + &v0.fee_binding_signature, + &fee_extra_sighash_data, + ) + }) + } + }, + StateTransition::TokenPurchaseFromShieldedPool(st) => match st { + TokenPurchaseFromShieldedPoolTransition::V0(v0) => { + let token_extra_sighash_data = dpp::shielded::token_purchase_from_shielded_pool_extra_sighash_data( + &v0.token_id.to_buffer(), + v0.token_count, + v0.total_agreed_price, + platform_version, + )?; + let fee_extra_sighash_data = + dpp::shielded::token_pool_fee_bundle_extra_sighash_data( + dpp::shielded::TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, + &v0.token_id.to_buffer(), + &v0.token_actions, + platform_version, + )?; + reconstruct_and_verify_bundle( + &v0.token_actions, + FLAGS_OUTPUTS_ONLY, + -(v0.token_count as i64), + &v0.token_anchor, + v0.token_proof.as_slice(), + &v0.token_binding_signature, + &token_extra_sighash_data, + ) + .and_then(|_| { + reconstruct_and_verify_bundle( + &v0.fee_actions, + FLAGS_SPENDS_AND_OUTPUTS, + v0.credit_amount as i64, + &v0.fee_anchor, + v0.fee_proof.as_slice(), + &v0.fee_binding_signature, + &fee_extra_sighash_data, + ) + }) + } + }, + StateTransition::IdentityTopUpFromShieldedPool(st) => match st { + IdentityTopUpFromShieldedPoolTransition::V0(v0) => { + let extra_sighash_data = + dpp::shielded::identity_top_up_from_shielded_extra_sighash_data( + &v0.identity_id.to_buffer(), + v0.top_up_amount, + platform_version, + )?; + reconstruct_and_verify_bundle( + &v0.actions, + FLAGS_SPENDS_AND_OUTPUTS, + v0.top_up_amount as i64, + &v0.anchor, + v0.proof.as_slice(), + &v0.binding_signature, + &extra_sighash_data, + ) + } + }, + StateTransition::ShieldedWithdrawal(st) => match st { + dpp::state_transition::shielded_withdrawal_transition::ShieldedWithdrawalTransition::V0(v0) => { + let extra_sighash_data = + dpp::shielded::shielded_withdrawal_extra_sighash_data( + v0.output_script.as_bytes(), + v0.unshielding_amount, + v0.core_fee_per_byte, + v0.pooling, + platform_version, + )?; + reconstruct_and_verify_bundle( + &v0.actions, + FLAGS_SPENDS_AND_OUTPUTS, + v0.unshielding_amount as i64, + &v0.anchor, + v0.proof.as_slice(), + &v0.binding_signature, + &extra_sighash_data, + ) + } + }, + StateTransition::IdentityCreateFromShieldedPool(st) => match st { + dpp::state_transition::identity_create_from_shielded_pool_transition::IdentityCreateFromShieldedPoolTransition::V0(v0) => { + // Bind the new identity id + denomination + FULL public-key set into the + // Orchard sighash so the bundle cannot be redirected to a different + // identity/keys (the surplus_output binding analog). The id is re-derived + // from the spend nullifiers — the canonical value — so the binding holds + // regardless of any (separately-validated) wire `identity_id`. + let identity_id = + dpp::state_transition::identity_create_from_shielded_pool_transition::derive_identity_id_from_actions(&v0.actions) + .to_buffer(); + let extra_sighash_data = + dpp::shielded::identity_create_from_shielded_extra_sighash_data( + &identity_id, + v0.denomination, + &v0.send_to_address_on_creation_failure, + &v0.public_keys, + platform_version, + )?; + // value_balance = denomination EXACTLY (the ShieldedTransfer exact-equality + // model): the binding signature proves the value commitments sum to exactly + // the denomination leaving the pool. + reconstruct_and_verify_bundle( + &v0.actions, + FLAGS_SPENDS_AND_OUTPUTS, + v0.denomination as i64, + &v0.anchor, + v0.proof.as_slice(), + &v0.binding_signature, + &extra_sighash_data, + ) + } + }, + // ShieldFromAssetLock retains proof verification in transform_into_action; + // its paid-failure action comes from the asset lock. + _ => return Ok(SimpleConsensusValidationResult::new()), + }; + + match result { + Ok(()) => Ok(SimpleConsensusValidationResult::new()), + Err(e) => Ok(SimpleConsensusValidationResult::new_with_error( + StateError::InvalidShieldedProofError(e).into(), + )), + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/state.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/state.rs index 771ad56e64a..70f47e9a45c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/state.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/state.rs @@ -234,6 +234,21 @@ impl StateTransitionStateValidation for StateTransition { "identity top up from shielded pool should not have state validation", ))) } + StateTransition::TokenShieldedTransferWithShieldedFee(_) => { + Err(Error::Execution(ExecutionError::CorruptedCodeExecution( + "token shielded transfer with shielded fee should not have state validation", + ))) + } + StateTransition::TokenUnshieldWithShieldedFee(_) => { + Err(Error::Execution(ExecutionError::CorruptedCodeExecution( + "token unshield with shielded fee should not have state validation", + ))) + } + StateTransition::TokenPurchaseFromShieldedPool(_) => { + Err(Error::Execution(ExecutionError::CorruptedCodeExecution( + "token purchase from shielded pool should not have state validation", + ))) + } StateTransition::ShieldFromAssetLock(_) => { Err(Error::Execution(ExecutionError::CorruptedCodeExecution( "shield from asset lock should not have state validation", @@ -316,6 +331,9 @@ impl StateTransitionStateValidation for StateTransition { | StateTransition::Shield(_) | StateTransition::ShieldedTransfer(_) | StateTransition::IdentityTopUpFromShieldedPool(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) | StateTransition::Unshield(_) | StateTransition::ShieldFromAssetLock(_) | StateTransition::ShieldedWithdrawal(_) => false, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 7149289c49e..f0f9e61d37e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -2223,3 +2223,497 @@ mod document_shielded_token_payment_tests { ); } } + +/// The identity-less token pool transitions (types 23, 24, 25): a bundle in the token's pool +/// and a fee bundle in the credit pool, no identity anywhere. The test wallet owns notes in +/// both pools; the credit pool is funded directly in state, the token pool through a shield. +mod token_pool_paid_transitions_tests { + use super::token_shielded_pool_tests::{ + assert_tokens_conserved, build_shield_bundle, dummy_bundle, enable_shielded_pool, + identity_token_balance, insert_token_pool_anchor, nullifier_is_spent, + platform_with_latest_version, pool_balance, process, spend_keys, spendable_note, + OWNER_INITIAL_BALANCE, + }; + use super::*; + use crate::execution::validation::state_transition::state_transitions::shielded_common::read_pool_total_balance; + use crate::execution::validation::state_transition::state_transitions::test_helpers::{ + get_proving_key, insert_anchor_into_state, set_pool_total_balance, + }; + use crate::rpc::core::MockCoreRPCLike; + use crate::test::helpers::setup::TempPlatform; + use dpp::address_funds::OrchardAddress; + use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters; + use dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; + use dpp::data_contract::change_control_rules::v0::ChangeControlRulesV0; + use dpp::data_contract::change_control_rules::ChangeControlRules; + use dpp::data_contract::DataContract; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::{Identity, IdentityPublicKey}; + use dpp::shielded::builder::{ + build_token_purchase_from_shielded_pool_transition, + build_token_shielded_transfer_with_shielded_fee_transition, + build_token_unshield_with_shielded_fee_transition, OrchardProver, ShieldedFeePayer, + SpendableNote, TokenPoolSpender, + }; + use dpp::state_transition::token_purchase_from_shielded_pool_transition::v0::TokenPurchaseFromShieldedPoolTransitionV0; + use dpp::state_transition::token_purchase_from_shielded_pool_transition::TokenPurchaseFromShieldedPoolTransition; + use dpp::state_transition::StateTransition; + use dpp::tokens::token_pricing_schedule::TokenPricingSchedule; + use dpp::version::feature_initial_protocol_versions::TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION; + use grovedb_commitment_tree::{Anchor, MerklePath, Note, ProvingKey}; + use platform_version::version::PlatformVersion; + use simple_signer::signer::SimpleSigner; + + /// A credit pool note the test wallet holds: enough for a purchase at 0.01 DASH a token. + const CREDIT_NOTE: u64 = 5_000_000_000; + const SHIELDED: u64 = 15; + + struct Prover; + + impl OrchardProver for Prover { + fn proving_key(&self) -> &ProvingKey { + get_proving_key() + } + } + + fn wallet_address() -> OrchardAddress { + let (_, _, address) = spend_keys(); + OrchardAddress::from_raw_bytes(&address.to_raw_address_bytes()) + .expect("valid orchard address bytes") + } + + fn spendable(note: Note, merkle_path: MerklePath) -> SpendableNote { + SpendableNote { note, merkle_path } + } + + /// Puts a `CREDIT_NOTE` note of the test wallet into the credit pool: its anchor is + /// recorded and the pool total covers it. + fn fund_credit_pool( + platform: &TempPlatform, + tag: u8, + ) -> (Note, Anchor, MerklePath) { + let (note, anchor, merkle_path) = spendable_note(CREDIT_NOTE, tag); + insert_anchor_into_state(platform, &anchor.to_bytes()); + set_pool_total_balance(platform, CREDIT_NOTE); + (note, anchor, merkle_path) + } + + fn credit_pool_balance(platform: &TempPlatform) -> u64 { + read_pool_total_balance( + &platform.drive, + None, + &mut vec![], + PlatformVersion::latest(), + ) + .expect("credit pool balance") + } + + /// Funds the buyer with `SHIELDED` tokens and shields all of them (identity contract nonce + /// 2, the contract creation used 1), then records an anchor for a `SHIELDED` note of the + /// test wallet in the token pool. + async fn fund_token_pool( + platform: &mut TempPlatform, + contract: &DataContract, + token_id: Identifier, + holder: &Identity, + key: &IdentityPublicKey, + signer: &SimpleSigner, + seed: u64, + tag: u8, + platform_version: &PlatformVersion, + ) -> (Note, Anchor, MerklePath) { + let shield = BatchTransition::new_token_shield_transition( + token_id, + holder.id(), + contract.id(), + 0, + SHIELDED, + build_shield_bundle(SHIELDED, seed), + key, + 2, + 0, + signer, + platform_version, + None, + ) + .await + .expect("token shield transition"); + let result = process(platform, &shield); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(platform, token_id), SHIELDED); + let (note, anchor, merkle_path) = spendable_note(SHIELDED, tag); + insert_token_pool_anchor(platform, token_id, &anchor); + (note, anchor, merkle_path) + } + + #[tokio::test] + async fn test_token_shielded_transfer_with_shielded_fee() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9401); + + let (owner, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + owner.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + let (token_note, token_anchor, token_path) = fund_token_pool( + &mut platform, + &contract, + token_id, + &owner, + &key, + &signer, + 51, + 11, + platform_version, + ) + .await; + let (credit_note, credit_anchor, credit_path) = fund_credit_pool(&platform, 12); + let (fvk, ask, _) = spend_keys(); + let address = wallet_address(); + + let (transition, fee) = build_token_shielded_transfer_with_shielded_fee_transition( + token_id, + contract.id(), + 0, + TokenPoolSpender { + spends: vec![spendable(token_note, token_path)], + change_address: &address, + fvk: &fvk, + ask: &ask, + anchor: token_anchor, + }, + &address, + 6, + [0u8; 36], + ShieldedFeePayer { + spends: vec![spendable(credit_note, credit_path)], + change_address: &address, + fvk: &fvk, + ask: &ask, + anchor: credit_anchor, + }, + &Prover, + platform_version, + ) + .expect("build transition"); + assert!(fee > 0); + + let result = process(&platform, &transition); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + // Nothing left the token pool; the fee left the credit pool; both spends are final. + assert_eq!(pool_balance(&platform, token_id), SHIELDED); + assert_eq!(credit_pool_balance(&platform), CREDIT_NOTE - fee); + assert_eq!( + identity_token_balance(&platform, token_id, owner.id()), + Some(OWNER_INITIAL_BALANCE - SHIELDED) + ); + let StateTransition::TokenShieldedTransferWithShieldedFee(inner) = &transition else { + unreachable!() + }; + use dpp::state_transition::token_shielded_transfer_with_shielded_fee_transition::accessors::TokenShieldedTransferWithShieldedFeeTransitionAccessorsV0; + assert!(nullifier_is_spent( + &platform, + token_id, + &inner.token_actions()[0].nullifier + )); + assert_tokens_conserved(&platform); + + // The same notes cannot be spent twice; the rejection is unpaid (no identity). + let result = process(&platform, &transition); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::StateError(StateError::NullifierAlreadySpentError(_)) + )] + ); + } + + #[tokio::test] + async fn test_token_unshield_with_shielded_fee() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9402); + + let (owner, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (recipient, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.1)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + owner.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + let (token_note, token_anchor, token_path) = fund_token_pool( + &mut platform, + &contract, + token_id, + &owner, + &key, + &signer, + 52, + 13, + platform_version, + ) + .await; + let (credit_note, credit_anchor, credit_path) = fund_credit_pool(&platform, 14); + let (fvk, ask, _) = spend_keys(); + let address = wallet_address(); + + let (transition, fee) = build_token_unshield_with_shielded_fee_transition( + token_id, + contract.id(), + 0, + TokenPoolSpender { + spends: vec![spendable(token_note, token_path)], + change_address: &address, + fvk: &fvk, + ask: &ask, + anchor: token_anchor, + }, + recipient.id(), + 10, + [0u8; 36], + ShieldedFeePayer { + spends: vec![spendable(credit_note, credit_path)], + change_address: &address, + fvk: &fvk, + ask: &ask, + anchor: credit_anchor, + }, + &Prover, + platform_version, + ) + .expect("build transition"); + + let result = process(&platform, &transition); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + assert_eq!(pool_balance(&platform, token_id), SHIELDED - 10); + assert_eq!( + identity_token_balance(&platform, token_id, recipient.id()), + Some(10) + ); + assert_eq!(credit_pool_balance(&platform), CREDIT_NOTE - fee); + assert_tokens_conserved(&platform); + } + + #[tokio::test] + async fn test_token_purchase_from_shielded_pool() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9403); + + let (seller, seller_signer, seller_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + seller.id(), + Some(|configuration: &mut TokenConfiguration| { + configuration.set_has_shielded_pool(true); + configuration + .distribution_rules_mut() + .set_change_direct_purchase_pricing_rules(ChangeControlRules::V0( + ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::ContractOwner, + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }, + )); + }), + None, + None, + None, + platform_version, + ); + let price_per_token = dash_to_credits!(0.01); + let set_price = BatchTransition::new_token_change_direct_purchase_price_transition( + token_id, + seller.id(), + contract.id(), + 0, + Some(TokenPricingSchedule::SinglePrice(price_per_token)), + None, + None, + &seller_key, + 2, + 0, + &seller_signer, + platform_version, + None, + ) + .await + .expect("set price transition"); + let result = process(&platform, &set_price); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + let seller_credits_before = platform + .drive + .fetch_identity_balance(seller.id().to_buffer(), None, platform_version) + .expect("balance") + .expect("seller exists"); + + let (credit_note, credit_anchor, credit_path) = fund_credit_pool(&platform, 15); + let (fvk, ask, _) = spend_keys(); + let address = wallet_address(); + let token_count = 3; + let price = price_per_token * token_count; + + let (transition, fee) = build_token_purchase_from_shielded_pool_transition( + token_id, + contract.id(), + 0, + &address, + &fvk, + token_count, + price, + [0u8; 36], + ShieldedFeePayer { + spends: vec![spendable(credit_note, credit_path)], + change_address: &address, + fvk: &fvk, + ask: &ask, + anchor: credit_anchor, + }, + &Prover, + platform_version, + ) + .expect("build transition"); + + let result = process(&platform, &transition); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + // The tokens were minted into the pool, the price reached the seller, the fee and the + // price left the credit pool. + assert_eq!(pool_balance(&platform, token_id), token_count); + let total_supply = platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("total supply") + .expect("supply exists"); + assert_eq!(total_supply, OWNER_INITIAL_BALANCE + token_count); + let seller_credits_after = platform + .drive + .fetch_identity_balance(seller.id().to_buffer(), None, platform_version) + .expect("balance") + .expect("seller exists"); + assert_eq!(seller_credits_after, seller_credits_before + price); + assert_eq!(credit_pool_balance(&platform), CREDIT_NOTE - price - fee); + assert_tokens_conserved(&platform); + + // Underpaying is rejected before any note is spent. + let (credit_note, credit_anchor, credit_path) = fund_credit_pool(&platform, 16); + let (underpaid, _) = build_token_purchase_from_shielded_pool_transition( + token_id, + contract.id(), + 0, + &address, + &fvk, + token_count, + price - 1, + [0u8; 36], + ShieldedFeePayer { + spends: vec![spendable(credit_note, credit_path)], + change_address: &address, + fvk: &fvk, + ask: &ask, + anchor: credit_anchor, + }, + &Prover, + platform_version, + ) + .expect("build transition"); + let result = process(&platform, &underpaid); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::StateError(StateError::TokenDirectPurchaseUserPriceTooLow(_)) + )] + ); + assert_eq!(pool_balance(&platform, token_id), token_count); + } + + #[tokio::test] + async fn test_token_pool_paid_transitions_rejected_before_protocol_version_15() { + let platform_version = + PlatformVersion::get(TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION - 1) + .expect("previous protocol version"); + let platform = TestPlatformBuilder::new() + .with_initial_protocol_version(platform_version.protocol_version) + .build_with_mock_rpc() + .set_genesis_state(); + let contract_id = Identifier::from([9u8; 32]); + let token_id: Identifier = + dpp::tokens::calculate_token_id(contract_id.as_bytes(), 0).into(); + let bundle = dummy_bundle(); + let transition: StateTransition = TokenPurchaseFromShieldedPoolTransition::V0( + TokenPurchaseFromShieldedPoolTransitionV0 { + data_contract_id: contract_id, + token_contract_position: 0, + token_id, + token_count: 1, + total_agreed_price: 1, + token_actions: bundle.actions.clone(), + token_anchor: bundle.anchor, + token_proof: bundle.proof.clone(), + token_binding_signature: bundle.binding_signature, + fee_actions: bundle.actions, + fee_anchor: bundle.anchor, + fee_proof: bundle.proof, + fee_binding_signature: bundle.binding_signature, + credit_amount: 2, + }, + ) + .into(); + + let platform_state = platform.state.load(); + let serialized = transition.serialize_to_bytes().expect("serialize"); + let transaction = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &[serialized], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("process state transition"); + + // A top-level transition outside its active version range does not even decode, exactly + // as on software that predates it; the batch token transitions are gated later because + // the batch itself decodes. + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::InternalError(message)] + if message.contains("TokenPurchaseFromShieldedPool") && message.contains("not active") + ); + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs index f9fea613216..b54dad97eaa 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/mod.rs @@ -69,6 +69,14 @@ pub mod shielded_common; pub mod shielded_transfer; /// Module for shielded withdrawal transition validation pub mod shielded_withdrawal; +/// Checks shared by the identity-less token pool transitions +pub mod token_pool_paid_common; +/// Token purchase paid from the credit shielded pool into a token's pool +pub mod token_purchase_from_shielded_pool; +/// Token shielded transfer with the fee paid from the credit shielded pool +pub mod token_shielded_transfer_with_shielded_fee; +/// Token unshield with the fee paid from the credit shielded pool +pub mod token_unshield_with_shielded_fee; /// Module for unshield transition validation pub mod unshield; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_pool_paid_common/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_pool_paid_common/mod.rs new file mode 100644 index 00000000000..34a20c5b5cd --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_pool_paid_common/mod.rs @@ -0,0 +1,208 @@ +//! Stateful checks shared by the identity-less token pool transitions +//! (`TokenShieldedTransferWithShieldedFee`, `TokenUnshieldWithShieldedFee`, +//! `TokenPurchaseFromShieldedPool`): each carries a bundle in a token's pool and a fee bundle in +//! the credit pool. The processor has already verified both Orchard proofs and the fee floor; +//! these functions check the pools against state. Nothing is metered to an identity: the flat +//! fee carved from the credit bundle pays for everything, exactly as for the credit pool's own +//! pool-paid transitions. + +use crate::error::Error; +use crate::execution::validation::state_transition::state_transitions::shielded_common::{ + read_pool_total_balance, validate_anchor_exists, validate_nullifiers, +}; +use dpp::consensus::state::data_contract::data_contract_not_found_error::DataContractNotFoundError; +use dpp::consensus::state::shielded::invalid_anchor_error::InvalidAnchorError; +use dpp::consensus::state::shielded::invalid_shielded_proof_error::InvalidShieldedProofError; +use dpp::consensus::state::shielded::nullifier_already_spent_error::NullifierAlreadySpentError; +use dpp::consensus::state::state_error::StateError; +use dpp::consensus::state::token::{TokenIsPausedError, TokenShieldedPoolNotEnabledError}; +use dpp::consensus::ConsensusError; +use dpp::data_contract::accessors::v1::DataContractV1Getters; +use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters; +use dpp::data_contract::associated_token::token_configuration::TokenConfiguration; +use dpp::data_contract::TokenContractPosition; +use dpp::fee::Credits; +use dpp::prelude::{ConsensusValidationResult, Identifier}; +use dpp::tokens::status::v0::TokenStatusV0Accessors; +use dpp::version::PlatformVersion; +use drive::drive::contract::DataContractFetchInfo; +use drive::drive::Drive; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::StateTransitionAction; +use std::collections::HashSet; +use std::sync::Arc; + +/// A resolved pooled token: its contract and its configuration. +pub(crate) type PooledToken = (Arc, TokenConfiguration); + +/// The consensus rejection of an identity-less token pool transition. +pub(crate) type PoolPaidRejection = ConsensusValidationResult; + +/// The contract and the token configuration of a token that must own a shielded pool and not +/// be paused. A missing contract or token is an invalid transition, not an execution error. +pub(crate) fn resolve_pooled_token( + drive: &Drive, + data_contract_id: Identifier, + token_contract_position: TokenContractPosition, + token_id: Identifier, + transaction: TransactionArg, + platform_version: &PlatformVersion, +) -> Result, Error> { + let Some(contract_fetch_info) = drive.get_contract_with_fetch_info( + data_contract_id.to_buffer(), + false, + transaction, + platform_version, + )? + else { + return Ok(Err(ConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::DataContractNotFoundError( + DataContractNotFoundError::new(data_contract_id), + )), + ))); + }; + let Some(configuration) = contract_fetch_info + .contract + .tokens() + .get(&token_contract_position) + .cloned() + else { + return Ok(Err(ConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::TokenShieldedPoolNotEnabledError( + TokenShieldedPoolNotEnabledError::new(token_id), + )), + ))); + }; + if !configuration.has_shielded_pool() { + return Ok(Err(ConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::TokenShieldedPoolNotEnabledError( + TokenShieldedPoolNotEnabledError::new(token_id), + )), + ))); + } + if let Some(status) = + drive.fetch_token_status(token_id.to_buffer(), transaction, platform_version)? + { + if status.paused() { + return Ok(Err(ConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::TokenIsPausedError( + TokenIsPausedError::new(token_id), + )), + ))); + } + } + Ok(Ok((contract_fetch_info, configuration))) +} + +/// The token bundle's anchor must be recorded for the token pool and its nullifiers must be +/// unspent there (and unique within the bundle). Returns the rejection, if any. +pub(crate) fn validate_token_pool_spend( + drive: &Drive, + token_id: Identifier, + anchor: &[u8; 32], + nullifiers: &[[u8; 32]], + transaction: TransactionArg, + platform_version: &PlatformVersion, +) -> Result>, Error> { + let token_id_bytes = token_id.to_buffer(); + let mut drive_operations = vec![]; + if !drive.has_token_pool_anchor( + &token_id_bytes, + anchor, + transaction, + &mut drive_operations, + platform_version, + )? { + return Ok(Some(ConsensusValidationResult::new_with_error( + StateError::InvalidAnchorError(InvalidAnchorError::new(*anchor)).into(), + ))); + } + let mut seen = HashSet::new(); + for nullifier in nullifiers { + if !seen.insert(nullifier) + || drive.has_token_pool_nullifier( + &token_id_bytes, + nullifier, + transaction, + &mut drive_operations, + platform_version, + )? + { + return Ok(Some(ConsensusValidationResult::new_with_error( + StateError::NullifierAlreadySpentError(NullifierAlreadySpentError::new(*nullifier)) + .into(), + ))); + } + } + Ok(None) +} + +/// The credit pool side of the fee bundle: anchor recorded, nullifiers unspent and the pool +/// holding `credits_leaving`. Returns the pool's current total on success. +pub(crate) fn validate_credit_pool_fee_spend( + drive: &Drive, + fee_anchor: &[u8; 32], + fee_nullifiers: &[[u8; 32]], + credits_leaving: Credits, + transaction: TransactionArg, + platform_version: &PlatformVersion, +) -> Result>, Error> { + let mut drive_operations = vec![]; + let current_total_balance = + read_pool_total_balance(drive, transaction, &mut drive_operations, platform_version)?; + if current_total_balance < credits_leaving { + return Ok(Err(ConsensusValidationResult::new_with_error( + StateError::InvalidShieldedProofError(InvalidShieldedProofError::new(format!( + "shielded pool has insufficient balance: pool has {} but the token pool transition requires {}", + current_total_balance, credits_leaving + ))) + .into(), + ))); + } + if let Some(consensus_error) = validate_anchor_exists( + drive, + fee_anchor, + transaction, + &mut drive_operations, + platform_version, + )? { + return Ok(Err(consensus_error)); + } + if let Some(consensus_error) = validate_nullifiers( + drive, + fee_nullifiers, + transaction, + &mut drive_operations, + platform_version, + )? { + return Ok(Err(consensus_error)); + } + Ok(Ok(current_total_balance)) +} + +/// The token pool must hold `amount` for it to leave. +pub(crate) fn validate_token_pool_holds( + drive: &Drive, + token_id: Identifier, + amount: u64, + transaction: TransactionArg, + platform_version: &PlatformVersion, +) -> Result>, Error> { + let mut drive_operations = vec![]; + let pool_balance = drive.read_token_shielded_pool_total_balance( + &token_id.to_buffer(), + transaction, + &mut drive_operations, + platform_version, + )?; + if pool_balance < amount { + return Ok(Some(ConsensusValidationResult::new_with_error( + StateError::InvalidShieldedProofError(InvalidShieldedProofError::new(format!( + "token shielded pool has insufficient balance: pool has {} but the transition requires {}", + pool_balance, amount + ))) + .into(), + ))); + } + Ok(None) +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/mod.rs new file mode 100644 index 00000000000..20809d3672d --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/mod.rs @@ -0,0 +1,50 @@ +mod transform_into_action; + +use dpp::state_transition::token_purchase_from_shielded_pool_transition::TokenPurchaseFromShieldedPoolTransition; +use dpp::validation::ConsensusValidationResult; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::StateTransitionAction; + +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::validation::state_transition::token_purchase_from_shielded_pool::transform_into_action::v0::TokenPurchaseFromShieldedPoolStateTransitionTransformIntoActionValidationV0; +use crate::platform_types::platform::PlatformRef; +use crate::platform_types::platform_state::PlatformStateV0Methods; +use crate::rpc::core::CoreRPCLike; + +/// A trait to transform into an action for the token purchase from shielded pool transition +pub trait StateTransitionTokenPurchaseFromShieldedPoolTransitionActionTransformer { + /// Transform into an action for the token purchase from shielded pool transition + fn transform_into_action_for_token_purchase_from_shielded_pool_transition( + &self, + platform: &PlatformRef, + tx: TransactionArg, + ) -> Result, Error>; +} + +impl StateTransitionTokenPurchaseFromShieldedPoolTransitionActionTransformer + for TokenPurchaseFromShieldedPoolTransition +{ + fn transform_into_action_for_token_purchase_from_shielded_pool_transition( + &self, + platform: &PlatformRef, + tx: TransactionArg, + ) -> Result, Error> { + let platform_version = platform.state.current_platform_version()?; + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .token_purchase_from_shielded_pool_state_transition + .transform_into_action + { + 0 => self.transform_into_action_v0(platform.drive, tx, platform_version), + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "token purchase from shielded pool transition: transform_into_action" + .to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/mod.rs new file mode 100644 index 00000000000..2d24cd45f58 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/mod.rs @@ -0,0 +1 @@ +pub mod v0; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs new file mode 100644 index 00000000000..3b241bf633a --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs @@ -0,0 +1,115 @@ +use crate::error::Error; +use crate::execution::validation::state_transition::state_transitions::token_pool_paid_common::{ + resolve_pooled_token, validate_credit_pool_fee_spend, +}; +use dpp::consensus::state::token::{TokenMintPastMaxSupplyError, TokenNotForDirectSale}; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use drive::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_transition_action::required_direct_purchase_price; +use dpp::prelude::ConsensusValidationResult; +use dpp::state_transition::token_purchase_from_shielded_pool_transition::TokenPurchaseFromShieldedPoolTransition; +use dpp::version::PlatformVersion; +use drive::drive::Drive; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::shielded::token_purchase_from_shielded_pool::TokenPurchaseFromShieldedPoolTransitionAction; +use drive::state_transition_action::StateTransitionAction; + +pub(in crate::execution::validation::state_transition::state_transitions::token_purchase_from_shielded_pool) trait TokenPurchaseFromShieldedPoolStateTransitionTransformIntoActionValidationV0 +{ + fn transform_into_action_v0( + &self, + drive: &Drive, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error>; +} + +impl TokenPurchaseFromShieldedPoolStateTransitionTransformIntoActionValidationV0 + for TokenPurchaseFromShieldedPoolTransition +{ + /// Both Orchard proofs and the fee floor were checked by the processor. Here the token + /// must own a pool and not be paused, the token bundle must spend recorded and unspent + /// notes of that pool, the fee bundle must spend recorded and unspent notes of the credit + /// pool which must hold what leaves it, and the transition's own rules apply. + fn transform_into_action_v0( + &self, + drive: &Drive, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let TokenPurchaseFromShieldedPoolTransition::V0(v0) = self; + let (contract_fetch_info, configuration) = match resolve_pooled_token( + drive, + v0.data_contract_id, + v0.token_contract_position, + v0.token_id, + transaction, + platform_version, + )? { + Ok(resolved) => resolved, + Err(rejection) => return Ok(rejection), + }; + // The agreed price must cover the token's direct purchase price for this count. + let Some(pricing_schedule) = drive.fetch_token_direct_purchase_price( + v0.token_id.to_buffer(), + transaction, + platform_version, + )? + else { + return Ok(ConsensusValidationResult::new_with_error( + TokenNotForDirectSale::new(v0.token_id).into(), + )); + }; + if let Err(consensus_error) = required_direct_purchase_price( + v0.token_id, + &pricing_schedule, + v0.token_count, + v0.total_agreed_price, + ) { + return Ok(ConsensusValidationResult::new_with_error(consensus_error)); + } + // Minting into the pool must respect the max supply. + let total_supply = drive + .fetch_token_total_supply(v0.token_id.to_buffer(), transaction, platform_version)? + .unwrap_or_default(); + if let Some(max_supply) = configuration.max_supply() { + match total_supply.checked_add(v0.token_count) { + Some(after) if after <= max_supply => {} + _ => { + return Ok(ConsensusValidationResult::new_with_error( + TokenMintPastMaxSupplyError::new( + v0.token_id, + v0.token_count, + total_supply, + max_supply, + ) + .into(), + )); + } + } + } + // The credit bundle carries the price plus the fee; the fee was pinned by the + // minimum-fee validation and the price by the schedule, so the split is exact. + let fee_amount = v0.credit_amount - v0.total_agreed_price; + let fee_nullifiers: Vec<[u8; 32]> = v0.fee_actions.iter().map(|a| a.nullifier).collect(); + let current_credit_pool_balance = match validate_credit_pool_fee_spend( + drive, + &v0.fee_anchor, + &fee_nullifiers, + v0.credit_amount, + transaction, + platform_version, + )? { + Ok(balance) => balance, + Err(rejection) => return Ok(rejection), + }; + let result = TokenPurchaseFromShieldedPoolTransitionAction::try_from_transition( + self, + fee_amount, + current_credit_pool_balance, + contract_fetch_info.contract.owner_id(), + true, + ); + Ok(result.map(|action| action.into())) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_shielded_transfer_with_shielded_fee/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_shielded_transfer_with_shielded_fee/mod.rs new file mode 100644 index 00000000000..7a0967dcb0a --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_shielded_transfer_with_shielded_fee/mod.rs @@ -0,0 +1,55 @@ +mod transform_into_action; + +use dpp::state_transition::token_shielded_transfer_with_shielded_fee_transition::TokenShieldedTransferWithShieldedFeeTransition; +use dpp::validation::ConsensusValidationResult; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::StateTransitionAction; + +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::validation::state_transition::token_shielded_transfer_with_shielded_fee::transform_into_action::v0::TokenShieldedTransferWithShieldedFeeStateTransitionTransformIntoActionValidationV0; +use crate::platform_types::platform::PlatformRef; +use crate::platform_types::platform_state::PlatformStateV0Methods; +use crate::rpc::core::CoreRPCLike; + +/// A trait to transform into an action for the token shielded transfer with shielded fee transition +pub trait StateTransitionTokenShieldedTransferWithShieldedFeeTransitionActionTransformer { + /// Transform into an action for the token shielded transfer with shielded fee transition + fn transform_into_action_for_token_shielded_transfer_with_shielded_fee_transition< + C: CoreRPCLike, + >( + &self, + platform: &PlatformRef, + tx: TransactionArg, + ) -> Result, Error>; +} + +impl StateTransitionTokenShieldedTransferWithShieldedFeeTransitionActionTransformer + for TokenShieldedTransferWithShieldedFeeTransition +{ + fn transform_into_action_for_token_shielded_transfer_with_shielded_fee_transition< + C: CoreRPCLike, + >( + &self, + platform: &PlatformRef, + tx: TransactionArg, + ) -> Result, Error> { + let platform_version = platform.state.current_platform_version()?; + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .token_shielded_transfer_with_shielded_fee_state_transition + .transform_into_action + { + 0 => self.transform_into_action_v0(platform.drive, tx, platform_version), + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: + "token shielded transfer with shielded fee transition: transform_into_action" + .to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_shielded_transfer_with_shielded_fee/transform_into_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_shielded_transfer_with_shielded_fee/transform_into_action/mod.rs new file mode 100644 index 00000000000..2d24cd45f58 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_shielded_transfer_with_shielded_fee/transform_into_action/mod.rs @@ -0,0 +1 @@ +pub mod v0; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_shielded_transfer_with_shielded_fee/transform_into_action/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_shielded_transfer_with_shielded_fee/transform_into_action/v0/mod.rs new file mode 100644 index 00000000000..5c0fd9f9f2d --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_shielded_transfer_with_shielded_fee/transform_into_action/v0/mod.rs @@ -0,0 +1,80 @@ +use crate::error::Error; +use crate::execution::validation::state_transition::state_transitions::token_pool_paid_common::{ + resolve_pooled_token, validate_credit_pool_fee_spend, validate_token_pool_spend, +}; +use dpp::prelude::ConsensusValidationResult; +use dpp::state_transition::token_shielded_transfer_with_shielded_fee_transition::TokenShieldedTransferWithShieldedFeeTransition; +use dpp::version::PlatformVersion; +use drive::drive::Drive; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::shielded::token_shielded_transfer_with_shielded_fee::TokenShieldedTransferWithShieldedFeeTransitionAction; +use drive::state_transition_action::StateTransitionAction; + +pub(in crate::execution::validation::state_transition::state_transitions::token_shielded_transfer_with_shielded_fee) trait TokenShieldedTransferWithShieldedFeeStateTransitionTransformIntoActionValidationV0 +{ + fn transform_into_action_v0( + &self, + drive: &Drive, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error>; +} + +impl TokenShieldedTransferWithShieldedFeeStateTransitionTransformIntoActionValidationV0 + for TokenShieldedTransferWithShieldedFeeTransition +{ + /// Both Orchard proofs and the fee floor were checked by the processor. Here the token + /// must own a pool and not be paused, the token bundle must spend recorded and unspent + /// notes of that pool, the fee bundle must spend recorded and unspent notes of the credit + /// pool which must hold what leaves it, and the transition's own rules apply. + fn transform_into_action_v0( + &self, + drive: &Drive, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let TokenShieldedTransferWithShieldedFeeTransition::V0(v0) = self; + let (_contract, _configuration) = match resolve_pooled_token( + drive, + v0.data_contract_id, + v0.token_contract_position, + v0.token_id, + transaction, + platform_version, + )? { + Ok(resolved) => resolved, + Err(rejection) => return Ok(rejection), + }; + let token_nullifiers: Vec<[u8; 32]> = + v0.token_actions.iter().map(|a| a.nullifier).collect(); + if let Some(rejection) = validate_token_pool_spend( + drive, + v0.token_id, + &v0.token_anchor, + &token_nullifiers, + transaction, + platform_version, + )? { + return Ok(rejection); + } + let fee_amount = v0.credit_amount; + let fee_nullifiers: Vec<[u8; 32]> = v0.fee_actions.iter().map(|a| a.nullifier).collect(); + let current_credit_pool_balance = match validate_credit_pool_fee_spend( + drive, + &v0.fee_anchor, + &fee_nullifiers, + fee_amount, + transaction, + platform_version, + )? { + Ok(balance) => balance, + Err(rejection) => return Ok(rejection), + }; + let result = TokenShieldedTransferWithShieldedFeeTransitionAction::try_from_transition( + self, + fee_amount, + current_credit_pool_balance, + ); + Ok(result.map(|action| action.into())) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_unshield_with_shielded_fee/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_unshield_with_shielded_fee/mod.rs new file mode 100644 index 00000000000..788ad9b5af4 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_unshield_with_shielded_fee/mod.rs @@ -0,0 +1,50 @@ +mod transform_into_action; + +use dpp::state_transition::token_unshield_with_shielded_fee_transition::TokenUnshieldWithShieldedFeeTransition; +use dpp::validation::ConsensusValidationResult; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::StateTransitionAction; + +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::validation::state_transition::token_unshield_with_shielded_fee::transform_into_action::v0::TokenUnshieldWithShieldedFeeStateTransitionTransformIntoActionValidationV0; +use crate::platform_types::platform::PlatformRef; +use crate::platform_types::platform_state::PlatformStateV0Methods; +use crate::rpc::core::CoreRPCLike; + +/// A trait to transform into an action for the token unshield with shielded fee transition +pub trait StateTransitionTokenUnshieldWithShieldedFeeTransitionActionTransformer { + /// Transform into an action for the token unshield with shielded fee transition + fn transform_into_action_for_token_unshield_with_shielded_fee_transition( + &self, + platform: &PlatformRef, + tx: TransactionArg, + ) -> Result, Error>; +} + +impl StateTransitionTokenUnshieldWithShieldedFeeTransitionActionTransformer + for TokenUnshieldWithShieldedFeeTransition +{ + fn transform_into_action_for_token_unshield_with_shielded_fee_transition( + &self, + platform: &PlatformRef, + tx: TransactionArg, + ) -> Result, Error> { + let platform_version = platform.state.current_platform_version()?; + match platform_version + .drive_abci + .validation_and_processing + .state_transitions + .token_unshield_with_shielded_fee_state_transition + .transform_into_action + { + 0 => self.transform_into_action_v0(platform.drive, tx, platform_version), + version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "token unshield with shielded fee transition: transform_into_action" + .to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_unshield_with_shielded_fee/transform_into_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_unshield_with_shielded_fee/transform_into_action/mod.rs new file mode 100644 index 00000000000..2d24cd45f58 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_unshield_with_shielded_fee/transform_into_action/mod.rs @@ -0,0 +1 @@ +pub mod v0; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_unshield_with_shielded_fee/transform_into_action/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_unshield_with_shielded_fee/transform_into_action/v0/mod.rs new file mode 100644 index 00000000000..24617145ad9 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_unshield_with_shielded_fee/transform_into_action/v0/mod.rs @@ -0,0 +1,120 @@ +use crate::error::Error; +use crate::execution::validation::state_transition::state_transitions::token_pool_paid_common::{ + resolve_pooled_token, validate_credit_pool_fee_spend, validate_token_pool_holds, + validate_token_pool_spend, +}; +use dpp::consensus::state::token::{ + IdentityTokenAccountFrozenError, TokenTransferRecipientIdentityNotExistError, +}; +use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dpp::prelude::ConsensusValidationResult; +use dpp::state_transition::token_unshield_with_shielded_fee_transition::TokenUnshieldWithShieldedFeeTransition; +use dpp::tokens::info::v0::IdentityTokenInfoV0Accessors; +use dpp::version::PlatformVersion; +use drive::drive::Drive; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::shielded::token_unshield_with_shielded_fee::TokenUnshieldWithShieldedFeeTransitionAction; +use drive::state_transition_action::StateTransitionAction; + +pub(in crate::execution::validation::state_transition::state_transitions::token_unshield_with_shielded_fee) trait TokenUnshieldWithShieldedFeeStateTransitionTransformIntoActionValidationV0 +{ + fn transform_into_action_v0( + &self, + drive: &Drive, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error>; +} + +impl TokenUnshieldWithShieldedFeeStateTransitionTransformIntoActionValidationV0 + for TokenUnshieldWithShieldedFeeTransition +{ + /// Both Orchard proofs and the fee floor were checked by the processor. Here the token + /// must own a pool and not be paused, the token bundle must spend recorded and unspent + /// notes of that pool, the fee bundle must spend recorded and unspent notes of the credit + /// pool which must hold what leaves it, and the transition's own rules apply. + fn transform_into_action_v0( + &self, + drive: &Drive, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let TokenUnshieldWithShieldedFeeTransition::V0(v0) = self; + let (_contract, configuration) = match resolve_pooled_token( + drive, + v0.data_contract_id, + v0.token_contract_position, + v0.token_id, + transaction, + platform_version, + )? { + Ok(resolved) => resolved, + Err(rejection) => return Ok(rejection), + }; + // The recipient must exist, and unless the token allows transfers into frozen + // balances its account must not be frozen. + if drive + .fetch_identity_balance(v0.recipient_id.to_buffer(), transaction, platform_version)? + .is_none() + { + return Ok(ConsensusValidationResult::new_with_error( + TokenTransferRecipientIdentityNotExistError::new(v0.recipient_id).into(), + )); + } + if !configuration.is_allowed_transfer_to_frozen_balance() { + if let Some(info) = drive.fetch_identity_token_info( + v0.token_id.to_buffer(), + v0.recipient_id.to_buffer(), + transaction, + platform_version, + )? { + if info.frozen() { + return Ok(ConsensusValidationResult::new_with_error( + IdentityTokenAccountFrozenError::new( + v0.token_id, + v0.recipient_id, + "token unshield with shielded fee".to_string(), + ) + .into(), + )); + } + } + } + let token_nullifiers: Vec<[u8; 32]> = + v0.token_actions.iter().map(|a| a.nullifier).collect(); + if let Some(rejection) = validate_token_pool_spend( + drive, + v0.token_id, + &v0.token_anchor, + &token_nullifiers, + transaction, + platform_version, + )? { + return Ok(rejection); + } + if let Some(rejection) = + validate_token_pool_holds(drive, v0.token_id, v0.amount, transaction, platform_version)? + { + return Ok(rejection); + } + let fee_amount = v0.credit_amount; + let fee_nullifiers: Vec<[u8; 32]> = v0.fee_actions.iter().map(|a| a.nullifier).collect(); + let current_credit_pool_balance = match validate_credit_pool_fee_spend( + drive, + &v0.fee_anchor, + &fee_nullifiers, + fee_amount, + transaction, + platform_version, + )? { + Ok(balance) => balance, + Err(rejection) => return Ok(rejection), + }; + let result = TokenUnshieldWithShieldedFeeTransitionAction::try_from_transition( + self, + fee_amount, + current_credit_pool_balance, + ); + Ok(result.map(|action| action.into())) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/transformer/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/transformer/mod.rs index ec238cd6da3..680ce0c40e1 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/transformer/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/transformer/mod.rs @@ -13,6 +13,9 @@ use crate::execution::validation::state_transition::shield::StateTransitionShiel use crate::execution::validation::state_transition::shield_from_asset_lock::StateTransitionShieldFromAssetLockTransitionActionTransformer; use crate::execution::validation::state_transition::shielded_transfer::StateTransitionShieldedTransferTransitionActionTransformer; use crate::execution::validation::state_transition::shielded_withdrawal::StateTransitionShieldedWithdrawalTransitionActionTransformer; +use crate::execution::validation::state_transition::token_purchase_from_shielded_pool::StateTransitionTokenPurchaseFromShieldedPoolTransitionActionTransformer; +use crate::execution::validation::state_transition::token_shielded_transfer_with_shielded_fee::StateTransitionTokenShieldedTransferWithShieldedFeeTransitionActionTransformer; +use crate::execution::validation::state_transition::token_unshield_with_shielded_fee::StateTransitionTokenUnshieldWithShieldedFeeTransitionActionTransformer; use crate::execution::validation::state_transition::unshield::StateTransitionUnshieldTransitionActionTransformer; use crate::execution::validation::state_transition::ValidationMode; use crate::platform_types::platform::PlatformRef; @@ -355,6 +358,18 @@ impl StateTransitionActionTransformer for StateTransition { .transform_into_action_for_identity_top_up_from_shielded_pool_transition( platform, tx, ), + StateTransition::TokenShieldedTransferWithShieldedFee(st) => st + .transform_into_action_for_token_shielded_transfer_with_shielded_fee_transition( + platform, tx, + ), + StateTransition::TokenUnshieldWithShieldedFee(st) => st + .transform_into_action_for_token_unshield_with_shielded_fee_transition( + platform, tx, + ), + StateTransition::TokenPurchaseFromShieldedPool(st) => st + .transform_into_action_for_token_purchase_from_shielded_pool_transition( + platform, tx, + ), StateTransition::ShieldFromAssetLock(st) => { let signable_bytes = self.signable_bytes()?; st.transform_into_action_for_shield_from_asset_lock_transition( diff --git a/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs b/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs index 5cda177a101..17266bf6468 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/verify_state_transitions.rs @@ -1317,7 +1317,10 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( | StateTransitionAction::IdentityKeyLimitsUpdateAction(_) | StateTransitionAction::ContractUserModerationAction(_) | StateTransitionAction::ContractFeeClaimAction(_) - | StateTransitionAction::IdentityTopUpFromShieldedPoolAction(_) => { + | StateTransitionAction::IdentityTopUpFromShieldedPoolAction(_) + | StateTransitionAction::TokenShieldedTransferWithShieldedFeeAction(_) + | StateTransitionAction::TokenUnshieldWithShieldedFeeAction(_) + | StateTransitionAction::TokenPurchaseFromShieldedPoolAction(_) => { // The strategy harness does not generate shielded transitions (no shielded // `OperationType`), so their proof-verification roundtrip isn't exercised here. // IdentityCreateFromShieldedPool's strict prove/verify is covered by the unit diff --git a/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs b/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs index 731e8376cb2..c707ae1f421 100644 --- a/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs +++ b/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs @@ -666,6 +666,31 @@ impl Drive { &platform_version.drive.grove_version, )? } + // The token bundle's spent nullifiers in the token pool bind the exact actions of + // this transfer. + StateTransition::TokenShieldedTransferWithShieldedFee(st) => { + use crate::drive::shielded::paths::token_shielded_pool_nullifiers_path_query; + use dpp::state_transition::token_shielded_transfer_with_shielded_fee_transition::accessors::TokenShieldedTransferWithShieldedFeeTransitionAccessorsV0; + + let nullifiers: Vec<[u8; 32]> = st.token_nullifiers(); + token_shielded_pool_nullifiers_path_query(st.token_id().to_buffer(), &nullifiers) + } + // The recipient's token balance after the unshield. + StateTransition::TokenUnshieldWithShieldedFee(st) => { + use dpp::state_transition::token_unshield_with_shielded_fee_transition::accessors::TokenUnshieldWithShieldedFeeTransitionAccessorsV0; + + Drive::token_balance_for_identity_id_query( + st.token_id().to_buffer(), + st.recipient_id().to_buffer(), + ) + } + // The token pool's total balance after the notes were minted into it. + StateTransition::TokenPurchaseFromShieldedPool(st) => { + use crate::drive::shielded::paths::token_shielded_pool_state_path_query; + use dpp::state_transition::token_purchase_from_shielded_pool_transition::accessors::TokenPurchaseFromShieldedPoolTransitionAccessorsV0; + + token_shielded_pool_state_path_query(st.token_id().to_buffer()) + } StateTransition::IdentityTopUpFromShieldedPool(st) => { use crate::drive::shielded::paths::shielded_credit_pool_nullifiers_path_vec; use dpp::state_transition::identity_top_up_from_shielded_pool_transition::accessors::IdentityTopUpFromShieldedPoolTransitionAccessorsV0; diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/mod.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/mod.rs index fadc4280f53..2355587538f 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/mod.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/mod.rs @@ -128,6 +128,15 @@ impl DriveHighLevelOperationConverter for StateTransitionAction { StateTransitionAction::IdentityTopUpFromShieldedPoolAction(action) => { action.into_high_level_drive_operations(epoch, platform_version) } + StateTransitionAction::TokenShieldedTransferWithShieldedFeeAction(action) => { + action.into_high_level_drive_operations(epoch, platform_version) + } + StateTransitionAction::TokenUnshieldWithShieldedFeeAction(action) => { + action.into_high_level_drive_operations(epoch, platform_version) + } + StateTransitionAction::TokenPurchaseFromShieldedPoolAction(action) => { + action.into_high_level_drive_operations(epoch, platform_version) + } StateTransitionAction::ShieldedTransferAction(shielded_transfer_action) => { shielded_transfer_action.into_high_level_drive_operations(epoch, platform_version) } diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/mod.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/mod.rs index ed4eae5143a..a06d8c21ce7 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/mod.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/mod.rs @@ -5,6 +5,9 @@ mod shield_from_identity_transition; mod shield_transition; mod shielded_transfer_transition; mod shielded_withdrawal_transition; +mod token_purchase_from_shielded_pool_transition; +mod token_shielded_transfer_with_shielded_fee_transition; +mod token_unshield_with_shielded_fee_transition; mod unshield_transition; use crate::state_transition_action::shielded::ShieldedActionNote; diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_purchase_from_shielded_pool_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_purchase_from_shielded_pool_transition.rs new file mode 100644 index 00000000000..ca85499b6ef --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_purchase_from_shielded_pool_transition.rs @@ -0,0 +1,79 @@ +use super::{insert_notes, insert_nullifiers, update_balance}; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::state_transition_action::action_convert_to_operations::DriveHighLevelOperationConverter; +use crate::state_transition_action::shielded::token_purchase_from_shielded_pool::TokenPurchaseFromShieldedPoolTransitionAction; +use crate::util::batch::drive_op_batch::TokenOperationType; +use crate::util::batch::DriveOperation; +use crate::util::batch::DriveOperation::{IdentityOperation, TokenOperation}; +use crate::util::batch::IdentityOperationType; +use dpp::block::epoch::Epoch; +use dpp::version::PlatformVersion; + +impl DriveHighLevelOperationConverter for TokenPurchaseFromShieldedPoolTransitionAction { + /// The token pool side is one token operation; the credit pool side spends the fee + /// bundle's nullifiers, appends its change notes and debits the credit pool by everything + /// that left it (the fee, plus the price for a purchase). No identity nonce is touched: + /// the spent nullifiers are the replay protection. + fn into_high_level_drive_operations<'a>( + self, + _epoch: &Epoch, + platform_version: &PlatformVersion, + ) -> Result>, Error> { + match platform_version + .drive + .methods + .state_transitions + .convert_to_high_level_operations + .token_purchase_from_shielded_pool_transition + { + 0 => match self { + TokenPurchaseFromShieldedPoolTransitionAction::V0(v0) => { + let mut ops: Vec> = Vec::new(); + ops.push(TokenOperation(TokenOperationType::TokenMintToPool { + token_id: v0.token_id, + amount: v0.token_count, + allow_first_mint: v0.allow_first_mint, + notes: v0.token_notes, + })); + // The price leaves the credit pool into the contract owner's balance; the + // fee leaves it to the fee pools. Both are right-hand-side terms of the + // credit conservation equation, so no system-credit adjustment. + if v0.total_agreed_price > 0 { + ops.push(IdentityOperation(IdentityOperationType::AddToIdentityBalance { + identity_id: v0.contract_owner_id.to_buffer(), + added_balance: v0.total_agreed_price, + })); + } + let credits_leaving = v0 + .total_agreed_price + .checked_add(v0.fee_amount) + .ok_or_else(|| { + Error::Drive(DriveError::CorruptedDriveState( + "token purchase price plus fee overflows".to_string(), + )) + })?; + + insert_nullifiers(&mut ops, &v0.fee_notes); + insert_notes(&mut ops, &v0.fee_notes); + let new_total_balance = v0 + .current_credit_pool_balance + .checked_sub(credits_leaving) + .ok_or_else(|| { + Error::Drive(DriveError::CorruptedDriveState( + "shielded pool total balance underflow when paying a token pool transition" + .to_string(), + )) + })?; + update_balance(&mut ops, new_total_balance); + Ok(ops) + } + }, + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "TokenPurchaseFromShieldedPoolTransitionAction::into_high_level_drive_operations".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_shielded_transfer_with_shielded_fee_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_shielded_transfer_with_shielded_fee_transition.rs new file mode 100644 index 00000000000..6572e2c4c79 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_shielded_transfer_with_shielded_fee_transition.rs @@ -0,0 +1,61 @@ +use super::{insert_notes, insert_nullifiers, update_balance}; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::state_transition_action::action_convert_to_operations::DriveHighLevelOperationConverter; +use crate::state_transition_action::shielded::token_shielded_transfer_with_shielded_fee::TokenShieldedTransferWithShieldedFeeTransitionAction; +use crate::util::batch::drive_op_batch::TokenOperationType; +use crate::util::batch::DriveOperation; +use crate::util::batch::DriveOperation::TokenOperation; +use dpp::block::epoch::Epoch; +use dpp::version::PlatformVersion; + +impl DriveHighLevelOperationConverter for TokenShieldedTransferWithShieldedFeeTransitionAction { + /// The token pool side is one token operation; the credit pool side spends the fee + /// bundle's nullifiers, appends its change notes and debits the credit pool by everything + /// that left it (the fee, plus the price for a purchase). No identity nonce is touched: + /// the spent nullifiers are the replay protection. + fn into_high_level_drive_operations<'a>( + self, + _epoch: &Epoch, + platform_version: &PlatformVersion, + ) -> Result>, Error> { + match platform_version + .drive + .methods + .state_transitions + .convert_to_high_level_operations + .token_shielded_transfer_with_shielded_fee_transition + { + 0 => match self { + TokenShieldedTransferWithShieldedFeeTransitionAction::V0(v0) => { + let mut ops: Vec> = Vec::new(); + ops.push(TokenOperation(TokenOperationType::TokenShieldedTransfer { + token_id: v0.token_id, + nullifiers: v0.token_notes.iter().map(|note| note.nullifier).collect(), + notes: v0.token_notes, + })); + let credits_leaving = v0.fee_amount; + + insert_nullifiers(&mut ops, &v0.fee_notes); + insert_notes(&mut ops, &v0.fee_notes); + let new_total_balance = v0 + .current_credit_pool_balance + .checked_sub(credits_leaving) + .ok_or_else(|| { + Error::Drive(DriveError::CorruptedDriveState( + "shielded pool total balance underflow when paying a token pool transition" + .to_string(), + )) + })?; + update_balance(&mut ops, new_total_balance); + Ok(ops) + } + }, + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: "TokenShieldedTransferWithShieldedFeeTransitionAction::into_high_level_drive_operations".to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_unshield_with_shielded_fee_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_unshield_with_shielded_fee_transition.rs new file mode 100644 index 00000000000..091bc7bf651 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_unshield_with_shielded_fee_transition.rs @@ -0,0 +1,65 @@ +use super::{insert_notes, insert_nullifiers, update_balance}; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::state_transition_action::action_convert_to_operations::DriveHighLevelOperationConverter; +use crate::state_transition_action::shielded::token_unshield_with_shielded_fee::TokenUnshieldWithShieldedFeeTransitionAction; +use crate::util::batch::drive_op_batch::TokenOperationType; +use crate::util::batch::DriveOperation; +use crate::util::batch::DriveOperation::TokenOperation; +use dpp::block::epoch::Epoch; +use dpp::version::PlatformVersion; + +impl DriveHighLevelOperationConverter for TokenUnshieldWithShieldedFeeTransitionAction { + /// The token pool side is one token operation; the credit pool side spends the fee + /// bundle's nullifiers, appends its change notes and debits the credit pool by everything + /// that left it (the fee, plus the price for a purchase). No identity nonce is touched: + /// the spent nullifiers are the replay protection. + fn into_high_level_drive_operations<'a>( + self, + _epoch: &Epoch, + platform_version: &PlatformVersion, + ) -> Result>, Error> { + match platform_version + .drive + .methods + .state_transitions + .convert_to_high_level_operations + .token_unshield_with_shielded_fee_transition + { + 0 => match self { + TokenUnshieldWithShieldedFeeTransitionAction::V0(v0) => { + let mut ops: Vec> = Vec::new(); + ops.push(TokenOperation(TokenOperationType::TokenUnshield { + token_id: v0.token_id, + recipient_id: v0.recipient_id, + amount: v0.amount, + nullifiers: v0.token_notes.iter().map(|note| note.nullifier).collect(), + notes: v0.token_notes, + })); + let credits_leaving = v0.fee_amount; + + insert_nullifiers(&mut ops, &v0.fee_notes); + insert_notes(&mut ops, &v0.fee_notes); + let new_total_balance = v0 + .current_credit_pool_balance + .checked_sub(credits_leaving) + .ok_or_else(|| { + Error::Drive(DriveError::CorruptedDriveState( + "shielded pool total balance underflow when paying a token pool transition" + .to_string(), + )) + })?; + update_balance(&mut ops, new_total_balance); + Ok(ops) + } + }, + version => Err(Error::Drive(DriveError::UnknownVersionMismatch { + method: + "TokenUnshieldWithShieldedFeeTransitionAction::into_high_level_drive_operations" + .to_string(), + known_versions: vec![0], + received: version, + })), + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/mod.rs index 0f08d930f34..a9891a039a4 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/mod.rs @@ -6,6 +6,7 @@ use dpp::fee::Credits; pub mod transformer; mod v0; +pub use v0::required_direct_purchase_price; pub(crate) use v0::resolve_direct_purchase_price; pub use v0::*; diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/mod.rs index 8dba24a07a4..3cf4832fa3f 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/mod.rs @@ -1,4 +1,5 @@ mod transformer; +pub use transformer::required_direct_purchase_price; pub(crate) use transformer::resolve_direct_purchase_price; use std::sync::Arc; diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/transformer.rs index c2e24ee3000..9254f47ca2e 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/transformer.rs @@ -181,7 +181,6 @@ impl TokenDirectPurchaseTransitionActionV0 { /// `set_prices.keys().next().expect("Map is not empty")`, which panics on an empty map. That /// panic was uncaught during per-state-transition processing and would deterministically halt /// the chain across the quorum. Here an empty schedule resolves to `TokenNotForDirectSale`. - /// Resolves the credits a direct purchase of `token_count` must pay right now, from the token's /// current pricing schedule: an error when the token is not for sale, the buyer agreed to too /// little, or the amount is under the minimum sale amount. Shared by `TokenDirectPurchase` and @@ -213,22 +212,39 @@ pub(crate) fn resolve_direct_purchase_price( StateError::TokenNotForDirectSale(TokenNotForDirectSale::new(base.token_id())), ))); }; + Ok(required_direct_purchase_price( + base.token_id(), + &pricing_schedule, + token_count, + total_agreed_price, + )) +} +/// The credits `token_count` tokens cost under `pricing_schedule`, or the consensus error +/// rejecting a purchase whose agreed price falls short of it. Shared by the batch direct +/// purchases (identity-paid, into a balance or the pool) and the identity-less purchase paid +/// from the credit shielded pool. +pub fn required_direct_purchase_price( + token_id: Identifier, + pricing_schedule: &TokenPricingSchedule, + token_count: TokenAmount, + total_agreed_price: Credits, +) -> Result { let required_price = match pricing_schedule { TokenPricingSchedule::SinglePrice(price_per_token) => { // We've already checked the user set price in structure validation // Hence we can do a saturating mul. let required_price = price_per_token.saturating_mul(token_count); if total_agreed_price < required_price { - return Ok(Err(ConsensusError::StateError( + return Err(ConsensusError::StateError( StateError::TokenDirectPurchaseUserPriceTooLow( TokenDirectPurchaseUserPriceTooLow::new( - base.token_id(), + token_id, total_agreed_price, required_price, ), ), - ))); + )); } required_price } @@ -238,19 +254,19 @@ pub(crate) fn resolve_direct_purchase_price( // helper so it can be unit-tested directly. On rejection we bump the nonce and // surface the consensus error. match resolve_set_prices_direct_purchase_price( - base.token_id(), - &set_prices, + token_id, + set_prices, token_count, total_agreed_price, ) { Ok(required_total) => required_total, Err(error) => { - return Ok(Err(error)); + return Err(error); } } } }; - Ok(Ok(required_price)) + Ok(required_price) } fn resolve_set_prices_direct_purchase_price( diff --git a/packages/rs-drive/src/state_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/mod.rs index bf7914816a7..a65dc122083 100644 --- a/packages/rs-drive/src/state_transition_action/mod.rs +++ b/packages/rs-drive/src/state_transition_action/mod.rs @@ -39,6 +39,9 @@ use crate::state_transition_action::shielded::shield_from_asset_lock::ShieldFrom use crate::state_transition_action::shielded::shield_from_identity::ShieldFromIdentityTransitionAction; use crate::state_transition_action::shielded::shielded_transfer::ShieldedTransferTransitionAction; use crate::state_transition_action::shielded::shielded_withdrawal::ShieldedWithdrawalTransitionAction; +use crate::state_transition_action::shielded::token_purchase_from_shielded_pool::TokenPurchaseFromShieldedPoolTransitionAction; +use crate::state_transition_action::shielded::token_shielded_transfer_with_shielded_fee::TokenShieldedTransferWithShieldedFeeTransitionAction; +use crate::state_transition_action::shielded::token_unshield_with_shielded_fee::TokenUnshieldWithShieldedFeeTransitionAction; use crate::state_transition_action::shielded::unshield::UnshieldTransitionAction; use crate::state_transition_action::system::bump_address_input_nonces_action::{ BumpAddressInputNonceActionAccessorsV0, BumpAddressInputNoncesAction, @@ -125,6 +128,14 @@ pub enum StateTransitionAction { ContractUserModerationAction(ContractUserModerationTransitionAction), /// contract fee claim: the payout of one of a contract's fee pots ContractFeeClaimAction(ContractFeeClaimTransitionAction), + /// a transfer inside a token's shielded pool, fee paid from the credit pool + TokenShieldedTransferWithShieldedFeeAction( + TokenShieldedTransferWithShieldedFeeTransitionAction, + ), + /// tokens leaving a token's shielded pool into an identity balance, fee paid from the credit pool + TokenUnshieldWithShieldedFeeAction(TokenUnshieldWithShieldedFeeTransitionAction), + /// tokens bought out of the credit pool and minted into the token's shielded pool + TokenPurchaseFromShieldedPoolAction(TokenPurchaseFromShieldedPoolTransitionAction), } impl StateTransitionAction { @@ -197,6 +208,11 @@ impl StateTransitionAction { StateTransitionAction::IdentityTopUpFromShieldedPoolAction(_) => { UserFeeIncrease::default() // 0 (fee is locked by Orchard binding signature) } + StateTransitionAction::TokenShieldedTransferWithShieldedFeeAction(_) + | StateTransitionAction::TokenUnshieldWithShieldedFeeAction(_) + | StateTransitionAction::TokenPurchaseFromShieldedPoolAction(_) => { + UserFeeIncrease::default() // 0 (fee is locked by Orchard binding signatures) + } } } } diff --git a/packages/rs-drive/src/state_transition_action/shielded/mod.rs b/packages/rs-drive/src/state_transition_action/shielded/mod.rs index 203c388ae64..6fde72ed8ea 100644 --- a/packages/rs-drive/src/state_transition_action/shielded/mod.rs +++ b/packages/rs-drive/src/state_transition_action/shielded/mod.rs @@ -12,6 +12,12 @@ pub mod shield_from_identity; pub mod shielded_transfer; /// Shielded withdrawal transition action pub mod shielded_withdrawal; +/// TokenPurchaseFromShieldedPool transition action +pub mod token_purchase_from_shielded_pool; +/// TokenShieldedTransferWithShieldedFee transition action +pub mod token_shielded_transfer_with_shielded_fee; +/// TokenUnshieldWithShieldedFee transition action +pub mod token_unshield_with_shielded_fee; /// Unshield transition action pub mod unshield; diff --git a/packages/rs-drive/src/state_transition_action/shielded/token_purchase_from_shielded_pool/mod.rs b/packages/rs-drive/src/state_transition_action/shielded/token_purchase_from_shielded_pool/mod.rs new file mode 100644 index 00000000000..754e282749b --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/shielded/token_purchase_from_shielded_pool/mod.rs @@ -0,0 +1,105 @@ +/// transformer +pub mod transformer; +/// v0 +pub mod v0; + +use crate::state_transition_action::shielded::token_purchase_from_shielded_pool::v0::TokenPurchaseFromShieldedPoolTransitionActionV0; +use crate::state_transition_action::shielded::ShieldedActionNote; +use derive_more::From; +use dpp::balances::credits::TokenAmount; +use dpp::fee::Credits; +use dpp::platform_value::Identifier; + +/// Token purchase paid from the credit pool transition action +#[derive(Debug, Clone, From)] +pub enum TokenPurchaseFromShieldedPoolTransitionAction { + /// v0 + V0(TokenPurchaseFromShieldedPoolTransitionActionV0), +} + +impl TokenPurchaseFromShieldedPoolTransitionAction { + /// The token whose pool the token bundle spends from or mints into + pub fn token_id(&self) -> Identifier { + match self { + TokenPurchaseFromShieldedPoolTransitionAction::V0(t) => t.token_id, + } + } + + /// Notes built 1:1 from the on-wire token bundle actions + pub fn token_notes(&self) -> &[ShieldedActionNote] { + match self { + TokenPurchaseFromShieldedPoolTransitionAction::V0(t) => &t.token_notes, + } + } + + /// The token pool anchor the token bundle was proven against + pub fn token_anchor(&self) -> &[u8; 32] { + match self { + TokenPurchaseFromShieldedPoolTransitionAction::V0(t) => &t.token_anchor, + } + } + + /// Notes built 1:1 from the on-wire credit pool fee bundle actions + pub fn fee_notes(&self) -> &[ShieldedActionNote] { + match self { + TokenPurchaseFromShieldedPoolTransitionAction::V0(t) => &t.fee_notes, + } + } + + /// The credit pool anchor the fee bundle was proven against + pub fn fee_anchor(&self) -> &[u8; 32] { + match self { + TokenPurchaseFromShieldedPoolTransitionAction::V0(t) => &t.fee_anchor, + } + } + + /// Flat fee carved from the credit pool and routed to the fee pools + pub fn fee_amount(&self) -> Credits { + match self { + TokenPurchaseFromShieldedPoolTransitionAction::V0(t) => t.fee_amount, + } + } + + /// Credit pool total balance read at transform time + pub fn current_credit_pool_balance(&self) -> Credits { + match self { + TokenPurchaseFromShieldedPoolTransitionAction::V0(t) => t.current_credit_pool_balance, + } + } + + /// The contract owner, credited with `total_agreed_price` + pub fn contract_owner_id(&self) -> Identifier { + match self { + TokenPurchaseFromShieldedPoolTransitionAction::V0(t) => t.contract_owner_id, + } + } + + /// Tokens minted into the token pool + pub fn token_count(&self) -> TokenAmount { + match self { + TokenPurchaseFromShieldedPoolTransitionAction::V0(t) => t.token_count, + } + } + + /// Credits paid to the contract owner out of the credit pool, on top of `fee_amount` + pub fn total_agreed_price(&self) -> Credits { + match self { + TokenPurchaseFromShieldedPoolTransitionAction::V0(t) => t.total_agreed_price, + } + } + + /// Whether the token may be minted for the first time by this purchase + pub fn allow_first_mint(&self) -> bool { + match self { + TokenPurchaseFromShieldedPoolTransitionAction::V0(t) => t.allow_first_mint, + } + } + + /// The nullifiers the token bundle spends + pub fn token_nullifiers(&self) -> Vec<[u8; 32]> { + self.token_notes() + .iter() + .map(|note| note.nullifier) + .collect() + } +} diff --git a/packages/rs-drive/src/state_transition_action/shielded/token_purchase_from_shielded_pool/transformer.rs b/packages/rs-drive/src/state_transition_action/shielded/token_purchase_from_shielded_pool/transformer.rs new file mode 100644 index 00000000000..37bebe47dc1 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/shielded/token_purchase_from_shielded_pool/transformer.rs @@ -0,0 +1,30 @@ +use crate::state_transition_action::shielded::token_purchase_from_shielded_pool::v0::TokenPurchaseFromShieldedPoolTransitionActionV0; +use crate::state_transition_action::shielded::token_purchase_from_shielded_pool::TokenPurchaseFromShieldedPoolTransitionAction; +use dpp::fee::Credits; +use dpp::platform_value::Identifier; +use dpp::prelude::ConsensusValidationResult; +use dpp::state_transition::token_purchase_from_shielded_pool_transition::TokenPurchaseFromShieldedPoolTransition; + +impl TokenPurchaseFromShieldedPoolTransitionAction { + /// Transforms the state transition into an action + pub fn try_from_transition( + value: &TokenPurchaseFromShieldedPoolTransition, + fee_amount: Credits, + current_credit_pool_balance: Credits, + contract_owner_id: Identifier, + allow_first_mint: bool, + ) -> ConsensusValidationResult { + match value { + TokenPurchaseFromShieldedPoolTransition::V0(v0) => { + TokenPurchaseFromShieldedPoolTransitionActionV0::try_from_transition( + v0, + fee_amount, + current_credit_pool_balance, + contract_owner_id, + allow_first_mint, + ) + .map(|action| action.into()) + } + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/shielded/token_purchase_from_shielded_pool/v0/mod.rs b/packages/rs-drive/src/state_transition_action/shielded/token_purchase_from_shielded_pool/v0/mod.rs new file mode 100644 index 00000000000..aee29fa567b --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/shielded/token_purchase_from_shielded_pool/v0/mod.rs @@ -0,0 +1,33 @@ +mod transformer; + +use crate::state_transition_action::shielded::ShieldedActionNote; +use dpp::balances::credits::TokenAmount; +use dpp::fee::Credits; +use dpp::platform_value::Identifier; + +/// Token purchase paid from the credit pool, version 0: the token pool side and the credit pool fee side of the spend. +#[derive(Debug, Clone)] +pub struct TokenPurchaseFromShieldedPoolTransitionActionV0 { + /// The token whose pool the token bundle spends from or mints into + pub token_id: Identifier, + /// Notes built 1:1 from the on-wire token bundle actions + pub token_notes: Vec, + /// The token pool anchor the token bundle was proven against + pub token_anchor: [u8; 32], + /// Notes built 1:1 from the on-wire credit pool fee bundle actions + pub fee_notes: Vec, + /// The credit pool anchor the fee bundle was proven against + pub fee_anchor: [u8; 32], + /// Flat fee carved from the credit pool and routed to the fee pools + pub fee_amount: Credits, + /// Credit pool total balance read at transform time + pub current_credit_pool_balance: Credits, + /// The contract owner, credited with `total_agreed_price` + pub contract_owner_id: Identifier, + /// Tokens minted into the token pool + pub token_count: TokenAmount, + /// Credits paid to the contract owner out of the credit pool, on top of `fee_amount` + pub total_agreed_price: Credits, + /// Whether the token may be minted for the first time by this purchase + pub allow_first_mint: bool, +} diff --git a/packages/rs-drive/src/state_transition_action/shielded/token_purchase_from_shielded_pool/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/shielded/token_purchase_from_shielded_pool/v0/transformer.rs new file mode 100644 index 00000000000..3647e879a1c --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/shielded/token_purchase_from_shielded_pool/v0/transformer.rs @@ -0,0 +1,41 @@ +use crate::state_transition_action::shielded::token_purchase_from_shielded_pool::v0::TokenPurchaseFromShieldedPoolTransitionActionV0; +use crate::state_transition_action::shielded::ShieldedActionNote; +use dpp::fee::Credits; +use dpp::platform_value::Identifier; +use dpp::prelude::ConsensusValidationResult; +use dpp::state_transition::state_transitions::shielded::token_purchase_from_shielded_pool_transition::v0::TokenPurchaseFromShieldedPoolTransitionV0; + +impl TokenPurchaseFromShieldedPoolTransitionActionV0 { + /// Builds the v0 action from the v0 transition, the flat fee and the credit pool total + pub fn try_from_transition( + value: &TokenPurchaseFromShieldedPoolTransitionV0, + fee_amount: Credits, + current_credit_pool_balance: Credits, + contract_owner_id: Identifier, + allow_first_mint: bool, + ) -> ConsensusValidationResult { + let token_notes: Vec = value + .token_actions + .iter() + .map(ShieldedActionNote::from) + .collect(); + let fee_notes: Vec = value + .fee_actions + .iter() + .map(ShieldedActionNote::from) + .collect(); + ConsensusValidationResult::new_with_data(TokenPurchaseFromShieldedPoolTransitionActionV0 { + token_id: value.token_id, + token_notes, + token_anchor: value.token_anchor, + fee_notes, + fee_anchor: value.fee_anchor, + fee_amount, + current_credit_pool_balance, + contract_owner_id, + token_count: value.token_count, + total_agreed_price: value.total_agreed_price, + allow_first_mint, + }) + } +} diff --git a/packages/rs-drive/src/state_transition_action/shielded/token_shielded_transfer_with_shielded_fee/mod.rs b/packages/rs-drive/src/state_transition_action/shielded/token_shielded_transfer_with_shielded_fee/mod.rs new file mode 100644 index 00000000000..478efe9f2bf --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/shielded/token_shielded_transfer_with_shielded_fee/mod.rs @@ -0,0 +1,78 @@ +/// transformer +pub mod transformer; +/// v0 +pub mod v0; + +use crate::state_transition_action::shielded::token_shielded_transfer_with_shielded_fee::v0::TokenShieldedTransferWithShieldedFeeTransitionActionV0; +use crate::state_transition_action::shielded::ShieldedActionNote; +use derive_more::From; +use dpp::fee::Credits; +use dpp::platform_value::Identifier; + +/// Token shielded transfer paid from the credit pool transition action +#[derive(Debug, Clone, From)] +pub enum TokenShieldedTransferWithShieldedFeeTransitionAction { + /// v0 + V0(TokenShieldedTransferWithShieldedFeeTransitionActionV0), +} + +impl TokenShieldedTransferWithShieldedFeeTransitionAction { + /// The token whose pool the token bundle spends from or mints into + pub fn token_id(&self) -> Identifier { + match self { + TokenShieldedTransferWithShieldedFeeTransitionAction::V0(t) => t.token_id, + } + } + + /// Notes built 1:1 from the on-wire token bundle actions + pub fn token_notes(&self) -> &[ShieldedActionNote] { + match self { + TokenShieldedTransferWithShieldedFeeTransitionAction::V0(t) => &t.token_notes, + } + } + + /// The token pool anchor the token bundle was proven against + pub fn token_anchor(&self) -> &[u8; 32] { + match self { + TokenShieldedTransferWithShieldedFeeTransitionAction::V0(t) => &t.token_anchor, + } + } + + /// Notes built 1:1 from the on-wire credit pool fee bundle actions + pub fn fee_notes(&self) -> &[ShieldedActionNote] { + match self { + TokenShieldedTransferWithShieldedFeeTransitionAction::V0(t) => &t.fee_notes, + } + } + + /// The credit pool anchor the fee bundle was proven against + pub fn fee_anchor(&self) -> &[u8; 32] { + match self { + TokenShieldedTransferWithShieldedFeeTransitionAction::V0(t) => &t.fee_anchor, + } + } + + /// Flat fee carved from the credit pool and routed to the fee pools + pub fn fee_amount(&self) -> Credits { + match self { + TokenShieldedTransferWithShieldedFeeTransitionAction::V0(t) => t.fee_amount, + } + } + + /// Credit pool total balance read at transform time + pub fn current_credit_pool_balance(&self) -> Credits { + match self { + TokenShieldedTransferWithShieldedFeeTransitionAction::V0(t) => { + t.current_credit_pool_balance + } + } + } + + /// The nullifiers the token bundle spends + pub fn token_nullifiers(&self) -> Vec<[u8; 32]> { + self.token_notes() + .iter() + .map(|note| note.nullifier) + .collect() + } +} diff --git a/packages/rs-drive/src/state_transition_action/shielded/token_shielded_transfer_with_shielded_fee/transformer.rs b/packages/rs-drive/src/state_transition_action/shielded/token_shielded_transfer_with_shielded_fee/transformer.rs new file mode 100644 index 00000000000..467467795f7 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/shielded/token_shielded_transfer_with_shielded_fee/transformer.rs @@ -0,0 +1,26 @@ +use crate::state_transition_action::shielded::token_shielded_transfer_with_shielded_fee::v0::TokenShieldedTransferWithShieldedFeeTransitionActionV0; +use crate::state_transition_action::shielded::token_shielded_transfer_with_shielded_fee::TokenShieldedTransferWithShieldedFeeTransitionAction; +use dpp::fee::Credits; + +use dpp::prelude::ConsensusValidationResult; +use dpp::state_transition::token_shielded_transfer_with_shielded_fee_transition::TokenShieldedTransferWithShieldedFeeTransition; + +impl TokenShieldedTransferWithShieldedFeeTransitionAction { + /// Transforms the state transition into an action + pub fn try_from_transition( + value: &TokenShieldedTransferWithShieldedFeeTransition, + fee_amount: Credits, + current_credit_pool_balance: Credits, + ) -> ConsensusValidationResult { + match value { + TokenShieldedTransferWithShieldedFeeTransition::V0(v0) => { + TokenShieldedTransferWithShieldedFeeTransitionActionV0::try_from_transition( + v0, + fee_amount, + current_credit_pool_balance, + ) + .map(|action| action.into()) + } + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/shielded/token_shielded_transfer_with_shielded_fee/v0/mod.rs b/packages/rs-drive/src/state_transition_action/shielded/token_shielded_transfer_with_shielded_fee/v0/mod.rs new file mode 100644 index 00000000000..171a811f577 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/shielded/token_shielded_transfer_with_shielded_fee/v0/mod.rs @@ -0,0 +1,24 @@ +mod transformer; + +use crate::state_transition_action::shielded::ShieldedActionNote; +use dpp::fee::Credits; +use dpp::platform_value::Identifier; + +/// Token shielded transfer paid from the credit pool, version 0: the token pool side and the credit pool fee side of the spend. +#[derive(Debug, Clone)] +pub struct TokenShieldedTransferWithShieldedFeeTransitionActionV0 { + /// The token whose pool the token bundle spends from or mints into + pub token_id: Identifier, + /// Notes built 1:1 from the on-wire token bundle actions + pub token_notes: Vec, + /// The token pool anchor the token bundle was proven against + pub token_anchor: [u8; 32], + /// Notes built 1:1 from the on-wire credit pool fee bundle actions + pub fee_notes: Vec, + /// The credit pool anchor the fee bundle was proven against + pub fee_anchor: [u8; 32], + /// Flat fee carved from the credit pool and routed to the fee pools + pub fee_amount: Credits, + /// Credit pool total balance read at transform time + pub current_credit_pool_balance: Credits, +} diff --git a/packages/rs-drive/src/state_transition_action/shielded/token_shielded_transfer_with_shielded_fee/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/shielded/token_shielded_transfer_with_shielded_fee/v0/transformer.rs new file mode 100644 index 00000000000..7a71a31dfa4 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/shielded/token_shielded_transfer_with_shielded_fee/v0/transformer.rs @@ -0,0 +1,37 @@ +use crate::state_transition_action::shielded::token_shielded_transfer_with_shielded_fee::v0::TokenShieldedTransferWithShieldedFeeTransitionActionV0; +use crate::state_transition_action::shielded::ShieldedActionNote; +use dpp::fee::Credits; + +use dpp::prelude::ConsensusValidationResult; +use dpp::state_transition::state_transitions::shielded::token_shielded_transfer_with_shielded_fee_transition::v0::TokenShieldedTransferWithShieldedFeeTransitionV0; + +impl TokenShieldedTransferWithShieldedFeeTransitionActionV0 { + /// Builds the v0 action from the v0 transition, the flat fee and the credit pool total + pub fn try_from_transition( + value: &TokenShieldedTransferWithShieldedFeeTransitionV0, + fee_amount: Credits, + current_credit_pool_balance: Credits, + ) -> ConsensusValidationResult { + let token_notes: Vec = value + .token_actions + .iter() + .map(ShieldedActionNote::from) + .collect(); + let fee_notes: Vec = value + .fee_actions + .iter() + .map(ShieldedActionNote::from) + .collect(); + ConsensusValidationResult::new_with_data( + TokenShieldedTransferWithShieldedFeeTransitionActionV0 { + token_id: value.token_id, + token_notes, + token_anchor: value.token_anchor, + fee_notes, + fee_anchor: value.fee_anchor, + fee_amount, + current_credit_pool_balance, + }, + ) + } +} diff --git a/packages/rs-drive/src/state_transition_action/shielded/token_unshield_with_shielded_fee/mod.rs b/packages/rs-drive/src/state_transition_action/shielded/token_unshield_with_shielded_fee/mod.rs new file mode 100644 index 00000000000..a3d8ea6bc06 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/shielded/token_unshield_with_shielded_fee/mod.rs @@ -0,0 +1,91 @@ +/// transformer +pub mod transformer; +/// v0 +pub mod v0; + +use crate::state_transition_action::shielded::token_unshield_with_shielded_fee::v0::TokenUnshieldWithShieldedFeeTransitionActionV0; +use crate::state_transition_action::shielded::ShieldedActionNote; +use derive_more::From; +use dpp::balances::credits::TokenAmount; +use dpp::fee::Credits; +use dpp::platform_value::Identifier; + +/// Identity-less unshield transition action +#[derive(Debug, Clone, From)] +pub enum TokenUnshieldWithShieldedFeeTransitionAction { + /// v0 + V0(TokenUnshieldWithShieldedFeeTransitionActionV0), +} + +impl TokenUnshieldWithShieldedFeeTransitionAction { + /// The token whose pool the token bundle spends from or mints into + pub fn token_id(&self) -> Identifier { + match self { + TokenUnshieldWithShieldedFeeTransitionAction::V0(t) => t.token_id, + } + } + + /// Notes built 1:1 from the on-wire token bundle actions + pub fn token_notes(&self) -> &[ShieldedActionNote] { + match self { + TokenUnshieldWithShieldedFeeTransitionAction::V0(t) => &t.token_notes, + } + } + + /// The token pool anchor the token bundle was proven against + pub fn token_anchor(&self) -> &[u8; 32] { + match self { + TokenUnshieldWithShieldedFeeTransitionAction::V0(t) => &t.token_anchor, + } + } + + /// Notes built 1:1 from the on-wire credit pool fee bundle actions + pub fn fee_notes(&self) -> &[ShieldedActionNote] { + match self { + TokenUnshieldWithShieldedFeeTransitionAction::V0(t) => &t.fee_notes, + } + } + + /// The credit pool anchor the fee bundle was proven against + pub fn fee_anchor(&self) -> &[u8; 32] { + match self { + TokenUnshieldWithShieldedFeeTransitionAction::V0(t) => &t.fee_anchor, + } + } + + /// Flat fee carved from the credit pool and routed to the fee pools + pub fn fee_amount(&self) -> Credits { + match self { + TokenUnshieldWithShieldedFeeTransitionAction::V0(t) => t.fee_amount, + } + } + + /// Credit pool total balance read at transform time + pub fn current_credit_pool_balance(&self) -> Credits { + match self { + TokenUnshieldWithShieldedFeeTransitionAction::V0(t) => t.current_credit_pool_balance, + } + } + + /// The identity whose token balance receives `amount` + pub fn recipient_id(&self) -> Identifier { + match self { + TokenUnshieldWithShieldedFeeTransitionAction::V0(t) => t.recipient_id, + } + } + + /// Tokens leaving the token pool into the recipient's balance + pub fn amount(&self) -> TokenAmount { + match self { + TokenUnshieldWithShieldedFeeTransitionAction::V0(t) => t.amount, + } + } + + /// The nullifiers the token bundle spends + pub fn token_nullifiers(&self) -> Vec<[u8; 32]> { + self.token_notes() + .iter() + .map(|note| note.nullifier) + .collect() + } +} diff --git a/packages/rs-drive/src/state_transition_action/shielded/token_unshield_with_shielded_fee/transformer.rs b/packages/rs-drive/src/state_transition_action/shielded/token_unshield_with_shielded_fee/transformer.rs new file mode 100644 index 00000000000..bebfb1a0f3d --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/shielded/token_unshield_with_shielded_fee/transformer.rs @@ -0,0 +1,26 @@ +use crate::state_transition_action::shielded::token_unshield_with_shielded_fee::v0::TokenUnshieldWithShieldedFeeTransitionActionV0; +use crate::state_transition_action::shielded::token_unshield_with_shielded_fee::TokenUnshieldWithShieldedFeeTransitionAction; +use dpp::fee::Credits; + +use dpp::prelude::ConsensusValidationResult; +use dpp::state_transition::token_unshield_with_shielded_fee_transition::TokenUnshieldWithShieldedFeeTransition; + +impl TokenUnshieldWithShieldedFeeTransitionAction { + /// Transforms the state transition into an action + pub fn try_from_transition( + value: &TokenUnshieldWithShieldedFeeTransition, + fee_amount: Credits, + current_credit_pool_balance: Credits, + ) -> ConsensusValidationResult { + match value { + TokenUnshieldWithShieldedFeeTransition::V0(v0) => { + TokenUnshieldWithShieldedFeeTransitionActionV0::try_from_transition( + v0, + fee_amount, + current_credit_pool_balance, + ) + .map(|action| action.into()) + } + } + } +} diff --git a/packages/rs-drive/src/state_transition_action/shielded/token_unshield_with_shielded_fee/v0/mod.rs b/packages/rs-drive/src/state_transition_action/shielded/token_unshield_with_shielded_fee/v0/mod.rs new file mode 100644 index 00000000000..31510aba8a6 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/shielded/token_unshield_with_shielded_fee/v0/mod.rs @@ -0,0 +1,29 @@ +mod transformer; + +use crate::state_transition_action::shielded::ShieldedActionNote; +use dpp::balances::credits::TokenAmount; +use dpp::fee::Credits; +use dpp::platform_value::Identifier; + +/// Identity-less unshield, version 0: the token pool side and the credit pool fee side of the spend. +#[derive(Debug, Clone)] +pub struct TokenUnshieldWithShieldedFeeTransitionActionV0 { + /// The token whose pool the token bundle spends from or mints into + pub token_id: Identifier, + /// Notes built 1:1 from the on-wire token bundle actions + pub token_notes: Vec, + /// The token pool anchor the token bundle was proven against + pub token_anchor: [u8; 32], + /// Notes built 1:1 from the on-wire credit pool fee bundle actions + pub fee_notes: Vec, + /// The credit pool anchor the fee bundle was proven against + pub fee_anchor: [u8; 32], + /// Flat fee carved from the credit pool and routed to the fee pools + pub fee_amount: Credits, + /// Credit pool total balance read at transform time + pub current_credit_pool_balance: Credits, + /// The identity whose token balance receives `amount` + pub recipient_id: Identifier, + /// Tokens leaving the token pool into the recipient's balance + pub amount: TokenAmount, +} diff --git a/packages/rs-drive/src/state_transition_action/shielded/token_unshield_with_shielded_fee/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/shielded/token_unshield_with_shielded_fee/v0/transformer.rs new file mode 100644 index 00000000000..ba6fb3b8876 --- /dev/null +++ b/packages/rs-drive/src/state_transition_action/shielded/token_unshield_with_shielded_fee/v0/transformer.rs @@ -0,0 +1,37 @@ +use crate::state_transition_action::shielded::token_unshield_with_shielded_fee::v0::TokenUnshieldWithShieldedFeeTransitionActionV0; +use crate::state_transition_action::shielded::ShieldedActionNote; +use dpp::fee::Credits; + +use dpp::prelude::ConsensusValidationResult; +use dpp::state_transition::state_transitions::shielded::token_unshield_with_shielded_fee_transition::v0::TokenUnshieldWithShieldedFeeTransitionV0; + +impl TokenUnshieldWithShieldedFeeTransitionActionV0 { + /// Builds the v0 action from the v0 transition, the flat fee and the credit pool total + pub fn try_from_transition( + value: &TokenUnshieldWithShieldedFeeTransitionV0, + fee_amount: Credits, + current_credit_pool_balance: Credits, + ) -> ConsensusValidationResult { + let token_notes: Vec = value + .token_actions + .iter() + .map(ShieldedActionNote::from) + .collect(); + let fee_notes: Vec = value + .fee_actions + .iter() + .map(ShieldedActionNote::from) + .collect(); + ConsensusValidationResult::new_with_data(TokenUnshieldWithShieldedFeeTransitionActionV0 { + token_id: value.token_id, + token_notes, + token_anchor: value.token_anchor, + fee_notes, + fee_anchor: value.fee_anchor, + fee_amount, + current_credit_pool_balance, + recipient_id: value.recipient_id, + amount: value.amount, + }) + } +} diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs index 48b85c1a2c2..bf679766143 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs @@ -2440,6 +2440,66 @@ impl Drive { VerifiedIdentityWithShieldedNullifiers(identity, statuses), )) } + StateTransition::TokenShieldedTransferWithShieldedFee(st) => { + use dpp::state_transition::proof_result::StateTransitionProofResult::VerifiedShieldedNullifiers; + use dpp::state_transition::token_shielded_transfer_with_shielded_fee_transition::accessors::TokenShieldedTransferWithShieldedFeeTransitionAccessorsV0; + + let nullifier_keys: Vec> = st.token_nullifiers(); + let (root_hash, statuses) = Drive::verify_token_shielded_pool_nullifiers( + proof, + st.token_id().to_buffer(), + &nullifier_keys, + false, + platform_version, + )?; + if statuses.len() != nullifier_keys.len() + || statuses.iter().any(|(_, spent)| !spent) + { + return Err(Error::Proof(ProofError::IncorrectProof( + "proof did not show every nullifier of the token shielded transfer as spent".to_string(), + ))); + } + Ok((root_hash, VerifiedShieldedNullifiers(statuses))) + } + StateTransition::TokenUnshieldWithShieldedFee(st) => { + use dpp::state_transition::token_unshield_with_shielded_fee_transition::accessors::TokenUnshieldWithShieldedFeeTransitionAccessorsV0; + + let recipient_id = st.recipient_id(); + let (root_hash, Some(balance)) = Drive::verify_token_balance_for_identity_id( + proof, + st.token_id().to_buffer(), + recipient_id.to_buffer(), + false, + platform_version, + )? + else { + return Err(Error::Proof(ProofError::IncorrectProof(format!( + "proof did not contain token balance for identity {} expected to exist because of state transition (token unshield with shielded fee)", + recipient_id + )))); + }; + Ok((root_hash, VerifiedTokenBalance(recipient_id, balance))) + } + StateTransition::TokenPurchaseFromShieldedPool(st) => { + use dpp::state_transition::token_purchase_from_shielded_pool_transition::accessors::TokenPurchaseFromShieldedPoolTransitionAccessorsV0; + + let token_id = st.token_id(); + let (root_hash, Some(balance)) = Drive::verify_token_shielded_pool_state( + proof, + token_id.to_buffer(), + false, + platform_version, + )? + else { + return Err(Error::Proof(ProofError::IncorrectProof( + "proof did not contain the token shielded pool balance expected to exist because of state transition (token purchase from shielded pool)".to_string(), + ))); + }; + Ok(( + root_hash, + VerifiedTokenShieldedPoolBalance(token_id, balance), + )) + } StateTransition::IdentityTopUpFromShieldedPool(st) => { use crate::drive::balances::balance_path; use crate::drive::identity::IdentityRootStructure::IdentityTreeRevision; @@ -2806,6 +2866,11 @@ impl Drive { // identity, and the credited identity's balance is a snapshot at // the proof's block. StateTransition::IdentityTopUpFromShieldedPool(_) => false, + // The token pool nullifiers bind the exact token bundle of this transfer. + StateTransition::TokenShieldedTransferWithShieldedFee(_) => true, + // Balance and pool snapshots at the proof's block: not bindable to one transition. + StateTransition::TokenUnshieldWithShieldedFee(_) => false, + StateTransition::TokenPurchaseFromShieldedPool(_) => false, }; Ok(binds) diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/mod.rs index 844b0ec9b2b..530d1ce0e71 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/mod.rs @@ -49,6 +49,11 @@ pub struct DPPStateTransitionSerializationVersions { pub identity_create_from_shielded_pool_state_transition: FeatureVersionBounds, pub shield_from_identity_state_transition: FeatureVersionBounds, pub identity_top_up_from_shielded_pool_state_transition: FeatureVersionBounds, + /// The identity-less token pool transitions (token pool bundle + credit pool fee bundle); + /// activation is gated by `TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION`. + pub token_shielded_transfer_with_shielded_fee_state_transition: FeatureVersionBounds, + pub token_unshield_with_shielded_fee_state_transition: FeatureVersionBounds, + pub token_purchase_from_shielded_pool_state_transition: FeatureVersionBounds, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v1.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v1.rs index 423fd3b2095..8d0841dc9be 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v1.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v1.rs @@ -189,4 +189,19 @@ pub const STATE_TRANSITION_SERIALIZATION_VERSIONS_V1: DPPStateTransitionSerializ max_version: 0, default_current_version: 0, }, + token_shielded_transfer_with_shielded_fee_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + token_unshield_with_shielded_fee_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + token_purchase_from_shielded_pool_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v2.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v2.rs index b2ff5de0ece..14b740316a5 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v2.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v2.rs @@ -189,4 +189,19 @@ pub const STATE_TRANSITION_SERIALIZATION_VERSIONS_V2: DPPStateTransitionSerializ max_version: 0, default_current_version: 0, }, + token_shielded_transfer_with_shielded_fee_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + token_unshield_with_shielded_fee_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + token_purchase_from_shielded_pool_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs index 1c77691f150..0022e6c7c67 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs @@ -205,4 +205,19 @@ pub const STATE_TRANSITION_SERIALIZATION_VERSIONS_V3: DPPStateTransitionSerializ max_version: 0, default_current_version: 0, }, + token_shielded_transfer_with_shielded_fee_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + token_unshield_with_shielded_fee_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, + token_purchase_from_shielded_pool_state_transition: FeatureVersionBounds { + min_version: 0, + max_version: 0, + default_current_version: 0, + }, }; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs index 40010b9fc51..a0175eae0cd 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs @@ -163,6 +163,12 @@ pub struct DriveAbciStateTransitionValidationVersions { pub shield_from_identity_state_transition: DriveAbciStateTransitionValidationVersion, pub identity_top_up_from_shielded_pool_state_transition: DriveAbciStateTransitionValidationVersion, + pub token_shielded_transfer_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion, + pub token_unshield_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion, + pub token_purchase_from_shielded_pool_state_transition: + DriveAbciStateTransitionValidationVersion, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs index c74980cecd5..d2389f30fbd 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs @@ -316,6 +316,33 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V1: DriveAbciValidationVersions = state: 0, transform_into_action: 0, }, + token_shielded_transfer_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_unshield_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_purchase_from_shielded_pool_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, }, has_nonce_validation: 0, has_address_witness_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs index 3d439a718a8..b533013b833 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs @@ -385,6 +385,33 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = state: 0, transform_into_action: 0, }, + token_shielded_transfer_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_unshield_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_purchase_from_shielded_pool_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, }, has_nonce_validation: 1, has_address_witness_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs index 31b62693fc9..1d033a58fdb 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs @@ -344,6 +344,33 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V11: DriveAbciValidationVersions = state: 0, transform_into_action: 0, }, + token_shielded_transfer_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_unshield_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_purchase_from_shielded_pool_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: Some(0), + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, }, has_nonce_validation: 1, has_address_witness_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs index 92c0c062401..42caa6d766d 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs @@ -316,6 +316,33 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V2: DriveAbciValidationVersions = state: 0, transform_into_action: 0, }, + token_shielded_transfer_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_unshield_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_purchase_from_shielded_pool_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, }, has_nonce_validation: 0, has_address_witness_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs index 791f923bf66..83d8b806e23 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs @@ -316,6 +316,33 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V3: DriveAbciValidationVersions = state: 0, transform_into_action: 0, }, + token_shielded_transfer_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_unshield_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_purchase_from_shielded_pool_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, }, has_nonce_validation: 0, has_address_witness_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs index 97e1081e3a4..11cf04c13bf 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs @@ -319,6 +319,33 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V4: DriveAbciValidationVersions = state: 0, transform_into_action: 0, }, + token_shielded_transfer_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_unshield_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_purchase_from_shielded_pool_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, }, has_nonce_validation: 1, // <---- changed this has_address_witness_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs index b25c4292743..a125295f142 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs @@ -320,6 +320,33 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V5: DriveAbciValidationVersions = state: 0, transform_into_action: 0, }, + token_shielded_transfer_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_unshield_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_purchase_from_shielded_pool_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, }, has_nonce_validation: 1, has_address_witness_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs index 00e7898b9d5..796de9835c0 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs @@ -323,6 +323,33 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V6: DriveAbciValidationVersions = state: 0, transform_into_action: 0, }, + token_shielded_transfer_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_unshield_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_purchase_from_shielded_pool_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, }, has_nonce_validation: 1, has_address_witness_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs index 16cdaed3b0e..2b2a0227107 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs @@ -317,6 +317,33 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V7: DriveAbciValidationVersions = state: 0, transform_into_action: 0, }, + token_shielded_transfer_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_unshield_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_purchase_from_shielded_pool_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, }, has_nonce_validation: 1, has_address_witness_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs index 5abca891eef..de6511b22e8 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs @@ -371,6 +371,33 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = state: 0, transform_into_action: 0, }, + token_shielded_transfer_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_unshield_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_purchase_from_shielded_pool_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, }, has_nonce_validation: 1, has_address_witness_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs index 78ac3f9e24a..1ff823c4634 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs @@ -367,6 +367,33 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V9: DriveAbciValidationVersions = state: 0, transform_into_action: 0, }, + token_shielded_transfer_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_unshield_with_shielded_fee_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, + token_purchase_from_shielded_pool_state_transition: + DriveAbciStateTransitionValidationVersion { + basic_structure: None, + advanced_structure: None, + identity_signatures: None, + nonce: None, + state: 0, + transform_into_action: 0, + }, }, has_nonce_validation: 1, has_address_witness_validation: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs index f3d992f5a3d..dc960993fdf 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/mod.rs @@ -81,6 +81,9 @@ pub struct DriveStateTransitionActionConvertToHighLevelOperationsMethodVersions pub identity_create_from_shielded_pool_transition: FeatureVersion, pub shield_from_identity_transition: FeatureVersion, pub identity_top_up_from_shielded_pool_transition: FeatureVersion, + pub token_shielded_transfer_with_shielded_fee_transition: FeatureVersion, + pub token_unshield_with_shielded_fee_transition: FeatureVersion, + pub token_purchase_from_shielded_pool_transition: FeatureVersion, } #[derive(Clone, Debug, Default)] diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v1.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v1.rs index 7b37560f4b7..255f358114f 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v1.rs @@ -69,6 +69,9 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V1: DriveStateTransitionMethodV identity_create_from_shielded_pool_transition: 0, shield_from_identity_transition: 0, identity_top_up_from_shielded_pool_transition: 0, + token_shielded_transfer_with_shielded_fee_transition: 0, + token_unshield_with_shielded_fee_transition: 0, + token_purchase_from_shielded_pool_transition: 0, }, document_from_action: DriveDocumentFromActionVersions { document_from_create_transition_action: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v2.rs index 1510faa63d0..352cae7e299 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v2.rs @@ -70,6 +70,9 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V2: DriveStateTransitionMethodV identity_create_from_shielded_pool_transition: 0, shield_from_identity_transition: 0, identity_top_up_from_shielded_pool_transition: 0, + token_shielded_transfer_with_shielded_fee_transition: 0, + token_unshield_with_shielded_fee_transition: 0, + token_purchase_from_shielded_pool_transition: 0, }, document_from_action: DriveDocumentFromActionVersions { document_from_create_transition_action: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v3.rs index 1bdaef8081a..864760ebb28 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v3.rs @@ -74,6 +74,9 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V3: DriveStateTransitionMethodV identity_create_from_shielded_pool_transition: 0, shield_from_identity_transition: 0, identity_top_up_from_shielded_pool_transition: 0, + token_shielded_transfer_with_shielded_fee_transition: 0, + token_unshield_with_shielded_fee_transition: 0, + token_purchase_from_shielded_pool_transition: 0, }, document_from_action: DriveDocumentFromActionVersions { document_from_create_transition_action: 0, diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs index 0bfc45d857f..1b80d15397f 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs @@ -74,6 +74,9 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4: DriveStateTransitionMethodV identity_create_from_shielded_pool_transition: 0, shield_from_identity_transition: 0, identity_top_up_from_shielded_pool_transition: 0, + token_shielded_transfer_with_shielded_fee_transition: 0, + token_unshield_with_shielded_fee_transition: 0, + token_purchase_from_shielded_pool_transition: 0, }, document_from_action: DriveDocumentFromActionVersions { document_from_create_transition_action: 1, // changed diff --git a/packages/wasm-dpp/src/state_transition/state_transition_factory.rs b/packages/wasm-dpp/src/state_transition/state_transition_factory.rs index b50aea34cbf..dcb4826864e 100644 --- a/packages/wasm-dpp/src/state_transition/state_transition_factory.rs +++ b/packages/wasm-dpp/src/state_transition/state_transition_factory.rs @@ -86,6 +86,9 @@ impl StateTransitionFactoryWasm { | StateTransition::ShieldedWithdrawal(_) | StateTransition::IdentityCreateFromShieldedPool(_) | StateTransition::ShieldFromIdentity(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) | StateTransition::IdentityTopUpFromShieldedPool(_) => Err(JsValue::from_str( "shielded transitions are not yet supported in wasm-dpp StateTransitionFactory", )), diff --git a/packages/wasm-dpp2/src/lib.rs b/packages/wasm-dpp2/src/lib.rs index 650662a4178..b81bffb664a 100644 --- a/packages/wasm-dpp2/src/lib.rs +++ b/packages/wasm-dpp2/src/lib.rs @@ -74,7 +74,10 @@ pub use platform_address::{ pub use shielded::{ AddressWitnessWasm, IdentityTopUpFromShieldedPoolTransitionWasm, SerializedOrchardActionWasm, ShieldFromAssetLockTransitionWasm, ShieldFromIdentityTransitionWasm, ShieldTransitionWasm, - ShieldedTransferTransitionWasm, ShieldedWithdrawalTransitionWasm, UnshieldTransitionWasm, + ShieldedTransferTransitionWasm, ShieldedWithdrawalTransitionWasm, + TokenPurchaseFromShieldedPoolTransitionWasm, + TokenShieldedTransferWithShieldedFeeTransitionWasm, TokenUnshieldWithShieldedFeeTransitionWasm, + UnshieldTransitionWasm, }; pub use state_transitions::base::{GroupStateTransitionInfoWasm, StateTransitionWasm}; pub use state_transitions::proof_result::{StateTransitionProofResultTypeJs, convert_proof_result}; diff --git a/packages/wasm-dpp2/src/shielded/mod.rs b/packages/wasm-dpp2/src/shielded/mod.rs index 657b3b47b34..919ead179ac 100644 --- a/packages/wasm-dpp2/src/shielded/mod.rs +++ b/packages/wasm-dpp2/src/shielded/mod.rs @@ -7,6 +7,9 @@ pub mod shield_from_identity_transition; pub mod shield_transition; pub mod shielded_transfer_transition; pub mod shielded_withdrawal_transition; +pub mod token_purchase_from_shielded_pool_transition; +pub mod token_shielded_transfer_with_shielded_fee_transition; +pub mod token_unshield_with_shielded_fee_transition; pub mod unshield_transition; pub use address_witness::{AddressWitnessWasm, input_witnesses_from_js_options}; @@ -18,6 +21,9 @@ pub use shield_from_identity_transition::ShieldFromIdentityTransitionWasm; pub use shield_transition::ShieldTransitionWasm; pub use shielded_transfer_transition::ShieldedTransferTransitionWasm; pub use shielded_withdrawal_transition::ShieldedWithdrawalTransitionWasm; +pub use token_purchase_from_shielded_pool_transition::TokenPurchaseFromShieldedPoolTransitionWasm; +pub use token_shielded_transfer_with_shielded_fee_transition::TokenShieldedTransferWithShieldedFeeTransitionWasm; +pub use token_unshield_with_shielded_fee_transition::TokenUnshieldWithShieldedFeeTransitionWasm; pub use unshield_transition::UnshieldTransitionWasm; use crate::error::WasmDppResult; diff --git a/packages/wasm-dpp2/src/shielded/token_purchase_from_shielded_pool_transition.rs b/packages/wasm-dpp2/src/shielded/token_purchase_from_shielded_pool_transition.rs new file mode 100644 index 00000000000..53149282c3b --- /dev/null +++ b/packages/wasm-dpp2/src/shielded/token_purchase_from_shielded_pool_transition.rs @@ -0,0 +1,331 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::identifier::IdentifierWasm; +use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; +use crate::state_transitions::StateTransitionWasm; +use crate::utils::{try_from_options, try_vec_to_fixed_bytes}; +use crate::{impl_wasm_conversions_inner, impl_wasm_type_info}; +use dpp::platform_value::string_encoding::Encoding::{Base64, Hex}; +use dpp::platform_value::string_encoding::{decode, encode}; +use dpp::serialization::{PlatformDeserializableUntrusted, PlatformSerializable}; +use dpp::state_transition::token_purchase_from_shielded_pool_transition::TokenPurchaseFromShieldedPoolTransition; +use dpp::state_transition::token_purchase_from_shielded_pool_transition::accessors::TokenPurchaseFromShieldedPoolTransitionAccessorsV0; +use dpp::state_transition::token_purchase_from_shielded_pool_transition::v0::TokenPurchaseFromShieldedPoolTransitionV0; +use dpp::state_transition::{StateTransition, StateTransitionLike}; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const TS_TYPES: &str = r#" +/** + * Options for constructing a TokenPurchaseFromShieldedPoolTransition (token purchase paid from the credit pool). Two Orchard + * bundles: the token pool bundle and the credit pool fee bundle. There is no platform signature. + */ +export interface TokenPurchaseFromShieldedPoolTransitionOptions { + dataContractId: IdentifierLike; + tokenContractPosition: number; + tokenId: IdentifierLike; + tokenCount: bigint; + totalAgreedPrice: bigint; + tokenActions: SerializedOrchardAction[]; + tokenAnchor: Uint8Array; + tokenProof: Uint8Array; + tokenBindingSignature: Uint8Array; + feeActions: SerializedOrchardAction[]; + feeAnchor: Uint8Array; + feeProof: Uint8Array; + feeBindingSignature: Uint8Array; + creditAmount: bigint; +} + +export interface TokenPurchaseFromShieldedPoolTransitionObject { + $formatVersion: string; + dataContractId: Uint8Array; + tokenContractPosition: number; + tokenId: Uint8Array; + tokenCount: bigint; + totalAgreedPrice: bigint; + tokenActions: SerializedOrchardActionObject[]; + tokenAnchor: Uint8Array; + tokenProof: Uint8Array; + tokenBindingSignature: Uint8Array; + feeActions: SerializedOrchardActionObject[]; + feeAnchor: Uint8Array; + feeProof: Uint8Array; + feeBindingSignature: Uint8Array; + creditAmount: bigint; +} + +export interface TokenPurchaseFromShieldedPoolTransitionJSON { + $formatVersion: string; + dataContractId: string; + tokenContractPosition: number; + tokenId: string; + tokenCount: number | string; + totalAgreedPrice: number | string; + tokenActions: SerializedOrchardActionJSON[]; + tokenAnchor: string; + tokenProof: string; + tokenBindingSignature: string; + feeActions: SerializedOrchardActionJSON[]; + feeAnchor: string; + feeProof: string; + feeBindingSignature: string; + creditAmount: number | string; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "TokenPurchaseFromShieldedPoolTransitionOptions")] + pub type TokenPurchaseFromShieldedPoolTransitionOptionsJs; + + #[wasm_bindgen(typescript_type = "TokenPurchaseFromShieldedPoolTransitionObject")] + pub type TokenPurchaseFromShieldedPoolTransitionObjectJs; + + #[wasm_bindgen(typescript_type = "TokenPurchaseFromShieldedPoolTransitionJSON")] + pub type TokenPurchaseFromShieldedPoolTransitionJSONJs; +} + +/// Non-WASM-instance fields extracted from the constructor options via serde. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct TokenPurchaseFromShieldedPoolTransitionSimpleFields { + token_contract_position: u16, + token_count: u64, + total_agreed_price: u64, + token_anchor: Vec, + token_proof: Vec, + token_binding_signature: Vec, + fee_anchor: Vec, + fee_proof: Vec, + fee_binding_signature: Vec, + credit_amount: u64, +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(transparent)] +#[wasm_bindgen(js_name = TokenPurchaseFromShieldedPoolTransition)] +pub struct TokenPurchaseFromShieldedPoolTransitionWasm(TokenPurchaseFromShieldedPoolTransition); + +impl From for TokenPurchaseFromShieldedPoolTransitionWasm { + fn from(v: TokenPurchaseFromShieldedPoolTransition) -> Self { + TokenPurchaseFromShieldedPoolTransitionWasm(v) + } +} + +impl From for TokenPurchaseFromShieldedPoolTransition { + fn from(v: TokenPurchaseFromShieldedPoolTransitionWasm) -> Self { + v.0 + } +} + +#[wasm_bindgen(js_class = TokenPurchaseFromShieldedPoolTransition)] +impl TokenPurchaseFromShieldedPoolTransitionWasm { + #[wasm_bindgen(constructor)] + pub fn new( + options: TokenPurchaseFromShieldedPoolTransitionOptionsJs, + ) -> WasmDppResult { + let js_opts: &JsValue = options.as_ref(); + let data_contract_id: IdentifierWasm = try_from_options(js_opts, "dataContractId")?; + let token_id: IdentifierWasm = try_from_options(js_opts, "tokenId")?; + let token_actions = actions_from_js_options(js_opts, "tokenActions")?; + let fee_actions = actions_from_js_options(js_opts, "feeActions")?; + let fields: TokenPurchaseFromShieldedPoolTransitionSimpleFields = + serde_wasm_bindgen::from_value(options.into()) + .map_err(|e| WasmDppError::invalid_argument(e.to_string()))?; + let token_anchor: [u8; 32] = try_vec_to_fixed_bytes(fields.token_anchor, "tokenAnchor")?; + let token_binding_signature: [u8; 64] = + try_vec_to_fixed_bytes(fields.token_binding_signature, "tokenBindingSignature")?; + let fee_anchor: [u8; 32] = try_vec_to_fixed_bytes(fields.fee_anchor, "feeAnchor")?; + let fee_binding_signature: [u8; 64] = + try_vec_to_fixed_bytes(fields.fee_binding_signature, "feeBindingSignature")?; + Ok(TokenPurchaseFromShieldedPoolTransitionWasm( + TokenPurchaseFromShieldedPoolTransition::V0( + TokenPurchaseFromShieldedPoolTransitionV0 { + data_contract_id: data_contract_id.into(), + token_contract_position: fields.token_contract_position, + token_id: token_id.into(), + token_count: fields.token_count, + total_agreed_price: fields.total_agreed_price, + token_actions: token_actions.into_iter().map(Into::into).collect(), + token_anchor, + token_proof: fields.token_proof, + token_binding_signature, + fee_actions: fee_actions.into_iter().map(Into::into).collect(), + fee_anchor, + fee_proof: fields.fee_proof, + fee_binding_signature, + credit_amount: fields.credit_amount, + }, + ), + )) + } + + #[wasm_bindgen(getter = "dataContractId")] + pub fn data_contract_id(&self) -> IdentifierWasm { + self.0.data_contract_id().into() + } + + #[wasm_bindgen(getter = "tokenContractPosition")] + pub fn token_contract_position(&self) -> u16 { + self.0.token_contract_position() + } + + #[wasm_bindgen(getter = "tokenId")] + pub fn token_id(&self) -> IdentifierWasm { + self.0.token_id().into() + } + + #[wasm_bindgen(getter = "tokenCount")] + pub fn token_count(&self) -> u64 { + self.0.token_count() + } + + #[wasm_bindgen(getter = "totalAgreedPrice")] + pub fn total_agreed_price(&self) -> u64 { + self.0.total_agreed_price() + } + + /// The serialized Orchard actions of the token pool bundle. + #[wasm_bindgen(getter = "tokenActions")] + pub fn token_actions(&self) -> Vec { + self.0 + .token_actions() + .iter() + .cloned() + .map(SerializedOrchardActionWasm::from) + .collect() + } + + #[wasm_bindgen(getter = "tokenAnchor")] + pub fn token_anchor(&self) -> Vec { + self.0.token_anchor().to_vec() + } + + #[wasm_bindgen(getter = "tokenProof")] + pub fn token_proof(&self) -> Vec { + self.0.token_proof().to_vec() + } + + #[wasm_bindgen(getter = "tokenBindingSignature")] + pub fn token_binding_signature(&self) -> Vec { + self.0.token_binding_signature().to_vec() + } + + /// The serialized Orchard actions of the credit pool fee bundle. + #[wasm_bindgen(getter = "feeActions")] + pub fn fee_actions(&self) -> Vec { + self.0 + .fee_actions() + .iter() + .cloned() + .map(SerializedOrchardActionWasm::from) + .collect() + } + + #[wasm_bindgen(getter = "feeAnchor")] + pub fn fee_anchor(&self) -> Vec { + self.0.fee_anchor().to_vec() + } + + #[wasm_bindgen(getter = "feeProof")] + pub fn fee_proof(&self) -> Vec { + self.0.fee_proof().to_vec() + } + + #[wasm_bindgen(getter = "feeBindingSignature")] + pub fn fee_binding_signature(&self) -> Vec { + self.0.fee_binding_signature().to_vec() + } + + /// Credits leaving the credit pool: the fee bundle's value balance. + #[wasm_bindgen(getter = "creditAmount")] + pub fn credit_amount(&self) -> u64 { + self.0.credit_amount() + } + + #[wasm_bindgen(js_name = getModifiedDataIds)] + pub fn modified_data_ids(&self) -> Vec { + self.0 + .modified_data_ids() + .into_iter() + .map(IdentifierWasm::from) + .collect() + } + + #[wasm_bindgen(js_name = toBytes)] + pub fn to_bytes(&self) -> WasmDppResult> { + Ok(PlatformSerializable::serialize_to_bytes( + &StateTransition::TokenPurchaseFromShieldedPool(self.0.clone()), + )?) + } + + #[wasm_bindgen(js_name = "toHex")] + pub fn to_hex(&self) -> WasmDppResult { + Ok(encode(self.to_bytes()?.as_slice(), Hex)) + } + + #[wasm_bindgen(js_name = "toBase64")] + pub fn to_base64(&self) -> WasmDppResult { + Ok(encode(self.to_bytes()?.as_slice(), Base64)) + } + + #[wasm_bindgen(js_name = fromBytes)] + pub fn from_bytes( + bytes: Vec, + ) -> WasmDppResult { + let st = StateTransition::deserialize_from_bytes_untrusted(&bytes)?; + match st { + StateTransition::TokenPurchaseFromShieldedPool(inner) => Ok(inner.into()), + _ => Err(WasmDppError::invalid_argument( + "Invalid state transition type: expected TokenPurchaseFromShieldedPool", + )), + } + } + + #[wasm_bindgen(js_name = "fromHex")] + pub fn from_hex(hex: String) -> WasmDppResult { + let bytes = + decode(hex.as_str(), Hex).map_err(|e| WasmDppError::serialization(e.to_string()))?; + TokenPurchaseFromShieldedPoolTransitionWasm::from_bytes(bytes) + } + + #[wasm_bindgen(js_name = "fromBase64")] + pub fn from_base64( + base64: String, + ) -> WasmDppResult { + let bytes = decode(base64.as_str(), Base64) + .map_err(|e| WasmDppError::serialization(e.to_string()))?; + TokenPurchaseFromShieldedPoolTransitionWasm::from_bytes(bytes) + } + + #[wasm_bindgen(js_name = toStateTransition)] + pub fn to_state_transition(&self) -> StateTransitionWasm { + StateTransition::TokenPurchaseFromShieldedPool(self.0.clone()).into() + } + + #[wasm_bindgen(js_name = "fromStateTransition")] + pub fn from_state_transition( + st: &StateTransitionWasm, + ) -> WasmDppResult { + let rs_st: StateTransition = st.clone().into(); + match rs_st { + StateTransition::TokenPurchaseFromShieldedPool(inner) => Ok(inner.into()), + _ => Err(WasmDppError::invalid_argument( + "Invalid state transition type: expected TokenPurchaseFromShieldedPool", + )), + } + } +} + +impl_wasm_conversions_inner!( + TokenPurchaseFromShieldedPoolTransitionWasm, + TokenPurchaseFromShieldedPoolTransition, + TokenPurchaseFromShieldedPoolTransition, + TokenPurchaseFromShieldedPoolTransitionObjectJs, + TokenPurchaseFromShieldedPoolTransitionJSONJs +); +impl_wasm_type_info!( + TokenPurchaseFromShieldedPoolTransitionWasm, + TokenPurchaseFromShieldedPoolTransition +); diff --git a/packages/wasm-dpp2/src/shielded/token_shielded_transfer_with_shielded_fee_transition.rs b/packages/wasm-dpp2/src/shielded/token_shielded_transfer_with_shielded_fee_transition.rs new file mode 100644 index 00000000000..fae059be355 --- /dev/null +++ b/packages/wasm-dpp2/src/shielded/token_shielded_transfer_with_shielded_fee_transition.rs @@ -0,0 +1,319 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::identifier::IdentifierWasm; +use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; +use crate::state_transitions::StateTransitionWasm; +use crate::utils::{try_from_options, try_vec_to_fixed_bytes}; +use crate::{impl_wasm_conversions_inner, impl_wasm_type_info}; +use dpp::platform_value::string_encoding::Encoding::{Base64, Hex}; +use dpp::platform_value::string_encoding::{decode, encode}; +use dpp::serialization::{PlatformDeserializableUntrusted, PlatformSerializable}; +use dpp::state_transition::token_shielded_transfer_with_shielded_fee_transition::TokenShieldedTransferWithShieldedFeeTransition; +use dpp::state_transition::token_shielded_transfer_with_shielded_fee_transition::accessors::TokenShieldedTransferWithShieldedFeeTransitionAccessorsV0; +use dpp::state_transition::token_shielded_transfer_with_shielded_fee_transition::v0::TokenShieldedTransferWithShieldedFeeTransitionV0; +use dpp::state_transition::{StateTransition, StateTransitionLike}; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const TS_TYPES: &str = r#" +/** + * Options for constructing a TokenShieldedTransferWithShieldedFeeTransition (token shielded transfer paid from the credit pool). Two Orchard + * bundles: the token pool bundle and the credit pool fee bundle. There is no platform signature. + */ +export interface TokenShieldedTransferWithShieldedFeeTransitionOptions { + dataContractId: IdentifierLike; + tokenContractPosition: number; + tokenId: IdentifierLike; + tokenActions: SerializedOrchardAction[]; + tokenAnchor: Uint8Array; + tokenProof: Uint8Array; + tokenBindingSignature: Uint8Array; + feeActions: SerializedOrchardAction[]; + feeAnchor: Uint8Array; + feeProof: Uint8Array; + feeBindingSignature: Uint8Array; + creditAmount: bigint; +} + +export interface TokenShieldedTransferWithShieldedFeeTransitionObject { + $formatVersion: string; + dataContractId: Uint8Array; + tokenContractPosition: number; + tokenId: Uint8Array; + tokenActions: SerializedOrchardActionObject[]; + tokenAnchor: Uint8Array; + tokenProof: Uint8Array; + tokenBindingSignature: Uint8Array; + feeActions: SerializedOrchardActionObject[]; + feeAnchor: Uint8Array; + feeProof: Uint8Array; + feeBindingSignature: Uint8Array; + creditAmount: bigint; +} + +export interface TokenShieldedTransferWithShieldedFeeTransitionJSON { + $formatVersion: string; + dataContractId: string; + tokenContractPosition: number; + tokenId: string; + tokenActions: SerializedOrchardActionJSON[]; + tokenAnchor: string; + tokenProof: string; + tokenBindingSignature: string; + feeActions: SerializedOrchardActionJSON[]; + feeAnchor: string; + feeProof: string; + feeBindingSignature: string; + creditAmount: number | string; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "TokenShieldedTransferWithShieldedFeeTransitionOptions")] + pub type TokenShieldedTransferWithShieldedFeeTransitionOptionsJs; + + #[wasm_bindgen(typescript_type = "TokenShieldedTransferWithShieldedFeeTransitionObject")] + pub type TokenShieldedTransferWithShieldedFeeTransitionObjectJs; + + #[wasm_bindgen(typescript_type = "TokenShieldedTransferWithShieldedFeeTransitionJSON")] + pub type TokenShieldedTransferWithShieldedFeeTransitionJSONJs; +} + +/// Non-WASM-instance fields extracted from the constructor options via serde. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct TokenShieldedTransferWithShieldedFeeTransitionSimpleFields { + token_contract_position: u16, + token_anchor: Vec, + token_proof: Vec, + token_binding_signature: Vec, + fee_anchor: Vec, + fee_proof: Vec, + fee_binding_signature: Vec, + credit_amount: u64, +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(transparent)] +#[wasm_bindgen(js_name = TokenShieldedTransferWithShieldedFeeTransition)] +pub struct TokenShieldedTransferWithShieldedFeeTransitionWasm( + TokenShieldedTransferWithShieldedFeeTransition, +); + +impl From + for TokenShieldedTransferWithShieldedFeeTransitionWasm +{ + fn from(v: TokenShieldedTransferWithShieldedFeeTransition) -> Self { + TokenShieldedTransferWithShieldedFeeTransitionWasm(v) + } +} + +impl From + for TokenShieldedTransferWithShieldedFeeTransition +{ + fn from(v: TokenShieldedTransferWithShieldedFeeTransitionWasm) -> Self { + v.0 + } +} + +#[wasm_bindgen(js_class = TokenShieldedTransferWithShieldedFeeTransition)] +impl TokenShieldedTransferWithShieldedFeeTransitionWasm { + #[wasm_bindgen(constructor)] + pub fn new( + options: TokenShieldedTransferWithShieldedFeeTransitionOptionsJs, + ) -> WasmDppResult { + let js_opts: &JsValue = options.as_ref(); + let data_contract_id: IdentifierWasm = try_from_options(js_opts, "dataContractId")?; + let token_id: IdentifierWasm = try_from_options(js_opts, "tokenId")?; + let token_actions = actions_from_js_options(js_opts, "tokenActions")?; + let fee_actions = actions_from_js_options(js_opts, "feeActions")?; + let fields: TokenShieldedTransferWithShieldedFeeTransitionSimpleFields = + serde_wasm_bindgen::from_value(options.into()) + .map_err(|e| WasmDppError::invalid_argument(e.to_string()))?; + let token_anchor: [u8; 32] = try_vec_to_fixed_bytes(fields.token_anchor, "tokenAnchor")?; + let token_binding_signature: [u8; 64] = + try_vec_to_fixed_bytes(fields.token_binding_signature, "tokenBindingSignature")?; + let fee_anchor: [u8; 32] = try_vec_to_fixed_bytes(fields.fee_anchor, "feeAnchor")?; + let fee_binding_signature: [u8; 64] = + try_vec_to_fixed_bytes(fields.fee_binding_signature, "feeBindingSignature")?; + Ok(TokenShieldedTransferWithShieldedFeeTransitionWasm( + TokenShieldedTransferWithShieldedFeeTransition::V0( + TokenShieldedTransferWithShieldedFeeTransitionV0 { + data_contract_id: data_contract_id.into(), + token_contract_position: fields.token_contract_position, + token_id: token_id.into(), + token_actions: token_actions.into_iter().map(Into::into).collect(), + token_anchor, + token_proof: fields.token_proof, + token_binding_signature, + fee_actions: fee_actions.into_iter().map(Into::into).collect(), + fee_anchor, + fee_proof: fields.fee_proof, + fee_binding_signature, + credit_amount: fields.credit_amount, + }, + ), + )) + } + + #[wasm_bindgen(getter = "dataContractId")] + pub fn data_contract_id(&self) -> IdentifierWasm { + self.0.data_contract_id().into() + } + + #[wasm_bindgen(getter = "tokenContractPosition")] + pub fn token_contract_position(&self) -> u16 { + self.0.token_contract_position() + } + + #[wasm_bindgen(getter = "tokenId")] + pub fn token_id(&self) -> IdentifierWasm { + self.0.token_id().into() + } + + /// The serialized Orchard actions of the token pool bundle. + #[wasm_bindgen(getter = "tokenActions")] + pub fn token_actions(&self) -> Vec { + self.0 + .token_actions() + .iter() + .cloned() + .map(SerializedOrchardActionWasm::from) + .collect() + } + + #[wasm_bindgen(getter = "tokenAnchor")] + pub fn token_anchor(&self) -> Vec { + self.0.token_anchor().to_vec() + } + + #[wasm_bindgen(getter = "tokenProof")] + pub fn token_proof(&self) -> Vec { + self.0.token_proof().to_vec() + } + + #[wasm_bindgen(getter = "tokenBindingSignature")] + pub fn token_binding_signature(&self) -> Vec { + self.0.token_binding_signature().to_vec() + } + + /// The serialized Orchard actions of the credit pool fee bundle. + #[wasm_bindgen(getter = "feeActions")] + pub fn fee_actions(&self) -> Vec { + self.0 + .fee_actions() + .iter() + .cloned() + .map(SerializedOrchardActionWasm::from) + .collect() + } + + #[wasm_bindgen(getter = "feeAnchor")] + pub fn fee_anchor(&self) -> Vec { + self.0.fee_anchor().to_vec() + } + + #[wasm_bindgen(getter = "feeProof")] + pub fn fee_proof(&self) -> Vec { + self.0.fee_proof().to_vec() + } + + #[wasm_bindgen(getter = "feeBindingSignature")] + pub fn fee_binding_signature(&self) -> Vec { + self.0.fee_binding_signature().to_vec() + } + + /// Credits leaving the credit pool: the fee bundle's value balance. + #[wasm_bindgen(getter = "creditAmount")] + pub fn credit_amount(&self) -> u64 { + self.0.credit_amount() + } + + #[wasm_bindgen(js_name = getModifiedDataIds)] + pub fn modified_data_ids(&self) -> Vec { + self.0 + .modified_data_ids() + .into_iter() + .map(IdentifierWasm::from) + .collect() + } + + #[wasm_bindgen(js_name = toBytes)] + pub fn to_bytes(&self) -> WasmDppResult> { + Ok(PlatformSerializable::serialize_to_bytes( + &StateTransition::TokenShieldedTransferWithShieldedFee(self.0.clone()), + )?) + } + + #[wasm_bindgen(js_name = "toHex")] + pub fn to_hex(&self) -> WasmDppResult { + Ok(encode(self.to_bytes()?.as_slice(), Hex)) + } + + #[wasm_bindgen(js_name = "toBase64")] + pub fn to_base64(&self) -> WasmDppResult { + Ok(encode(self.to_bytes()?.as_slice(), Base64)) + } + + #[wasm_bindgen(js_name = fromBytes)] + pub fn from_bytes( + bytes: Vec, + ) -> WasmDppResult { + let st = StateTransition::deserialize_from_bytes_untrusted(&bytes)?; + match st { + StateTransition::TokenShieldedTransferWithShieldedFee(inner) => Ok(inner.into()), + _ => Err(WasmDppError::invalid_argument( + "Invalid state transition type: expected TokenShieldedTransferWithShieldedFee", + )), + } + } + + #[wasm_bindgen(js_name = "fromHex")] + pub fn from_hex( + hex: String, + ) -> WasmDppResult { + let bytes = + decode(hex.as_str(), Hex).map_err(|e| WasmDppError::serialization(e.to_string()))?; + TokenShieldedTransferWithShieldedFeeTransitionWasm::from_bytes(bytes) + } + + #[wasm_bindgen(js_name = "fromBase64")] + pub fn from_base64( + base64: String, + ) -> WasmDppResult { + let bytes = decode(base64.as_str(), Base64) + .map_err(|e| WasmDppError::serialization(e.to_string()))?; + TokenShieldedTransferWithShieldedFeeTransitionWasm::from_bytes(bytes) + } + + #[wasm_bindgen(js_name = toStateTransition)] + pub fn to_state_transition(&self) -> StateTransitionWasm { + StateTransition::TokenShieldedTransferWithShieldedFee(self.0.clone()).into() + } + + #[wasm_bindgen(js_name = "fromStateTransition")] + pub fn from_state_transition( + st: &StateTransitionWasm, + ) -> WasmDppResult { + let rs_st: StateTransition = st.clone().into(); + match rs_st { + StateTransition::TokenShieldedTransferWithShieldedFee(inner) => Ok(inner.into()), + _ => Err(WasmDppError::invalid_argument( + "Invalid state transition type: expected TokenShieldedTransferWithShieldedFee", + )), + } + } +} + +impl_wasm_conversions_inner!( + TokenShieldedTransferWithShieldedFeeTransitionWasm, + TokenShieldedTransferWithShieldedFeeTransition, + TokenShieldedTransferWithShieldedFeeTransition, + TokenShieldedTransferWithShieldedFeeTransitionObjectJs, + TokenShieldedTransferWithShieldedFeeTransitionJSONJs +); +impl_wasm_type_info!( + TokenShieldedTransferWithShieldedFeeTransitionWasm, + TokenShieldedTransferWithShieldedFeeTransition +); diff --git a/packages/wasm-dpp2/src/shielded/token_unshield_with_shielded_fee_transition.rs b/packages/wasm-dpp2/src/shielded/token_unshield_with_shielded_fee_transition.rs new file mode 100644 index 00000000000..bd1c98d4ecc --- /dev/null +++ b/packages/wasm-dpp2/src/shielded/token_unshield_with_shielded_fee_transition.rs @@ -0,0 +1,327 @@ +use crate::error::{WasmDppError, WasmDppResult}; +use crate::identifier::IdentifierWasm; +use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; +use crate::state_transitions::StateTransitionWasm; +use crate::utils::{try_from_options, try_vec_to_fixed_bytes}; +use crate::{impl_wasm_conversions_inner, impl_wasm_type_info}; +use dpp::platform_value::string_encoding::Encoding::{Base64, Hex}; +use dpp::platform_value::string_encoding::{decode, encode}; +use dpp::serialization::{PlatformDeserializableUntrusted, PlatformSerializable}; +use dpp::state_transition::token_unshield_with_shielded_fee_transition::TokenUnshieldWithShieldedFeeTransition; +use dpp::state_transition::token_unshield_with_shielded_fee_transition::accessors::TokenUnshieldWithShieldedFeeTransitionAccessorsV0; +use dpp::state_transition::token_unshield_with_shielded_fee_transition::v0::TokenUnshieldWithShieldedFeeTransitionV0; +use dpp::state_transition::{StateTransition, StateTransitionLike}; +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const TS_TYPES: &str = r#" +/** + * Options for constructing a TokenUnshieldWithShieldedFeeTransition (identity-less unshield). Two Orchard + * bundles: the token pool bundle and the credit pool fee bundle. There is no platform signature. + */ +export interface TokenUnshieldWithShieldedFeeTransitionOptions { + dataContractId: IdentifierLike; + tokenContractPosition: number; + tokenId: IdentifierLike; + recipientId: IdentifierLike; + amount: bigint; + tokenActions: SerializedOrchardAction[]; + tokenAnchor: Uint8Array; + tokenProof: Uint8Array; + tokenBindingSignature: Uint8Array; + feeActions: SerializedOrchardAction[]; + feeAnchor: Uint8Array; + feeProof: Uint8Array; + feeBindingSignature: Uint8Array; + creditAmount: bigint; +} + +export interface TokenUnshieldWithShieldedFeeTransitionObject { + $formatVersion: string; + dataContractId: Uint8Array; + tokenContractPosition: number; + tokenId: Uint8Array; + recipientId: Uint8Array; + amount: bigint; + tokenActions: SerializedOrchardActionObject[]; + tokenAnchor: Uint8Array; + tokenProof: Uint8Array; + tokenBindingSignature: Uint8Array; + feeActions: SerializedOrchardActionObject[]; + feeAnchor: Uint8Array; + feeProof: Uint8Array; + feeBindingSignature: Uint8Array; + creditAmount: bigint; +} + +export interface TokenUnshieldWithShieldedFeeTransitionJSON { + $formatVersion: string; + dataContractId: string; + tokenContractPosition: number; + tokenId: string; + recipientId: string; + amount: number | string; + tokenActions: SerializedOrchardActionJSON[]; + tokenAnchor: string; + tokenProof: string; + tokenBindingSignature: string; + feeActions: SerializedOrchardActionJSON[]; + feeAnchor: string; + feeProof: string; + feeBindingSignature: string; + creditAmount: number | string; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "TokenUnshieldWithShieldedFeeTransitionOptions")] + pub type TokenUnshieldWithShieldedFeeTransitionOptionsJs; + + #[wasm_bindgen(typescript_type = "TokenUnshieldWithShieldedFeeTransitionObject")] + pub type TokenUnshieldWithShieldedFeeTransitionObjectJs; + + #[wasm_bindgen(typescript_type = "TokenUnshieldWithShieldedFeeTransitionJSON")] + pub type TokenUnshieldWithShieldedFeeTransitionJSONJs; +} + +/// Non-WASM-instance fields extracted from the constructor options via serde. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct TokenUnshieldWithShieldedFeeTransitionSimpleFields { + token_contract_position: u16, + amount: u64, + token_anchor: Vec, + token_proof: Vec, + token_binding_signature: Vec, + fee_anchor: Vec, + fee_proof: Vec, + fee_binding_signature: Vec, + credit_amount: u64, +} + +#[derive(Clone, Serialize, Deserialize)] +#[serde(transparent)] +#[wasm_bindgen(js_name = TokenUnshieldWithShieldedFeeTransition)] +pub struct TokenUnshieldWithShieldedFeeTransitionWasm(TokenUnshieldWithShieldedFeeTransition); + +impl From for TokenUnshieldWithShieldedFeeTransitionWasm { + fn from(v: TokenUnshieldWithShieldedFeeTransition) -> Self { + TokenUnshieldWithShieldedFeeTransitionWasm(v) + } +} + +impl From for TokenUnshieldWithShieldedFeeTransition { + fn from(v: TokenUnshieldWithShieldedFeeTransitionWasm) -> Self { + v.0 + } +} + +#[wasm_bindgen(js_class = TokenUnshieldWithShieldedFeeTransition)] +impl TokenUnshieldWithShieldedFeeTransitionWasm { + #[wasm_bindgen(constructor)] + pub fn new( + options: TokenUnshieldWithShieldedFeeTransitionOptionsJs, + ) -> WasmDppResult { + let js_opts: &JsValue = options.as_ref(); + let data_contract_id: IdentifierWasm = try_from_options(js_opts, "dataContractId")?; + let token_id: IdentifierWasm = try_from_options(js_opts, "tokenId")?; + let recipient_id: IdentifierWasm = try_from_options(js_opts, "recipientId")?; + let token_actions = actions_from_js_options(js_opts, "tokenActions")?; + let fee_actions = actions_from_js_options(js_opts, "feeActions")?; + let fields: TokenUnshieldWithShieldedFeeTransitionSimpleFields = + serde_wasm_bindgen::from_value(options.into()) + .map_err(|e| WasmDppError::invalid_argument(e.to_string()))?; + let token_anchor: [u8; 32] = try_vec_to_fixed_bytes(fields.token_anchor, "tokenAnchor")?; + let token_binding_signature: [u8; 64] = + try_vec_to_fixed_bytes(fields.token_binding_signature, "tokenBindingSignature")?; + let fee_anchor: [u8; 32] = try_vec_to_fixed_bytes(fields.fee_anchor, "feeAnchor")?; + let fee_binding_signature: [u8; 64] = + try_vec_to_fixed_bytes(fields.fee_binding_signature, "feeBindingSignature")?; + Ok(TokenUnshieldWithShieldedFeeTransitionWasm( + TokenUnshieldWithShieldedFeeTransition::V0(TokenUnshieldWithShieldedFeeTransitionV0 { + data_contract_id: data_contract_id.into(), + token_contract_position: fields.token_contract_position, + token_id: token_id.into(), + recipient_id: recipient_id.into(), + amount: fields.amount, + token_actions: token_actions.into_iter().map(Into::into).collect(), + token_anchor, + token_proof: fields.token_proof, + token_binding_signature, + fee_actions: fee_actions.into_iter().map(Into::into).collect(), + fee_anchor, + fee_proof: fields.fee_proof, + fee_binding_signature, + credit_amount: fields.credit_amount, + }), + )) + } + + #[wasm_bindgen(getter = "dataContractId")] + pub fn data_contract_id(&self) -> IdentifierWasm { + self.0.data_contract_id().into() + } + + #[wasm_bindgen(getter = "tokenContractPosition")] + pub fn token_contract_position(&self) -> u16 { + self.0.token_contract_position() + } + + #[wasm_bindgen(getter = "tokenId")] + pub fn token_id(&self) -> IdentifierWasm { + self.0.token_id().into() + } + + #[wasm_bindgen(getter = "recipientId")] + pub fn recipient_id(&self) -> IdentifierWasm { + self.0.recipient_id().into() + } + + #[wasm_bindgen(getter = "amount")] + pub fn amount(&self) -> u64 { + self.0.amount() + } + + /// The serialized Orchard actions of the token pool bundle. + #[wasm_bindgen(getter = "tokenActions")] + pub fn token_actions(&self) -> Vec { + self.0 + .token_actions() + .iter() + .cloned() + .map(SerializedOrchardActionWasm::from) + .collect() + } + + #[wasm_bindgen(getter = "tokenAnchor")] + pub fn token_anchor(&self) -> Vec { + self.0.token_anchor().to_vec() + } + + #[wasm_bindgen(getter = "tokenProof")] + pub fn token_proof(&self) -> Vec { + self.0.token_proof().to_vec() + } + + #[wasm_bindgen(getter = "tokenBindingSignature")] + pub fn token_binding_signature(&self) -> Vec { + self.0.token_binding_signature().to_vec() + } + + /// The serialized Orchard actions of the credit pool fee bundle. + #[wasm_bindgen(getter = "feeActions")] + pub fn fee_actions(&self) -> Vec { + self.0 + .fee_actions() + .iter() + .cloned() + .map(SerializedOrchardActionWasm::from) + .collect() + } + + #[wasm_bindgen(getter = "feeAnchor")] + pub fn fee_anchor(&self) -> Vec { + self.0.fee_anchor().to_vec() + } + + #[wasm_bindgen(getter = "feeProof")] + pub fn fee_proof(&self) -> Vec { + self.0.fee_proof().to_vec() + } + + #[wasm_bindgen(getter = "feeBindingSignature")] + pub fn fee_binding_signature(&self) -> Vec { + self.0.fee_binding_signature().to_vec() + } + + /// Credits leaving the credit pool: the fee bundle's value balance. + #[wasm_bindgen(getter = "creditAmount")] + pub fn credit_amount(&self) -> u64 { + self.0.credit_amount() + } + + #[wasm_bindgen(js_name = getModifiedDataIds)] + pub fn modified_data_ids(&self) -> Vec { + self.0 + .modified_data_ids() + .into_iter() + .map(IdentifierWasm::from) + .collect() + } + + #[wasm_bindgen(js_name = toBytes)] + pub fn to_bytes(&self) -> WasmDppResult> { + Ok(PlatformSerializable::serialize_to_bytes( + &StateTransition::TokenUnshieldWithShieldedFee(self.0.clone()), + )?) + } + + #[wasm_bindgen(js_name = "toHex")] + pub fn to_hex(&self) -> WasmDppResult { + Ok(encode(self.to_bytes()?.as_slice(), Hex)) + } + + #[wasm_bindgen(js_name = "toBase64")] + pub fn to_base64(&self) -> WasmDppResult { + Ok(encode(self.to_bytes()?.as_slice(), Base64)) + } + + #[wasm_bindgen(js_name = fromBytes)] + pub fn from_bytes(bytes: Vec) -> WasmDppResult { + let st = StateTransition::deserialize_from_bytes_untrusted(&bytes)?; + match st { + StateTransition::TokenUnshieldWithShieldedFee(inner) => Ok(inner.into()), + _ => Err(WasmDppError::invalid_argument( + "Invalid state transition type: expected TokenUnshieldWithShieldedFee", + )), + } + } + + #[wasm_bindgen(js_name = "fromHex")] + pub fn from_hex(hex: String) -> WasmDppResult { + let bytes = + decode(hex.as_str(), Hex).map_err(|e| WasmDppError::serialization(e.to_string()))?; + TokenUnshieldWithShieldedFeeTransitionWasm::from_bytes(bytes) + } + + #[wasm_bindgen(js_name = "fromBase64")] + pub fn from_base64( + base64: String, + ) -> WasmDppResult { + let bytes = decode(base64.as_str(), Base64) + .map_err(|e| WasmDppError::serialization(e.to_string()))?; + TokenUnshieldWithShieldedFeeTransitionWasm::from_bytes(bytes) + } + + #[wasm_bindgen(js_name = toStateTransition)] + pub fn to_state_transition(&self) -> StateTransitionWasm { + StateTransition::TokenUnshieldWithShieldedFee(self.0.clone()).into() + } + + #[wasm_bindgen(js_name = "fromStateTransition")] + pub fn from_state_transition( + st: &StateTransitionWasm, + ) -> WasmDppResult { + let rs_st: StateTransition = st.clone().into(); + match rs_st { + StateTransition::TokenUnshieldWithShieldedFee(inner) => Ok(inner.into()), + _ => Err(WasmDppError::invalid_argument( + "Invalid state transition type: expected TokenUnshieldWithShieldedFee", + )), + } + } +} + +impl_wasm_conversions_inner!( + TokenUnshieldWithShieldedFeeTransitionWasm, + TokenUnshieldWithShieldedFeeTransition, + TokenUnshieldWithShieldedFeeTransition, + TokenUnshieldWithShieldedFeeTransitionObjectJs, + TokenUnshieldWithShieldedFeeTransitionJSONJs +); +impl_wasm_type_info!( + TokenUnshieldWithShieldedFeeTransitionWasm, + TokenUnshieldWithShieldedFeeTransition +); diff --git a/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs b/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs index cda6a045da5..b859cb1336a 100644 --- a/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs +++ b/packages/wasm-dpp2/src/state_transitions/base/state_transition.rs @@ -368,6 +368,9 @@ impl StateTransitionWasm { IdentityKeyLimitsUpdate(_) => 23, ContractUserModeration(_) => 24, ContractFeeClaim(_) => 25, + TokenShieldedTransferWithShieldedFee(_) => 26, + TokenUnshieldWithShieldedFee(_) => 27, + TokenPurchaseFromShieldedPool(_) => 28, } } @@ -462,6 +465,9 @@ impl StateTransitionWasm { | ShieldFromAssetLock(_) | ShieldedWithdrawal(_) | IdentityCreateFromShieldedPool(_) + | TokenShieldedTransferWithShieldedFee(_) + | TokenUnshieldWithShieldedFee(_) + | TokenPurchaseFromShieldedPool(_) | IdentityTopUpFromShieldedPool(_) => None, } } @@ -495,6 +501,9 @@ impl StateTransitionWasm { | ShieldFromAssetLock(_) | ShieldedWithdrawal(_) | IdentityCreateFromShieldedPool(_) + | TokenShieldedTransferWithShieldedFee(_) + | TokenUnshieldWithShieldedFee(_) + | TokenPurchaseFromShieldedPool(_) | IdentityTopUpFromShieldedPool(_) => None, } } @@ -658,6 +667,9 @@ impl StateTransitionWasm { | ShieldFromAssetLock(_) | ShieldedWithdrawal(_) | IdentityCreateFromShieldedPool(_) + | TokenShieldedTransferWithShieldedFee(_) + | TokenUnshieldWithShieldedFee(_) + | TokenPurchaseFromShieldedPool(_) | IdentityTopUpFromShieldedPool(_) => { return Err(WasmDppError::invalid_argument( "Cannot set owner for shielded transition", @@ -753,6 +765,9 @@ impl StateTransitionWasm { | ShieldFromAssetLock(_) | ShieldedWithdrawal(_) | IdentityCreateFromShieldedPool(_) + | TokenShieldedTransferWithShieldedFee(_) + | TokenUnshieldWithShieldedFee(_) + | TokenPurchaseFromShieldedPool(_) | IdentityTopUpFromShieldedPool(_) => { return Err(WasmDppError::invalid_argument( "Cannot set identity contract nonce for shielded transition", @@ -875,6 +890,9 @@ impl StateTransitionWasm { | ShieldFromAssetLock(_) | ShieldedWithdrawal(_) | IdentityCreateFromShieldedPool(_) + | TokenShieldedTransferWithShieldedFee(_) + | TokenUnshieldWithShieldedFee(_) + | TokenPurchaseFromShieldedPool(_) | IdentityTopUpFromShieldedPool(_) => { return Err(WasmDppError::invalid_argument( "Cannot set identity nonce for shielded transition", From c46dfd807d0457368ff2e85aa4c25bca48ed4508 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Wed, 16 Sep 2026 19:37:59 +0700 Subject: [PATCH 08/94] fix(drive-abci): drop trailing whitespace rustfmt refused in the pool purchase validator Co-Authored-By: Claude Fable 5.1 --- .../transform_into_action/v0/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs index 3b241bf633a..94b9f874099 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs @@ -1,6 +1,6 @@ use crate::error::Error; use crate::execution::validation::state_transition::state_transitions::token_pool_paid_common::{ - resolve_pooled_token, validate_credit_pool_fee_spend, + resolve_pooled_token, validate_credit_pool_fee_spend, }; use dpp::consensus::state::token::{TokenMintPastMaxSupplyError, TokenNotForDirectSale}; use dpp::data_contract::accessors::v0::DataContractV0Getters; From f67c8e42574b0e41637da1dd5a256e5720c0ca7d Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 18 Sep 2026 08:54:46 +0700 Subject: [PATCH 09/94] fix(platform): bind a group burn from the pool to its proposer and harden the pool purchase builder A TokenBurnFromPool bundle's sighash bound the batch owner, but a group action pins the digest of the proposer's signed actions, so every confirmer must submit that bundle unchanged and its proof failed under the confirmer's identity: a group burn from the pool could never complete with two signers. The sighash now binds the burner: the batch owner for a direct burn, the stored group action's proposer for a confirmer. The builder refuses to prove a fresh bundle for another signer, since consensus would reject it as a modified group action. A drive-abci test runs a burn by a group of two with real proofs, including a substituted bundle being rejected. The identity-less purchase builder rejects a token count above i64::MAX before proving instead of overflowing on negation, and proves the outputs-only bundle once over the purchase sighash instead of proving a throwaway bundle first. Co-Authored-By: Claude Fable 5.1 --- book/src/data-model/token-shielded-pools.md | 10 +- .../shielded/builder/token_burn_from_pool.rs | 60 +++++ .../src/shielded/builder/token_pool_paid.rs | 102 +++++--- packages/rs-dpp/src/shielded/sighash.rs | 23 +- .../batch_transition/methods/v1/mod.rs | 8 +- .../state_v0/mod.rs | 11 +- .../batch/tests/token/shielded/mod.rs | 224 +++++++++++++++++- 7 files changed, 382 insertions(+), 56 deletions(-) diff --git a/book/src/data-model/token-shielded-pools.md b/book/src/data-model/token-shielded-pools.md index ae9c586324b..8a9331383be 100644 --- a/book/src/data-model/token-shielded-pools.md +++ b/book/src/data-model/token-shielded-pools.md @@ -79,7 +79,7 @@ fees, so unlike the credit pool nothing is carved from the bundle's value balanc | `TokenUnshield` | spends and outputs | `+amount` | `token_id, owner_id, recipient_id, amount` | Notes are spent; `amount` is credited to `recipient_id`; change comes back as new notes. | | `TokenShieldedTransfer` | spends and outputs | `0` | `token_id, owner_id` | Notes are spent and recreated; the pool balance is unchanged. | | `TokenMintToPool` | outputs only | `-amount` | none | An authorized minter (manual minting rules, group actions supported) mints `amount` into new notes; the supply and the pool balance grow. | -| `TokenBurnFromPool` | spends and outputs | `+amount` | `token_id, owner_id, amount` | An authorized burner (manual burning rules) spends notes and destroys `amount`; the supply and the pool balance shrink. | +| `TokenBurnFromPool` | spends and outputs | `+amount` | `token_id, burner_id, amount` | An authorized burner (manual burning rules, group actions supported) spends notes and destroys `amount`; the supply and the pool balance shrink. `burner_id` is the batch owner, or the proposer of a group action. | | `TokenClaimToPool` | outputs only | `-amount` | none | A distribution claim released into new notes instead of the claimant's balance; a perpetual claim names the cycle-aligned moment it claims up to so the amount is predictable. | | `TokenDirectPurchaseToPool` | outputs only | `-token_count` | none | The buyer pays credits at the direct purchase price and the tokens are minted into new notes. | @@ -97,8 +97,12 @@ name an anchor the pool has recorded. A mint or burn into the pool that goes through a group action stores `TokenEvent::MintToPool` / `TokenEvent::BurnFromPool` with a digest of the serialized actions -(`serialized_actions_digest`), so every signer commits to exactly the same notes. No token -history document is written for pool operations: shielded activity is not recorded publicly. +(`serialized_actions_digest`), so every signer commits to exactly the same notes. A burn's +bundle is therefore proven once, by the proposer: the digest covers the spend authorization +signatures, so the sighash cannot depend on which signer's batch carries the bundle. It binds +the group action's proposer as `burner_id` (the batch owner for a direct burn) and every other +signer submits the proposer's bundle unchanged. No token history document is written for pool +operations: shielded activity is not recorded publicly. ## Documents paid from the pool diff --git a/packages/rs-dpp/src/shielded/builder/token_burn_from_pool.rs b/packages/rs-dpp/src/shielded/builder/token_burn_from_pool.rs index ddbbbbbc601..d0001c9beea 100644 --- a/packages/rs-dpp/src/shielded/builder/token_burn_from_pool.rs +++ b/packages/rs-dpp/src/shielded/builder/token_burn_from_pool.rs @@ -19,6 +19,13 @@ use super::{build_spend_bundle, serialize_authorized_bundle, OrchardProver, Spen /// pool, destroys `amount` and returns the remainder to `change_address`, then wraps the bundle /// in a batch transition signed by `owner_id`, who must be authorized to burn and pays the fee /// in credits. The token id, owner id and amount are bound into the Orchard sighash. +/// +/// A group action burn is proven once, by the proposer +/// (`GroupStateTransitionInfoProposer`): the group action pins the digest of the actions and +/// the sighash binds the proposer, so every other signer submits the proposer's bundle unchanged +/// through `new_token_burn_from_pool_transition` with `GroupStateTransitionInfoOtherSigner`. +/// Asking this builder for a fresh bundle on behalf of another signer is refused, since consensus +/// would reject it as a modification of the group action. #[allow(clippy::too_many_arguments)] pub async fn build_token_burn_from_pool_transition< S: Signer, @@ -56,6 +63,14 @@ pub async fn build_token_burn_from_pool_transition< i64::MAX as u64 ))); } + if matches!( + using_group_info, + Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(_)) + ) { + return Err(ProtocolError::ShieldedBuildError( + "a group action burn from pool is proven once by the proposer; another signer submits the proposer's bundle unchanged instead of building its own".to_string(), + )); + } let total_spent: u64 = spends .iter() @@ -127,6 +142,7 @@ pub async fn build_token_burn_from_pool_transition< #[cfg(test)] mod tests { use super::*; + use crate::group::GroupStateTransitionInfo; use crate::shielded::builder::test_helpers::{ test_identity_key, test_orchard_address, test_spendable_note, DummyIdentitySigner, TestProver, @@ -168,4 +184,48 @@ mod tests { "unexpected error: {err}" ); } + + #[tokio::test] + async fn rejects_a_fresh_bundle_for_another_group_signer() { + let sk = SpendingKey::from_bytes([42u8; 32]).expect("valid spending key bytes"); + let fvk = FullViewingKey::from(&sk); + let ask = SpendAuthorizingKey::from(&sk); + let key = test_identity_key(); + let err = build_token_burn_from_pool_transition( + Identifier::from([1u8; 32]), + Identifier::from([2u8; 32]), + Identifier::from([3u8; 32]), + 0, + vec![test_spendable_note(1_000)], + 100, + &test_orchard_address(), + &fvk, + &ask, + Anchor::empty_tree(), + [0u8; 36], + None, + Some( + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: 0, + action_id: Identifier::from([4u8; 32]), + action_is_proposer: false, + }, + ), + ), + &key, + 1, + 0, + &DummyIdentitySigner, + &TestProver, + PlatformVersion::latest(), + ) + .await + .expect_err("another signer must reuse the proposer's bundle") + .to_string(); + assert!( + err.contains("proven once by the proposer"), + "unexpected error: {err}" + ); + } } diff --git a/packages/rs-dpp/src/shielded/builder/token_pool_paid.rs b/packages/rs-dpp/src/shielded/builder/token_pool_paid.rs index 3909ae82a0c..5ad9f18e59a 100644 --- a/packages/rs-dpp/src/shielded/builder/token_pool_paid.rs +++ b/packages/rs-dpp/src/shielded/builder/token_pool_paid.rs @@ -1,5 +1,5 @@ use grovedb_commitment_tree::{ - Anchor, Builder, BundleType, DashMemo, FullViewingKey, NoteValue, PaymentAddress, Scope, + Anchor, Builder, BundleType, DashMemo, Flags, FullViewingKey, NoteValue, PaymentAddress, Scope, SpendAuthorizingKey, }; @@ -29,8 +29,8 @@ use platform_value::Identifier; use platform_version::version::PlatformVersion; use super::{ - build_output_only_bundle, build_spend_bundle, prove_and_sign_bundle, - serialize_authorized_bundle, OrchardProver, SerializedBundle, SpendableNote, + build_spend_bundle, prove_and_sign_bundle, serialize_authorized_bundle, OrchardProver, + SerializedBundle, SpendableNote, }; /// The credit pool side of an identity-less token pool transition: the wallet's credit pool @@ -350,43 +350,38 @@ pub fn build_token_purchase_from_shielded_pool_transition( "token purchase count must be greater than zero".to_string(), )); } - let token_bundle = build_output_only_bundle( - recipient, + if token_count > i64::MAX as u64 { + return Err(ProtocolError::ShieldedBuildError(format!( + "token purchase count {} exceeds maximum allowed value {}", + token_count, + i64::MAX as u64 + ))); + } + // An outputs-only bundle carries no spend-auth signatures; its binding signature still + // commits to the token id, count and price, so the bundle is proven over that sighash. + let token_extra = token_purchase_from_shielded_pool_extra_sighash_data( + &token_id.to_buffer(), token_count, - memo, - Some(recipient_fvk.to_ovk(Scope::External)), - 0, - prover, + total_agreed_price, + platform_version, )?; - // An outputs-only bundle carries no spend-auth signatures; its binding signature must - // still commit to the token id, count and price, so re-sign it over that sighash. - let token_sb = { - let mut builder = Builder::::new( - BundleType::Transactional { - flags: grovedb_commitment_tree::Flags::SPENDS_DISABLED, - bundle_required: false, - }, - Anchor::empty_tree(), - ); - builder - .add_output( - Some(recipient_fvk.to_ovk(Scope::External)), - PaymentAddress::from(recipient), - NoteValue::from_raw(token_count), - memo, - ) - .map_err(|e| { - ProtocolError::ShieldedBuildError(format!("failed to add output: {:?}", e)) - })?; - let token_extra = token_purchase_from_shielded_pool_extra_sighash_data( - &token_id.to_buffer(), - token_count, - total_agreed_price, - platform_version, - )?; - drop(token_bundle); - serialize_authorized_bundle(&prove_and_sign_bundle(builder, prover, &[], &token_extra)?) - }; + let mut builder = Builder::::new( + BundleType::Transactional { + flags: Flags::SPENDS_DISABLED, + bundle_required: false, + }, + Anchor::empty_tree(), + ); + builder + .add_output( + Some(recipient_fvk.to_ovk(Scope::External)), + PaymentAddress::from(recipient), + NoteValue::from_raw(token_count), + memo, + ) + .map_err(|e| ProtocolError::ShieldedBuildError(format!("failed to add output: {:?}", e)))?; + let token_sb = + serialize_authorized_bundle(&prove_and_sign_bundle(builder, prover, &[], &token_extra)?); if token_sb.value_balance != -(token_count as i64) { return Err(ProtocolError::ShieldedBuildError(format!( "token purchase bundle value balance {} does not equal minus the token count {}", @@ -514,6 +509,37 @@ mod tests { assert!(err.contains("greater than zero"), "unexpected error: {err}"); } + #[test] + fn rejects_a_purchase_count_outside_the_value_balance_range() { + let (fvk, ask) = keys(); + let address = test_orchard_address(); + let err = build_token_purchase_from_shielded_pool_transition( + Identifier::from([1u8; 32]), + Identifier::from([2u8; 32]), + 0, + &address, + &fvk, + 1u64 << 63, + 1_000, + [0u8; 36], + ShieldedFeePayer { + spends: vec![test_spendable_note(1_000_000_000)], + change_address: &address, + fvk: &fvk, + ask: &ask, + anchor: Anchor::empty_tree(), + }, + &TestProver, + PlatformVersion::latest(), + ) + .expect_err("a count that cannot be negated as i64 must be rejected before proving") + .to_string(); + assert!( + err.contains("exceeds maximum allowed value"), + "unexpected error: {err}" + ); + } + #[test] fn rejects_a_purchase_the_fee_notes_cannot_pay() { let (fvk, ask) = keys(); diff --git a/packages/rs-dpp/src/shielded/sighash.rs b/packages/rs-dpp/src/shielded/sighash.rs index 8ec533aab46..8782f9b1af7 100644 --- a/packages/rs-dpp/src/shielded/sighash.rs +++ b/packages/rs-dpp/src/shielded/sighash.rs @@ -355,18 +355,23 @@ pub fn token_unshield_extra_sighash_data_v0( data } -/// Extra sighash data of a batch `TokenBurnFromPool`: the token id, the burning identity and the -/// amount destroyed, so a bundle proven for one burn cannot be replayed for another token, -/// signer or amount (72 bytes; no other layout has that length). +/// Extra sighash data of a batch `TokenBurnFromPool`: the token id, the burner and the amount +/// destroyed, so a bundle proven for one burn cannot be replayed for another token, burner or +/// amount (72 bytes; no other layout has that length). +/// +/// `burner_id` is the identity the burn is attributed to: the batch owner of a direct burn, or +/// the proposer of a group action burn. A group action pins the digest of the bundle's actions +/// (spend authorization signatures included), so every other signer submits the proposer's +/// bundle unchanged and the sighash must not depend on whose batch carries it. pub fn token_burn_from_pool_extra_sighash_data( token_id: &[u8; 32], - owner_id: &[u8; 32], + burner_id: &[u8; 32], amount: u64, platform_version: &PlatformVersion, ) -> Result, ProtocolError> { match platform_version.dpp.methods.shielded_extra_sighash_data { 0 => Ok(token_burn_from_pool_extra_sighash_data_v0( - token_id, owner_id, amount, + token_id, burner_id, amount, )), version => Err(ProtocolError::UnknownVersionMismatch { method: "token_burn_from_pool_extra_sighash_data".to_string(), @@ -376,15 +381,15 @@ pub fn token_burn_from_pool_extra_sighash_data( } } -/// Version 0 layout: `token_id (32) || owner_id (32) || amount (8, little endian)`. +/// Version 0 layout: `token_id (32) || burner_id (32) || amount (8, little endian)`. pub fn token_burn_from_pool_extra_sighash_data_v0( token_id: &[u8; 32], - owner_id: &[u8; 32], + burner_id: &[u8; 32], amount: u64, ) -> Vec { let mut data = Vec::with_capacity(32 + 32 + 8); data.extend_from_slice(token_id); - data.extend_from_slice(owner_id); + data.extend_from_slice(burner_id); data.extend_from_slice(&amount.to_le_bytes()); data } @@ -960,7 +965,7 @@ mod tests { } #[test] - fn token_burn_from_pool_layout_is_token_owner_amount() { + fn token_burn_from_pool_layout_is_token_burner_amount() { let data = token_burn_from_pool_extra_sighash_data_v0(&[1u8; 32], &[2u8; 32], 300); assert_eq!(data.len(), 72); assert_eq!(&data[..32], &[1u8; 32]); diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs index 2a12c597283..5afa7835c81 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs @@ -194,7 +194,13 @@ pub trait DocumentsBatchTransitionMethodsV1: DocumentsBatchTransitionAccessorsV0 /// Creates a `StateTransition` destroying `amount` of a token held in the token's shielded /// pool by spending the notes in `bundle`. `owner_id` must be authorized to burn; it signs - /// and pays the credits fee. A group action is set up exactly like a transparent burn. + /// and pays the credits fee. + /// + /// A group action is set up exactly like a transparent burn, with one difference: `bundle` + /// is always the proposer's. Its spend authorization is signed over + /// `token_burn_from_pool_extra_sighash_data(token_id, proposer_id, amount)` and the group + /// action pins the digest of its actions, so every other signer passes the proposer's + /// `bundle` unchanged. #[cfg(feature = "state-transition-signing")] #[allow(clippy::too_many_arguments)] async fn new_token_burn_from_pool_transition>( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_burn_from_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_burn_from_pool_transition_action/state_v0/mod.rs index fe32bcc18aa..ac4f969d997 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_burn_from_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_burn_from_pool_transition_action/state_v0/mod.rs @@ -48,7 +48,7 @@ impl TokenBurnFromPoolTransitionActionStateValidationV0 for TokenBurnFromPoolTra /// Runs the transparent burn's authorization (group action included, with the actions digest /// as a group parameter), then the pool spend checks (paused, anchor, nullifiers, balance) /// and verifies the spend bundle whose value balance is `+amount`, bound to the token, the - /// burner and the amount. + /// burner (the batch owner, or the proposer of a group action) and the amount. fn validate_state_v0( &self, platform: &PlatformStateRef, @@ -231,9 +231,16 @@ impl TokenBurnFromPoolTransitionActionStateValidationV0 for TokenBurnFromPoolTra )); } + // A group action's bundle is proven once by the proposer and submitted unchanged by every + // other signer (the digest check above pins it), so the sighash binds the proposer rather + // than the batch owner. A direct burn's proposer is the batch owner. + let burner_id = match self.base().original_group_action() { + Some(original_group_action) => original_group_action.proposer_id(), + None => owner_id, + }; let extra_sighash_data = token_burn_from_pool_extra_sighash_data( &token_id_bytes, - &owner_id.to_buffer(), + &burner_id.to_buffer(), self.amount(), platform_version, )?; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index f0f9e61d37e..8aba6ff142d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -1097,7 +1097,7 @@ mod token_shielded_pool_tests { mod token_pool_mint_burn_claim_purchase_tests { use super::token_shielded_pool_tests::{ assert_tokens_conserved, build_shield_bundle, build_spend_bundle, dummy_bundle, - enable_shielded_pool, identity_token_balance, insert_token_pool_anchor, + enable_shielded_pool, identity_token_balance, insert_token_pool_anchor, nullifier_is_spent, platform_with_latest_version, pool_balance, pool_notes_count, process, spendable_note, OWNER_INITIAL_BALANCE, }; @@ -1111,8 +1111,10 @@ mod token_pool_mint_burn_claim_purchase_tests { use dpp::data_contract::associated_token::token_pre_programmed_distribution::v0::TokenPreProgrammedDistributionV0; use dpp::data_contract::associated_token::token_pre_programmed_distribution::TokenPreProgrammedDistribution; use dpp::identity::accessors::IdentityGettersV0; - use dpp::shielded::token_burn_from_pool_extra_sighash_data_v0; - use dpp::state_transition::batch_transition::TokenSetPriceForDirectPurchaseTransition; + use dpp::shielded::{serialized_actions_digest, token_burn_from_pool_extra_sighash_data_v0}; + use dpp::state_transition::batch_transition::{ + TokenBurnFromPoolTransition, TokenSetPriceForDirectPurchaseTransition, + }; use dpp::tokens::token_pricing_schedule::TokenPricingSchedule; use platform_version::version::PlatformVersion; @@ -1407,6 +1409,222 @@ mod token_pool_mint_burn_claim_purchase_tests { ); } + /// A burn from the pool authorized by a group of two. The proposer proves the bundle, bound + /// to itself; the second signer submits that very bundle and the burn executes on the second + /// signature. A bundle the second signer built itself is refused: the group action pins the + /// digest of the proposer's actions. + #[tokio::test] + async fn test_token_burn_from_pool_by_group_of_two() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9105); + + let (proposer, proposer_signer, proposer_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (confirmer, confirmer_signer, confirmer_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + proposer.id(), + Some(|token_configuration: &mut TokenConfiguration| { + enable_shielded_pool(token_configuration); + token_configuration.set_manual_burning_rules(ChangeControlRules::V0( + ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::Group(0), + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }, + )); + }), + None, + Some( + [( + 0, + Group::V0(GroupV0 { + members: [(proposer.id(), 1), (confirmer.id(), 1)].into(), + required_power: 2, + }), + )] + .into(), + ), + None, + platform_version, + ); + let token = token_id.to_buffer(); + + // Fund the pool first. + let shield = BatchTransition::new_token_shield_transition( + token_id, + proposer.id(), + contract.id(), + 0, + 10_000, + build_shield_bundle(10_000, 24), + &proposer_key, + 2, + 0, + &proposer_signer, + platform_version, + None, + ) + .await + .expect("token shield transition"); + let result = process(&platform, &shield); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + // The proposer proves a burn of 4_000 out of a 6_000 note, bound to the proposer. + let (note, anchor, merkle_path) = spendable_note(6_000, 4); + insert_token_pool_anchor(&platform, token_id, &anchor); + let burn_amount = 4_000; + let extra = token_burn_from_pool_extra_sighash_data_v0( + &token, + &proposer.id().to_buffer(), + burn_amount, + ); + let (burn_bundle, value_balance) = + build_spend_bundle(note, merkle_path, anchor, burn_amount, &extra, 25); + assert_eq!(value_balance, burn_amount as i64); + let proposer_nonce = 3; + + let proposal = BatchTransition::new_token_burn_from_pool_transition( + token_id, + proposer.id(), + contract.id(), + 0, + burn_amount, + burn_bundle.clone(), + None, + Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(0)), + &proposer_key, + proposer_nonce, + 0, + &proposer_signer, + platform_version, + None, + ) + .await + .expect("token burn from pool proposal"); + let result = process(&platform, &proposal); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + // One signature of two: nothing has left the pool yet. + assert_eq!(pool_balance(&platform, token_id), 10_000); + assert_eq!(total_supply(&platform, token_id), OWNER_INITIAL_BALANCE); + assert!(!nullifier_is_spent( + &platform, + token_id, + &burn_bundle.actions[0].nullifier + )); + + let action_id = TokenBurnFromPoolTransition::calculate_action_id_with_fields( + &token, + proposer.id().as_bytes(), + proposer_nonce, + burn_amount, + &serialized_actions_digest(&burn_bundle.actions), + ); + let as_other_signer = || { + Some( + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: 0, + action_id, + action_is_proposer: false, + }, + ), + ) + }; + + // The confirmer cannot substitute a bundle of its own, even for the same amount. + let (other_note, other_anchor, other_path) = spendable_note(6_000, 5); + insert_token_pool_anchor(&platform, token_id, &other_anchor); + let confirmer_extra = token_burn_from_pool_extra_sighash_data_v0( + &token, + &confirmer.id().to_buffer(), + burn_amount, + ); + let (substituted_bundle, _) = build_spend_bundle( + other_note, + other_path, + other_anchor, + burn_amount, + &confirmer_extra, + 26, + ); + let substituted = BatchTransition::new_token_burn_from_pool_transition( + token_id, + confirmer.id(), + contract.id(), + 0, + burn_amount, + substituted_bundle, + None, + as_other_signer(), + &confirmer_key, + 2, + 0, + &confirmer_signer, + platform_version, + None, + ) + .await + .expect("token burn from pool confirmation with a substituted bundle"); + let result = process(&platform, &substituted); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError( + StateError::ModificationOfGroupActionMainParametersNotPermittedError(_) + ), + .. + }] + ); + assert_eq!(pool_balance(&platform, token_id), 10_000); + + // The confirmer submits the proposer's bundle unchanged and the burn executes. + let confirmation = BatchTransition::new_token_burn_from_pool_transition( + token_id, + confirmer.id(), + contract.id(), + 0, + burn_amount, + burn_bundle.clone(), + None, + as_other_signer(), + &confirmer_key, + 3, + 0, + &confirmer_signer, + platform_version, + None, + ) + .await + .expect("token burn from pool confirmation"); + let result = process(&platform, &confirmation); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(&platform, token_id), 10_000 - burn_amount); + assert_eq!( + total_supply(&platform, token_id), + OWNER_INITIAL_BALANCE - burn_amount + ); + assert!(nullifier_is_spent( + &platform, + token_id, + &burn_bundle.actions[0].nullifier + )); + assert_tokens_conserved(&platform); + } + #[tokio::test] async fn test_token_claim_to_pool_pre_programmed() { let platform_version = PlatformVersion::latest(); From 5f5395870a7584d54e4182ef85482562d1cbe668 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 21 Sep 2026 08:46:53 +0700 Subject: [PATCH 10/94] fix(platform): reconcile token pools with protocol 14 changes Preserve the current protocol 14 version tables and key restrictions while enabling token pools through new protocol 15 generations. Keep transition IDs, error discriminants, signature preimages and bindings consistent with the rebased base. Handle once-per-identity claims into pools, bind document payment fixtures to nonce-derived IDs, and include token pools in the GroveDB structure description and regression fixtures. --- book/src/data-model/token-shielded-pools.md | 16 +- book/src/fees/shielded-fees.md | 4 +- .../src/errors/consensus/basic/basic_error.rs | 9 +- .../src/errors/consensus/state/state_error.rs | 4 +- .../builder/document_token_payment.rs | 4 + packages/rs-dpp/src/shielded/sighash.rs | 22 +- .../state_transition_types.rs | 2 +- .../document_base_transition/v2/v2_methods.rs | 2 +- .../document/batch_transition/methods/mod.rs | 4 +- .../v0/mod.rs | 4 +- .../processor/traits/shielded_proof.rs | 1590 +++++++++-------- .../batch/tests/token/shielded/mod.rs | 127 +- packages/rs-drive/grovedb-structure.json | 288 ++- .../rs-drive/src/drive/initialization/mod.rs | 4 +- .../src/drive/initialization/v4/mod.rs | 4 - .../src/drive/initialization/v5/mod.rs | 19 + .../tokens/shielded_pool/insert_root_tree.rs | 2 +- .../rs-drive/src/drive/tokens/structure.rs | 121 +- .../token/token_claim_to_pool_transition.rs | 9 + .../v0/transformer.rs | 44 +- packages/rs-drive/src/structure/tests.rs | 57 +- .../dpp_versions/dpp_contract_versions/v7.rs | 90 +- .../drive_abci_method_versions/v11.rs | 144 +- .../drive_abci_validation_versions/v11.rs | 432 +---- .../drive_token_method_versions/mod.rs | 1 + .../drive_token_method_versions/v2.rs | 1 - .../drive_token_method_versions/v3.rs | 8 + .../src/version/drive_versions/v10.rs | 145 +- .../rs-platform-version/src/version/v15.rs | 75 +- .../token_transition/claim_to_pool.rs | 1 + 30 files changed, 1520 insertions(+), 1713 deletions(-) create mode 100644 packages/rs-drive/src/drive/initialization/v5/mod.rs create mode 100644 packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v3.rs diff --git a/book/src/data-model/token-shielded-pools.md b/book/src/data-model/token-shielded-pools.md index 8a9331383be..c22f4109e1c 100644 --- a/book/src/data-model/token-shielded-pools.md +++ b/book/src/data-model/token-shielded-pools.md @@ -62,7 +62,7 @@ see those notes. Rather than let an issuer advertise controls that cover only tr balances, a token with `hasShieldedPool` must disable them permanently: `freezeRules`, `unfreezeRules` and `destroyFrozenFundsRules` must each authorize no one to take the action and have no admin action takers, so no later configuration update can switch them on. Contract -create and update reject anything else with `TokenShieldedPoolIncompatibleRulesError` (10277). +create and update reject anything else with `TokenShieldedPoolIncompatibleRulesError` (10278). Pausing still works: shield, unshield and shielded transfer are all rejected while the token is paused. The frozen-account checks in the shield and unshield validators remain as defence in depth for state written before this rule. @@ -115,8 +115,8 @@ binds the token id, the batch owner, the document's contract and id and the amou (`document_token_payment_extra_sighash_data`), so it cannot pay for another document. The transformer rejects a payment whose amount is not the document type's cost -(`TokenShieldedPaymentAmountMismatchError`, 40723) and a shielded payment on an action with no -token cost (`TokenShieldedPaymentNotRequiredError`, 40724). Document base state validation +(`TokenShieldedPaymentAmountMismatchError`, 40724) and a shielded payment on an action with no +token cost (`TokenShieldedPaymentNotRequiredError`, 40725). Document base state validation version 1 skips the owner's balance and frozen-account checks for a shielded payment; the pool side (pool exists, token not paused, anchor, unspent nullifiers, pool balance, proof) is validated once the document action itself is valid. The lowering pays the cost from the pool: a @@ -133,9 +133,9 @@ shielded pool that pays the fee, both authorized only by Orchard spend keys. | Type | Transition | Token bundle | Fee bundle | Effect | |---|---|---|---|---| -| 23 | `TokenShieldedTransferWithShieldedFee` | spends, value `0` | spends, value = fee | Notes spent and recreated in the token pool; the fee leaves the credit pool. | -| 24 | `TokenUnshieldWithShieldedFee` | spends, value `+amount` | spends, value = fee | `amount` leaves the token pool into `recipient_id`'s token balance. | -| 25 | `TokenPurchaseFromShieldedPool` | outputs only, value `-token_count` | spends, value = price + fee | `token_count` is minted into the token pool at the direct purchase price; the price is credited to the contract owner. | +| 26 | `TokenShieldedTransferWithShieldedFee` | spends, value `0` | spends, value = fee | Notes spent and recreated in the token pool; the fee leaves the credit pool. | +| 27 | `TokenUnshieldWithShieldedFee` | spends, value `+amount` | spends, value = fee | `amount` leaves the token pool into `recipient_id`'s token balance. | +| 28 | `TokenPurchaseFromShieldedPool` | outputs only, value `-token_count` | spends, value = price + fee | `token_count` is minted into the token pool at the direct purchase price; the price is credited to the contract owner. | Every transition names the contract, the token position and the token id (which must derive from the two), the two bundles and `credit_amount`, the fee bundle's value balance. The token @@ -247,7 +247,9 @@ proves the spent nullifiers in the token's pool. `build_token_claim_to_pool_transition` and `build_token_direct_purchase_to_pool_transition`. They prove the bundle, bind the extra sighash data, and call the batch constructors (`new_token_shield_transition` and siblings) which sign with the identity key. -`build_document_shielded_token_payment` builds the bundle of a `TokenPaymentInfo::V1`, and +`build_document_shielded_token_payment` builds the bundle of a `TokenPaymentInfo::V1`. +For document creation, call `Document::set_id_for_creation` with the creation nonce before +building the payment: the proof must bind the final document ID, not the factory placeholder. `build_token_shielded_transfer_with_shielded_fee_transition`, `build_token_unshield_with_shielded_fee_transition` and `build_token_purchase_from_shielded_pool_transition` build the identity-less transitions from a diff --git a/book/src/fees/shielded-fees.md b/book/src/fees/shielded-fees.md index c5096c6a740..6b6646b1448 100644 --- a/book/src/fees/shielded-fees.md +++ b/book/src/fees/shielded-fees.md @@ -67,8 +67,8 @@ failure: the identity is charged, its identity contract nonce advances, and no t ### Identity-less token pool transitions -`TokenShieldedTransferWithShieldedFee` (23), `TokenUnshieldWithShieldedFee` (24) and -`TokenPurchaseFromShieldedPool` (25) have no identity: the fee is carved from a second bundle +`TokenShieldedTransferWithShieldedFee` (26), `TokenUnshieldWithShieldedFee` (27) and +`TokenPurchaseFromShieldedPool` (28) have no identity: the fee is carved from a second bundle spent in the credit shielded pool, exactly as `ShieldedTransfer` carves its own fee. | Transition | Fee Formula | Explanation | diff --git a/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs b/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs index 0b5233c23e0..218f69798d0 100644 --- a/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs +++ b/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs @@ -39,11 +39,10 @@ use crate::consensus::basic::data_contract::{ NewTokensDestinationIdentityOptionRequiredError, NonContiguousContractGroupPositionsError, NonContiguousContractTokenPositionsError, PreProgrammedDistributionAmountOverLimitError, RedundantDocumentPaidForByTokenWithContractId, SystemPropertyIndexAlreadyPresentError, - TokenShieldedPoolIncompatibleRulesError, - UndefinedIndexPropertyError, UniqueIndicesLimitReachedError, - UnknownDocumentCreationRestrictionModeError, UnknownGasFeesPaidByError, - UnknownSecurityLevelError, UnknownStorageKeyRequirementsError, UnknownTradeModeError, - UnknownTransferableTypeError, + TokenShieldedPoolIncompatibleRulesError, UndefinedIndexPropertyError, + UniqueIndicesLimitReachedError, UnknownDocumentCreationRestrictionModeError, + UnknownGasFeesPaidByError, UnknownSecurityLevelError, UnknownStorageKeyRequirementsError, + UnknownTradeModeError, UnknownTransferableTypeError, }; use crate::consensus::basic::data_contract::{ InvalidJsonSchemaRefError, TokenPaymentByBurningOnlyAllowedOnInternalTokenError, diff --git a/packages/rs-dpp/src/errors/consensus/state/state_error.rs b/packages/rs-dpp/src/errors/consensus/state/state_error.rs index 2460330dd87..85cedb06697 100644 --- a/packages/rs-dpp/src/errors/consensus/state/state_error.rs +++ b/packages/rs-dpp/src/errors/consensus/state/state_error.rs @@ -1124,7 +1124,7 @@ mod tests { "create".to_string(), ) )), - 102 + 145 ); assert_eq!( discriminant_of(StateError::TokenShieldedPaymentNotRequiredError( @@ -1133,7 +1133,7 @@ mod tests { "create".to_string(), ) )), - 103 + 146 ); } } diff --git a/packages/rs-dpp/src/shielded/builder/document_token_payment.rs b/packages/rs-dpp/src/shielded/builder/document_token_payment.rs index c41b07ca4b2..dc19b9ade1a 100644 --- a/packages/rs-dpp/src/shielded/builder/document_token_payment.rs +++ b/packages/rs-dpp/src/shielded/builder/document_token_payment.rs @@ -16,6 +16,10 @@ use super::{build_spend_bundle, serialize_authorized_bundle, OrchardProver, Spen /// amount into the Orchard sighash. Put the result into a `TokenPaymentInfo::V1` on the /// document transition's base; the identity signing the batch still pays the credit fee. /// +/// For document creation, `document_id` must be the final ID derived from the creation +/// nonce. Call `Document::set_id_for_creation` before building this payment; a document +/// factory's placeholder ID is replaced when the create transition is built. +/// /// `value_balance == amount` exactly; the pool pays the cost, nothing is carved for fees. #[allow(clippy::too_many_arguments)] pub fn build_document_shielded_token_payment( diff --git a/packages/rs-dpp/src/shielded/sighash.rs b/packages/rs-dpp/src/shielded/sighash.rs index 8782f9b1af7..22f6799f9a4 100644 --- a/packages/rs-dpp/src/shielded/sighash.rs +++ b/packages/rs-dpp/src/shielded/sighash.rs @@ -21,11 +21,11 @@ const SIGHASH_DOMAIN: &[u8] = b"DashPlatformSighash"; /// The state transition type byte the token bundle of a `TokenShieldedTransferWithShieldedFee` /// commits to (`StateTransitionType::TokenShieldedTransferWithShieldedFee`). -pub const TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE: u8 = 23; +pub const TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE: u8 = 26; /// The state transition type byte the token bundle of a `TokenUnshieldWithShieldedFee` commits to. -pub const TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE: u8 = 24; +pub const TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE: u8 = 27; /// The state transition type byte the token bundle of a `TokenPurchaseFromShieldedPool` commits to. -pub const TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE: u8 = 25; +pub const TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE: u8 = 28; /// Computes the platform sighash from an Orchard bundle commitment and optional /// transparent field data. @@ -458,7 +458,7 @@ pub fn token_shielded_transfer_with_shielded_fee_extra_sighash_data( } } -/// Version 0 layout: `state transition type (1, = 23) || token_id (32)`. Frozen. +/// Version 0 layout: `state transition type (1, = 26) || token_id (32)`. Frozen. pub fn token_shielded_transfer_with_shielded_fee_extra_sighash_data_v0( token_id: &[u8; 32], ) -> Vec { @@ -491,7 +491,7 @@ pub fn token_unshield_with_shielded_fee_extra_sighash_data( } } -/// Version 0 layout: `state transition type (1, = 24) || token_id (32) || recipient_id (32) +/// Version 0 layout: `state transition type (1, = 27) || token_id (32) || recipient_id (32) /// || amount (8, little endian)`. Frozen. pub fn token_unshield_with_shielded_fee_extra_sighash_data_v0( token_id: &[u8; 32], @@ -528,7 +528,7 @@ pub fn token_purchase_from_shielded_pool_extra_sighash_data( } } -/// Version 0 layout: `state transition type (1, = 25) || token_id (32) || token_count (8, LE) +/// Version 0 layout: `state transition type (1, = 28) || token_id (32) || token_count (8, LE) /// || total_agreed_price (8, LE)`. Frozen. pub fn token_purchase_from_shielded_pool_extra_sighash_data_v0( token_id: &[u8; 32], @@ -940,26 +940,26 @@ mod tests { fn token_pool_paid_layouts_start_with_the_state_transition_type() { let transfer = token_shielded_transfer_with_shielded_fee_extra_sighash_data_v0(&[1u8; 32]); assert_eq!(transfer.len(), 33); - assert_eq!(transfer[0], 23); + assert_eq!(transfer[0], 26); assert_eq!(&transfer[1..], &[1u8; 32]); let unshield = token_unshield_with_shielded_fee_extra_sighash_data_v0(&[1u8; 32], &[2u8; 32], 300); assert_eq!(unshield.len(), 73); - assert_eq!(unshield[0], 24); + assert_eq!(unshield[0], 27); assert_eq!(&unshield[1..33], &[1u8; 32]); assert_eq!(&unshield[33..65], &[2u8; 32]); assert_eq!(&unshield[65..], &300u64.to_le_bytes()); let purchase = token_purchase_from_shielded_pool_extra_sighash_data_v0(&[1u8; 32], 5, 900); assert_eq!(purchase.len(), 49); - assert_eq!(purchase[0], 25); + assert_eq!(purchase[0], 28); assert_eq!(&purchase[33..41], &5u64.to_le_bytes()); assert_eq!(&purchase[41..], &900u64.to_le_bytes()); - let fee = token_pool_fee_bundle_extra_sighash_data_v0(24, &[1u8; 32], &[3u8; 32]); + let fee = token_pool_fee_bundle_extra_sighash_data_v0(27, &[1u8; 32], &[3u8; 32]); assert_eq!(fee.len(), 65); - assert_eq!(fee[0], 24); + assert_eq!(fee[0], 27); assert_eq!(&fee[1..33], &[1u8; 32]); assert_eq!(&fee[33..], &[3u8; 32]); } diff --git a/packages/rs-dpp/src/state_transition/state_transition_types.rs b/packages/rs-dpp/src/state_transition/state_transition_types.rs index 8efec0a6386..36245d0eb52 100644 --- a/packages/rs-dpp/src/state_transition/state_transition_types.rs +++ b/packages/rs-dpp/src/state_transition/state_transition_types.rs @@ -216,7 +216,7 @@ mod tests { #[test] fn test_try_from_u8_invalid() { - assert!(StateTransitionType::try_from(26u8).is_err()); + assert!(StateTransitionType::try_from(29u8).is_err()); assert!(StateTransitionType::try_from(255u8).is_err()); } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v2/v2_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v2/v2_methods.rs index 5dba1e19766..91d5d1c7861 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v2/v2_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v2/v2_methods.rs @@ -35,7 +35,7 @@ impl DocumentBaseTransitionV2Methods for DocumentBaseTransitionV2 { impl DocumentBaseTransitionV1Methods for DocumentBaseTransitionV2 { fn token_payment_info(&self) -> Option { - self.token_payment_info + self.token_payment_info.clone() } fn token_payment_info_ref(&self) -> &Option { diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs index 1974f12f723..29fc73642e6 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/mod.rs @@ -21,11 +21,11 @@ use crate::identity::IdentityPublicKey; use crate::prelude::IdentityNonce; #[cfg(feature = "state-transition-signing")] use crate::prelude::UserFeeIncrease; +#[cfg(feature = "state-transition-signing")] +use crate::shielded::OrchardBundleParams; use crate::state_transition::batch_transition::batched_transition::document_transition::{ DocumentTransition, DocumentTransitionV0Methods, }; -#[cfg(feature = "state-transition-signing")] -use crate::shielded::OrchardBundleParams; use crate::state_transition::batch_transition::batched_transition::BatchedTransition; use crate::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1; diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index e5e27ff6428..aad476dd146 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -820,7 +820,7 @@ impl Platform { /// When transitioning to version 15 we insert the token shielded pools root: the BigSumTree /// under the Tokens tree that holds one Orchard pool per token opting in /// (`TokenConfigurationV1::has_shielded_pool`). CONSENSUS-CRITICAL: the genesis-v15 path - /// (`Drive::create_initial_state_structure_v4`) calls the same helper, so a chain born at v15 + /// (`Drive::create_initial_state_structure_v5`) calls the same helper, so a chain born at v15 /// and one upgraded to it build a byte-identical `[Tokens]` subtree. fn transition_to_version_15( &self, @@ -3112,7 +3112,7 @@ mod tests { /// because both paths now call the shared /// `Drive::insert_shielded_pool_structure`). /// The token shielded pools root is built by two paths that must coincide byte for byte: - /// `Drive::create_initial_state_structure_v4` at a v15 genesis and + /// `Drive::create_initial_state_structure_v5` at a v15 genesis and /// `transition_to_version_15` on a chain upgraded from v14. #[test] fn test_genesis_v15_and_upgrade_to_v15_build_identical_token_shielded_pools_root() { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs index 64625bfdab4..c002a3a77c1 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs @@ -930,6 +930,443 @@ fn key_not_allowed_in_shielded_creation( }) } +/// Verifies every token shielded bundle a batch carries, statelessly. +fn validate_batch_token_shielded_proofs( + batch: &BatchTransition, + platform_version: &PlatformVersion, +) -> Result { + let owner_id = batch.owner_id().to_buffer(); + for transition in batch.transitions_iter() { + let result = match transition { + BatchedTransitionRef::Token(TokenTransition::Shield(t)) => { + reconstruct_and_verify_bundle( + t.actions(), + FLAGS_OUTPUTS_ONLY, + -(t.amount() as i64), + t.anchor(), + t.proof(), + t.binding_signature(), + &[], + ) + } + BatchedTransitionRef::Token(TokenTransition::Unshield(t)) => { + let extra_sighash_data = dpp::shielded::token_unshield_extra_sighash_data( + &t.base().token_id().to_buffer(), + &owner_id, + &t.recipient_id().to_buffer(), + t.amount(), + platform_version, + )?; + reconstruct_and_verify_bundle( + t.actions(), + FLAGS_SPENDS_AND_OUTPUTS, + t.amount() as i64, + t.anchor(), + t.proof(), + t.binding_signature(), + &extra_sighash_data, + ) + } + BatchedTransitionRef::Token(TokenTransition::ShieldedTransfer(t)) => { + let extra_sighash_data = dpp::shielded::token_shielded_transfer_extra_sighash_data( + &t.base().token_id().to_buffer(), + &owner_id, + platform_version, + )?; + reconstruct_and_verify_bundle( + t.actions(), + FLAGS_SPENDS_AND_OUTPUTS, + 0, + t.anchor(), + t.proof(), + t.binding_signature(), + &extra_sighash_data, + ) + } + BatchedTransitionRef::Token(TokenTransition::MintToPool(t)) => { + reconstruct_and_verify_bundle( + t.actions(), + FLAGS_OUTPUTS_ONLY, + -(t.amount() as i64), + t.anchor(), + t.proof(), + t.binding_signature(), + &[], + ) + } + BatchedTransitionRef::Token(TokenTransition::BurnFromPool(t)) => { + let extra_sighash_data = dpp::shielded::token_burn_from_pool_extra_sighash_data( + &t.base().token_id().to_buffer(), + &owner_id, + t.amount(), + platform_version, + )?; + reconstruct_and_verify_bundle( + t.actions(), + FLAGS_SPENDS_AND_OUTPUTS, + t.amount() as i64, + t.anchor(), + t.proof(), + t.binding_signature(), + &extra_sighash_data, + ) + } + // The claimable amount is only known against state, so a claim into the pool is + // verified in block validation only; its proof is still admitted per nonce. + BatchedTransitionRef::Token(TokenTransition::ClaimToPool(_)) => continue, + BatchedTransitionRef::Token(TokenTransition::DirectPurchaseToPool(t)) => { + reconstruct_and_verify_bundle( + t.actions(), + FLAGS_OUTPUTS_ONLY, + -(t.token_count() as i64), + t.anchor(), + t.proof(), + t.binding_signature(), + &[], + ) + } + // A document whose token cost is paid from the token's shielded pool: the payment + // states the amount its bundle proves, and state validation rejects the document + // when that is not the document type's cost, so the bundle can be checked here. + BatchedTransitionRef::Document(document_transition) => { + let base = document_transition.base(); + let Some(token_payment_info) = base.token_payment_info_ref() else { + continue; + }; + let Some(payment) = token_payment_info.shielded_payment() else { + continue; + }; + let extra_sighash_data = dpp::shielded::document_token_payment_extra_sighash_data( + &token_payment_info + .token_id(base.data_contract_id()) + .to_buffer(), + &owner_id, + &base.data_contract_id().to_buffer(), + &base.id().to_buffer(), + payment.amount, + platform_version, + )?; + reconstruct_and_verify_bundle( + &payment.actions, + FLAGS_SPENDS_AND_OUTPUTS, + payment.amount as i64, + &payment.anchor, + &payment.proof, + &payment.binding_signature, + &extra_sighash_data, + ) + } + _ => continue, + }; + if let Err(e) = result { + return Ok(SimpleConsensusValidationResult::new_with_error( + StateError::InvalidShieldedProofError(e).into(), + )); + } + } + Ok(SimpleConsensusValidationResult::new()) +} + +/// Protocol 15 proof validation adds token pools while retaining the identity key restrictions. +fn validate_shielded_proof_v2( + state_transition: &StateTransition, + platform_version: &PlatformVersion, +) -> Result { + if let Some(error) = key_not_allowed_in_shielded_creation(state_transition) { + return Ok(SimpleConsensusValidationResult::new_with_error(error)); + } + // A batch carries no pool-paid bundle of its own; its token shielded + // transitions are verified one by one against the sighash data each binds. + // Block processing verifies them again inside state validation, where a + // failure is a paid nonce bump; this stateless pass is CheckTx's admission + // filter, run under the nonce-aware limiter. + if let StateTransition::Batch(batch) = state_transition { + return validate_batch_token_shielded_proofs(batch, platform_version); + } + + // `IdentityCreateFromShieldedPool` is the only shielded transition carrying separate + // per-key proof-of-possession signatures that are NOT covered by the Orchard proof + // (they sign the platform signable bytes, and only id+denomination+keys — not the PoP + // sigs — are bound into `extra_sighash_data`). Validate the CHEAP key structure + + // per-key PoP here, BEFORE the expensive Halo 2 bundle verification, so a relayer + // who flips a PoP byte on an observed transition is rejected without the node paying + // for proof verification (DoS hardening). Same `signable_bytes` the transformer uses. + if let StateTransition::IdentityCreateFromShieldedPool(st) = state_transition { + let IdentityCreateFromShieldedPoolTransition::V0(v0) = st; + + let key_structure_result = + IdentityPublicKeyInCreation::validate_identity_public_keys_structure( + &v0.public_keys, + true, + platform_version, + )?; + if !key_structure_result.is_valid() { + return Ok(key_structure_result); + } + + let signable_bytes = state_transition.signable_bytes()?; + for key in v0.public_keys.iter() { + let pop_result = signable_bytes.as_slice().verify_signature( + key.key_type(), + key.data().as_slice(), + key.signature().as_slice(), + ); + if !pop_result.is_valid() { + return Ok(pop_result); + } + } + } + + let result = match state_transition { + StateTransition::Shield(st) => match st { + dpp::state_transition::shield_transition::ShieldTransition::V0(v0) => { + reconstruct_and_verify_bundle( + &v0.actions, + FLAGS_OUTPUTS_ONLY, + -(v0.amount as i64), + &v0.anchor, + v0.proof.as_slice(), + &v0.binding_signature, + &[], // No transparent fields for shield + ) + } + }, + StateTransition::ShieldFromIdentity(st) => match st { + ShieldFromIdentityTransition::V0(v0) => reconstruct_and_verify_bundle( + &v0.actions, + FLAGS_OUTPUTS_ONLY, + -(v0.amount as i64), + &v0.anchor, + v0.proof.as_slice(), + &v0.binding_signature, + &[], + ), + }, + StateTransition::ShieldedTransfer(st) => match st { + dpp::state_transition::shielded_transfer_transition::ShieldedTransferTransition::V0(v0) => { + reconstruct_and_verify_bundle( + &v0.actions, + FLAGS_SPENDS_AND_OUTPUTS, + v0.value_balance as i64, + &v0.anchor, + v0.proof.as_slice(), + &v0.binding_signature, + &[], // No transparent fields for shielded transfer + ) + } + }, + StateTransition::Unshield(st) => match st { + dpp::state_transition::unshield_transition::UnshieldTransition::V0(v0) => { + let extra_sighash_data = dpp::shielded::unshield_extra_sighash_data( + &v0.output_address.to_bytes(), + v0.unshielding_amount, + platform_version, + )?; + reconstruct_and_verify_bundle( + &v0.actions, + FLAGS_SPENDS_AND_OUTPUTS, + v0.unshielding_amount as i64, + &v0.anchor, + v0.proof.as_slice(), + &v0.binding_signature, + &extra_sighash_data, + ) + } + }, + StateTransition::TokenShieldedTransferWithShieldedFee(st) => match st { + TokenShieldedTransferWithShieldedFeeTransition::V0(v0) => { + let token_extra_sighash_data = dpp::shielded::token_shielded_transfer_with_shielded_fee_extra_sighash_data( + &v0.token_id.to_buffer(), + platform_version, + )?; + let fee_extra_sighash_data = + dpp::shielded::token_pool_fee_bundle_extra_sighash_data( + dpp::shielded::TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE, + &v0.token_id.to_buffer(), + &v0.token_actions, + platform_version, + )?; + reconstruct_and_verify_bundle( + &v0.token_actions, + FLAGS_SPENDS_AND_OUTPUTS, + 0, + &v0.token_anchor, + v0.token_proof.as_slice(), + &v0.token_binding_signature, + &token_extra_sighash_data, + ) + .and_then(|_| { + reconstruct_and_verify_bundle( + &v0.fee_actions, + FLAGS_SPENDS_AND_OUTPUTS, + v0.credit_amount as i64, + &v0.fee_anchor, + v0.fee_proof.as_slice(), + &v0.fee_binding_signature, + &fee_extra_sighash_data, + ) + }) + } + }, + StateTransition::TokenUnshieldWithShieldedFee(st) => match st { + TokenUnshieldWithShieldedFeeTransition::V0(v0) => { + let token_extra_sighash_data = dpp::shielded::token_unshield_with_shielded_fee_extra_sighash_data( + &v0.token_id.to_buffer(), + &v0.recipient_id.to_buffer(), + v0.amount, + platform_version, + )?; + let fee_extra_sighash_data = + dpp::shielded::token_pool_fee_bundle_extra_sighash_data( + dpp::shielded::TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE, + &v0.token_id.to_buffer(), + &v0.token_actions, + platform_version, + )?; + reconstruct_and_verify_bundle( + &v0.token_actions, + FLAGS_SPENDS_AND_OUTPUTS, + v0.amount as i64, + &v0.token_anchor, + v0.token_proof.as_slice(), + &v0.token_binding_signature, + &token_extra_sighash_data, + ) + .and_then(|_| { + reconstruct_and_verify_bundle( + &v0.fee_actions, + FLAGS_SPENDS_AND_OUTPUTS, + v0.credit_amount as i64, + &v0.fee_anchor, + v0.fee_proof.as_slice(), + &v0.fee_binding_signature, + &fee_extra_sighash_data, + ) + }) + } + }, + StateTransition::TokenPurchaseFromShieldedPool(st) => match st { + TokenPurchaseFromShieldedPoolTransition::V0(v0) => { + let token_extra_sighash_data = dpp::shielded::token_purchase_from_shielded_pool_extra_sighash_data( + &v0.token_id.to_buffer(), + v0.token_count, + v0.total_agreed_price, + platform_version, + )?; + let fee_extra_sighash_data = + dpp::shielded::token_pool_fee_bundle_extra_sighash_data( + dpp::shielded::TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, + &v0.token_id.to_buffer(), + &v0.token_actions, + platform_version, + )?; + reconstruct_and_verify_bundle( + &v0.token_actions, + FLAGS_OUTPUTS_ONLY, + -(v0.token_count as i64), + &v0.token_anchor, + v0.token_proof.as_slice(), + &v0.token_binding_signature, + &token_extra_sighash_data, + ) + .and_then(|_| { + reconstruct_and_verify_bundle( + &v0.fee_actions, + FLAGS_SPENDS_AND_OUTPUTS, + v0.credit_amount as i64, + &v0.fee_anchor, + v0.fee_proof.as_slice(), + &v0.fee_binding_signature, + &fee_extra_sighash_data, + ) + }) + } + }, + StateTransition::IdentityTopUpFromShieldedPool(st) => match st { + IdentityTopUpFromShieldedPoolTransition::V0(v0) => { + let extra_sighash_data = + dpp::shielded::identity_top_up_from_shielded_extra_sighash_data( + &v0.identity_id.to_buffer(), + v0.top_up_amount, + platform_version, + )?; + reconstruct_and_verify_bundle( + &v0.actions, + FLAGS_SPENDS_AND_OUTPUTS, + v0.top_up_amount as i64, + &v0.anchor, + v0.proof.as_slice(), + &v0.binding_signature, + &extra_sighash_data, + ) + } + }, + StateTransition::ShieldedWithdrawal(st) => match st { + dpp::state_transition::shielded_withdrawal_transition::ShieldedWithdrawalTransition::V0(v0) => { + let extra_sighash_data = + dpp::shielded::shielded_withdrawal_extra_sighash_data( + v0.output_script.as_bytes(), + v0.unshielding_amount, + v0.core_fee_per_byte, + v0.pooling, + platform_version, + )?; + reconstruct_and_verify_bundle( + &v0.actions, + FLAGS_SPENDS_AND_OUTPUTS, + v0.unshielding_amount as i64, + &v0.anchor, + v0.proof.as_slice(), + &v0.binding_signature, + &extra_sighash_data, + ) + } + }, + StateTransition::IdentityCreateFromShieldedPool(st) => match st { + dpp::state_transition::identity_create_from_shielded_pool_transition::IdentityCreateFromShieldedPoolTransition::V0(v0) => { + // Bind the new identity id + denomination + FULL public-key set into the + // Orchard sighash so the bundle cannot be redirected to a different + // identity/keys (the surplus_output binding analog). The id is re-derived + // from the spend nullifiers — the canonical value — so the binding holds + // regardless of any (separately-validated) wire `identity_id`. + let identity_id = + dpp::state_transition::identity_create_from_shielded_pool_transition::derive_identity_id_from_actions(&v0.actions) + .to_buffer(); + let extra_sighash_data = + dpp::shielded::identity_create_from_shielded_extra_sighash_data( + &identity_id, + v0.denomination, + &v0.send_to_address_on_creation_failure, + &v0.public_keys, + platform_version, + )?; + // value_balance = denomination EXACTLY (the ShieldedTransfer exact-equality + // model): the binding signature proves the value commitments sum to exactly + // the denomination leaving the pool. + reconstruct_and_verify_bundle( + &v0.actions, + FLAGS_SPENDS_AND_OUTPUTS, + v0.denomination as i64, + &v0.anchor, + v0.proof.as_slice(), + &v0.binding_signature, + &extra_sighash_data, + ) + } + }, + // ShieldFromAssetLock retains proof verification in transform_into_action; + // its paid-failure action comes from the asset lock. + _ => return Ok(SimpleConsensusValidationResult::new()), + }; + + match result { + Ok(()) => Ok(SimpleConsensusValidationResult::new()), + Err(e) => Ok(SimpleConsensusValidationResult::new_with_error( + StateError::InvalidShieldedProofError(e).into(), + )), + } +} + #[cfg(test)] mod tests { use super::*; @@ -1047,58 +1484,9 @@ mod tests { use super::*; #[test] - fn should_return_true_for_shield_shielded_transfer_unshield_shielded_withdrawal() { - let transitions: Vec<(&str, StateTransition)> = vec![ - ("Shield", make_shield()), - ("ShieldedTransfer", make_shielded_transfer()), - ("Unshield", make_unshield()), - ("ShieldedWithdrawal", make_shielded_withdrawal()), - ( - "IdentityCreateFromShieldedPool", - make_identity_create_from_shielded_pool(), - ), - ]; - for (name, st) in transitions { - assert!( - st.has_shielded_proof_validation(), - "expected true for {}", - name - ); - } - } - - #[test] - fn should_return_false_for_shield_from_asset_lock_and_non_shielded() { - let transitions: Vec<(&str, StateTransition)> = vec![ - ("ShieldFromAssetLock", make_shield_from_asset_lock()), - ("DataContractCreate", make_data_contract_create_st()), - ( - "IdentityCreate", - StateTransition::IdentityCreate(IdentityCreateTransition::V0( - IdentityCreateTransitionV0::default(), - )), - ), - ( - "Batch", - StateTransition::Batch(BatchTransition::V0(BatchTransitionV0::default())), - ), - ]; - for (name, st) in transitions { - assert!( - !st.has_shielded_proof_validation(), - "expected false for {}", - name - ); - } - } - } - - mod has_shielded_minimum_fee_validation { - use super::*; - - #[test] - fn should_return_true_for_spending_transitions() { + fn should_return_true_for_shield_shielded_transfer_unshield_shielded_withdrawal() { let transitions: Vec<(&str, StateTransition)> = vec![ + ("Shield", make_shield()), ("ShieldedTransfer", make_shielded_transfer()), ("Unshield", make_unshield()), ("ShieldedWithdrawal", make_shielded_withdrawal()), @@ -1109,7 +1497,7 @@ mod tests { ]; for (name, st) in transitions { assert!( - st.has_shielded_minimum_fee_validation(), + st.has_shielded_proof_validation(), "expected true for {}", name ); @@ -1117,758 +1505,418 @@ mod tests { } #[test] - fn should_return_false_for_shield_and_non_shielded() { + fn should_return_false_for_shield_from_asset_lock_and_non_shielded() { let transitions: Vec<(&str, StateTransition)> = vec![ - ("Shield", make_shield()), ("ShieldFromAssetLock", make_shield_from_asset_lock()), ("DataContractCreate", make_data_contract_create_st()), - ]; - for (name, st) in transitions { - assert!( - !st.has_shielded_minimum_fee_validation(), - "expected false for {}", - name - ); - } - } - } - - mod validate_minimum_shielded_fee { - use super::*; - use dpp::consensus::ConsensusError; - - /// A ShieldedWithdrawal transition (no actions) with the given gross amount. - fn shielded_withdrawal_with_amount(unshielding_amount: u64) -> StateTransition { - StateTransition::ShieldedWithdrawal(ShieldedWithdrawalTransition::V0( - ShieldedWithdrawalTransitionV0 { - actions: vec![], - unshielding_amount, - anchor: [0u8; 32], - proof: vec![], - binding_signature: [0u8; 64], - core_fee_per_byte: 0, - pooling: Default::default(), - output_script: Default::default(), - }, - )) - } - - #[test] - fn should_pass_for_non_shielded_transition() { - let platform_version = &platform_version::version::v9::PLATFORM_V9; - let st = make_data_contract_create_st(); - let result = st - .validate_minimum_shielded_fee(platform_version) - .expect("should not error"); - assert!(result.is_valid()); - } - - #[test] - fn should_reject_shielded_withdrawal_with_net_below_min_withdrawal_amount() { - let platform_version = PlatformVersion::latest(); - // ShieldedWithdrawal validation carves `compute_shielded_withdrawal_fee` (base + - // document cost), so the net the gate checks is `unshielding_amount - withdrawal_fee`. - // Use the SAME fee here so the constructed gross lands the net exactly at the - // withdrawal-range boundary under test. - let min_fee = dpp::shielded::compute_shielded_withdrawal_fee(0, platform_version) - .expect("fee computation should not overflow"); - let min_withdrawal = platform_version.system_limits.min_withdrawal_amount; - // net = min_withdrawal_amount - 1 → just below the Core dust floor. - let st = shielded_withdrawal_with_amount(min_fee + min_withdrawal - 1); - let result = st - .validate_minimum_shielded_fee(platform_version) - .expect("should not error"); - assert!(!result.is_valid()); - assert!( - matches!( - result.errors.first(), - Some(ConsensusError::BasicError( - BasicError::WithdrawalBelowMinAmountError(_) - )) - ), - "below-min must reject with WithdrawalBelowMinAmountError; got {:?}", - result.errors - ); - } - - #[test] - fn should_accept_shielded_withdrawal_with_net_at_min_withdrawal_amount() { - let platform_version = PlatformVersion::latest(); - // ShieldedWithdrawal validation carves `compute_shielded_withdrawal_fee` (base + - // document cost), so the net the gate checks is `unshielding_amount - withdrawal_fee`. - // Use the SAME fee here so the constructed gross lands the net exactly at the - // withdrawal-range boundary under test. - let min_fee = dpp::shielded::compute_shielded_withdrawal_fee(0, platform_version) - .expect("fee computation should not overflow"); - let min_withdrawal = platform_version.system_limits.min_withdrawal_amount; - // net = min_withdrawal_amount exactly → at the floor, accepted. - let st = shielded_withdrawal_with_amount(min_fee + min_withdrawal); - let result = st - .validate_minimum_shielded_fee(platform_version) - .expect("should not error"); - assert!( - result.is_valid(), - "a withdrawal whose net equals min_withdrawal_amount must be accepted" - ); - } - - #[test] - fn should_reject_shielded_withdrawal_with_net_above_max_withdrawal_amount() { - let platform_version = PlatformVersion::latest(); - // ShieldedWithdrawal validation carves `compute_shielded_withdrawal_fee` (base + - // document cost), so the net the gate checks is `unshielding_amount - withdrawal_fee`. - // Use the SAME fee here so the constructed gross lands the net exactly at the - // withdrawal-range boundary under test. - let min_fee = dpp::shielded::compute_shielded_withdrawal_fee(0, platform_version) - .expect("fee computation should not overflow"); - let max = platform_version.system_limits.max_withdrawal_amount; - // net = max_withdrawal_amount + 1 → just over the per-transition policy cap. - let st = shielded_withdrawal_with_amount(min_fee + max + 1); - let result = st - .validate_minimum_shielded_fee(platform_version) - .expect("should not error"); - assert!(!result.is_valid()); - // Must reject with the amount-RANGE error, not the below-min error — locks in the - // "over-max is not below-min" reason accuracy the cap introduced. - assert!( - matches!( - result.errors.first(), - Some(ConsensusError::BasicError( - BasicError::InvalidIdentityCreditWithdrawalTransitionAmountError(_) - )) - ), - "over-max must reject with InvalidIdentityCreditWithdrawalTransitionAmountError; got {:?}", - result.errors - ); - } - - #[test] - fn should_accept_shielded_withdrawal_with_net_at_max_withdrawal_amount() { - let platform_version = PlatformVersion::latest(); - // ShieldedWithdrawal validation carves `compute_shielded_withdrawal_fee` (base + - // document cost), so the net the gate checks is `unshielding_amount - withdrawal_fee`. - // Use the SAME fee here so the constructed gross lands the net exactly at the - // withdrawal-range boundary under test. - let min_fee = dpp::shielded::compute_shielded_withdrawal_fee(0, platform_version) - .expect("fee computation should not overflow"); - let max = platform_version.system_limits.max_withdrawal_amount; - // net = max_withdrawal_amount exactly → at the cap, accepted. - let st = shielded_withdrawal_with_amount(min_fee + max); - let result = st - .validate_minimum_shielded_fee(platform_version) - .expect("should not error"); - assert!( - result.is_valid(), - "a withdrawal whose net equals max_withdrawal_amount must be accepted" - ); - } - - #[test] - fn should_reject_amount_exceeding_i64_max_via_guard() { - let platform_version = PlatformVersion::latest(); - // `unshielding_amount > i64::MAX` wraps to a negative i64 in the validator's cast; - // the defensive `fee < 0` guard must reject it (rather than wrapping back to a huge - // u64 and sailing past the min-fee check). - let st = shielded_withdrawal_with_amount((i64::MAX as u64) + 1); - let result = st - .validate_minimum_shielded_fee(platform_version) - .expect("should not error"); - assert!(!result.is_valid()); - assert!( - matches!( - result.errors.first(), - Some(ConsensusError::BasicError( - BasicError::ShieldedInvalidValueBalanceError(_) - )) - ), - "amount > i64::MAX must be rejected by the fee<0 guard; got {:?}", - result.errors - ); - } - - /// Build an `IdentityCreateFromShieldedPool` with `num_actions` actions, `num_keys` keys, and - /// the given `denomination` (the min-fee gate only reads those three). - fn identity_create_from_shielded_pool( - denomination: u64, - num_actions: usize, - num_keys: usize, - ) -> StateTransition { - use dpp::identity::{KeyType, Purpose, SecurityLevel}; - use dpp::platform_value::BinaryData; - use dpp::shielded::SerializedAction; - use dpp::state_transition::public_key_in_creation::v0::IdentityPublicKeyInCreationV0; - use dpp::state_transition::public_key_in_creation::IdentityPublicKeyInCreation; - use dpp::state_transition::state_transitions::shielded::identity_create_from_shielded_pool_transition::v0::IdentityCreateFromShieldedPoolTransitionV0; - use dpp::state_transition::state_transitions::shielded::identity_create_from_shielded_pool_transition::IdentityCreateFromShieldedPoolTransition; - let actions = (0..num_actions as u8) - .map(|i| SerializedAction { - nullifier: [i; 32], - rk: [0u8; 32], - cmx: [0u8; 32], - encrypted_note: vec![0u8; 216], - cv_net: [0u8; 32], - spend_auth_sig: [0u8; 64], - }) - .collect(); - let public_keys = (0..num_keys as u32) - .map(|i| { - IdentityPublicKeyInCreation::V0(IdentityPublicKeyInCreationV0 { - id: i, - key_type: KeyType::ECDSA_SECP256K1, - purpose: Purpose::AUTHENTICATION, - security_level: SecurityLevel::MASTER, - contract_bounds: None, - read_only: false, - data: BinaryData::new(vec![i as u8; 33]), - signature: BinaryData::new(vec![]), - }) - }) - .collect(); - StateTransition::IdentityCreateFromShieldedPool( - IdentityCreateFromShieldedPoolTransition::V0( - IdentityCreateFromShieldedPoolTransitionV0 { - public_keys, - denomination, - actions, - anchor: [0u8; 32], - proof: vec![], - binding_signature: [0u8; 64], - send_to_address_on_creation_failure: - dpp::address_funds::PlatformAddress::P2pkh([0u8; 20]), - identity_id: Default::default(), - }, - ), - ) - } - - #[test] - fn should_reject_identity_create_denomination_below_min_fee() { - let platform_version = PlatformVersion::latest(); - let (num_actions, num_keys) = (2usize, 1usize); - let min_fee = dpp::shielded::compute_shielded_identity_create_fee( - num_actions, - num_keys, - platform_version, - ) - .expect("fee"); - let st = identity_create_from_shielded_pool(min_fee - 1, num_actions, num_keys); - let result = st - .validate_minimum_shielded_fee(platform_version) - .expect("no error"); - assert!(!result.is_valid()); - assert!( - matches!( - result.errors.first(), - Some(ConsensusError::StateError( - StateError::InsufficientShieldedFeeError(_) - )) + ( + "IdentityCreate", + StateTransition::IdentityCreate(IdentityCreateTransition::V0( + IdentityCreateTransitionV0::default(), + )), ), - "a denomination below the min fee must reject with InsufficientShieldedFeeError; got {:?}", - result.errors - ); + ( + "Batch", + StateTransition::Batch(BatchTransition::V0(BatchTransitionV0::default())), + ), + ]; + for (name, st) in transitions { + assert!( + !st.has_shielded_proof_validation(), + "expected false for {}", + name + ); + } } + } + + mod has_shielded_minimum_fee_validation { + use super::*; #[test] - fn should_accept_identity_create_denomination_at_min_fee() { - let platform_version = PlatformVersion::latest(); - let (num_actions, num_keys) = (2usize, 1usize); - let min_fee = dpp::shielded::compute_shielded_identity_create_fee( - num_actions, - num_keys, - platform_version, - ) - .expect("fee"); - let st = identity_create_from_shielded_pool(min_fee, num_actions, num_keys); - assert!( - st.validate_minimum_shielded_fee(platform_version) - .expect("no error") - .is_valid(), - "a denomination equal to the min fee must be accepted" - ); + fn should_return_true_for_spending_transitions() { + let transitions: Vec<(&str, StateTransition)> = vec![ + ("ShieldedTransfer", make_shielded_transfer()), + ("Unshield", make_unshield()), + ("ShieldedWithdrawal", make_shielded_withdrawal()), + ( + "IdentityCreateFromShieldedPool", + make_identity_create_from_shielded_pool(), + ), + ]; + for (name, st) in transitions { + assert!( + st.has_shielded_minimum_fee_validation(), + "expected true for {}", + name + ); + } } #[test] - fn should_scale_identity_create_min_fee_with_key_count() { - let platform_version = PlatformVersion::latest(); - let num_actions = 2usize; - let one_key_fee = dpp::shielded::compute_shielded_identity_create_fee( - num_actions, - 1, - platform_version, - ) - .expect("fee"); - let five_key_fee = dpp::shielded::compute_shielded_identity_create_fee( - num_actions, - 5, - platform_version, - ) - .expect("fee"); - assert!(five_key_fee > one_key_fee, "more keys must cost more"); - // A 1-key-sized denomination must be REJECTED for a 5-key identity (the fee scaled up). - let st = identity_create_from_shielded_pool(one_key_fee, num_actions, 5); - assert!( - !st.validate_minimum_shielded_fee(platform_version) - .expect("no error") - .is_valid(), - "a 1-key-sized denomination must be rejected once the identity has 5 keys" - ); - // ...and accepted once the denomination covers the scaled fee. - let st_ok = identity_create_from_shielded_pool(five_key_fee, num_actions, 5); - assert!(st_ok - .validate_minimum_shielded_fee(platform_version) - .expect("no error") - .is_valid()); + fn should_return_false_for_shield_and_non_shielded() { + let transitions: Vec<(&str, StateTransition)> = vec![ + ("Shield", make_shield()), + ("ShieldFromAssetLock", make_shield_from_asset_lock()), + ("DataContractCreate", make_data_contract_create_st()), + ]; + for (name, st) in transitions { + assert!( + !st.has_shielded_minimum_fee_validation(), + "expected false for {}", + name + ); + } } } - mod validate_shielded_proof { + mod validate_minimum_shielded_fee { use super::*; + use dpp::consensus::ConsensusError; + + /// A ShieldedWithdrawal transition (no actions) with the given gross amount. + fn shielded_withdrawal_with_amount(unshielding_amount: u64) -> StateTransition { + StateTransition::ShieldedWithdrawal(ShieldedWithdrawalTransition::V0( + ShieldedWithdrawalTransitionV0 { + actions: vec![], + unshielding_amount, + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + core_fee_per_byte: 0, + pooling: Default::default(), + output_script: Default::default(), + }, + )) + } #[test] fn should_pass_for_non_shielded_transition() { let platform_version = &platform_version::version::v9::PLATFORM_V9; let st = make_data_contract_create_st(); let result = st - .validate_shielded_proof(platform_version) + .validate_minimum_shielded_fee(platform_version) .expect("should not error"); assert!(result.is_valid()); } - } -} -/// Verifies every token shielded bundle a batch carries, statelessly. -fn validate_batch_token_shielded_proofs( - batch: &BatchTransition, - platform_version: &PlatformVersion, -) -> Result { - let owner_id = batch.owner_id().to_buffer(); - for transition in batch.transitions_iter() { - let result = match transition { - BatchedTransitionRef::Token(TokenTransition::Shield(t)) => { - reconstruct_and_verify_bundle( - t.actions(), - FLAGS_OUTPUTS_ONLY, - -(t.amount() as i64), - t.anchor(), - t.proof(), - t.binding_signature(), - &[], - ) - } - BatchedTransitionRef::Token(TokenTransition::Unshield(t)) => { - let extra_sighash_data = dpp::shielded::token_unshield_extra_sighash_data( - &t.base().token_id().to_buffer(), - &owner_id, - &t.recipient_id().to_buffer(), - t.amount(), - platform_version, - )?; - reconstruct_and_verify_bundle( - t.actions(), - FLAGS_SPENDS_AND_OUTPUTS, - t.amount() as i64, - t.anchor(), - t.proof(), - t.binding_signature(), - &extra_sighash_data, - ) - } - BatchedTransitionRef::Token(TokenTransition::ShieldedTransfer(t)) => { - let extra_sighash_data = dpp::shielded::token_shielded_transfer_extra_sighash_data( - &t.base().token_id().to_buffer(), - &owner_id, - platform_version, - )?; - reconstruct_and_verify_bundle( - t.actions(), - FLAGS_SPENDS_AND_OUTPUTS, - 0, - t.anchor(), - t.proof(), - t.binding_signature(), - &extra_sighash_data, - ) - } - BatchedTransitionRef::Token(TokenTransition::MintToPool(t)) => { - reconstruct_and_verify_bundle( - t.actions(), - FLAGS_OUTPUTS_ONLY, - -(t.amount() as i64), - t.anchor(), - t.proof(), - t.binding_signature(), - &[], - ) - } - BatchedTransitionRef::Token(TokenTransition::BurnFromPool(t)) => { - let extra_sighash_data = dpp::shielded::token_burn_from_pool_extra_sighash_data( - &t.base().token_id().to_buffer(), - &owner_id, - t.amount(), - platform_version, - )?; - reconstruct_and_verify_bundle( - t.actions(), - FLAGS_SPENDS_AND_OUTPUTS, - t.amount() as i64, - t.anchor(), - t.proof(), - t.binding_signature(), - &extra_sighash_data, - ) - } - // The claimable amount is only known against state, so a claim into the pool is - // verified in block validation only; its proof is still admitted per nonce. - BatchedTransitionRef::Token(TokenTransition::ClaimToPool(_)) => continue, - BatchedTransitionRef::Token(TokenTransition::DirectPurchaseToPool(t)) => { - reconstruct_and_verify_bundle( - t.actions(), - FLAGS_OUTPUTS_ONLY, - -(t.token_count() as i64), - t.anchor(), - t.proof(), - t.binding_signature(), - &[], - ) - } - // A document whose token cost is paid from the token's shielded pool: the payment - // states the amount its bundle proves, and state validation rejects the document - // when that is not the document type's cost, so the bundle can be checked here. - BatchedTransitionRef::Document(document_transition) => { - let base = document_transition.base(); - let Some(token_payment_info) = base.token_payment_info_ref() else { - continue; - }; - let Some(payment) = token_payment_info.shielded_payment() else { - continue; - }; - let extra_sighash_data = dpp::shielded::document_token_payment_extra_sighash_data( - &token_payment_info - .token_id(base.data_contract_id()) - .to_buffer(), - &owner_id, - &base.data_contract_id().to_buffer(), - &base.id().to_buffer(), - payment.amount, - platform_version, - )?; - reconstruct_and_verify_bundle( - &payment.actions, - FLAGS_SPENDS_AND_OUTPUTS, - payment.amount as i64, - &payment.anchor, - &payment.proof, - &payment.binding_signature, - &extra_sighash_data, - ) - } - _ => continue, - }; - if let Err(e) = result { - return Ok(SimpleConsensusValidationResult::new_with_error( - StateError::InvalidShieldedProofError(e).into(), - )); + #[test] + fn should_reject_shielded_withdrawal_with_net_below_min_withdrawal_amount() { + let platform_version = PlatformVersion::latest(); + // ShieldedWithdrawal validation carves `compute_shielded_withdrawal_fee` (base + + // document cost), so the net the gate checks is `unshielding_amount - withdrawal_fee`. + // Use the SAME fee here so the constructed gross lands the net exactly at the + // withdrawal-range boundary under test. + let min_fee = dpp::shielded::compute_shielded_withdrawal_fee(0, platform_version) + .expect("fee computation should not overflow"); + let min_withdrawal = platform_version.system_limits.min_withdrawal_amount; + // net = min_withdrawal_amount - 1 → just below the Core dust floor. + let st = shielded_withdrawal_with_amount(min_fee + min_withdrawal - 1); + let result = st + .validate_minimum_shielded_fee(platform_version) + .expect("should not error"); + assert!(!result.is_valid()); + assert!( + matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::WithdrawalBelowMinAmountError(_) + )) + ), + "below-min must reject with WithdrawalBelowMinAmountError; got {:?}", + result.errors + ); } - } - Ok(SimpleConsensusValidationResult::new()) -} -/// Protocol 15 proof validation adds token pools while retaining the identity key restrictions. -fn validate_shielded_proof_v2( - state_transition: &StateTransition, - platform_version: &PlatformVersion, -) -> Result { - if let Some(error) = key_not_allowed_in_shielded_creation(state_transition) { - return Ok(SimpleConsensusValidationResult::new_with_error(error)); - } - // A batch carries no pool-paid bundle of its own; its token shielded - // transitions are verified one by one against the sighash data each binds. - // Block processing verifies them again inside state validation, where a - // failure is a paid nonce bump; this stateless pass is CheckTx's admission - // filter, run under the nonce-aware limiter. - if let StateTransition::Batch(batch) = state_transition { - return validate_batch_token_shielded_proofs(batch, platform_version); - } + #[test] + fn should_accept_shielded_withdrawal_with_net_at_min_withdrawal_amount() { + let platform_version = PlatformVersion::latest(); + // ShieldedWithdrawal validation carves `compute_shielded_withdrawal_fee` (base + + // document cost), so the net the gate checks is `unshielding_amount - withdrawal_fee`. + // Use the SAME fee here so the constructed gross lands the net exactly at the + // withdrawal-range boundary under test. + let min_fee = dpp::shielded::compute_shielded_withdrawal_fee(0, platform_version) + .expect("fee computation should not overflow"); + let min_withdrawal = platform_version.system_limits.min_withdrawal_amount; + // net = min_withdrawal_amount exactly → at the floor, accepted. + let st = shielded_withdrawal_with_amount(min_fee + min_withdrawal); + let result = st + .validate_minimum_shielded_fee(platform_version) + .expect("should not error"); + assert!( + result.is_valid(), + "a withdrawal whose net equals min_withdrawal_amount must be accepted" + ); + } - // `IdentityCreateFromShieldedPool` is the only shielded transition carrying separate - // per-key proof-of-possession signatures that are NOT covered by the Orchard proof - // (they sign the platform signable bytes, and only id+denomination+keys — not the PoP - // sigs — are bound into `extra_sighash_data`). Validate the CHEAP key structure + - // per-key PoP here, BEFORE the expensive Halo 2 bundle verification, so a relayer - // who flips a PoP byte on an observed transition is rejected without the node paying - // for proof verification (DoS hardening). Same `signable_bytes` the transformer uses. - if let StateTransition::IdentityCreateFromShieldedPool(st) = state_transition { - let IdentityCreateFromShieldedPoolTransition::V0(v0) = st; + #[test] + fn should_reject_shielded_withdrawal_with_net_above_max_withdrawal_amount() { + let platform_version = PlatformVersion::latest(); + // ShieldedWithdrawal validation carves `compute_shielded_withdrawal_fee` (base + + // document cost), so the net the gate checks is `unshielding_amount - withdrawal_fee`. + // Use the SAME fee here so the constructed gross lands the net exactly at the + // withdrawal-range boundary under test. + let min_fee = dpp::shielded::compute_shielded_withdrawal_fee(0, platform_version) + .expect("fee computation should not overflow"); + let max = platform_version.system_limits.max_withdrawal_amount; + // net = max_withdrawal_amount + 1 → just over the per-transition policy cap. + let st = shielded_withdrawal_with_amount(min_fee + max + 1); + let result = st + .validate_minimum_shielded_fee(platform_version) + .expect("should not error"); + assert!(!result.is_valid()); + // Must reject with the amount-RANGE error, not the below-min error — locks in the + // "over-max is not below-min" reason accuracy the cap introduced. + assert!( + matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::InvalidIdentityCreditWithdrawalTransitionAmountError(_) + )) + ), + "over-max must reject with InvalidIdentityCreditWithdrawalTransitionAmountError; got {:?}", + result.errors + ); + } - let key_structure_result = - IdentityPublicKeyInCreation::validate_identity_public_keys_structure( - &v0.public_keys, - true, - platform_version, - )?; - if !key_structure_result.is_valid() { - return Ok(key_structure_result); - } + #[test] + fn should_accept_shielded_withdrawal_with_net_at_max_withdrawal_amount() { + let platform_version = PlatformVersion::latest(); + // ShieldedWithdrawal validation carves `compute_shielded_withdrawal_fee` (base + + // document cost), so the net the gate checks is `unshielding_amount - withdrawal_fee`. + // Use the SAME fee here so the constructed gross lands the net exactly at the + // withdrawal-range boundary under test. + let min_fee = dpp::shielded::compute_shielded_withdrawal_fee(0, platform_version) + .expect("fee computation should not overflow"); + let max = platform_version.system_limits.max_withdrawal_amount; + // net = max_withdrawal_amount exactly → at the cap, accepted. + let st = shielded_withdrawal_with_amount(min_fee + max); + let result = st + .validate_minimum_shielded_fee(platform_version) + .expect("should not error"); + assert!( + result.is_valid(), + "a withdrawal whose net equals max_withdrawal_amount must be accepted" + ); + } - let signable_bytes = state_transition.signable_bytes()?; - for key in v0.public_keys.iter() { - let pop_result = signable_bytes.as_slice().verify_signature( - key.key_type(), - key.data().as_slice(), - key.signature().as_slice(), - ); - if !pop_result.is_valid() { - return Ok(pop_result); - } - } - } + #[test] + fn should_reject_amount_exceeding_i64_max_via_guard() { + let platform_version = PlatformVersion::latest(); + // `unshielding_amount > i64::MAX` wraps to a negative i64 in the validator's cast; + // the defensive `fee < 0` guard must reject it (rather than wrapping back to a huge + // u64 and sailing past the min-fee check). + let st = shielded_withdrawal_with_amount((i64::MAX as u64) + 1); + let result = st + .validate_minimum_shielded_fee(platform_version) + .expect("should not error"); + assert!(!result.is_valid()); + assert!( + matches!( + result.errors.first(), + Some(ConsensusError::BasicError( + BasicError::ShieldedInvalidValueBalanceError(_) + )) + ), + "amount > i64::MAX must be rejected by the fee<0 guard; got {:?}", + result.errors + ); + } - let result = match state_transition { - StateTransition::Shield(st) => match st { - dpp::state_transition::shield_transition::ShieldTransition::V0(v0) => { - reconstruct_and_verify_bundle( - &v0.actions, - FLAGS_OUTPUTS_ONLY, - -(v0.amount as i64), - &v0.anchor, - v0.proof.as_slice(), - &v0.binding_signature, - &[], // No transparent fields for shield - ) - } - }, - StateTransition::ShieldFromIdentity(st) => match st { - ShieldFromIdentityTransition::V0(v0) => reconstruct_and_verify_bundle( - &v0.actions, - FLAGS_OUTPUTS_ONLY, - -(v0.amount as i64), - &v0.anchor, - v0.proof.as_slice(), - &v0.binding_signature, - &[], - ), - }, - StateTransition::ShieldedTransfer(st) => match st { - dpp::state_transition::shielded_transfer_transition::ShieldedTransferTransition::V0(v0) => { - reconstruct_and_verify_bundle( - &v0.actions, - FLAGS_SPENDS_AND_OUTPUTS, - v0.value_balance as i64, - &v0.anchor, - v0.proof.as_slice(), - &v0.binding_signature, - &[], // No transparent fields for shielded transfer - ) - } - }, - StateTransition::Unshield(st) => match st { - dpp::state_transition::unshield_transition::UnshieldTransition::V0(v0) => { - let extra_sighash_data = dpp::shielded::unshield_extra_sighash_data( - &v0.output_address.to_bytes(), - v0.unshielding_amount, - platform_version, - )?; - reconstruct_and_verify_bundle( - &v0.actions, - FLAGS_SPENDS_AND_OUTPUTS, - v0.unshielding_amount as i64, - &v0.anchor, - v0.proof.as_slice(), - &v0.binding_signature, - &extra_sighash_data, - ) - } - }, - StateTransition::TokenShieldedTransferWithShieldedFee(st) => match st { - TokenShieldedTransferWithShieldedFeeTransition::V0(v0) => { - let token_extra_sighash_data = dpp::shielded::token_shielded_transfer_with_shielded_fee_extra_sighash_data( - &v0.token_id.to_buffer(), - platform_version, - )?; - let fee_extra_sighash_data = - dpp::shielded::token_pool_fee_bundle_extra_sighash_data( - dpp::shielded::TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE, - &v0.token_id.to_buffer(), - &v0.token_actions, - platform_version, - )?; - reconstruct_and_verify_bundle( - &v0.token_actions, - FLAGS_SPENDS_AND_OUTPUTS, - 0, - &v0.token_anchor, - v0.token_proof.as_slice(), - &v0.token_binding_signature, - &token_extra_sighash_data, - ) - .and_then(|_| { - reconstruct_and_verify_bundle( - &v0.fee_actions, - FLAGS_SPENDS_AND_OUTPUTS, - v0.credit_amount as i64, - &v0.fee_anchor, - v0.fee_proof.as_slice(), - &v0.fee_binding_signature, - &fee_extra_sighash_data, - ) - }) - } + /// Build an `IdentityCreateFromShieldedPool` with `num_actions` actions, `num_keys` keys, and + /// the given `denomination` (the min-fee gate only reads those three). + pub(super) fn identity_create_from_shielded_pool( + denomination: u64, + num_actions: usize, + num_keys: usize, + ) -> StateTransition { + use dpp::identity::{KeyType, Purpose, SecurityLevel}; + use dpp::platform_value::BinaryData; + use dpp::shielded::SerializedAction; + use dpp::state_transition::public_key_in_creation::v0::IdentityPublicKeyInCreationV0; + use dpp::state_transition::public_key_in_creation::IdentityPublicKeyInCreation; + use dpp::state_transition::state_transitions::shielded::identity_create_from_shielded_pool_transition::v0::IdentityCreateFromShieldedPoolTransitionV0; + use dpp::state_transition::state_transitions::shielded::identity_create_from_shielded_pool_transition::IdentityCreateFromShieldedPoolTransition; + let actions = (0..num_actions as u8) + .map(|i| SerializedAction { + nullifier: [i; 32], + rk: [0u8; 32], + cmx: [0u8; 32], + encrypted_note: vec![0u8; 216], + cv_net: [0u8; 32], + spend_auth_sig: [0u8; 64], + }) + .collect(); + let public_keys = (0..num_keys as u32) + .map(|i| { + IdentityPublicKeyInCreation::V0(IdentityPublicKeyInCreationV0 { + id: i, + key_type: KeyType::ECDSA_SECP256K1, + purpose: Purpose::AUTHENTICATION, + security_level: SecurityLevel::MASTER, + contract_bounds: None, + read_only: false, + data: BinaryData::new(vec![i as u8; 33]), + signature: BinaryData::new(vec![]), + }) + }) + .collect(); + StateTransition::IdentityCreateFromShieldedPool( + IdentityCreateFromShieldedPoolTransition::V0( + IdentityCreateFromShieldedPoolTransitionV0 { + public_keys, + denomination, + actions, + anchor: [0u8; 32], + proof: vec![], + binding_signature: [0u8; 64], + send_to_address_on_creation_failure: + dpp::address_funds::PlatformAddress::P2pkh([0u8; 20]), + identity_id: Default::default(), }, - StateTransition::TokenUnshieldWithShieldedFee(st) => match st { - TokenUnshieldWithShieldedFeeTransition::V0(v0) => { - let token_extra_sighash_data = dpp::shielded::token_unshield_with_shielded_fee_extra_sighash_data( - &v0.token_id.to_buffer(), - &v0.recipient_id.to_buffer(), - v0.amount, + ), + ) + } + + #[test] + fn should_reject_identity_create_denomination_below_min_fee() { + let platform_version = PlatformVersion::latest(); + let (num_actions, num_keys) = (2usize, 1usize); + let min_fee = dpp::shielded::compute_shielded_identity_create_fee( + num_actions, + num_keys, platform_version, - )?; - let fee_extra_sighash_data = - dpp::shielded::token_pool_fee_bundle_extra_sighash_data( - dpp::shielded::TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE, - &v0.token_id.to_buffer(), - &v0.token_actions, - platform_version, - )?; - reconstruct_and_verify_bundle( - &v0.token_actions, - FLAGS_SPENDS_AND_OUTPUTS, - v0.amount as i64, - &v0.token_anchor, - v0.token_proof.as_slice(), - &v0.token_binding_signature, - &token_extra_sighash_data, - ) - .and_then(|_| { - reconstruct_and_verify_bundle( - &v0.fee_actions, - FLAGS_SPENDS_AND_OUTPUTS, - v0.credit_amount as i64, - &v0.fee_anchor, - v0.fee_proof.as_slice(), - &v0.fee_binding_signature, - &fee_extra_sighash_data, - ) - }) - } - }, - StateTransition::TokenPurchaseFromShieldedPool(st) => match st { - TokenPurchaseFromShieldedPoolTransition::V0(v0) => { - let token_extra_sighash_data = dpp::shielded::token_purchase_from_shielded_pool_extra_sighash_data( - &v0.token_id.to_buffer(), - v0.token_count, - v0.total_agreed_price, + ) + .expect("fee"); + let st = identity_create_from_shielded_pool(min_fee - 1, num_actions, num_keys); + let result = st + .validate_minimum_shielded_fee(platform_version) + .expect("no error"); + assert!(!result.is_valid()); + assert!( + matches!( + result.errors.first(), + Some(ConsensusError::StateError( + StateError::InsufficientShieldedFeeError(_) + )) + ), + "a denomination below the min fee must reject with InsufficientShieldedFeeError; got {:?}", + result.errors + ); + } + + #[test] + fn should_accept_identity_create_denomination_at_min_fee() { + let platform_version = PlatformVersion::latest(); + let (num_actions, num_keys) = (2usize, 1usize); + let min_fee = dpp::shielded::compute_shielded_identity_create_fee( + num_actions, + num_keys, platform_version, - )?; - let fee_extra_sighash_data = - dpp::shielded::token_pool_fee_bundle_extra_sighash_data( - dpp::shielded::TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, - &v0.token_id.to_buffer(), - &v0.token_actions, - platform_version, - )?; - reconstruct_and_verify_bundle( - &v0.token_actions, - FLAGS_OUTPUTS_ONLY, - -(v0.token_count as i64), - &v0.token_anchor, - v0.token_proof.as_slice(), - &v0.token_binding_signature, - &token_extra_sighash_data, - ) - .and_then(|_| { - reconstruct_and_verify_bundle( - &v0.fee_actions, - FLAGS_SPENDS_AND_OUTPUTS, - v0.credit_amount as i64, - &v0.fee_anchor, - v0.fee_proof.as_slice(), - &v0.fee_binding_signature, - &fee_extra_sighash_data, - ) - }) - } - }, - StateTransition::IdentityTopUpFromShieldedPool(st) => match st { - IdentityTopUpFromShieldedPoolTransition::V0(v0) => { - let extra_sighash_data = - dpp::shielded::identity_top_up_from_shielded_extra_sighash_data( - &v0.identity_id.to_buffer(), - v0.top_up_amount, - platform_version, - )?; - reconstruct_and_verify_bundle( - &v0.actions, - FLAGS_SPENDS_AND_OUTPUTS, - v0.top_up_amount as i64, - &v0.anchor, - v0.proof.as_slice(), - &v0.binding_signature, - &extra_sighash_data, - ) - } - }, - StateTransition::ShieldedWithdrawal(st) => match st { - dpp::state_transition::shielded_withdrawal_transition::ShieldedWithdrawalTransition::V0(v0) => { - let extra_sighash_data = - dpp::shielded::shielded_withdrawal_extra_sighash_data( - v0.output_script.as_bytes(), - v0.unshielding_amount, - v0.core_fee_per_byte, - v0.pooling, - platform_version, - )?; - reconstruct_and_verify_bundle( - &v0.actions, - FLAGS_SPENDS_AND_OUTPUTS, - v0.unshielding_amount as i64, - &v0.anchor, - v0.proof.as_slice(), - &v0.binding_signature, - &extra_sighash_data, - ) - } - }, - StateTransition::IdentityCreateFromShieldedPool(st) => match st { - dpp::state_transition::identity_create_from_shielded_pool_transition::IdentityCreateFromShieldedPoolTransition::V0(v0) => { - // Bind the new identity id + denomination + FULL public-key set into the - // Orchard sighash so the bundle cannot be redirected to a different - // identity/keys (the surplus_output binding analog). The id is re-derived - // from the spend nullifiers — the canonical value — so the binding holds - // regardless of any (separately-validated) wire `identity_id`. - let identity_id = - dpp::state_transition::identity_create_from_shielded_pool_transition::derive_identity_id_from_actions(&v0.actions) - .to_buffer(); - let extra_sighash_data = - dpp::shielded::identity_create_from_shielded_extra_sighash_data( - &identity_id, - v0.denomination, - &v0.send_to_address_on_creation_failure, - &v0.public_keys, - platform_version, - )?; - // value_balance = denomination EXACTLY (the ShieldedTransfer exact-equality - // model): the binding signature proves the value commitments sum to exactly - // the denomination leaving the pool. - reconstruct_and_verify_bundle( - &v0.actions, - FLAGS_SPENDS_AND_OUTPUTS, - v0.denomination as i64, - &v0.anchor, - v0.proof.as_slice(), - &v0.binding_signature, - &extra_sighash_data, - ) - } - }, - // ShieldFromAssetLock retains proof verification in transform_into_action; - // its paid-failure action comes from the asset lock. - _ => return Ok(SimpleConsensusValidationResult::new()), - }; + ) + .expect("fee"); + let st = identity_create_from_shielded_pool(min_fee, num_actions, num_keys); + assert!( + st.validate_minimum_shielded_fee(platform_version) + .expect("no error") + .is_valid(), + "a denomination equal to the min fee must be accepted" + ); + } - match result { - Ok(()) => Ok(SimpleConsensusValidationResult::new()), - Err(e) => Ok(SimpleConsensusValidationResult::new_with_error( - StateError::InvalidShieldedProofError(e).into(), - )), + #[test] + fn should_scale_identity_create_min_fee_with_key_count() { + let platform_version = PlatformVersion::latest(); + let num_actions = 2usize; + let one_key_fee = dpp::shielded::compute_shielded_identity_create_fee( + num_actions, + 1, + platform_version, + ) + .expect("fee"); + let five_key_fee = dpp::shielded::compute_shielded_identity_create_fee( + num_actions, + 5, + platform_version, + ) + .expect("fee"); + assert!(five_key_fee > one_key_fee, "more keys must cost more"); + // A 1-key-sized denomination must be REJECTED for a 5-key identity (the fee scaled up). + let st = identity_create_from_shielded_pool(one_key_fee, num_actions, 5); + assert!( + !st.validate_minimum_shielded_fee(platform_version) + .expect("no error") + .is_valid(), + "a 1-key-sized denomination must be rejected once the identity has 5 keys" + ); + // ...and accepted once the denomination covers the scaled fee. + let st_ok = identity_create_from_shielded_pool(five_key_fee, num_actions, 5); + assert!(st_ok + .validate_minimum_shielded_fee(platform_version) + .expect("no error") + .is_valid()); + } + } + + mod validate_shielded_proof { + use super::validate_minimum_shielded_fee::identity_create_from_shielded_pool; + use super::*; + use dpp::identity::contract_bounds::ContractBounds; + use dpp::state_transition::public_key_in_creation::v1::IdentityPublicKeyInCreationV1; + + #[test] + fn should_preserve_identity_key_restrictions_after_token_pool_activation() { + for platform_version in [ + PlatformVersion::get(14).expect("v14"), + PlatformVersion::latest(), + ] { + for limits in [false, true] { + let mut transition = identity_create_from_shielded_pool(10_000_000_000, 2, 1); + let StateTransition::IdentityCreateFromShieldedPool( + IdentityCreateFromShieldedPoolTransition::V0(ref mut creation), + ) = transition + else { + panic!("expected the identity creation fixture"); + }; + let IdentityPublicKeyInCreation::V0(mut key) = creation.public_keys.remove(0) + else { + panic!("expected a version 0 key"); + }; + creation.public_keys = vec![if limits { + IdentityPublicKeyInCreation::V1( + IdentityPublicKeyInCreationV1::from_v0_with_limits( + key, + Some(100), + None, + ), + ) + } else { + key.contract_bounds = + Some(ContractBounds::ContractGroup { id: [1; 32].into() }); + IdentityPublicKeyInCreation::V0(key) + }]; + // These restrictions must run before the deliberately invalid signatures + // and Orchard proof in the fixture, in both protocol versions. + let result = transition + .validate_shielded_proof(platform_version) + .expect("consensus rejection"); + if limits { + assert!(matches!(result.errors.as_slice(), [ConsensusError::BasicError(BasicError::IdentityPublicKeyLimitsNotAllowedInShieldedIdentityCreationError(_))])); + } else { + assert!(matches!(result.errors.as_slice(), [ConsensusError::BasicError(BasicError::ContractGroupBoundKeyNotAllowedInShieldedIdentityCreationError(_))])); + } } + } + } + + #[test] + fn should_pass_for_non_shielded_transition() { + let platform_version = &platform_version::version::v9::PLATFORM_V9; + let st = make_data_contract_create_st(); + let result = st + .validate_shielded_proof(platform_version) + .expect("should not error"); + assert!(result.is_valid()); + } + } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 8aba6ff142d..0fc4dc5c0b2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -1108,6 +1108,9 @@ mod token_pool_mint_burn_claim_purchase_tests { use dpp::block::epoch::Epoch; use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters; use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; + use dpp::data_contract::associated_token::token_distribution_rules::accessors::v1::TokenDistributionRulesV1Setters; + use dpp::data_contract::associated_token::token_once_per_identity_distribution::v0::TokenOncePerIdentityDistributionV0; + use dpp::data_contract::associated_token::token_once_per_identity_distribution::TokenOncePerIdentityDistribution; use dpp::data_contract::associated_token::token_pre_programmed_distribution::v0::TokenPreProgrammedDistributionV0; use dpp::data_contract::associated_token::token_pre_programmed_distribution::TokenPreProgrammedDistribution; use dpp::identity::accessors::IdentityGettersV0; @@ -1627,6 +1630,15 @@ mod token_pool_mint_burn_claim_purchase_tests { #[tokio::test] async fn test_token_claim_to_pool_pre_programmed() { + assert_token_claim_to_pool(TokenDistributionType::PreProgrammed).await; + } + + #[tokio::test] + async fn should_claim_once_per_identity_into_the_pool_only_once() { + assert_token_claim_to_pool(TokenDistributionType::OncePerIdentity).await; + } + + async fn assert_token_claim_to_pool(distribution_type: TokenDistributionType) { let platform_version = PlatformVersion::latest(); let mut platform = platform_with_latest_version(); let mut rng = StdRng::seed_from_u64(9105); @@ -1640,13 +1652,23 @@ mod token_pool_mint_burn_claim_purchase_tests { owner.id(), Some(move |configuration: &mut TokenConfiguration| { configuration.set_has_shielded_pool(true); - configuration - .distribution_rules_mut() - .set_pre_programmed_distribution(Some(TokenPreProgrammedDistribution::V0( - TokenPreProgrammedDistributionV0 { - distributions: [(100, [(claimant_id, 445)].into())].into(), - }, - ))); + if distribution_type == TokenDistributionType::OncePerIdentity { + configuration + .distribution_rules_mut() + .set_once_per_identity_distribution(Some( + TokenOncePerIdentityDistribution::V0( + TokenOncePerIdentityDistributionV0 { amount: 445 }, + ), + )); + } else { + configuration + .distribution_rules_mut() + .set_pre_programmed_distribution(Some(TokenPreProgrammedDistribution::V0( + TokenPreProgrammedDistributionV0 { + distributions: [(100, [(claimant_id, 445)].into())].into(), + }, + ))); + } }), None, None, @@ -1669,7 +1691,7 @@ mod token_pool_mint_burn_claim_purchase_tests { claimant_id, contract.id(), 0, - TokenDistributionType::PreProgrammed, + distribution_type, None, build_shield_bundle(444, 24), None, @@ -1715,7 +1737,7 @@ mod token_pool_mint_burn_claim_purchase_tests { claimant_id, contract.id(), 0, - TokenDistributionType::PreProgrammed, + distribution_type, None, claim_bundle.clone(), None, @@ -1773,7 +1795,7 @@ mod token_pool_mint_burn_claim_purchase_tests { claimant_id, contract.id(), 0, - TokenDistributionType::PreProgrammed, + distribution_type, None, build_shield_bundle(445, 26), None, @@ -1799,13 +1821,28 @@ mod token_pool_mint_burn_claim_purchase_tests { None, ) .expect("process state transition"); - assert_matches!( - result.execution_results().as_slice(), - [StateTransitionExecutionResult::PaidConsensusError { - error: ConsensusError::StateError(StateError::InvalidTokenClaimNoCurrentRewards(_)), - .. - }] - ); + if distribution_type == TokenDistributionType::OncePerIdentity { + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError( + StateError::TokenOncePerIdentityDistributionAlreadyClaimedError(_) + ), + .. + }] + ); + } else { + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError( + StateError::InvalidTokenClaimNoCurrentRewards(_) + ), + .. + }] + ); + } + assert_eq!(pool_balance(&platform, token_id), 445); } #[tokio::test] @@ -1969,6 +2006,7 @@ mod document_shielded_token_payment_tests { use dpp::document::{Document, DocumentV0Getters}; use dpp::identity::accessors::IdentityGettersV0; use dpp::identity::{Identity, IdentityPublicKey}; + use dpp::prelude::IdentityNonce; use dpp::shielded::{document_token_payment_extra_sighash_data_v0, OrchardBundleParams}; use dpp::tokens::calculate_token_id; use dpp::tokens::gas_fees_paid_by::GasFeesPaidBy; @@ -2027,6 +2065,7 @@ mod document_shielded_token_payment_tests { rng: &mut StdRng, card_document_type: DocumentTypeRef, owner_id: Identifier, + identity_contract_nonce: IdentityNonce, platform_version: &PlatformVersion, ) -> (Document, Bytes32) { let entropy = Bytes32::random_with_rng(rng); @@ -2040,6 +2079,15 @@ mod document_shielded_token_payment_tests { platform_version, ) .expect("expected a random document"); + // The proof must bind the final nonce-derived creation ID. + document + .set_id_for_creation( + card_document_type, + &entropy.0, + identity_contract_nonce, + platform_version, + ) + .expect("creation document id"); document.set("attack", 4.into()); document.set("defense", 7.into()); (document, entropy) @@ -2135,8 +2183,13 @@ mod document_shielded_token_payment_tests { let card_document_type = contract .document_type_for_name("card") .expect("card document type"); - let (document, entropy) = - random_card(&mut rng, card_document_type, buyer.id(), platform_version); + let (document, entropy) = random_card( + &mut rng, + card_document_type, + buyer.id(), + 2, + platform_version, + ); // A 15 note the wallet holds pays the 10 cost; 5 return to the pool as change. let (note, anchor, merkle_path) = spendable_note(SHIELDED, 4); @@ -2193,8 +2246,13 @@ mod document_shielded_token_payment_tests { assert_tokens_conserved(&platform); // The spent notes cannot pay for another document. - let (replay_document, replay_entropy) = - random_card(&mut rng, card_document_type, buyer.id(), platform_version); + let (replay_document, replay_entropy) = random_card( + &mut rng, + card_document_type, + buyer.id(), + 3, + platform_version, + ); let replay = BatchTransition::new_document_creation_transition_from_document( replay_document, card_document_type, @@ -2251,8 +2309,13 @@ mod document_shielded_token_payment_tests { let card_document_type = contract .document_type_for_name("card") .expect("card document type"); - let (document, entropy) = - random_card(&mut rng, card_document_type, buyer.id(), platform_version); + let (document, entropy) = random_card( + &mut rng, + card_document_type, + buyer.id(), + 2, + platform_version, + ); let (note, anchor, merkle_path) = spendable_note(SHIELDED, 5); insert_token_pool_anchor(&platform, token_id, &anchor); @@ -2321,8 +2384,13 @@ mod document_shielded_token_payment_tests { let card_document_type = contract .document_type_for_name("card") .expect("card document type"); - let (document, entropy) = - random_card(&mut rng, card_document_type, buyer.id(), platform_version); + let (document, entropy) = random_card( + &mut rng, + card_document_type, + buyer.id(), + 2, + platform_version, + ); // A bundle proving 9 where the card costs 10. let underpaid = CARD_COST - 1; @@ -2399,8 +2467,13 @@ mod document_shielded_token_payment_tests { let card_document_type = contract .document_type_for_name("card") .expect("card document type"); - let (document, entropy) = - random_card(&mut rng, card_document_type, buyer.id(), platform_version); + let (document, entropy) = random_card( + &mut rng, + card_document_type, + buyer.id(), + 1, + platform_version, + ); let transition = BatchTransition::new_document_creation_transition_from_document( document, diff --git a/packages/rs-drive/grovedb-structure.json b/packages/rs-drive/grovedb-structure.json index 8a074d22cd9..e93b7c915f3 100644 --- a/packages/rs-drive/grovedb-structure.json +++ b/packages/rs-drive/grovedb-structure.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "latest_protocol_version": 14, + "latest_protocol_version": 15, "element_kinds": [ { "name": "Item", @@ -944,6 +944,203 @@ ] } ] + }, + { + "id": "tokens.shielded_pools", + "key": { + "type": "fixed", + "hex": "e0", + "label": "TokenShieldedPools", + "constant": "TOKEN_SHIELDED_POOLS_KEY" + }, + "kinds": [ + "BigSumTree" + ], + "since": 15, + "presence": "always", + "source": "packages/rs-drive/src/drive/tokens/paths.rs", + "book": "data-model/token-shielded-pools.md", + "description": "The shielded balances of tokens that enable an Orchard pool.", + "children": [ + { + "id": "tokens.shielded_pools.token", + "key": { + "type": "dynamic", + "name": "token_id", + "matcher": { + "type": "len", + "len": 32 + }, + "encoding": "identifier32", + "description": "The token id" + }, + "kinds": [ + "SumTree" + ], + "since": 15, + "presence": "always", + "source": "packages/rs-drive/src/drive/shielded/paths.rs", + "description": "The Orchard pool for one token.", + "children": [ + { + "id": "tokens.shielded_pools.token.total_balance", + "key": { + "type": "fixed", + "hex": "20", + "label": "TotalBalance", + "constant": "SHIELDED_TOTAL_BALANCE_KEY" + }, + "kinds": [ + "SumItem" + ], + "value": "token amount", + "since": 15, + "presence": "always", + "source": "packages/rs-drive/src/drive/shielded/paths.rs", + "description": "Every token in the pool.", + "children": [] + }, + { + "id": "tokens.shielded_pools.token.nullifiers", + "key": { + "type": "fixed", + "hex": "40", + "label": "Nullifiers", + "constant": "SHIELDED_NULLIFIERS_KEY" + }, + "kinds": [ + "ProvableCountTree" + ], + "since": 15, + "presence": "always", + "source": "packages/rs-drive/src/drive/shielded/paths.rs", + "description": "The nullifier of every spent note, so none is spent twice.", + "children": [ + { + "id": "tokens.shielded_pools.token.nullifiers.nullifier", + "key": { + "type": "dynamic", + "name": "nullifier", + "matcher": { + "type": "len", + "len": 32 + }, + "encoding": "hash32", + "description": "The nullifier" + }, + "kinds": [ + "Item" + ], + "value": "empty", + "since": 15, + "presence": "always", + "source": "packages/rs-drive/src/drive/shielded/paths.rs", + "description": "One spent note.", + "children": [] + } + ] + }, + { + "id": "tokens.shielded_pools.token.anchors_by_height", + "key": { + "type": "fixed", + "hex": "60", + "label": "AnchorsByHeight", + "constant": "SHIELDED_ANCHORS_BY_HEIGHT_KEY" + }, + "kinds": [ + "Tree" + ], + "since": 15, + "presence": "always", + "source": "packages/rs-drive/src/drive/shielded/paths.rs", + "description": "The commitment tree anchor after each block that added notes, for pruning old anchors.", + "children": [ + { + "id": "tokens.shielded_pools.token.anchors_by_height.height", + "key": { + "type": "dynamic", + "name": "block_height", + "matcher": { + "type": "len", + "len": 8 + }, + "encoding": "u64_be", + "description": "The block height" + }, + "kinds": [ + "Item" + ], + "value": "the anchor, 32 bytes", + "since": 15, + "presence": "always", + "source": "packages/rs-drive/src/drive/shielded/paths.rs", + "description": "The anchor at that height.", + "children": [] + } + ] + }, + { + "id": "tokens.shielded_pools.token.notes", + "key": { + "type": "fixed", + "hex": "80", + "label": "Notes", + "constant": "SHIELDED_NOTES_KEY" + }, + "kinds": [ + "CommitmentTree" + ], + "since": 15, + "presence": "always", + "source": "packages/rs-drive/src/drive/shielded/paths.rs", + "description": "Every note ever added to the pool. Read by every wallet sync.", + "opaque": "Note commitments with their encrypted notes in a bulk append tree, plus the Sinsemilla frontier the anchors are computed from.", + "children": [] + }, + { + "id": "tokens.shielded_pools.token.anchors_in_pool", + "key": { + "type": "fixed", + "hex": "c0", + "label": "AnchorsInPool", + "constant": "SHIELDED_ANCHORS_IN_POOL_KEY" + }, + "kinds": [ + "Tree" + ], + "since": 15, + "presence": "always", + "source": "packages/rs-drive/src/drive/shielded/paths.rs", + "description": "The anchors a spend may still prove against.", + "children": [ + { + "id": "tokens.shielded_pools.token.anchors_in_pool.anchor", + "key": { + "type": "dynamic", + "name": "anchor", + "matcher": { + "type": "len", + "len": 32 + }, + "encoding": "hash32", + "description": "The anchor" + }, + "kinds": [ + "Item" + ], + "value": "the block height, u64 big endian", + "since": 15, + "presence": "always", + "source": "packages/rs-drive/src/drive/shielded/paths.rs", + "description": "One valid anchor.", + "children": [] + } + ] + } + ] + } + ] } ] }, @@ -5050,7 +5247,7 @@ }, "layer_shapes": { "contract_groups": { - "origin": "genesis@14", + "origin": "genesis@15", "tree": { "hex": "00", "right": { @@ -5059,7 +5256,7 @@ } }, "contract_groups.groups.group": { - "origin": "fixture contract_groups_and_bound_keys@14", + "origin": "fixture contract_groups_and_bound_keys@15", "tree": { "hex": "02", "left": { @@ -5074,7 +5271,7 @@ } }, "contract_groups.members.contract": { - "origin": "fixture contract_groups_and_bound_keys@14", + "origin": "fixture contract_groups_and_bound_keys@15", "tree": { "hex": "01", "left": { @@ -5086,7 +5283,7 @@ } }, "contracts.contract": { - "origin": "fixture contracts_with_documents@14", + "origin": "fixture contracts_with_documents@15", "tree": { "hex": "01", "left": { @@ -5098,7 +5295,7 @@ } }, "contracts.contract.other": { - "origin": "fixture moderated_contract@14", + "origin": "fixture moderated_contract@15", "tree": { "hex": "80", "left": { @@ -5116,7 +5313,7 @@ } }, "group_actions.contract.group": { - "origin": "fixture tokens_and_group_actions@14", + "origin": "fixture tokens_and_group_actions@15", "tree": { "hex": "4d", "left": { @@ -5128,7 +5325,7 @@ } }, "group_actions.contract.group.active.action": { - "origin": "fixture tokens_and_group_actions@14", + "origin": "fixture tokens_and_group_actions@15", "tree": { "hex": "53", "left": { @@ -5137,7 +5334,7 @@ } }, "group_actions.contract.group.closed.action": { - "origin": "fixture tokens_and_group_actions@14", + "origin": "fixture tokens_and_group_actions@15", "tree": { "hex": "53", "left": { @@ -5146,7 +5343,7 @@ } }, "identities.identity": { - "origin": "fixture contract_groups_and_bound_keys@14", + "origin": "fixture contract_groups_and_bound_keys@15", "tree": { "hex": "80", "left": { @@ -5171,7 +5368,7 @@ "states": [ { "state": "created", - "origin": "fixture identities@14", + "origin": "fixture identities@15", "tree": { "hex": "80", "left": { @@ -5190,7 +5387,7 @@ }, { "state": "used_with_a_contract", - "origin": "fixture identities@14", + "origin": "fixture identities@15", "tree": { "hex": "80", "left": { @@ -5212,7 +5409,7 @@ }, { "state": "budgeted_key_and_contract", - "origin": "fixture contract_groups_and_bound_keys@14", + "origin": "fixture contract_groups_and_bound_keys@15", "tree": { "hex": "80", "left": { @@ -5238,7 +5435,7 @@ ] }, "identities.identity.contract_info.bound": { - "origin": "fixture contract_groups_and_bound_keys@14", + "origin": "fixture contract_groups_and_bound_keys@15", "tree": { "hex": "01", "left": { @@ -5247,7 +5444,7 @@ } }, "identities.identity.key_references": { - "origin": "fixture identities@14", + "origin": "fixture identities@15", "tree": { "hex": "03", "left": { @@ -5259,7 +5456,7 @@ } }, "identities.identity.key_references.authentication": { - "origin": "fixture identities@14", + "origin": "fixture identities@15", "tree": { "hex": "02", "left": { @@ -5274,7 +5471,7 @@ } }, "misc": { - "origin": "genesis@14", + "origin": "genesis@15", "tree": { "hex": "54", "left": { @@ -5283,7 +5480,7 @@ } }, "pools.epoch": { - "origin": "fixture current_epoch@14", + "origin": "fixture current_epoch@15", "tree": { "hex": "6d", "left": { @@ -5311,14 +5508,14 @@ "states": [ { "state": "future", - "origin": "fixture current_epoch@14", + "origin": "fixture current_epoch@15", "tree": { "hex": "73" } }, { "state": "running", - "origin": "fixture current_epoch@14", + "origin": "fixture current_epoch@15", "tree": { "hex": "6d", "left": { @@ -5346,7 +5543,7 @@ }, { "state": "paid", - "origin": "fixture paid_epoch@14", + "origin": "fixture paid_epoch@15", "tree": { "hex": "74", "left": { @@ -5369,7 +5566,7 @@ ] }, "prefunded_balances": { - "origin": "genesis@14", + "origin": "genesis@15", "tree": { "hex": "80", "left": { @@ -5381,7 +5578,7 @@ } }, "root": { - "origin": "genesis@14", + "origin": "genesis@15", "tree": { "hex": "40", "left": { @@ -5438,7 +5635,7 @@ } }, "saved_block_transactions": { - "origin": "genesis@14", + "origin": "genesis@15", "tree": { "hex": "65", "left": { @@ -5450,7 +5647,7 @@ } }, "shielded_balances.main_pool": { - "origin": "genesis@14", + "origin": "genesis@15", "tree": { "hex": "80", "left": { @@ -5468,7 +5665,7 @@ } }, "tokens": { - "origin": "genesis@14", + "origin": "genesis@15", "tree": { "hex": "80", "left": { @@ -5484,12 +5681,15 @@ "hex": "c0", "left": { "hex": "a0" + }, + "right": { + "hex": "e0" } } } }, "tokens.distributions": { - "origin": "genesis@14", + "origin": "genesis@15", "tree": { "hex": "80", "left": { @@ -5504,7 +5704,7 @@ } }, "tokens.distributions.perpetual.token": { - "origin": "fixture token_distributions_unclaimed@14", + "origin": "fixture token_distributions_unclaimed@15", "tree": { "hex": "c0", "left": { @@ -5513,7 +5713,7 @@ } }, "tokens.distributions.timed": { - "origin": "genesis@14", + "origin": "genesis@15", "tree": { "hex": "80", "left": { @@ -5524,8 +5724,26 @@ } } }, + "tokens.shielded_pools.token": { + "origin": "fixture token_shielded_pool@15", + "tree": { + "hex": "60", + "left": { + "hex": "40", + "left": { + "hex": "20" + } + }, + "right": { + "hex": "c0", + "left": { + "hex": "80" + } + } + } + }, "versions": { - "origin": "genesis@14", + "origin": "genesis@15", "tree": { "hex": "01", "left": { @@ -5534,7 +5752,7 @@ } }, "votes": { - "origin": "genesis@14", + "origin": "genesis@15", "tree": { "hex": "64", "left": { @@ -5546,7 +5764,7 @@ } }, "votes.contested_resource": { - "origin": "genesis@14", + "origin": "genesis@15", "tree": { "hex": "70", "left": { @@ -5555,7 +5773,7 @@ } }, "votes.contested_resource.active_polls.contract.document_type": { - "origin": "fixture contested_documents@14", + "origin": "fixture contested_documents@15", "tree": { "hex": "01", "left": { @@ -5564,7 +5782,7 @@ } }, "votes.contested_resource.active_polls.contract.document_type.indexes.value.contender": { - "origin": "fixture contested_documents@14", + "origin": "fixture contested_documents@15", "tree": { "hex": "01", "left": { @@ -5573,7 +5791,7 @@ } }, "withdrawals": { - "origin": "genesis@14", + "origin": "genesis@15", "tree": { "hex": "03", "left": { diff --git a/packages/rs-drive/src/drive/initialization/mod.rs b/packages/rs-drive/src/drive/initialization/mod.rs index ce157e315c0..5db1db9fdb7 100644 --- a/packages/rs-drive/src/drive/initialization/mod.rs +++ b/packages/rs-drive/src/drive/initialization/mod.rs @@ -6,6 +6,7 @@ mod v1; mod v2; mod v3; mod v4; +mod v5; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -32,9 +33,10 @@ impl Drive { 2 => self.create_initial_state_structure_v2(transaction, platform_version), 3 => self.create_initial_state_structure_v3(transaction, platform_version), 4 => self.create_initial_state_structure_v4(transaction, platform_version), + 5 => self.create_initial_state_structure_v5(transaction, platform_version), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "create_initial_state_structure".to_string(), - known_versions: vec![0, 1, 2, 3, 4], + known_versions: vec![0, 1, 2, 3, 4, 5], received: version, })), } diff --git a/packages/rs-drive/src/drive/initialization/v4/mod.rs b/packages/rs-drive/src/drive/initialization/v4/mod.rs index a28d3cf9dc2..c89df4de467 100644 --- a/packages/rs-drive/src/drive/initialization/v4/mod.rs +++ b/packages/rs-drive/src/drive/initialization/v4/mod.rs @@ -103,10 +103,6 @@ impl Drive { // through the same helper: the prefunded balances Merk is then built by the same // sequence of inserts on both node populations. self.insert_contract_fee_pot_trees(transaction, platform_version)?; - // Token shielded pools root (v4, protocol version 15): an empty BigSumTree under the - // Tokens tree. Inserted after the batch so `[Tokens]` already exists, through the - // helper the in-place upgrade (`Platform::transition_to_version_15`) also calls. - self.insert_token_shielded_pools_root_tree(transaction, platform_version)?; Ok(()) } diff --git a/packages/rs-drive/src/drive/initialization/v5/mod.rs b/packages/rs-drive/src/drive/initialization/v5/mod.rs new file mode 100644 index 00000000000..b1de2e8d2a1 --- /dev/null +++ b/packages/rs-drive/src/drive/initialization/v5/mod.rs @@ -0,0 +1,19 @@ +//! Initialization of token shielded pools in protocol version 15. + +use crate::drive::Drive; +use crate::error::Error; +use dpp::version::PlatformVersion; +use grovedb::TransactionArg; + +impl Drive { + /// Creates protocol 14 storage followed by the token shielded pools root. + pub(super) fn create_initial_state_structure_v5( + &self, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + self.create_initial_state_structure_v4(transaction, platform_version)?; + self.insert_token_shielded_pools_root_tree(transaction, platform_version)?; + Ok(()) + } +} diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/insert_root_tree.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/insert_root_tree.rs index e9b87405731..d0f4475a302 100644 --- a/packages/rs-drive/src/drive/tokens/shielded_pool/insert_root_tree.rs +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/insert_root_tree.rs @@ -9,7 +9,7 @@ impl Drive { /// Inserts the token shielded pools root tree `[Tokens, TOKEN_SHIELDED_POOLS_KEY]` (an empty /// BigSumTree) if it does not exist yet. /// - /// CONSENSUS-CRITICAL: both the genesis path (`Drive::create_initial_state_structure_v4`) + /// CONSENSUS-CRITICAL: both the genesis path (`Drive::create_initial_state_structure_v5`) /// and the in-place upgrade path (`Platform::transition_to_version_15`) call this one helper /// so a chain born at protocol version 15 and a chain upgraded to it build a byte-identical /// `[Tokens]` subtree. The tree stays empty until a token with a shielded pool is registered. diff --git a/packages/rs-drive/src/drive/tokens/structure.rs b/packages/rs-drive/src/drive/tokens/structure.rs index 34cacabad7c..7dfc54888d5 100644 --- a/packages/rs-drive/src/drive/tokens/structure.rs +++ b/packages/rs-drive/src/drive/tokens/structure.rs @@ -1,3 +1,7 @@ +use crate::drive::shielded::paths::{ + SHIELDED_ANCHORS_BY_HEIGHT_KEY, SHIELDED_ANCHORS_IN_POOL_KEY, SHIELDED_NOTES_KEY, + SHIELDED_NULLIFIERS_KEY, SHIELDED_TOTAL_BALANCE_KEY, +}; use crate::drive::tokens::paths::{ TOKEN_BALANCES_KEY, TOKEN_BLOCK_TIMED_DISTRIBUTIONS_KEY, TOKEN_CONTRACT_INFO_KEY, TOKEN_DIRECT_SELL_PRICE_KEY, TOKEN_DISTRIBUTIONS_KEY, TOKEN_EPOCH_TIMED_DISTRIBUTIONS_KEY, @@ -6,7 +10,8 @@ use crate::drive::tokens::paths::{ TOKEN_PERPETUAL_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY, TOKEN_PERPETUAL_DISTRIBUTIONS_INFO_KEY, TOKEN_PERPETUAL_DISTRIBUTIONS_KEY, TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_FOR_IDENTITIES_LAST_CLAIM_KEY, - TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_KEY, TOKEN_STATUS_INFO_KEY, TOKEN_TIMED_DISTRIBUTIONS_KEY, + TOKEN_PRE_PROGRAMMED_DISTRIBUTIONS_KEY, TOKEN_SHIELDED_POOLS_KEY, TOKEN_STATUS_INFO_KEY, + TOKEN_TIMED_DISTRIBUTIONS_KEY, }; use crate::drive::RootTree; use crate::structure::{ElementKind, FlagsKind, KeyEncoding, KeyMatcher, StructureNode}; @@ -38,6 +43,7 @@ pub(crate) fn structure() -> StructureNode { ) .children(vec![ distributions(), + shielded_pools(), StructureNode::fixed( "status", &[TOKEN_STATUS_INFO_KEY], @@ -365,3 +371,116 @@ fn distributions() -> StructureNode { ), ]) } + +/// One Orchard pool per token that enables shielded balances in protocol 15. +fn shielded_pools() -> StructureNode { + StructureNode::fixed( + "shielded_pools", + &[TOKEN_SHIELDED_POOLS_KEY], + "TokenShieldedPools", + "TOKEN_SHIELDED_POOLS_KEY", + ) + .kind(ElementKind::BigSumTree) + .since(15) + .source(SOURCE) + .book("data-model/token-shielded-pools.md") + .describe("The shielded balances of tokens that enable an Orchard pool.") + .child( + token("The Orchard pool for one token.") + .kind(ElementKind::SumTree) + .source("packages/rs-drive/src/drive/shielded/paths.rs") + .children(vec![ + StructureNode::fixed( + "total_balance", + &[SHIELDED_TOTAL_BALANCE_KEY], + "TotalBalance", + "SHIELDED_TOTAL_BALANCE_KEY", + ) + .kind(ElementKind::SumItem) + .value("token amount") + .describe("Every token in the pool."), + StructureNode::fixed( + "nullifiers", + &[SHIELDED_NULLIFIERS_KEY], + "Nullifiers", + "SHIELDED_NULLIFIERS_KEY", + ) + .kind(ElementKind::ProvableCountTree) + .describe( + "The nullifier of every spent note, so none is \ + spent twice.", + ) + .child( + StructureNode::dynamic( + "nullifier", + "nullifier", + KeyMatcher::Len(32), + KeyEncoding::Hash32, + "The nullifier", + ) + .kind(ElementKind::Item) + .value("empty") + .describe("One spent note."), + ), + StructureNode::fixed( + "anchors_by_height", + &[SHIELDED_ANCHORS_BY_HEIGHT_KEY], + "AnchorsByHeight", + "SHIELDED_ANCHORS_BY_HEIGHT_KEY", + ) + .kind(ElementKind::Tree) + .describe( + "The commitment tree anchor after each block that \ + added notes, for pruning old anchors.", + ) + .child( + StructureNode::dynamic( + "height", + "block_height", + KeyMatcher::Len(8), + KeyEncoding::U64Be, + "The block height", + ) + .kind(ElementKind::Item) + .value("the anchor, 32 bytes") + .describe("The anchor at that height."), + ), + StructureNode::fixed( + "notes", + &[SHIELDED_NOTES_KEY], + "Notes", + "SHIELDED_NOTES_KEY", + ) + .kind(ElementKind::CommitmentTree) + .opaque( + "Note commitments with their encrypted notes in a \ + bulk append tree, plus the Sinsemilla frontier \ + the anchors are computed from.", + ) + .describe( + "Every note ever added to the pool. Read by every \ + wallet sync.", + ), + StructureNode::fixed( + "anchors_in_pool", + &[SHIELDED_ANCHORS_IN_POOL_KEY], + "AnchorsInPool", + "SHIELDED_ANCHORS_IN_POOL_KEY", + ) + .kind(ElementKind::Tree) + .describe("The anchors a spend may still prove against.") + .child( + StructureNode::dynamic( + "anchor", + "anchor", + KeyMatcher::Len(32), + KeyEncoding::Hash32, + "The anchor", + ) + .kind(ElementKind::Item) + .value("the block height, u64 big endian") + .describe("One valid anchor."), + ), + ]), + ) +} diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_claim_to_pool_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_claim_to_pool_transition.rs index 7b0acc2cf70..aec43353023 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_claim_to_pool_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/batch/token/token_claim_to_pool_transition.rs @@ -64,6 +64,15 @@ impl DriveHighLevelBatchOperationConverter for TokenClaimToPoolTransitionAction }, )); } + TokenDistributionInfo::OncePerIdentity(claimed_at_ms, _) => { + ops.push(TokenOperation( + TokenOperationType::TokenMarkOncePerIdentityReleaseAsDistributed { + token_id, + recipient_id: owner_id, + claimed_at_ms: *claimed_at_ms, + }, + )); + } } Ok(ops) diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs index 1e0960e4cc5..5a46b1fbf90 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs @@ -601,24 +601,50 @@ pub(crate) fn resolve_token_claim( ) } TokenDistributionType::OncePerIdentity => { - let Some(distribution) = token_config.distribution_rules().once_per_identity_distribution() else { + let Some(distribution) = token_config + .distribution_rules() + .once_per_identity_distribution() + else { return Ok(Err(StateError::InvalidTokenClaimPropertyMismatch( - InvalidTokenClaimPropertyMismatch::new("once per identity distribution", base.token_id()), - ).into())); + InvalidTokenClaimPropertyMismatch::new( + "once per identity distribution", + base.token_id(), + ), + ) + .into())); }; let mut claim_operations = vec![]; let already_claimed_at = drive.fetch_once_per_identity_distribution_claim_operations( - base.token_id().to_buffer(), owner_id, &mut claim_operations, transaction, platform_version, + base.token_id().to_buffer(), + owner_id, + &mut claim_operations, + transaction, + platform_version, )?; fee_result.checked_add_assign(Drive::calculate_fee( - None, Some(claim_operations), &block_info.epoch, drive.config.epochs_per_era, platform_version, None, + None, + Some(claim_operations), + &block_info.epoch, + drive.config.epochs_per_era, + platform_version, + None, )?)?; if let Some(claimed_at_ms) = already_claimed_at { - return Ok(Err(StateError::TokenOncePerIdentityDistributionAlreadyClaimedError( - TokenOncePerIdentityDistributionAlreadyClaimedError::new(base.token_id(), owner_id, claimed_at_ms), - ).into())); + return Ok(Err( + StateError::TokenOncePerIdentityDistributionAlreadyClaimedError( + TokenOncePerIdentityDistributionAlreadyClaimedError::new( + base.token_id(), + owner_id, + claimed_at_ms, + ), + ) + .into(), + )); } - (distribution.amount(), TokenDistributionInfo::OncePerIdentity(block_info.time_ms, owner_id)) + ( + distribution.amount(), + TokenDistributionInfo::OncePerIdentity(block_info.time_ms, owner_id), + ) } }; Ok(Ok((amount, distribution_info))) diff --git a/packages/rs-drive/src/structure/tests.rs b/packages/rs-drive/src/structure/tests.rs index 8babc356afe..205c594157b 100644 --- a/packages/rs-drive/src/structure/tests.rs +++ b/packages/rs-drive/src/structure/tests.rs @@ -119,7 +119,13 @@ fn should_record_a_contract_layer_with_its_documents_on_top() { // fixture. Documents are read most and sit at the root of the layer; the // contract itself and everything else hang below. let contract = &json["layer_shapes"]["contracts.contract"]; - assert_eq!(contract["origin"], "fixture contracts_with_documents@14"); + assert_eq!( + contract["origin"], + format!( + "fixture contracts_with_documents@{}", + PlatformVersion::latest().protocol_version + ) + ); assert_eq!(contract["tree"]["hex"], "01"); assert_eq!(contract["tree"]["left"]["hex"], "00"); assert_eq!(contract["tree"]["right"]["hex"], "02"); @@ -1498,6 +1504,54 @@ mod fixtures { conformance_of(&drive, "spent_nullifiers", run); } + fn token_shielded_pool(run: &mut FixtureRun) { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + let token_id = [7; 32]; + let operations = drive + .create_token_shielded_pool_trees_operations( + token_id, + false, + &mut None, + None, + platform_version, + ) + .expect("expected the token pool operations"); + apply_operations(&drive, operations); + apply_operations( + &drive, + Drive::insert_token_pool_note_op( + token_id, + [1; 32], + [2; 32], + [3; 32], + vec![1; 216], + platform_version, + ) + .expect("expected the token note operations"), + ); + apply_operations( + &drive, + Drive::insert_token_pool_nullifiers(token_id, &[[4; 32]], platform_version) + .expect("expected the token nullifier operations"), + ); + let transaction = drive.grove.start_transaction(); + drive + .record_token_shielded_pool_anchor_if_changed( + token_id, + 1, + &transaction, + platform_version, + ) + .expect("expected to record the token pool anchor"); + drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit the token anchor"); + conformance_of(&drive, "token_shielded_pool", run); + } + /// Runs every fixture pub(super) fn run_all() -> FixtureRun { let mut run = FixtureRun::default(); @@ -1513,6 +1567,7 @@ mod fixtures { token_distributions(&mut run); contract_groups_and_bound_keys(&mut run); spent_nullifiers(&mut run); + token_shielded_pool(&mut run); run } diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v7.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v7.rs index b38c7d0193a..ff942a1786b 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v7.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v7.rs @@ -1,96 +1,16 @@ -use crate::version::dpp_versions::dpp_contract_versions::{ - DPPContractVersions, DataContractMethodVersions, DocumentTypeClassMethodVersions, - DocumentTypeIndexVersions, DocumentTypeMethodVersions, DocumentTypeSchemaVersions, - DocumentTypeVersions, RecursiveSchemaValidatorVersions, TokenVersions, -}; +use super::v6::CONTRACT_VERSIONS_V6; +use super::{DPPContractVersions, TokenVersions}; use versioned_feature_core::FeatureVersionBounds; -// Introduced in protocol version 15 with token shielded pools. Identical to v6 except -// `token_configuration_format` admits format 1 (`TokenConfiguration::V1`, the per-token -// shielded pool flag). v6 keeps max 0 for protocol version 14 chain replay, so a -// pre-activation contract cannot carry the flag. +/// Protocol 15 admits token configuration format 1, retaining all protocol 14 contract rules. pub const CONTRACT_VERSIONS_V7: DPPContractVersions = DPPContractVersions { - max_serialized_size: 65000, - contract_serialization_version: FeatureVersionBounds { - min_version: 0, - max_version: 1, - default_current_version: 1, - }, - contract_structure_version: 1, - created_data_contract_structure: 0, - config: FeatureVersionBounds { - min_version: 1, - max_version: 1, - default_current_version: 1, - }, - methods: DataContractMethodVersions { - validate_document: 0, - // Generation 1 (requiredSince): feeds the new contract version into - // per-document-type update validation and validates requiredSince - // annotations on document types introduced by the update, which the - // per-type pass never sees. Generation 0 stays byte-identical for - // replay of pre-v14 blocks. - validate_update: 1, - schema: 0, - validate_groups: 0, - equal_ignoring_time_fields: 0, - registration_cost: 1, - }, - document_type_versions: DocumentTypeVersions { - index_versions: DocumentTypeIndexVersions { - index_levels_from_indices: 0, - }, - class_method_versions: DocumentTypeClassMethodVersions { - try_from_schema: 3, // changed: parser generation 3 — generation 2 plus the ranked index keywords - create_document_types_from_document_schemas: 1, - }, - structure_version: 0, - schema: DocumentTypeSchemaVersions { - document_type_schema: 3, // changed: v3 document meta-schema — v2 plus the ranked index keywords, and the gate the index parser reads - should_add_creator_id: 1, - enrich_with_base_schema: 1, - find_identifier_and_binary_paths: 0, - apply_property_reference: Some(0), // changed: the meta-schema v3 `refersTo` keyword is folded into the parsed property type; None before this version means the keyword is ignored, as it was before it existed - apply_required_since: Some(0), // changed: the meta-schema v3 `requiredSince` keyword (contract version a property is required from) is parsed onto the property; None before this version means the keyword is ignored, as it was before it existed - validate_max_depth: 0, - max_depth: 256, - recursive_schema_validator_versions: RecursiveSchemaValidatorVersions { - traversal_validator: 0, - }, - validate_schema_compatibility: 1, // changed: strips `indices` before diffing — index changes are validated by `validate_update` v1, so an index-order-only diff no longer hard-errors - }, - methods: DocumentTypeMethodVersions { - create_document_from_data: 0, - create_document_with_prevalidated_properties: 0, - prefunded_voting_balance_for_document: 0, - contested_vote_poll_for_document: 0, - estimated_size: 1, // changed: adds the document serialization format 3 contract-version stamp varint (worst case 5 bytes) to the estimate - // Changed: v1 requires a query's bound fields to cover a - // contiguous prefix of the candidate index (equalities exactly - // covering the leading properties, range/in immediately after, - // no unused property before an order-by field). v0 matched by - // positionless set membership, so a query binding only later - // index properties selected an index the positional path - // lowering misaligns on — returning cryptographically proven - // wrong or empty results. Gapped candidates are now skipped - // per-candidate, letting a well-shaped index win or the query - // fail with WhereClauseOnNonIndexedProperty. v0 stays frozen - // for replay at protocol versions <= 13. - index_for_types: 1, - max_size: 0, - serialize_value_for_key: 0, - deserialize_value_for_key: 0, - }, - }, token_versions: TokenVersions { - // 1: an epoch-based perpetual distribution needs an interval of at least one epoch. - validate_structure_interval: 1, - // changed: `TokenConfigurationV1` (the per-token shielded pool flag) is admitted from - // protocol version 15; v6 keeps max 0 so a pre-activation contract cannot carry it. token_configuration_format: FeatureVersionBounds { min_version: 0, max_version: 1, default_current_version: 0, }, + ..CONTRACT_VERSIONS_V6.token_versions }, + ..CONTRACT_VERSIONS_V6 }; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs index c5ac3e7f50d..97e6d190bab 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs @@ -1,143 +1,11 @@ -use crate::version::drive_abci_versions::drive_abci_method_versions::{ - DriveAbciBlockEndMethodVersions, DriveAbciBlockFeeProcessingMethodVersions, - DriveAbciBlockStartMethodVersions, DriveAbciCoreBasedUpdatesMethodVersions, - DriveAbciCoreChainLockMethodVersionsAndConstants, DriveAbciCoreInstantSendLockMethodVersions, - DriveAbciEngineMethodVersions, DriveAbciEpochMethodVersions, - DriveAbciFeePoolInwardsDistributionMethodVersions, - DriveAbciFeePoolOutwardsDistributionMethodVersions, - DriveAbciIdentityCreditWithdrawalMethodVersions, DriveAbciInitializationMethodVersions, - DriveAbciMasternodeIdentitiesUpdatesMethodVersions, DriveAbciMethodVersions, - DriveAbciPlatformStateStorageMethodVersions, DriveAbciProtocolUpgradeMethodVersions, - DriveAbciStateTransitionProcessingMethodVersions, DriveAbciTokensProcessingMethodVersions, - DriveAbciVotingMethodVersions, -}; +use super::v10::DRIVE_ABCI_METHOD_VERSIONS_V10; +use super::{DriveAbciBlockEndMethodVersions, DriveAbciMethodVersions}; -/// Drive ABCI method versions 11. Introduced in protocol v15 with token shielded pools: turns -/// on `record_token_shielded_pool_anchors` (`Some(0)`), the block-end step that records and -/// prunes the anchors of every token pool the block touched. -/// Everything else matches `DRIVE_ABCI_METHOD_VERSIONS_V10`. +/// Protocol 15 records and prunes the anchors of every token shielded pool touched by a block. pub const DRIVE_ABCI_METHOD_VERSIONS_V11: DriveAbciMethodVersions = DriveAbciMethodVersions { - engine: DriveAbciEngineMethodVersions { - init_chain: 0, - check_tx: 0, - run_block_proposal: 0, - finalize_block_proposal: 0, - consensus_params_update: 1, - }, - initialization: DriveAbciInitializationMethodVersions { - initial_core_height_and_time: 0, - create_genesis_state: 1, - }, - core_based_updates: DriveAbciCoreBasedUpdatesMethodVersions { - update_core_info: 0, - update_masternode_list: 0, - update_quorum_info: 0, - masternode_updates: DriveAbciMasternodeIdentitiesUpdatesMethodVersions { - get_voter_identity_key: 0, - get_operator_identity_keys: 0, - get_owner_identity_withdrawal_key: 0, - get_owner_identity_owner_key: 0, - get_voter_identifier_from_masternode_list_item: 0, - get_operator_identifier_from_masternode_list_item: 0, - create_operator_identity: 0, - create_owner_identity: 1, - create_voter_identity: 0, - disable_identity_keys: 0, - update_masternode_identities: 0, - update_operator_identity: 0, - update_owner_withdrawal_address: 1, - update_voter_identity: 0, - }, - }, - protocol_upgrade: DriveAbciProtocolUpgradeMethodVersions { - check_for_desired_protocol_upgrade: 1, - upgrade_protocol_version_on_epoch_change: 0, - perform_events_on_first_block_of_protocol_change: Some(1), - protocol_version_upgrade_percentage_needed: 67, - }, - block_fee_processing: DriveAbciBlockFeeProcessingMethodVersions { - add_process_epoch_change_operations: 0, - process_block_fees_and_validate_sum_trees: 1, - }, - tokens_processing: DriveAbciTokensProcessingMethodVersions { - validate_token_aggregated_balance: 0, - }, - core_chain_lock: DriveAbciCoreChainLockMethodVersionsAndConstants { - choose_quorum: 0, - verify_chain_lock: 0, - verify_chain_lock_locally: 0, - verify_chain_lock_through_core: 0, - make_sure_core_is_synced_to_chain_lock: 0, - recent_block_count_amount: 2, - }, - core_instant_send_lock: DriveAbciCoreInstantSendLockMethodVersions { - verify_recent_signature_locally: 0, - }, - fee_pool_inwards_distribution: DriveAbciFeePoolInwardsDistributionMethodVersions { - add_distribute_block_fees_into_pools_operations: 0, - add_distribute_storage_fee_to_epochs_operations: 0, - }, - fee_pool_outwards_distribution: DriveAbciFeePoolOutwardsDistributionMethodVersions { - add_distribute_fees_from_oldest_unpaid_epoch_pool_to_proposers_operations: 1, - add_epoch_pool_to_proposers_payout_operations: 0, - find_oldest_epoch_needing_payment: 0, - fetch_reward_shares_list_for_masternode: 0, - }, - withdrawals: DriveAbciIdentityCreditWithdrawalMethodVersions { - build_untied_withdrawal_transactions_from_documents: 0, - dequeue_and_build_unsigned_withdrawal_transactions: 0, - fetch_transactions_block_inclusion_status: 0, - pool_withdrawals_into_transactions_queue: 1, - update_broadcasted_withdrawal_statuses: 0, - rebroadcast_expired_withdrawal_documents: 2, // changed in v14: an expired withdrawal whose payout is Core dust is marked FAILED instead of re-signed - append_signatures_and_broadcast_withdrawal_transactions: 0, - cleanup_expired_locks_of_withdrawal_amounts: 1, // changed in v14: also prunes expired entries of the credit inflows sum tree - record_credit_inflows_for_withdrawals: Some(0), // new in v14: the block's credit mints recorded as an inflow for the net daily withdrawal limit - record_total_credits_history_for_withdrawals: Some(0), // changed in v14: per-block total credits history for the day-lagged daily withdrawal limit - }, - voting: DriveAbciVotingMethodVersions { - keep_record_of_finished_contested_resource_vote_poll: 0, - clean_up_after_vote_poll_end: 0, - clean_up_after_contested_resources_vote_poll_end: 1, - check_for_ended_vote_polls: 0, - tally_votes_for_contested_document_resource_vote_poll: 0, - award_document_to_winner: 0, - delay_vote_poll: 0, - run_dao_platform_events: 0, - remove_votes_for_removed_masternodes: 0, - }, - state_transition_processing: DriveAbciStateTransitionProcessingMethodVersions { - execute_event: 0, - process_raw_state_transitions: 0, - // unchanged from V9: v1 since v13 (records the balance effects of paid-INVALID / - // unsuccessful-paid transitions) - process_validation_result: 1, - decode_raw_state_transitions: 0, - validate_fees_of_event: 0, - store_address_balances_to_recent_block_storage: Some(0), - cleanup_recent_block_storage_address_balances: Some(0), - // unchanged from V9: v1 since v13 (records shielded-spend transparent credits) - record_added_balance_outputs: 1, - }, - epoch: DriveAbciEpochMethodVersions { - gather_epoch_info: 0, - get_genesis_time: 0, - }, - block_start: DriveAbciBlockStartMethodVersions { - clear_drive_block_cache: 0, - }, block_end: DriveAbciBlockEndMethodVersions { - update_state_cache: 0, - update_drive_cache: 0, - validator_set_update: 2, - should_checkpoint: Some(0), - update_checkpoints: Some(0), - record_shielded_pool_anchor: Some(0), - prune_shielded_pool_anchors: Some(0), - record_token_shielded_pool_anchors: Some(0), // changed: per-token shielded pool anchors - }, - platform_state_storage: DriveAbciPlatformStateStorageMethodVersions { - fetch_platform_state: 0, - store_platform_state: 0, + record_token_shielded_pool_anchors: Some(0), + ..DRIVE_ABCI_METHOD_VERSIONS_V10.block_end }, + ..DRIVE_ABCI_METHOD_VERSIONS_V10 }; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs index 1d033a58fdb..1679726214e 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs @@ -1,432 +1,42 @@ -use crate::version::drive_abci_versions::drive_abci_validation_versions::{ - DriveAbciAssetLockValidationVersions, DriveAbciDocumentsStateTransitionValidationVersions, - DriveAbciStateTransitionCommonValidationVersions, DriveAbciStateTransitionValidationVersion, - DriveAbciStateTransitionValidationVersions, DriveAbciValidationConstants, - DriveAbciValidationDataTriggerAndBindingVersions, DriveAbciValidationDataTriggerVersions, - DriveAbciValidationVersions, PenaltyAmounts, +use super::v10::DRIVE_ABCI_VALIDATION_VERSIONS_V10; +use super::{ + DriveAbciDocumentsStateTransitionValidationVersions, DriveAbciStateTransitionValidationVersion, + DriveAbciStateTransitionValidationVersions, DriveAbciValidationVersions, }; -// PROTOCOL_VERSION_15: token shielded pools. The batch validators of the token pool -// transitions (`token_shield`, `token_unshield`, `token_shielded_transfer`, -// `token_mint_to_pool`, `token_burn_from_pool`, `token_claim_to_pool` and -// `token_direct_purchase_to_pool`) are reachable from this version on; the transitions -// themselves are gated by `TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION` in -// `is_allowed`. v10 remains unchanged for PROTOCOL_VERSION_14 chain replay. +/// Protocol 15 enables token pool proofs and payments, retaining protocol 14 validation. pub const DRIVE_ABCI_VALIDATION_VERSIONS_V11: DriveAbciValidationVersions = DriveAbciValidationVersions { state_transitions: DriveAbciStateTransitionValidationVersions { - common_validation_methods: DriveAbciStateTransitionCommonValidationVersions { - asset_locks: DriveAbciAssetLockValidationVersions { - fetch_asset_lock_transaction_output_sync: 0, - verify_asset_lock_is_not_spent_and_has_enough_balance: 0, - }, - validate_identity_public_key_contract_bounds: 1, - validate_identity_public_key_ids_dont_exist_in_state: 0, - validate_identity_public_key_ids_exist_in_state: 0, - validate_state_transition_identity_signed: 0, - validate_unique_identity_public_key_hashes_in_state: 1, - validate_master_key_uniqueness: 0, - validate_non_masternode_identity_exists: 0, - validate_identity_exists: 0, - }, - max_asset_lock_usage_attempts: 16, - identity_create_state_transition: DriveAbciStateTransitionValidationVersion { - basic_structure: Some(0), - advanced_structure: Some(0), - identity_signatures: Some(0), - nonce: None, - state: 0, - transform_into_action: 0, - }, - identity_update_state_transition: DriveAbciStateTransitionValidationVersion { - basic_structure: Some(0), - advanced_structure: Some(0), - identity_signatures: Some(0), - nonce: Some(0), - state: 0, - transform_into_action: 0, - }, - identity_top_up_state_transition: DriveAbciStateTransitionValidationVersion { - basic_structure: Some(0), - advanced_structure: None, - identity_signatures: None, - nonce: None, - state: 0, - transform_into_action: 0, - }, - identity_credit_withdrawal_state_transition: - DriveAbciStateTransitionValidationVersion { - // v1 adds config min_version enforcement: since protocol version 12, V0 config is no longer - // accepted because it lacks sized_integer_types support. - basic_structure: Some(1), - advanced_structure: None, - identity_signatures: None, - nonce: Some(0), - state: 0, - transform_into_action: 0, - }, - identity_credit_withdrawal_state_transition_purpose_matches_requirements: 0, - identity_credit_transfer_state_transition: DriveAbciStateTransitionValidationVersion { - basic_structure: Some(0), - advanced_structure: None, - identity_signatures: None, - nonce: Some(0), - state: 0, - transform_into_action: 0, - }, - identity_credit_transfer_to_addresses_state_transition: - DriveAbciStateTransitionValidationVersion { - basic_structure: Some(0), - advanced_structure: None, - identity_signatures: None, - nonce: Some(0), - state: 0, - transform_into_action: 0, - }, - masternode_vote_state_transition: DriveAbciStateTransitionValidationVersion { - basic_structure: None, - advanced_structure: Some(0), - identity_signatures: None, - nonce: Some(1), - state: 0, - transform_into_action: 0, - }, - masternode_vote_state_transition_balance_pre_check: 0, - contract_create_state_transition: DriveAbciStateTransitionValidationVersion { - basic_structure: Some(2), // changed: rejects `requiredSince` other than 1 on a newly created contract — the annotation must name the version the change arrives with, and a fresh contract is version 1 - advanced_structure: Some(1), - identity_signatures: None, - nonce: Some(0), - state: 1, // changed: runs data_contract_reference_validation on the created contract's refersTo declarations - transform_into_action: 0, - }, - contract_update_state_transition: DriveAbciStateTransitionValidationVersion { - basic_structure: Some(1), - advanced_structure: None, - identity_signatures: None, - nonce: Some(0), - state: 1, // changed: runs data_contract_reference_validation on the updated contract's refersTo declarations - transform_into_action: 0, - }, - data_contract_reference_validation: 0, batch_state_transition: DriveAbciDocumentsStateTransitionValidationVersions { - basic_structure: 0, - advanced_structure: 0, - state: 0, - revision: 0, - // PROTOCOL_VERSION_12 (v3.1 hard fork): batch state transition - // fee accounting fixes. This single field gates multiple - // related billing changes so they all activate together at - // the same hard fork. On v0 every behavior below is the - // legacy under-billing, preserved verbatim for - // PROTOCOL_VERSION_11 chain replay. - // - // Gated by `transform_into_action: 1`: - // * B7 — outer `execution_context` is threaded through the - // batch transformer (was a dropped local) so per- - // transition fee_results in `try_into_action_v0` are - // billed. - // * B4 — `query_documents` cost in - // `fetch_documents_for_transitions_knowing_contract_and_document_type` - // is added to `execution_context`. - // * B5 — `query_documents` cost in `fetch_document_with_id` - // is added to `execution_context`. - // * T1 — DPNS data trigger parent-domain - // `query_documents` cost. - // * T2 — DPNS data trigger preorder `query_documents` cost. - // * T3 — DashPay data trigger recipient identity-balance - // fetch cost (switched to `fetch_identity_balance_with_costs`). - // * T4 — withdrawals data trigger `query_documents` cost. - transform_into_action: 1, - // PROTOCOL_VERSION_12 (v3.1 hard fork): per-transition - // failure paths in `transform_document_transition` now emit - // a `BumpIdentityDataContractNonce` action so the user pays - // for the validation work that already ran (fetch + - // ownership/revision check). v0 stays for chain - // reproducibility on PROTOCOL_VERSION_11 and below. - failed_per_transition_action: 1, - // PROTOCOL_VERSION_12 (v3.1 hard fork): fetch_documents - // helpers bumped to v1 which bill the grovedb cost of - // their query_documents calls. v0 stays for PV11 chain - // replay (the v0 helpers pass epoch=None and never call - // add_operation — byte-identical to pre-PR behavior). - fetch_documents_for_transitions_knowing_contract_and_document_type: 1, - fetch_document_with_id: 1, - data_triggers: DriveAbciValidationDataTriggerAndBindingVersions { - // PROTOCOL_VERSION_14: v2 adds DashPay `profile` - // Create/Replace triggers enforcing the DIP-33 - // payment-address type byte (0x00 P2PKH / 0x01 P2SH). - // It keeps v1's PROTOCOL_VERSION_13 change: no reject - // bindings for Transfer, Purchase and UpdatePrice on - // DPNS `domain` documents (username transfers/sales). - bindings: 2, - triggers: DriveAbciValidationDataTriggerVersions { - // PROTOCOL_VERSION_12 (v3.1 hard fork): triggers - // that perform drive reads now have `_v1` versions - // that bill the cost via add_operation on the - // outer execution_context. v0 versions remain - // byte-identical to PV11 (don't bill). - create_contact_request_data_trigger: 1, - validate_profile_payment_addresses_data_trigger: 0, - create_domain_data_trigger: 1, - create_identity_data_trigger: 0, - create_feature_flag_data_trigger: 0, - create_masternode_reward_shares_data_trigger: 0, - delete_withdrawal_data_trigger: 2, // changed in v14: FAILED withdrawals may be deleted like COMPLETE ones - // Reject does no drive reads — stays at v0. - reject_data_trigger: 0, - }, - }, - is_allowed: 0, - document_create_transition_structure_validation: 1, - // Reject deletes on legacy keep-history types as paid consensus errors. - // Protocols through 13 retain the original internal-error outcome. - document_delete_transition_structure_validation: 1, - document_index_only_delete_transition_structure_validation: 0, - document_replace_transition_structure_validation: 0, - document_transfer_transition_structure_validation: 0, - document_purchase_transition_structure_validation: 0, - document_update_price_transition_structure_validation: 0, - document_base_transition_state_validation: 1, // changed: a token cost paid from the token's shielded pool skips the owner's balance checks - document_create_transition_state_validation: 2, - document_delete_transition_state_validation: 0, - document_index_only_delete_transition_state_validation: 0, - document_replace_transition_state_validation: 1, - document_transfer_transition_state_validation: 0, - document_purchase_transition_state_validation: 0, - document_update_price_transition_state_validation: 0, - document_reference_validation: 0, - token_mint_transition_structure_validation: 0, - token_burn_transition_structure_validation: 0, - token_transfer_transition_structure_validation: 0, - token_mint_transition_state_validation: 0, - token_burn_transition_state_validation: 0, - token_transfer_transition_state_validation: 0, - token_base_transition_structure_validation: 0, - token_base_transition_state_validation: 1, - token_freeze_transition_structure_validation: 0, - token_unfreeze_transition_structure_validation: 0, - token_freeze_transition_state_validation: 0, - token_unfreeze_transition_state_validation: 0, - token_destroy_frozen_funds_transition_structure_validation: 0, - token_destroy_frozen_funds_transition_state_validation: 0, - token_emergency_action_transition_structure_validation: 0, - token_emergency_action_transition_state_validation: 0, - token_config_update_transition_structure_validation: 0, - token_config_update_transition_state_validation: 1, - token_base_transition_group_action_validation: 1, - token_claim_transition_structure_validation: 0, - token_claim_transition_state_validation: 0, - token_direct_purchase_transition_structure_validation: 0, - token_direct_purchase_transition_state_validation: 0, - token_set_price_for_direct_purchase_transition_structure_validation: 0, - token_set_price_for_direct_purchase_transition_state_validation: 0, - token_shield_transition_structure_validation: 0, - token_shield_transition_state_validation: 0, - token_mint_to_pool_transition_structure_validation: 0, - token_mint_to_pool_transition_state_validation: 0, - token_burn_from_pool_transition_structure_validation: 0, - token_burn_from_pool_transition_state_validation: 0, - token_claim_to_pool_transition_structure_validation: 0, - token_claim_to_pool_transition_state_validation: 0, - token_direct_purchase_to_pool_transition_structure_validation: 0, - token_direct_purchase_to_pool_transition_state_validation: 0, - token_unshield_transition_structure_validation: 0, - token_unshield_transition_state_validation: 0, - token_shielded_transfer_transition_structure_validation: 0, - token_shielded_transfer_transition_state_validation: 0, - }, - identity_create_from_addresses_state_transition: - DriveAbciStateTransitionValidationVersion { - basic_structure: Some(0), - advanced_structure: Some(0), - identity_signatures: Some(0), - nonce: Some(0), - state: 0, - transform_into_action: 0, - }, - identity_top_up_from_addresses_state_transition: - DriveAbciStateTransitionValidationVersion { - basic_structure: Some(0), - advanced_structure: None, - identity_signatures: None, - nonce: Some(0), - state: 0, - transform_into_action: 0, - }, - address_credit_withdrawal: DriveAbciStateTransitionValidationVersion { - basic_structure: Some(0), - advanced_structure: None, - identity_signatures: None, - nonce: Some(0), - state: 0, - transform_into_action: 0, - }, - address_funds_from_asset_lock: DriveAbciStateTransitionValidationVersion { - basic_structure: Some(0), - advanced_structure: Some(0), - identity_signatures: None, - nonce: Some(0), - state: 0, - transform_into_action: 0, - }, - address_funds_transfer: DriveAbciStateTransitionValidationVersion { - basic_structure: Some(0), - advanced_structure: None, - identity_signatures: None, - nonce: Some(0), - state: 0, - transform_into_action: 0, - }, - shield_state_transition: DriveAbciStateTransitionValidationVersion { - basic_structure: Some(0), - advanced_structure: None, - identity_signatures: None, - nonce: None, - state: 0, - transform_into_action: 0, - }, - shielded_transfer_state_transition: DriveAbciStateTransitionValidationVersion { - basic_structure: Some(0), - advanced_structure: None, - identity_signatures: None, - nonce: None, - state: 0, - transform_into_action: 0, - }, - unshield_state_transition: DriveAbciStateTransitionValidationVersion { - basic_structure: Some(0), - advanced_structure: None, - identity_signatures: None, - nonce: None, - state: 0, - transform_into_action: 0, - }, - shield_from_asset_lock_state_transition: DriveAbciStateTransitionValidationVersion { - basic_structure: Some(0), - advanced_structure: None, - identity_signatures: None, - nonce: None, - state: 0, - transform_into_action: 0, - }, - shielded_withdrawal_state_transition: DriveAbciStateTransitionValidationVersion { - basic_structure: Some(0), - advanced_structure: None, - identity_signatures: None, - nonce: None, - state: 0, - transform_into_action: 0, + document_base_transition_state_validation: 1, + ..DRIVE_ABCI_VALIDATION_VERSIONS_V10 + .state_transitions + .batch_state_transition }, - identity_create_from_shielded_pool_state_transition: - DriveAbciStateTransitionValidationVersion { - basic_structure: Some(0), - advanced_structure: None, - identity_signatures: None, - nonce: None, - state: 0, - transform_into_action: 0, - }, - shield_from_identity_state_transition: DriveAbciStateTransitionValidationVersion { - basic_structure: Some(0), - advanced_structure: None, - identity_signatures: Some(0), - nonce: Some(0), - state: 0, - transform_into_action: 0, - }, - identity_top_up_from_shielded_pool_state_transition: - DriveAbciStateTransitionValidationVersion { - basic_structure: Some(0), - advanced_structure: None, - identity_signatures: None, - nonce: None, - state: 0, - transform_into_action: 0, - }, token_shielded_transfer_with_shielded_fee_state_transition: DriveAbciStateTransitionValidationVersion { basic_structure: Some(0), - advanced_structure: None, - identity_signatures: None, - nonce: None, - state: 0, - transform_into_action: 0, + ..DRIVE_ABCI_VALIDATION_VERSIONS_V10 + .state_transitions + .token_shielded_transfer_with_shielded_fee_state_transition }, token_unshield_with_shielded_fee_state_transition: DriveAbciStateTransitionValidationVersion { basic_structure: Some(0), - advanced_structure: None, - identity_signatures: None, - nonce: None, - state: 0, - transform_into_action: 0, + ..DRIVE_ABCI_VALIDATION_VERSIONS_V10 + .state_transitions + .token_unshield_with_shielded_fee_state_transition }, token_purchase_from_shielded_pool_state_transition: DriveAbciStateTransitionValidationVersion { basic_structure: Some(0), - advanced_structure: None, - identity_signatures: None, - nonce: None, - state: 0, - transform_into_action: 0, + ..DRIVE_ABCI_VALIDATION_VERSIONS_V10 + .state_transitions + .token_purchase_from_shielded_pool_state_transition }, + ..DRIVE_ABCI_VALIDATION_VERSIONS_V10.state_transitions }, - has_nonce_validation: 1, - has_address_witness_validation: 0, - validate_address_witnesses: 0, - validate_shielded_proof: 0, - validate_minimum_shielded_fee: 0, - process_state_transition: 0, - state_transition_to_execution_event_for_check_tx: 0, - penalties: PenaltyAmounts { - identity_id_not_correct: 50000000, - unique_key_already_present: 10000000, - validation_of_added_keys_structure_failure: 10000000, - validation_of_added_keys_proof_of_possession_failure: 50000000, - address_funds_insufficient_balance: 10000000, - shielded_proof_verification_failure: 50000000, - }, - event_constants: DriveAbciValidationConstants { - maximum_vote_polls_to_process: 2, - maximum_contenders_to_consider: 100, - minimum_pool_notes_for_outgoing: 250, - minimum_token_pool_notes_for_outgoing: 0, - shielded_anchor_retention_blocks: 1000, - shielded_anchor_pruning_interval: 100, - // Rebalanced for protocol 14: one Halo 2 bundle verification is - // ~5 ms; at the fee model's ~8M credits/ms of CPU this prices it - // at ~27x a BLS signature verification — reserved for compute - // alone, never for database work (the storage allowance below - // covers that independently). - shielded_proof_verification_fee: 40_000_000, - // Retained from protocol 13, versioned independently of the - // rebalanced bundle proof fee above: it prices the per-action - // work — the marginal Halo 2 verification CPU (~1.1 ms/action), - // the RedPallas spend-auth check, the nullifier duplicate check - // and the tree-insertion processing — and its ~18M headroom over - // the ~3.5M credits of metered per-append GroveDB processing is - // deliberate, not a shared calibration rate with the 40M bundle - // fee. - shielded_per_action_processing_fee: 22_000_000, - // Rebalanced for protocol 14 alongside the proof fee: the - // GROVE_V4 fixed per-append model meters a 1-action transfer at - // 17,882,707 credits total, 14,337,000 of it storage — 523 - // bytes at the full 27,400 credits/byte rate, the declared - // 344-byte payload plus Merk framing, dense path records and - // the amortized chunk framing. 550 covers that with headroom, - // so the storage component alone pays for the database work and - // the compute fees above stay reserved for compute. - shielded_storage_bytes_per_action: 550, - shielded_implicit_fee_cap: 20_000_000_000, - // 0.1, 0.3, 0.5, 1.0 DASH in credits (1 DASH = 10^8 duffs, CREDITS_PER_DUFF = 1000). - // v13 revises the v8 set: adds 0.03 and 0.25 DASH, retires 0.3 DASH. - shielded_identity_create_denominations: &[ - 3_000_000_000, // 0.03 DASH - 10_000_000_000, // 0.1 DASH - 25_000_000_000, // 0.25 DASH - 50_000_000_000, // 0.5 DASH - 100_000_000_000, // 1 DASH - ], - }, + validate_shielded_proof: 2, + ..DRIVE_ABCI_VALIDATION_VERSIONS_V10 }; diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/mod.rs index a5c0e65d8c6..901166381de 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/mod.rs @@ -2,6 +2,7 @@ use versioned_feature_core::FeatureVersion; pub mod v1; pub mod v2; +pub mod v3; #[derive(Clone, Debug, Default)] pub struct DriveTokenMethodVersions { diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v2.rs index 22156b66455..89a564d343d 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v2.rs @@ -19,7 +19,6 @@ use crate::version::drive_versions::drive_token_method_versions::{ /// default node; the processing fee drops by the existence read the later /// tokens no longer make. pub const DRIVE_TOKEN_METHOD_VERSIONS_V2: DriveTokenMethodVersions = DriveTokenMethodVersions { - calculate_total_tokens_balance: 1, distribution: DriveTokenDistributionMethodVersions { add_pre_programmed_distributions: 1, ..DRIVE_TOKEN_METHOD_VERSIONS_V1.distribution diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v3.rs new file mode 100644 index 00000000000..a1d40088d02 --- /dev/null +++ b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v3.rs @@ -0,0 +1,8 @@ +use super::v2::DRIVE_TOKEN_METHOD_VERSIONS_V2; +use super::DriveTokenMethodVersions; + +/// Protocol 15 includes shielded pool balances in token conservation. +pub const DRIVE_TOKEN_METHOD_VERSIONS_V3: DriveTokenMethodVersions = DriveTokenMethodVersions { + calculate_total_tokens_balance: 1, + ..DRIVE_TOKEN_METHOD_VERSIONS_V2 +}; diff --git a/packages/rs-platform-version/src/version/drive_versions/v10.rs b/packages/rs-platform-version/src/version/drive_versions/v10.rs index 6f6bfbce3e5..47ee3ba61c3 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v10.rs @@ -1,144 +1,15 @@ -use crate::version::drive_versions::drive_address_funds_method_versions::v2::DRIVE_ADDRESS_FUNDS_METHOD_VERSIONS_V2; -use crate::version::drive_versions::drive_contract_method_versions::v4::DRIVE_CONTRACT_METHOD_VERSIONS_V4; -use crate::version::drive_versions::drive_credit_pool_method_versions::v1::CREDIT_POOL_METHOD_VERSIONS_V1; -use crate::version::drive_versions::drive_document_method_versions::v4::DRIVE_DOCUMENT_METHOD_VERSIONS_V4; -use crate::version::drive_versions::drive_group_method_versions::v1::DRIVE_GROUP_METHOD_VERSIONS_V1; -use crate::version::drive_versions::drive_group_method_versions::DriveShieldedMethodVersions; -use crate::version::drive_versions::drive_grove_method_versions::v1::DRIVE_GROVE_METHOD_VERSIONS_V1; -use crate::version::drive_versions::drive_identity_method_versions::v2::DRIVE_IDENTITY_METHOD_VERSIONS_V2; -use crate::version::drive_versions::drive_state_transition_method_versions::v4::DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4; -use crate::version::drive_versions::drive_structure_version::v1::DRIVE_STRUCTURE_V1; -use crate::version::drive_versions::drive_token_method_versions::v2::DRIVE_TOKEN_METHOD_VERSIONS_V2; -use crate::version::drive_versions::drive_verify_method_versions::v2::DRIVE_VERIFY_METHOD_VERSIONS_V2; -use crate::version::drive_versions::drive_vote_method_versions::v2::DRIVE_VOTE_METHOD_VERSIONS_V2; -use crate::version::drive_versions::{ - DriveAssetLockMethodVersions, DriveBalancesMethodVersions, DriveBatchOperationsMethodVersion, - DriveEstimatedCostsMethodVersions, DriveFeesMethodVersions, DriveFetchMethodVersions, - DriveInitializationMethodVersions, DriveMethodVersions, DriveOperationsMethodVersion, - DrivePlatformStateMethodVersions, DrivePlatformSystemMethodVersions, - DrivePrefundedSpecializedMethodVersions, DriveProtocolUpgradeVersions, - DriveProveMethodVersions, DriveSavedBlockTransactionsMethodVersions, - DriveSystemEstimationCostsMethodVersions, DriveVersion, -}; -use grovedb_version::version::v4::GROVE_V4; +use super::drive_token_method_versions::v3::DRIVE_TOKEN_METHOD_VERSIONS_V3; +use super::v9::DRIVE_VERSION_V9; +use super::{DriveInitializationMethodVersions, DriveMethodVersions, DriveVersion}; -/// Drive version 10. Introduced in protocol version 15 with token shielded pools: -/// `create_initial_state_structure` 4 adds the token shielded pools BigSumTree under the -/// Tokens root at genesis, and `DRIVE_TOKEN_METHOD_VERSIONS_V2` carries the pool operations -/// (pool creation, shield, unshield, shielded transfer, mint to and burn from the pool) plus -/// `calculate_total_tokens_balance` v1, which adds the pool balances to the conservation -/// check. -/// -/// Everything else matches `DRIVE_VERSION_V9`. +/// Protocol 15 extends protocol 14 storage with token shielded pools and their conservation check. pub const DRIVE_VERSION_V10: DriveVersion = DriveVersion { - structure: DRIVE_STRUCTURE_V1, methods: DriveMethodVersions { initialization: DriveInitializationMethodVersions { - create_initial_state_structure: 4, // changed in v10: adds the token shielded pools BigSumTree under the Tokens root (v8 added the credit shielded pool trees) - }, - credit_pools: CREDIT_POOL_METHOD_VERSIONS_V1, - protocol_upgrade: DriveProtocolUpgradeVersions { - clear_version_information: 0, - fetch_versions_with_counter: 0, - fetch_proved_versions_with_counter: 0, - fetch_validator_version_votes: 0, - fetch_proved_validator_version_votes: 0, - remove_validators_proposed_app_versions: 0, - update_validator_proposed_app_version: 0, - }, - prove: DriveProveMethodVersions { - prove_elements: 0, - prove_multiple_state_transition_results: 0, - prove_state_transition: 0, - }, - balances: DriveBalancesMethodVersions { - add_to_system_credits: 0, - add_to_system_credits_operations: 0, - remove_from_system_credits: 0, - remove_from_system_credits_operations: 0, - calculate_total_credits_balance: 2, // ShieldedBalances root tree adds a fifth term to the equation - }, - document: DRIVE_DOCUMENT_METHOD_VERSIONS_V4, // unchanged since v9: v2 index walkers + v1 update walker (shared-prefix aggregate indexes become insertable) and the detect_ranked_mode slot - vote: DRIVE_VOTE_METHOD_VERSIONS_V2, - contract: DRIVE_CONTRACT_METHOD_VERSIONS_V4, // unchanged since v9: add_contract_to_storage v1 writes the contract version item beside the contract - fees: DriveFeesMethodVersions { calculate_fee: 0 }, - estimated_costs: DriveEstimatedCostsMethodVersions { - add_estimation_costs_for_levels_up_to_contract: 0, - add_estimation_costs_for_levels_up_to_contract_document_type_excluded: 0, - add_estimation_costs_for_contested_document_tree_levels_up_to_contract: 0, - add_estimation_costs_for_contested_document_tree_levels_up_to_contract_document_type_excluded: 0, - }, - asset_lock: DriveAssetLockMethodVersions { - add_asset_lock_outpoint: 0, - add_estimation_costs_for_adding_asset_lock: 0, - fetch_asset_lock_outpoint_info: 0, - }, - verify: DRIVE_VERIFY_METHOD_VERSIONS_V2, // changed in v8: compacted address-balance proof envelope (verify v1) - identity: DRIVE_IDENTITY_METHOD_VERSIONS_V2, // unchanged since v9: v1 withdrawal-by-transaction-index query builder (structural, identical lowering) - token: DRIVE_TOKEN_METHOD_VERSIONS_V2, // changed in v10: token shielded pools (pool creation + shield / unshield / shielded transfer / mint to / burn from ops, conservation v1) - platform_system: DrivePlatformSystemMethodVersions { - estimation_costs: DriveSystemEstimationCostsMethodVersions { - for_total_system_credits_update: 0, - }, - }, - operations: DriveOperationsMethodVersion { - rollback_transaction: 0, - drop_cache: 0, - commit_transaction: 0, - apply_partial_batch_low_level_drive_operations: 0, - apply_partial_batch_grovedb_operations: 0, - apply_batch_low_level_drive_operations: 0, - apply_batch_grovedb_operations: 0, - }, - state_transitions: DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4, // changed: document_from_action generation 1 stamps built documents with the contract version (create assigns, replace re-assigns; paired with document serialization format 3) - batch_operations: DriveBatchOperationsMethodVersion { - convert_drive_operations_to_grove_operations: 0, - apply_drive_operations: 0, - }, - platform_state: DrivePlatformStateMethodVersions { - fetch_platform_state_bytes: 0, - store_platform_state_bytes: 0, - }, - fetch: DriveFetchMethodVersions { fetch_elements: 0 }, - prefunded_specialized_balances: DrivePrefundedSpecializedMethodVersions { - fetch_single: 0, - prove_single: 0, - add_prefunded_specialized_balance: 0, - add_prefunded_specialized_balance_operations: 1, - deduct_from_prefunded_specialized_balance: 1, - deduct_from_prefunded_specialized_balance_operations: 0, - estimated_cost_for_prefunded_specialized_balance_update: 0, - empty_prefunded_specialized_balance: 0, - }, - group: DRIVE_GROUP_METHOD_VERSIONS_V1, - address_funds: DRIVE_ADDRESS_FUNDS_METHOD_VERSIONS_V2, - shielded: DriveShieldedMethodVersions { - insert_note: 0, - insert_nullifiers: 0, - update_total_balance: 0, - record_anchor_if_changed: 0, - prune_anchors: 0, - has_anchor: 0, - has_nullifier: 0, - read_total_balance: 0, - notes_count: 0, - }, - saved_block_transactions: DriveSavedBlockTransactionsMethodVersions { - store_address_balances: 0, - fetch_address_balances: 0, - prove_compacted_address_balance_changes: 1, - compact_address_balances: 0, - cleanup_expired_address_balances: 0, - max_blocks_before_compaction: 64, - max_addresses_before_compaction: 2048, + create_initial_state_structure: 5, }, + token: DRIVE_TOKEN_METHOD_VERSIONS_V3, + ..DRIVE_VERSION_V9.methods }, - grove_methods: DRIVE_GROVE_METHOD_VERSIONS_V1, - // unchanged since v9: GROVE_V4 activates the indexed-tree batch cleanup - // gates (overwrite inspection + delete-tree actual-type cleanup). - // Indexed trees only exist from protocol v14, so activating the - // stricter cleanup with them costs older versions nothing; staying - // on V3 would let a batch overwrite of a ranked index orphan its - // per-axis secondary storage. - grove_version: GROVE_V4, + ..DRIVE_VERSION_V9 }; diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs index 6c0c7bfbabe..503558e78ef 100644 --- a/packages/rs-platform-version/src/version/v15.rs +++ b/packages/rs-platform-version/src/version/v15.rs @@ -1,32 +1,11 @@ -use crate::version::consensus_versions::ConsensusVersions; -use crate::version::dpp_versions::dpp_asset_lock_versions::v1::DPP_ASSET_LOCK_VERSIONS_V1; -use crate::version::dpp_versions::dpp_contract_versions::v7::CONTRACT_VERSIONS_V7; -use crate::version::dpp_versions::dpp_costs_versions::v1::DPP_COSTS_VERSIONS_V1; -use crate::version::dpp_versions::dpp_document_versions::v4::DOCUMENT_VERSIONS_V4; -use crate::version::dpp_versions::dpp_factory_versions::v1::DPP_FACTORY_VERSIONS_V1; -use crate::version::dpp_versions::dpp_identity_versions::v1::IDENTITY_VERSIONS_V1; -use crate::version::dpp_versions::dpp_method_versions::v3::DPP_METHOD_VERSIONS_V3; -use crate::version::dpp_versions::dpp_state_transition_conversion_versions::v2::STATE_TRANSITION_CONVERSION_VERSIONS_V2; -use crate::version::dpp_versions::dpp_state_transition_method_versions::v1::STATE_TRANSITION_METHOD_VERSIONS_V1; -use crate::version::dpp_versions::dpp_state_transition_serialization_versions::v3::STATE_TRANSITION_SERIALIZATION_VERSIONS_V3; -use crate::version::dpp_versions::dpp_state_transition_versions::v3::STATE_TRANSITION_VERSIONS_V3; -use crate::version::dpp_versions::dpp_token_versions::v3::TOKEN_VERSIONS_V3; -use crate::version::dpp_versions::dpp_validation_versions::v5::DPP_VALIDATION_VERSIONS_V5; -use crate::version::dpp_versions::dpp_voting_versions::v2::VOTING_VERSION_V2; -use crate::version::dpp_versions::DPPVersion; -use crate::version::drive_abci_versions::drive_abci_checkpoint_parameters::v1::DRIVE_ABCI_CHECKPOINT_PARAMETERS_V1; -use crate::version::drive_abci_versions::drive_abci_method_versions::v11::DRIVE_ABCI_METHOD_VERSIONS_V11; -use crate::version::drive_abci_versions::drive_abci_query_versions::v3::DRIVE_ABCI_QUERY_VERSIONS_V3; -use crate::version::drive_abci_versions::drive_abci_structure_versions::v1::DRIVE_ABCI_STRUCTURE_VERSIONS_V1; -use crate::version::drive_abci_versions::drive_abci_validation_versions::v11::DRIVE_ABCI_VALIDATION_VERSIONS_V11; -use crate::version::drive_abci_versions::drive_abci_withdrawal_constants::v3::DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V3; -use crate::version::drive_abci_versions::DriveAbciVersion; -use crate::version::drive_versions::v10::DRIVE_VERSION_V10; -use crate::version::fee::v3::FEE_VERSION3; -use crate::version::protocol_version::PlatformVersion; -use crate::version::system_data_contract_versions::v3::SYSTEM_DATA_CONTRACT_VERSIONS_V3; -use crate::version::system_limits::v4::SYSTEM_LIMITS_V4; -use crate::version::ProtocolVersion; +use super::dpp_versions::dpp_contract_versions::v7::CONTRACT_VERSIONS_V7; +use super::dpp_versions::DPPVersion; +use super::drive_abci_versions::drive_abci_method_versions::v11::DRIVE_ABCI_METHOD_VERSIONS_V11; +use super::drive_abci_versions::drive_abci_validation_versions::v11::DRIVE_ABCI_VALIDATION_VERSIONS_V11; +use super::drive_abci_versions::DriveAbciVersion; +use super::drive_versions::v10::DRIVE_VERSION_V10; +use super::v14::PLATFORM_V14; +use super::{PlatformVersion, ProtocolVersion}; pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; @@ -49,45 +28,25 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// token id and owner id (plus the recipient and amount where tokens leave /// the pool) into the Orchard sighash. The pool balances are a term of the /// token conservation check (`calculate_total_tokens_balance` v1 in -/// `DRIVE_TOKEN_METHOD_VERSIONS_V2`, reached through `DRIVE_VERSION_V10`). +/// `DRIVE_TOKEN_METHOD_VERSIONS_V3`, reached through `DRIVE_VERSION_V10`). /// `record_token_shielded_pool_anchors` (`DRIVE_ABCI_METHOD_VERSIONS_V11`) /// records and prunes the anchors of the pools a block touched. The pools /// root tree is inserted by the version 15 upgrade transition and by -/// `create_initial_state_structure` v4; the six shielded queries accept an +/// `create_initial_state_structure` v5; the six shielded queries accept an /// optional `token_id` to target a token pool. /// /// Everything else matches v14. pub const PLATFORM_V15: PlatformVersion = PlatformVersion { protocol_version: PROTOCOL_VERSION_15, - drive: DRIVE_VERSION_V10, // changed: token shielded pools (genesis structure v4 + token method versions v2) + drive: DRIVE_VERSION_V10, drive_abci: DriveAbciVersion { - structs: DRIVE_ABCI_STRUCTURE_VERSIONS_V1, - methods: DRIVE_ABCI_METHOD_VERSIONS_V11, // changed: records and prunes the token shielded pool anchors at block end - validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V11, // changed: token shielded pool batch validators - withdrawal_constants: DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V3, - query: DRIVE_ABCI_QUERY_VERSIONS_V3, - checkpoints: DRIVE_ABCI_CHECKPOINT_PARAMETERS_V1, + methods: DRIVE_ABCI_METHOD_VERSIONS_V11, + validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V11, + ..PLATFORM_V14.drive_abci }, dpp: DPPVersion { - costs: DPP_COSTS_VERSIONS_V1, - validation: DPP_VALIDATION_VERSIONS_V5, - state_transition_serialization_versions: STATE_TRANSITION_SERIALIZATION_VERSIONS_V3, - state_transition_conversion_versions: STATE_TRANSITION_CONVERSION_VERSIONS_V2, - state_transition_method_versions: STATE_TRANSITION_METHOD_VERSIONS_V1, - state_transitions: STATE_TRANSITION_VERSIONS_V3, - contract_versions: CONTRACT_VERSIONS_V7, // changed: token configuration format 1 (the shielded pool flag) is admitted - document_versions: DOCUMENT_VERSIONS_V4, - identity_versions: IDENTITY_VERSIONS_V1, - voting_versions: VOTING_VERSION_V2, - token_versions: TOKEN_VERSIONS_V3, - asset_lock_versions: DPP_ASSET_LOCK_VERSIONS_V1, - methods: DPP_METHOD_VERSIONS_V3, - factory_versions: DPP_FACTORY_VERSIONS_V1, - }, - system_data_contracts: SYSTEM_DATA_CONTRACT_VERSIONS_V3, - fee_version: FEE_VERSION3, - system_limits: SYSTEM_LIMITS_V4, - consensus: ConsensusVersions { - tenderdash_consensus_version: 1, + contract_versions: CONTRACT_VERSIONS_V7, + ..PLATFORM_V14.dpp }, + ..PLATFORM_V14 }; diff --git a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/claim_to_pool.rs b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/claim_to_pool.rs index 2f3e7b4c4e0..861412dab1c 100644 --- a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/claim_to_pool.rs +++ b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/claim_to_pool.rs @@ -20,6 +20,7 @@ impl TokenClaimToPoolTransitionWasm { match self.0.distribution_type() { TokenDistributionType::PreProgrammed => 0, TokenDistributionType::Perpetual => 1, + TokenDistributionType::OncePerIdentity => 2, } } From faf5fa1e8cfabd6292017a6e3ca49ced63bf515c Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 21 Sep 2026 09:27:28 +0700 Subject: [PATCH 11/94] fix!: address token shielded pool review findings Defer proposer-bound group burn proofs for confirmations to stateful validation. Restore shipped contract generations and select new pool-aware validation and storage methods only from protocol 15. Keep the format activation gate before paid processing. Regenerate all DAPI clients for token pool query selectors and group events. Add admission, activation, storage, immutability, and cross-client serialization regression coverage. --- .../clients/drive/v0/nodejs/drive_pbjs.js | 783 +++++++++++- .../platform/v0/nodejs/platform_pbjs.js | 783 +++++++++++- .../platform/v0/nodejs/platform_protoc.js | 1062 ++++++++++++++++- .../platform/v0/objective-c/Platform.pbobjc.h | 105 ++ .../platform/v0/objective-c/Platform.pbobjc.m | 226 ++++ .../platform/v0/python/platform_pb2.py | 568 ++++++--- .../clients/platform/v0/web/platform_pb.d.ts | 126 ++ .../clients/platform/v0/web/platform_pb.js | 1062 ++++++++++++++++- .../protos/platform/v0/platform.proto | 2 +- .../test/unit/shielded-requests.spec.js | 51 + .../mod.rs | 10 +- .../v0/mod.rs | 15 - .../v1/mod.rs | 91 ++ .../processor/traits/is_allowed.rs | 32 +- .../processor/traits/shielded_proof.rs | 8 + .../batch/tests/token/shielded/mod.rs | 196 +++ .../basic_structure/mod.rs | 1 + .../basic_structure/v0/mod.rs | 19 - .../basic_structure/v3/mod.rs | 41 + .../data_contract_create/mod.rs | 6 +- .../basic_structure/mod.rs | 1 + .../basic_structure/v0/mod.rs | 19 - .../basic_structure/v3/mod.rs | 41 + .../data_contract_update/mod.rs | 4 +- .../contract/insert/insert_contract/mod.rs | 17 +- .../contract/insert/insert_contract/v1/mod.rs | 11 - .../contract/insert/insert_contract/v3/mod.rs | 128 ++ .../contract/update/update_contract/mod.rs | 34 +- .../contract/update/update_contract/v1/mod.rs | 11 - .../contract/update/update_contract/v3/mod.rs | 324 +++++ .../dpp_validation_versions/mod.rs | 1 + .../dpp_validation_versions/v6.rs | 11 + .../drive_abci_validation_versions/v11.rs | 12 + .../drive_contract_method_versions/mod.rs | 1 + .../drive_contract_method_versions/v5.rs | 19 + .../src/version/drive_versions/v10.rs | 2 + .../rs-platform-version/src/version/v15.rs | 2 + 37 files changed, 5539 insertions(+), 286 deletions(-) create mode 100644 packages/dapi-grpc/test/unit/shielded-requests.spec.js create mode 100644 packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/v1/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v3/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v3/mod.rs create mode 100644 packages/rs-drive/src/drive/contract/insert/insert_contract/v3/mod.rs create mode 100644 packages/rs-drive/src/drive/contract/update/update_contract/v3/mod.rs create mode 100644 packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v6.rs create mode 100644 packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v5.rs diff --git a/packages/dapi-grpc/clients/drive/v0/nodejs/drive_pbjs.js b/packages/dapi-grpc/clients/drive/v0/nodejs/drive_pbjs.js index daba28b97b4..3a51a53c42b 100644 --- a/packages/dapi-grpc/clients/drive/v0/nodejs/drive_pbjs.js +++ b/packages/dapi-grpc/clients/drive/v0/nodejs/drive_pbjs.js @@ -98770,6 +98770,516 @@ $root.org = (function() { return UpdateDirectPurchasePriceEvent; })(); + GetGroupActionsResponseV0.MintToPoolEvent = (function() { + + /** + * Properties of a MintToPoolEvent. + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0 + * @interface IMintToPoolEvent + * @property {number|Long|null} [amount] MintToPoolEvent amount + * @property {Uint8Array|null} [actionsDigest] MintToPoolEvent actionsDigest + * @property {string|null} [publicNote] MintToPoolEvent publicNote + */ + + /** + * Constructs a new MintToPoolEvent. + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0 + * @classdesc Represents a MintToPoolEvent. + * @implements IMintToPoolEvent + * @constructor + * @param {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IMintToPoolEvent=} [properties] Properties to set + */ + function MintToPoolEvent(properties) { + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) + this[keys[i]] = properties[keys[i]]; + } + + /** + * MintToPoolEvent amount. + * @member {number|Long} amount + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @instance + */ + MintToPoolEvent.prototype.amount = $util.Long ? $util.Long.fromBits(0,0,true) : 0; + + /** + * MintToPoolEvent actionsDigest. + * @member {Uint8Array} actionsDigest + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @instance + */ + MintToPoolEvent.prototype.actionsDigest = $util.newBuffer([]); + + /** + * MintToPoolEvent publicNote. + * @member {string} publicNote + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @instance + */ + MintToPoolEvent.prototype.publicNote = ""; + + /** + * Creates a new MintToPoolEvent instance using the specified properties. + * @function create + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @static + * @param {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IMintToPoolEvent=} [properties] Properties to set + * @returns {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} MintToPoolEvent instance + */ + MintToPoolEvent.create = function create(properties) { + return new MintToPoolEvent(properties); + }; + + /** + * Encodes the specified MintToPoolEvent message. Does not implicitly {@link org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.verify|verify} messages. + * @function encode + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @static + * @param {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IMintToPoolEvent} message MintToPoolEvent message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + MintToPoolEvent.encode = function encode(message, writer) { + if (!writer) + writer = $Writer.create(); + if (message.amount != null && Object.hasOwnProperty.call(message, "amount")) + writer.uint32(/* id 1, wireType 0 =*/8).uint64(message.amount); + if (message.actionsDigest != null && Object.hasOwnProperty.call(message, "actionsDigest")) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.actionsDigest); + if (message.publicNote != null && Object.hasOwnProperty.call(message, "publicNote")) + writer.uint32(/* id 3, wireType 2 =*/26).string(message.publicNote); + return writer; + }; + + /** + * Encodes the specified MintToPoolEvent message, length delimited. Does not implicitly {@link org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.verify|verify} messages. + * @function encodeDelimited + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @static + * @param {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IMintToPoolEvent} message MintToPoolEvent message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + MintToPoolEvent.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a MintToPoolEvent message from the specified reader or buffer. + * @function decode + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} MintToPoolEvent + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + MintToPoolEvent.decode = function decode(reader, length) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent(); + while (reader.pos < end) { + var tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.amount = reader.uint64(); + break; + case 2: + message.actionsDigest = reader.bytes(); + break; + case 3: + message.publicNote = reader.string(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes a MintToPoolEvent message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} MintToPoolEvent + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + MintToPoolEvent.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a MintToPoolEvent message. + * @function verify + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + MintToPoolEvent.verify = function verify(message) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (message.amount != null && message.hasOwnProperty("amount")) + if (!$util.isInteger(message.amount) && !(message.amount && $util.isInteger(message.amount.low) && $util.isInteger(message.amount.high))) + return "amount: integer|Long expected"; + if (message.actionsDigest != null && message.hasOwnProperty("actionsDigest")) + if (!(message.actionsDigest && typeof message.actionsDigest.length === "number" || $util.isString(message.actionsDigest))) + return "actionsDigest: buffer expected"; + if (message.publicNote != null && message.hasOwnProperty("publicNote")) + if (!$util.isString(message.publicNote)) + return "publicNote: string expected"; + return null; + }; + + /** + * Creates a MintToPoolEvent message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @static + * @param {Object.} object Plain object + * @returns {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} MintToPoolEvent + */ + MintToPoolEvent.fromObject = function fromObject(object) { + if (object instanceof $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent) + return object; + var message = new $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent(); + if (object.amount != null) + if ($util.Long) + (message.amount = $util.Long.fromValue(object.amount)).unsigned = true; + else if (typeof object.amount === "string") + message.amount = parseInt(object.amount, 10); + else if (typeof object.amount === "number") + message.amount = object.amount; + else if (typeof object.amount === "object") + message.amount = new $util.LongBits(object.amount.low >>> 0, object.amount.high >>> 0).toNumber(true); + if (object.actionsDigest != null) + if (typeof object.actionsDigest === "string") + $util.base64.decode(object.actionsDigest, message.actionsDigest = $util.newBuffer($util.base64.length(object.actionsDigest)), 0); + else if (object.actionsDigest.length >= 0) + message.actionsDigest = object.actionsDigest; + if (object.publicNote != null) + message.publicNote = String(object.publicNote); + return message; + }; + + /** + * Creates a plain object from a MintToPoolEvent message. Also converts values to other types if specified. + * @function toObject + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @static + * @param {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} message MintToPoolEvent + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + MintToPoolEvent.toObject = function toObject(message, options) { + if (!options) + options = {}; + var object = {}; + if (options.defaults) { + if ($util.Long) { + var long = new $util.Long(0, 0, true); + object.amount = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : long; + } else + object.amount = options.longs === String ? "0" : 0; + if (options.bytes === String) + object.actionsDigest = ""; + else { + object.actionsDigest = []; + if (options.bytes !== Array) + object.actionsDigest = $util.newBuffer(object.actionsDigest); + } + object.publicNote = ""; + } + if (message.amount != null && message.hasOwnProperty("amount")) + if (typeof message.amount === "number") + object.amount = options.longs === String ? String(message.amount) : message.amount; + else + object.amount = options.longs === String ? $util.Long.prototype.toString.call(message.amount) : options.longs === Number ? new $util.LongBits(message.amount.low >>> 0, message.amount.high >>> 0).toNumber(true) : message.amount; + if (message.actionsDigest != null && message.hasOwnProperty("actionsDigest")) + object.actionsDigest = options.bytes === String ? $util.base64.encode(message.actionsDigest, 0, message.actionsDigest.length) : options.bytes === Array ? Array.prototype.slice.call(message.actionsDigest) : message.actionsDigest; + if (message.publicNote != null && message.hasOwnProperty("publicNote")) + object.publicNote = message.publicNote; + return object; + }; + + /** + * Converts this MintToPoolEvent to JSON. + * @function toJSON + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @instance + * @returns {Object.} JSON object + */ + MintToPoolEvent.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + return MintToPoolEvent; + })(); + + GetGroupActionsResponseV0.BurnFromPoolEvent = (function() { + + /** + * Properties of a BurnFromPoolEvent. + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0 + * @interface IBurnFromPoolEvent + * @property {number|Long|null} [amount] BurnFromPoolEvent amount + * @property {Uint8Array|null} [actionsDigest] BurnFromPoolEvent actionsDigest + * @property {string|null} [publicNote] BurnFromPoolEvent publicNote + */ + + /** + * Constructs a new BurnFromPoolEvent. + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0 + * @classdesc Represents a BurnFromPoolEvent. + * @implements IBurnFromPoolEvent + * @constructor + * @param {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IBurnFromPoolEvent=} [properties] Properties to set + */ + function BurnFromPoolEvent(properties) { + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) + this[keys[i]] = properties[keys[i]]; + } + + /** + * BurnFromPoolEvent amount. + * @member {number|Long} amount + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @instance + */ + BurnFromPoolEvent.prototype.amount = $util.Long ? $util.Long.fromBits(0,0,true) : 0; + + /** + * BurnFromPoolEvent actionsDigest. + * @member {Uint8Array} actionsDigest + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @instance + */ + BurnFromPoolEvent.prototype.actionsDigest = $util.newBuffer([]); + + /** + * BurnFromPoolEvent publicNote. + * @member {string} publicNote + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @instance + */ + BurnFromPoolEvent.prototype.publicNote = ""; + + /** + * Creates a new BurnFromPoolEvent instance using the specified properties. + * @function create + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @static + * @param {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IBurnFromPoolEvent=} [properties] Properties to set + * @returns {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} BurnFromPoolEvent instance + */ + BurnFromPoolEvent.create = function create(properties) { + return new BurnFromPoolEvent(properties); + }; + + /** + * Encodes the specified BurnFromPoolEvent message. Does not implicitly {@link org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.verify|verify} messages. + * @function encode + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @static + * @param {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IBurnFromPoolEvent} message BurnFromPoolEvent message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + BurnFromPoolEvent.encode = function encode(message, writer) { + if (!writer) + writer = $Writer.create(); + if (message.amount != null && Object.hasOwnProperty.call(message, "amount")) + writer.uint32(/* id 1, wireType 0 =*/8).uint64(message.amount); + if (message.actionsDigest != null && Object.hasOwnProperty.call(message, "actionsDigest")) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.actionsDigest); + if (message.publicNote != null && Object.hasOwnProperty.call(message, "publicNote")) + writer.uint32(/* id 3, wireType 2 =*/26).string(message.publicNote); + return writer; + }; + + /** + * Encodes the specified BurnFromPoolEvent message, length delimited. Does not implicitly {@link org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.verify|verify} messages. + * @function encodeDelimited + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @static + * @param {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IBurnFromPoolEvent} message BurnFromPoolEvent message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + BurnFromPoolEvent.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a BurnFromPoolEvent message from the specified reader or buffer. + * @function decode + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} BurnFromPoolEvent + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + BurnFromPoolEvent.decode = function decode(reader, length) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent(); + while (reader.pos < end) { + var tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.amount = reader.uint64(); + break; + case 2: + message.actionsDigest = reader.bytes(); + break; + case 3: + message.publicNote = reader.string(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes a BurnFromPoolEvent message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} BurnFromPoolEvent + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + BurnFromPoolEvent.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a BurnFromPoolEvent message. + * @function verify + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + BurnFromPoolEvent.verify = function verify(message) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (message.amount != null && message.hasOwnProperty("amount")) + if (!$util.isInteger(message.amount) && !(message.amount && $util.isInteger(message.amount.low) && $util.isInteger(message.amount.high))) + return "amount: integer|Long expected"; + if (message.actionsDigest != null && message.hasOwnProperty("actionsDigest")) + if (!(message.actionsDigest && typeof message.actionsDigest.length === "number" || $util.isString(message.actionsDigest))) + return "actionsDigest: buffer expected"; + if (message.publicNote != null && message.hasOwnProperty("publicNote")) + if (!$util.isString(message.publicNote)) + return "publicNote: string expected"; + return null; + }; + + /** + * Creates a BurnFromPoolEvent message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @static + * @param {Object.} object Plain object + * @returns {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} BurnFromPoolEvent + */ + BurnFromPoolEvent.fromObject = function fromObject(object) { + if (object instanceof $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent) + return object; + var message = new $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent(); + if (object.amount != null) + if ($util.Long) + (message.amount = $util.Long.fromValue(object.amount)).unsigned = true; + else if (typeof object.amount === "string") + message.amount = parseInt(object.amount, 10); + else if (typeof object.amount === "number") + message.amount = object.amount; + else if (typeof object.amount === "object") + message.amount = new $util.LongBits(object.amount.low >>> 0, object.amount.high >>> 0).toNumber(true); + if (object.actionsDigest != null) + if (typeof object.actionsDigest === "string") + $util.base64.decode(object.actionsDigest, message.actionsDigest = $util.newBuffer($util.base64.length(object.actionsDigest)), 0); + else if (object.actionsDigest.length >= 0) + message.actionsDigest = object.actionsDigest; + if (object.publicNote != null) + message.publicNote = String(object.publicNote); + return message; + }; + + /** + * Creates a plain object from a BurnFromPoolEvent message. Also converts values to other types if specified. + * @function toObject + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @static + * @param {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} message BurnFromPoolEvent + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + BurnFromPoolEvent.toObject = function toObject(message, options) { + if (!options) + options = {}; + var object = {}; + if (options.defaults) { + if ($util.Long) { + var long = new $util.Long(0, 0, true); + object.amount = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : long; + } else + object.amount = options.longs === String ? "0" : 0; + if (options.bytes === String) + object.actionsDigest = ""; + else { + object.actionsDigest = []; + if (options.bytes !== Array) + object.actionsDigest = $util.newBuffer(object.actionsDigest); + } + object.publicNote = ""; + } + if (message.amount != null && message.hasOwnProperty("amount")) + if (typeof message.amount === "number") + object.amount = options.longs === String ? String(message.amount) : message.amount; + else + object.amount = options.longs === String ? $util.Long.prototype.toString.call(message.amount) : options.longs === Number ? new $util.LongBits(message.amount.low >>> 0, message.amount.high >>> 0).toNumber(true) : message.amount; + if (message.actionsDigest != null && message.hasOwnProperty("actionsDigest")) + object.actionsDigest = options.bytes === String ? $util.base64.encode(message.actionsDigest, 0, message.actionsDigest.length) : options.bytes === Array ? Array.prototype.slice.call(message.actionsDigest) : message.actionsDigest; + if (message.publicNote != null && message.hasOwnProperty("publicNote")) + object.publicNote = message.publicNote; + return object; + }; + + /** + * Converts this BurnFromPoolEvent to JSON. + * @function toJSON + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @instance + * @returns {Object.} JSON object + */ + BurnFromPoolEvent.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + return BurnFromPoolEvent; + })(); + GetGroupActionsResponseV0.GroupActionEvent = (function() { /** @@ -99877,6 +100387,8 @@ $root.org = (function() { * @property {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IEmergencyActionEvent|null} [emergencyAction] TokenEvent emergencyAction * @property {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ITokenConfigUpdateEvent|null} [tokenConfigUpdate] TokenEvent tokenConfigUpdate * @property {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IUpdateDirectPurchasePriceEvent|null} [updatePrice] TokenEvent updatePrice + * @property {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IMintToPoolEvent|null} [mintToPool] TokenEvent mintToPool + * @property {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IBurnFromPoolEvent|null} [burnFromPool] TokenEvent burnFromPool */ /** @@ -99958,17 +100470,33 @@ $root.org = (function() { */ TokenEvent.prototype.updatePrice = null; + /** + * TokenEvent mintToPool. + * @member {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IMintToPoolEvent|null|undefined} mintToPool + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent + * @instance + */ + TokenEvent.prototype.mintToPool = null; + + /** + * TokenEvent burnFromPool. + * @member {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IBurnFromPoolEvent|null|undefined} burnFromPool + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent + * @instance + */ + TokenEvent.prototype.burnFromPool = null; + // OneOf field names bound to virtual getters and setters var $oneOfFields; /** * TokenEvent type. - * @member {"mint"|"burn"|"freeze"|"unfreeze"|"destroyFrozenFunds"|"emergencyAction"|"tokenConfigUpdate"|"updatePrice"|undefined} type + * @member {"mint"|"burn"|"freeze"|"unfreeze"|"destroyFrozenFunds"|"emergencyAction"|"tokenConfigUpdate"|"updatePrice"|"mintToPool"|"burnFromPool"|undefined} type * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent * @instance */ Object.defineProperty(TokenEvent.prototype, "type", { - get: $util.oneOfGetter($oneOfFields = ["mint", "burn", "freeze", "unfreeze", "destroyFrozenFunds", "emergencyAction", "tokenConfigUpdate", "updatePrice"]), + get: $util.oneOfGetter($oneOfFields = ["mint", "burn", "freeze", "unfreeze", "destroyFrozenFunds", "emergencyAction", "tokenConfigUpdate", "updatePrice", "mintToPool", "burnFromPool"]), set: $util.oneOfSetter($oneOfFields) }); @@ -100012,6 +100540,10 @@ $root.org = (function() { $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenConfigUpdateEvent.encode(message.tokenConfigUpdate, writer.uint32(/* id 7, wireType 2 =*/58).fork()).ldelim(); if (message.updatePrice != null && Object.hasOwnProperty.call(message, "updatePrice")) $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.encode(message.updatePrice, writer.uint32(/* id 8, wireType 2 =*/66).fork()).ldelim(); + if (message.mintToPool != null && Object.hasOwnProperty.call(message, "mintToPool")) + $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.encode(message.mintToPool, writer.uint32(/* id 9, wireType 2 =*/74).fork()).ldelim(); + if (message.burnFromPool != null && Object.hasOwnProperty.call(message, "burnFromPool")) + $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.encode(message.burnFromPool, writer.uint32(/* id 10, wireType 2 =*/82).fork()).ldelim(); return writer; }; @@ -100070,6 +100602,12 @@ $root.org = (function() { case 8: message.updatePrice = $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.decode(reader, reader.uint32()); break; + case 9: + message.mintToPool = $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.decode(reader, reader.uint32()); + break; + case 10: + message.burnFromPool = $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.decode(reader, reader.uint32()); + break; default: reader.skipType(tag & 7); break; @@ -100184,6 +100722,26 @@ $root.org = (function() { return "updatePrice." + error; } } + if (message.mintToPool != null && message.hasOwnProperty("mintToPool")) { + if (properties.type === 1) + return "type: multiple values"; + properties.type = 1; + { + var error = $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.verify(message.mintToPool); + if (error) + return "mintToPool." + error; + } + } + if (message.burnFromPool != null && message.hasOwnProperty("burnFromPool")) { + if (properties.type === 1) + return "type: multiple values"; + properties.type = 1; + { + var error = $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.verify(message.burnFromPool); + if (error) + return "burnFromPool." + error; + } + } return null; }; @@ -100239,6 +100797,16 @@ $root.org = (function() { throw TypeError(".org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.updatePrice: object expected"); message.updatePrice = $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.fromObject(object.updatePrice); } + if (object.mintToPool != null) { + if (typeof object.mintToPool !== "object") + throw TypeError(".org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.mintToPool: object expected"); + message.mintToPool = $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.fromObject(object.mintToPool); + } + if (object.burnFromPool != null) { + if (typeof object.burnFromPool !== "object") + throw TypeError(".org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.burnFromPool: object expected"); + message.burnFromPool = $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.fromObject(object.burnFromPool); + } return message; }; @@ -100295,6 +100863,16 @@ $root.org = (function() { if (options.oneofs) object.type = "updatePrice"; } + if (message.mintToPool != null && message.hasOwnProperty("mintToPool")) { + object.mintToPool = $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.toObject(message.mintToPool, options); + if (options.oneofs) + object.type = "mintToPool"; + } + if (message.burnFromPool != null && message.hasOwnProperty("burnFromPool")) { + object.burnFromPool = $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.toObject(message.burnFromPool, options); + if (options.oneofs) + object.type = "burnFromPool"; + } return object; }; @@ -110394,6 +110972,7 @@ $root.org = (function() { * @property {number|Long|null} [startIndex] GetShieldedEncryptedNotesRequestV0 startIndex * @property {number|null} [count] GetShieldedEncryptedNotesRequestV0 count * @property {boolean|null} [prove] GetShieldedEncryptedNotesRequestV0 prove + * @property {Uint8Array|null} [tokenId] GetShieldedEncryptedNotesRequestV0 tokenId */ /** @@ -110435,6 +111014,14 @@ $root.org = (function() { */ GetShieldedEncryptedNotesRequestV0.prototype.prove = false; + /** + * GetShieldedEncryptedNotesRequestV0 tokenId. + * @member {Uint8Array} tokenId + * @memberof org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0 + * @instance + */ + GetShieldedEncryptedNotesRequestV0.prototype.tokenId = $util.newBuffer([]); + /** * Creates a new GetShieldedEncryptedNotesRequestV0 instance using the specified properties. * @function create @@ -110465,6 +111052,8 @@ $root.org = (function() { writer.uint32(/* id 2, wireType 0 =*/16).uint32(message.count); if (message.prove != null && Object.hasOwnProperty.call(message, "prove")) writer.uint32(/* id 3, wireType 0 =*/24).bool(message.prove); + if (message.tokenId != null && Object.hasOwnProperty.call(message, "tokenId")) + writer.uint32(/* id 4, wireType 2 =*/34).bytes(message.tokenId); return writer; }; @@ -110508,6 +111097,9 @@ $root.org = (function() { case 3: message.prove = reader.bool(); break; + case 4: + message.tokenId = reader.bytes(); + break; default: reader.skipType(tag & 7); break; @@ -110552,6 +111144,9 @@ $root.org = (function() { if (message.prove != null && message.hasOwnProperty("prove")) if (typeof message.prove !== "boolean") return "prove: boolean expected"; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + if (!(message.tokenId && typeof message.tokenId.length === "number" || $util.isString(message.tokenId))) + return "tokenId: buffer expected"; return null; }; @@ -110580,6 +111175,11 @@ $root.org = (function() { message.count = object.count >>> 0; if (object.prove != null) message.prove = Boolean(object.prove); + if (object.tokenId != null) + if (typeof object.tokenId === "string") + $util.base64.decode(object.tokenId, message.tokenId = $util.newBuffer($util.base64.length(object.tokenId)), 0); + else if (object.tokenId.length >= 0) + message.tokenId = object.tokenId; return message; }; @@ -110604,6 +111204,13 @@ $root.org = (function() { object.startIndex = options.longs === String ? "0" : 0; object.count = 0; object.prove = false; + if (options.bytes === String) + object.tokenId = ""; + else { + object.tokenId = []; + if (options.bytes !== Array) + object.tokenId = $util.newBuffer(object.tokenId); + } } if (message.startIndex != null && message.hasOwnProperty("startIndex")) if (typeof message.startIndex === "number") @@ -110614,6 +111221,8 @@ $root.org = (function() { object.count = message.count; if (message.prove != null && message.hasOwnProperty("prove")) object.prove = message.prove; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + object.tokenId = options.bytes === String ? $util.base64.encode(message.tokenId, 0, message.tokenId.length) : options.bytes === Array ? Array.prototype.slice.call(message.tokenId) : message.tokenId; return object; }; @@ -111831,6 +112440,7 @@ $root.org = (function() { * @memberof org.dash.platform.dapi.v0.GetShieldedAnchorsRequest * @interface IGetShieldedAnchorsRequestV0 * @property {boolean|null} [prove] GetShieldedAnchorsRequestV0 prove + * @property {Uint8Array|null} [tokenId] GetShieldedAnchorsRequestV0 tokenId */ /** @@ -111856,6 +112466,14 @@ $root.org = (function() { */ GetShieldedAnchorsRequestV0.prototype.prove = false; + /** + * GetShieldedAnchorsRequestV0 tokenId. + * @member {Uint8Array} tokenId + * @memberof org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0 + * @instance + */ + GetShieldedAnchorsRequestV0.prototype.tokenId = $util.newBuffer([]); + /** * Creates a new GetShieldedAnchorsRequestV0 instance using the specified properties. * @function create @@ -111882,6 +112500,8 @@ $root.org = (function() { writer = $Writer.create(); if (message.prove != null && Object.hasOwnProperty.call(message, "prove")) writer.uint32(/* id 1, wireType 0 =*/8).bool(message.prove); + if (message.tokenId != null && Object.hasOwnProperty.call(message, "tokenId")) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.tokenId); return writer; }; @@ -111919,6 +112539,9 @@ $root.org = (function() { case 1: message.prove = reader.bool(); break; + case 2: + message.tokenId = reader.bytes(); + break; default: reader.skipType(tag & 7); break; @@ -111957,6 +112580,9 @@ $root.org = (function() { if (message.prove != null && message.hasOwnProperty("prove")) if (typeof message.prove !== "boolean") return "prove: boolean expected"; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + if (!(message.tokenId && typeof message.tokenId.length === "number" || $util.isString(message.tokenId))) + return "tokenId: buffer expected"; return null; }; @@ -111974,6 +112600,11 @@ $root.org = (function() { var message = new $root.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0(); if (object.prove != null) message.prove = Boolean(object.prove); + if (object.tokenId != null) + if (typeof object.tokenId === "string") + $util.base64.decode(object.tokenId, message.tokenId = $util.newBuffer($util.base64.length(object.tokenId)), 0); + else if (object.tokenId.length >= 0) + message.tokenId = object.tokenId; return message; }; @@ -111990,10 +112621,20 @@ $root.org = (function() { if (!options) options = {}; var object = {}; - if (options.defaults) + if (options.defaults) { object.prove = false; + if (options.bytes === String) + object.tokenId = ""; + else { + object.tokenId = []; + if (options.bytes !== Array) + object.tokenId = $util.newBuffer(object.tokenId); + } + } if (message.prove != null && message.hasOwnProperty("prove")) object.prove = message.prove; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + object.tokenId = options.bytes === String ? $util.base64.encode(message.tokenId, 0, message.tokenId.length) : options.bytes === Array ? Array.prototype.slice.call(message.tokenId) : message.tokenId; return object; }; @@ -112919,6 +113560,7 @@ $root.org = (function() { * @memberof org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest * @interface IGetMostRecentShieldedAnchorRequestV0 * @property {boolean|null} [prove] GetMostRecentShieldedAnchorRequestV0 prove + * @property {Uint8Array|null} [tokenId] GetMostRecentShieldedAnchorRequestV0 tokenId */ /** @@ -112944,6 +113586,14 @@ $root.org = (function() { */ GetMostRecentShieldedAnchorRequestV0.prototype.prove = false; + /** + * GetMostRecentShieldedAnchorRequestV0 tokenId. + * @member {Uint8Array} tokenId + * @memberof org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0 + * @instance + */ + GetMostRecentShieldedAnchorRequestV0.prototype.tokenId = $util.newBuffer([]); + /** * Creates a new GetMostRecentShieldedAnchorRequestV0 instance using the specified properties. * @function create @@ -112970,6 +113620,8 @@ $root.org = (function() { writer = $Writer.create(); if (message.prove != null && Object.hasOwnProperty.call(message, "prove")) writer.uint32(/* id 1, wireType 0 =*/8).bool(message.prove); + if (message.tokenId != null && Object.hasOwnProperty.call(message, "tokenId")) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.tokenId); return writer; }; @@ -113007,6 +113659,9 @@ $root.org = (function() { case 1: message.prove = reader.bool(); break; + case 2: + message.tokenId = reader.bytes(); + break; default: reader.skipType(tag & 7); break; @@ -113045,6 +113700,9 @@ $root.org = (function() { if (message.prove != null && message.hasOwnProperty("prove")) if (typeof message.prove !== "boolean") return "prove: boolean expected"; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + if (!(message.tokenId && typeof message.tokenId.length === "number" || $util.isString(message.tokenId))) + return "tokenId: buffer expected"; return null; }; @@ -113062,6 +113720,11 @@ $root.org = (function() { var message = new $root.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0(); if (object.prove != null) message.prove = Boolean(object.prove); + if (object.tokenId != null) + if (typeof object.tokenId === "string") + $util.base64.decode(object.tokenId, message.tokenId = $util.newBuffer($util.base64.length(object.tokenId)), 0); + else if (object.tokenId.length >= 0) + message.tokenId = object.tokenId; return message; }; @@ -113078,10 +113741,20 @@ $root.org = (function() { if (!options) options = {}; var object = {}; - if (options.defaults) + if (options.defaults) { object.prove = false; + if (options.bytes === String) + object.tokenId = ""; + else { + object.tokenId = []; + if (options.bytes !== Array) + object.tokenId = $util.newBuffer(object.tokenId); + } + } if (message.prove != null && message.hasOwnProperty("prove")) object.prove = message.prove; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + object.tokenId = options.bytes === String ? $util.base64.encode(message.tokenId, 0, message.tokenId.length) : options.bytes === Array ? Array.prototype.slice.call(message.tokenId) : message.tokenId; return object; }; @@ -113798,6 +114471,7 @@ $root.org = (function() { * @memberof org.dash.platform.dapi.v0.GetShieldedPoolStateRequest * @interface IGetShieldedPoolStateRequestV0 * @property {boolean|null} [prove] GetShieldedPoolStateRequestV0 prove + * @property {Uint8Array|null} [tokenId] GetShieldedPoolStateRequestV0 tokenId */ /** @@ -113823,6 +114497,14 @@ $root.org = (function() { */ GetShieldedPoolStateRequestV0.prototype.prove = false; + /** + * GetShieldedPoolStateRequestV0 tokenId. + * @member {Uint8Array} tokenId + * @memberof org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0 + * @instance + */ + GetShieldedPoolStateRequestV0.prototype.tokenId = $util.newBuffer([]); + /** * Creates a new GetShieldedPoolStateRequestV0 instance using the specified properties. * @function create @@ -113849,6 +114531,8 @@ $root.org = (function() { writer = $Writer.create(); if (message.prove != null && Object.hasOwnProperty.call(message, "prove")) writer.uint32(/* id 1, wireType 0 =*/8).bool(message.prove); + if (message.tokenId != null && Object.hasOwnProperty.call(message, "tokenId")) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.tokenId); return writer; }; @@ -113886,6 +114570,9 @@ $root.org = (function() { case 1: message.prove = reader.bool(); break; + case 2: + message.tokenId = reader.bytes(); + break; default: reader.skipType(tag & 7); break; @@ -113924,6 +114611,9 @@ $root.org = (function() { if (message.prove != null && message.hasOwnProperty("prove")) if (typeof message.prove !== "boolean") return "prove: boolean expected"; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + if (!(message.tokenId && typeof message.tokenId.length === "number" || $util.isString(message.tokenId))) + return "tokenId: buffer expected"; return null; }; @@ -113941,6 +114631,11 @@ $root.org = (function() { var message = new $root.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0(); if (object.prove != null) message.prove = Boolean(object.prove); + if (object.tokenId != null) + if (typeof object.tokenId === "string") + $util.base64.decode(object.tokenId, message.tokenId = $util.newBuffer($util.base64.length(object.tokenId)), 0); + else if (object.tokenId.length >= 0) + message.tokenId = object.tokenId; return message; }; @@ -113957,10 +114652,20 @@ $root.org = (function() { if (!options) options = {}; var object = {}; - if (options.defaults) + if (options.defaults) { object.prove = false; + if (options.bytes === String) + object.tokenId = ""; + else { + object.tokenId = []; + if (options.bytes !== Array) + object.tokenId = $util.newBuffer(object.tokenId); + } + } if (message.prove != null && message.hasOwnProperty("prove")) object.prove = message.prove; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + object.tokenId = options.bytes === String ? $util.base64.encode(message.tokenId, 0, message.tokenId.length) : options.bytes === Array ? Array.prototype.slice.call(message.tokenId) : message.tokenId; return object; }; @@ -114684,6 +115389,7 @@ $root.org = (function() { * @memberof org.dash.platform.dapi.v0.GetShieldedNotesCountRequest * @interface IGetShieldedNotesCountRequestV0 * @property {boolean|null} [prove] GetShieldedNotesCountRequestV0 prove + * @property {Uint8Array|null} [tokenId] GetShieldedNotesCountRequestV0 tokenId */ /** @@ -114709,6 +115415,14 @@ $root.org = (function() { */ GetShieldedNotesCountRequestV0.prototype.prove = false; + /** + * GetShieldedNotesCountRequestV0 tokenId. + * @member {Uint8Array} tokenId + * @memberof org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0 + * @instance + */ + GetShieldedNotesCountRequestV0.prototype.tokenId = $util.newBuffer([]); + /** * Creates a new GetShieldedNotesCountRequestV0 instance using the specified properties. * @function create @@ -114735,6 +115449,8 @@ $root.org = (function() { writer = $Writer.create(); if (message.prove != null && Object.hasOwnProperty.call(message, "prove")) writer.uint32(/* id 1, wireType 0 =*/8).bool(message.prove); + if (message.tokenId != null && Object.hasOwnProperty.call(message, "tokenId")) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.tokenId); return writer; }; @@ -114772,6 +115488,9 @@ $root.org = (function() { case 1: message.prove = reader.bool(); break; + case 2: + message.tokenId = reader.bytes(); + break; default: reader.skipType(tag & 7); break; @@ -114810,6 +115529,9 @@ $root.org = (function() { if (message.prove != null && message.hasOwnProperty("prove")) if (typeof message.prove !== "boolean") return "prove: boolean expected"; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + if (!(message.tokenId && typeof message.tokenId.length === "number" || $util.isString(message.tokenId))) + return "tokenId: buffer expected"; return null; }; @@ -114827,6 +115549,11 @@ $root.org = (function() { var message = new $root.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0(); if (object.prove != null) message.prove = Boolean(object.prove); + if (object.tokenId != null) + if (typeof object.tokenId === "string") + $util.base64.decode(object.tokenId, message.tokenId = $util.newBuffer($util.base64.length(object.tokenId)), 0); + else if (object.tokenId.length >= 0) + message.tokenId = object.tokenId; return message; }; @@ -114843,10 +115570,20 @@ $root.org = (function() { if (!options) options = {}; var object = {}; - if (options.defaults) + if (options.defaults) { object.prove = false; + if (options.bytes === String) + object.tokenId = ""; + else { + object.tokenId = []; + if (options.bytes !== Array) + object.tokenId = $util.newBuffer(object.tokenId); + } + } if (message.prove != null && message.hasOwnProperty("prove")) object.prove = message.prove; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + object.tokenId = options.bytes === String ? $util.base64.encode(message.tokenId, 0, message.tokenId.length) : options.bytes === Array ? Array.prototype.slice.call(message.tokenId) : message.tokenId; return object; }; @@ -115571,6 +116308,7 @@ $root.org = (function() { * @interface IGetShieldedNullifiersRequestV0 * @property {Array.|null} [nullifiers] GetShieldedNullifiersRequestV0 nullifiers * @property {boolean|null} [prove] GetShieldedNullifiersRequestV0 prove + * @property {Uint8Array|null} [tokenId] GetShieldedNullifiersRequestV0 tokenId */ /** @@ -115605,6 +116343,14 @@ $root.org = (function() { */ GetShieldedNullifiersRequestV0.prototype.prove = false; + /** + * GetShieldedNullifiersRequestV0 tokenId. + * @member {Uint8Array} tokenId + * @memberof org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0 + * @instance + */ + GetShieldedNullifiersRequestV0.prototype.tokenId = $util.newBuffer([]); + /** * Creates a new GetShieldedNullifiersRequestV0 instance using the specified properties. * @function create @@ -115634,6 +116380,8 @@ $root.org = (function() { writer.uint32(/* id 1, wireType 2 =*/10).bytes(message.nullifiers[i]); if (message.prove != null && Object.hasOwnProperty.call(message, "prove")) writer.uint32(/* id 2, wireType 0 =*/16).bool(message.prove); + if (message.tokenId != null && Object.hasOwnProperty.call(message, "tokenId")) + writer.uint32(/* id 3, wireType 2 =*/26).bytes(message.tokenId); return writer; }; @@ -115676,6 +116424,9 @@ $root.org = (function() { case 2: message.prove = reader.bool(); break; + case 3: + message.tokenId = reader.bytes(); + break; default: reader.skipType(tag & 7); break; @@ -115721,6 +116472,9 @@ $root.org = (function() { if (message.prove != null && message.hasOwnProperty("prove")) if (typeof message.prove !== "boolean") return "prove: boolean expected"; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + if (!(message.tokenId && typeof message.tokenId.length === "number" || $util.isString(message.tokenId))) + return "tokenId: buffer expected"; return null; }; @@ -115748,6 +116502,11 @@ $root.org = (function() { } if (object.prove != null) message.prove = Boolean(object.prove); + if (object.tokenId != null) + if (typeof object.tokenId === "string") + $util.base64.decode(object.tokenId, message.tokenId = $util.newBuffer($util.base64.length(object.tokenId)), 0); + else if (object.tokenId.length >= 0) + message.tokenId = object.tokenId; return message; }; @@ -115766,8 +116525,16 @@ $root.org = (function() { var object = {}; if (options.arrays || options.defaults) object.nullifiers = []; - if (options.defaults) + if (options.defaults) { object.prove = false; + if (options.bytes === String) + object.tokenId = ""; + else { + object.tokenId = []; + if (options.bytes !== Array) + object.tokenId = $util.newBuffer(object.tokenId); + } + } if (message.nullifiers && message.nullifiers.length) { object.nullifiers = []; for (var j = 0; j < message.nullifiers.length; ++j) @@ -115775,6 +116542,8 @@ $root.org = (function() { } if (message.prove != null && message.hasOwnProperty("prove")) object.prove = message.prove; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + object.tokenId = options.bytes === String ? $util.base64.encode(message.tokenId, 0, message.tokenId.length) : options.bytes === Array ? Array.prototype.slice.call(message.tokenId) : message.tokenId; return object; }; diff --git a/packages/dapi-grpc/clients/platform/v0/nodejs/platform_pbjs.js b/packages/dapi-grpc/clients/platform/v0/nodejs/platform_pbjs.js index fa98490966b..5a26870e759 100644 --- a/packages/dapi-grpc/clients/platform/v0/nodejs/platform_pbjs.js +++ b/packages/dapi-grpc/clients/platform/v0/nodejs/platform_pbjs.js @@ -98262,6 +98262,516 @@ $root.org = (function() { return UpdateDirectPurchasePriceEvent; })(); + GetGroupActionsResponseV0.MintToPoolEvent = (function() { + + /** + * Properties of a MintToPoolEvent. + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0 + * @interface IMintToPoolEvent + * @property {number|Long|null} [amount] MintToPoolEvent amount + * @property {Uint8Array|null} [actionsDigest] MintToPoolEvent actionsDigest + * @property {string|null} [publicNote] MintToPoolEvent publicNote + */ + + /** + * Constructs a new MintToPoolEvent. + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0 + * @classdesc Represents a MintToPoolEvent. + * @implements IMintToPoolEvent + * @constructor + * @param {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IMintToPoolEvent=} [properties] Properties to set + */ + function MintToPoolEvent(properties) { + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) + this[keys[i]] = properties[keys[i]]; + } + + /** + * MintToPoolEvent amount. + * @member {number|Long} amount + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @instance + */ + MintToPoolEvent.prototype.amount = $util.Long ? $util.Long.fromBits(0,0,true) : 0; + + /** + * MintToPoolEvent actionsDigest. + * @member {Uint8Array} actionsDigest + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @instance + */ + MintToPoolEvent.prototype.actionsDigest = $util.newBuffer([]); + + /** + * MintToPoolEvent publicNote. + * @member {string} publicNote + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @instance + */ + MintToPoolEvent.prototype.publicNote = ""; + + /** + * Creates a new MintToPoolEvent instance using the specified properties. + * @function create + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @static + * @param {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IMintToPoolEvent=} [properties] Properties to set + * @returns {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} MintToPoolEvent instance + */ + MintToPoolEvent.create = function create(properties) { + return new MintToPoolEvent(properties); + }; + + /** + * Encodes the specified MintToPoolEvent message. Does not implicitly {@link org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.verify|verify} messages. + * @function encode + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @static + * @param {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IMintToPoolEvent} message MintToPoolEvent message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + MintToPoolEvent.encode = function encode(message, writer) { + if (!writer) + writer = $Writer.create(); + if (message.amount != null && Object.hasOwnProperty.call(message, "amount")) + writer.uint32(/* id 1, wireType 0 =*/8).uint64(message.amount); + if (message.actionsDigest != null && Object.hasOwnProperty.call(message, "actionsDigest")) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.actionsDigest); + if (message.publicNote != null && Object.hasOwnProperty.call(message, "publicNote")) + writer.uint32(/* id 3, wireType 2 =*/26).string(message.publicNote); + return writer; + }; + + /** + * Encodes the specified MintToPoolEvent message, length delimited. Does not implicitly {@link org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.verify|verify} messages. + * @function encodeDelimited + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @static + * @param {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IMintToPoolEvent} message MintToPoolEvent message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + MintToPoolEvent.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a MintToPoolEvent message from the specified reader or buffer. + * @function decode + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} MintToPoolEvent + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + MintToPoolEvent.decode = function decode(reader, length) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent(); + while (reader.pos < end) { + var tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.amount = reader.uint64(); + break; + case 2: + message.actionsDigest = reader.bytes(); + break; + case 3: + message.publicNote = reader.string(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes a MintToPoolEvent message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} MintToPoolEvent + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + MintToPoolEvent.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a MintToPoolEvent message. + * @function verify + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + MintToPoolEvent.verify = function verify(message) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (message.amount != null && message.hasOwnProperty("amount")) + if (!$util.isInteger(message.amount) && !(message.amount && $util.isInteger(message.amount.low) && $util.isInteger(message.amount.high))) + return "amount: integer|Long expected"; + if (message.actionsDigest != null && message.hasOwnProperty("actionsDigest")) + if (!(message.actionsDigest && typeof message.actionsDigest.length === "number" || $util.isString(message.actionsDigest))) + return "actionsDigest: buffer expected"; + if (message.publicNote != null && message.hasOwnProperty("publicNote")) + if (!$util.isString(message.publicNote)) + return "publicNote: string expected"; + return null; + }; + + /** + * Creates a MintToPoolEvent message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @static + * @param {Object.} object Plain object + * @returns {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} MintToPoolEvent + */ + MintToPoolEvent.fromObject = function fromObject(object) { + if (object instanceof $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent) + return object; + var message = new $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent(); + if (object.amount != null) + if ($util.Long) + (message.amount = $util.Long.fromValue(object.amount)).unsigned = true; + else if (typeof object.amount === "string") + message.amount = parseInt(object.amount, 10); + else if (typeof object.amount === "number") + message.amount = object.amount; + else if (typeof object.amount === "object") + message.amount = new $util.LongBits(object.amount.low >>> 0, object.amount.high >>> 0).toNumber(true); + if (object.actionsDigest != null) + if (typeof object.actionsDigest === "string") + $util.base64.decode(object.actionsDigest, message.actionsDigest = $util.newBuffer($util.base64.length(object.actionsDigest)), 0); + else if (object.actionsDigest.length >= 0) + message.actionsDigest = object.actionsDigest; + if (object.publicNote != null) + message.publicNote = String(object.publicNote); + return message; + }; + + /** + * Creates a plain object from a MintToPoolEvent message. Also converts values to other types if specified. + * @function toObject + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @static + * @param {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} message MintToPoolEvent + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + MintToPoolEvent.toObject = function toObject(message, options) { + if (!options) + options = {}; + var object = {}; + if (options.defaults) { + if ($util.Long) { + var long = new $util.Long(0, 0, true); + object.amount = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : long; + } else + object.amount = options.longs === String ? "0" : 0; + if (options.bytes === String) + object.actionsDigest = ""; + else { + object.actionsDigest = []; + if (options.bytes !== Array) + object.actionsDigest = $util.newBuffer(object.actionsDigest); + } + object.publicNote = ""; + } + if (message.amount != null && message.hasOwnProperty("amount")) + if (typeof message.amount === "number") + object.amount = options.longs === String ? String(message.amount) : message.amount; + else + object.amount = options.longs === String ? $util.Long.prototype.toString.call(message.amount) : options.longs === Number ? new $util.LongBits(message.amount.low >>> 0, message.amount.high >>> 0).toNumber(true) : message.amount; + if (message.actionsDigest != null && message.hasOwnProperty("actionsDigest")) + object.actionsDigest = options.bytes === String ? $util.base64.encode(message.actionsDigest, 0, message.actionsDigest.length) : options.bytes === Array ? Array.prototype.slice.call(message.actionsDigest) : message.actionsDigest; + if (message.publicNote != null && message.hasOwnProperty("publicNote")) + object.publicNote = message.publicNote; + return object; + }; + + /** + * Converts this MintToPoolEvent to JSON. + * @function toJSON + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent + * @instance + * @returns {Object.} JSON object + */ + MintToPoolEvent.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + return MintToPoolEvent; + })(); + + GetGroupActionsResponseV0.BurnFromPoolEvent = (function() { + + /** + * Properties of a BurnFromPoolEvent. + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0 + * @interface IBurnFromPoolEvent + * @property {number|Long|null} [amount] BurnFromPoolEvent amount + * @property {Uint8Array|null} [actionsDigest] BurnFromPoolEvent actionsDigest + * @property {string|null} [publicNote] BurnFromPoolEvent publicNote + */ + + /** + * Constructs a new BurnFromPoolEvent. + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0 + * @classdesc Represents a BurnFromPoolEvent. + * @implements IBurnFromPoolEvent + * @constructor + * @param {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IBurnFromPoolEvent=} [properties] Properties to set + */ + function BurnFromPoolEvent(properties) { + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) + this[keys[i]] = properties[keys[i]]; + } + + /** + * BurnFromPoolEvent amount. + * @member {number|Long} amount + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @instance + */ + BurnFromPoolEvent.prototype.amount = $util.Long ? $util.Long.fromBits(0,0,true) : 0; + + /** + * BurnFromPoolEvent actionsDigest. + * @member {Uint8Array} actionsDigest + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @instance + */ + BurnFromPoolEvent.prototype.actionsDigest = $util.newBuffer([]); + + /** + * BurnFromPoolEvent publicNote. + * @member {string} publicNote + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @instance + */ + BurnFromPoolEvent.prototype.publicNote = ""; + + /** + * Creates a new BurnFromPoolEvent instance using the specified properties. + * @function create + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @static + * @param {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IBurnFromPoolEvent=} [properties] Properties to set + * @returns {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} BurnFromPoolEvent instance + */ + BurnFromPoolEvent.create = function create(properties) { + return new BurnFromPoolEvent(properties); + }; + + /** + * Encodes the specified BurnFromPoolEvent message. Does not implicitly {@link org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.verify|verify} messages. + * @function encode + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @static + * @param {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IBurnFromPoolEvent} message BurnFromPoolEvent message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + BurnFromPoolEvent.encode = function encode(message, writer) { + if (!writer) + writer = $Writer.create(); + if (message.amount != null && Object.hasOwnProperty.call(message, "amount")) + writer.uint32(/* id 1, wireType 0 =*/8).uint64(message.amount); + if (message.actionsDigest != null && Object.hasOwnProperty.call(message, "actionsDigest")) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.actionsDigest); + if (message.publicNote != null && Object.hasOwnProperty.call(message, "publicNote")) + writer.uint32(/* id 3, wireType 2 =*/26).string(message.publicNote); + return writer; + }; + + /** + * Encodes the specified BurnFromPoolEvent message, length delimited. Does not implicitly {@link org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.verify|verify} messages. + * @function encodeDelimited + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @static + * @param {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IBurnFromPoolEvent} message BurnFromPoolEvent message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + BurnFromPoolEvent.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a BurnFromPoolEvent message from the specified reader or buffer. + * @function decode + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} BurnFromPoolEvent + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + BurnFromPoolEvent.decode = function decode(reader, length) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent(); + while (reader.pos < end) { + var tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + message.amount = reader.uint64(); + break; + case 2: + message.actionsDigest = reader.bytes(); + break; + case 3: + message.publicNote = reader.string(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes a BurnFromPoolEvent message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} BurnFromPoolEvent + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + BurnFromPoolEvent.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a BurnFromPoolEvent message. + * @function verify + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + BurnFromPoolEvent.verify = function verify(message) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (message.amount != null && message.hasOwnProperty("amount")) + if (!$util.isInteger(message.amount) && !(message.amount && $util.isInteger(message.amount.low) && $util.isInteger(message.amount.high))) + return "amount: integer|Long expected"; + if (message.actionsDigest != null && message.hasOwnProperty("actionsDigest")) + if (!(message.actionsDigest && typeof message.actionsDigest.length === "number" || $util.isString(message.actionsDigest))) + return "actionsDigest: buffer expected"; + if (message.publicNote != null && message.hasOwnProperty("publicNote")) + if (!$util.isString(message.publicNote)) + return "publicNote: string expected"; + return null; + }; + + /** + * Creates a BurnFromPoolEvent message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @static + * @param {Object.} object Plain object + * @returns {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} BurnFromPoolEvent + */ + BurnFromPoolEvent.fromObject = function fromObject(object) { + if (object instanceof $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent) + return object; + var message = new $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent(); + if (object.amount != null) + if ($util.Long) + (message.amount = $util.Long.fromValue(object.amount)).unsigned = true; + else if (typeof object.amount === "string") + message.amount = parseInt(object.amount, 10); + else if (typeof object.amount === "number") + message.amount = object.amount; + else if (typeof object.amount === "object") + message.amount = new $util.LongBits(object.amount.low >>> 0, object.amount.high >>> 0).toNumber(true); + if (object.actionsDigest != null) + if (typeof object.actionsDigest === "string") + $util.base64.decode(object.actionsDigest, message.actionsDigest = $util.newBuffer($util.base64.length(object.actionsDigest)), 0); + else if (object.actionsDigest.length >= 0) + message.actionsDigest = object.actionsDigest; + if (object.publicNote != null) + message.publicNote = String(object.publicNote); + return message; + }; + + /** + * Creates a plain object from a BurnFromPoolEvent message. Also converts values to other types if specified. + * @function toObject + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @static + * @param {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} message BurnFromPoolEvent + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + BurnFromPoolEvent.toObject = function toObject(message, options) { + if (!options) + options = {}; + var object = {}; + if (options.defaults) { + if ($util.Long) { + var long = new $util.Long(0, 0, true); + object.amount = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : long; + } else + object.amount = options.longs === String ? "0" : 0; + if (options.bytes === String) + object.actionsDigest = ""; + else { + object.actionsDigest = []; + if (options.bytes !== Array) + object.actionsDigest = $util.newBuffer(object.actionsDigest); + } + object.publicNote = ""; + } + if (message.amount != null && message.hasOwnProperty("amount")) + if (typeof message.amount === "number") + object.amount = options.longs === String ? String(message.amount) : message.amount; + else + object.amount = options.longs === String ? $util.Long.prototype.toString.call(message.amount) : options.longs === Number ? new $util.LongBits(message.amount.low >>> 0, message.amount.high >>> 0).toNumber(true) : message.amount; + if (message.actionsDigest != null && message.hasOwnProperty("actionsDigest")) + object.actionsDigest = options.bytes === String ? $util.base64.encode(message.actionsDigest, 0, message.actionsDigest.length) : options.bytes === Array ? Array.prototype.slice.call(message.actionsDigest) : message.actionsDigest; + if (message.publicNote != null && message.hasOwnProperty("publicNote")) + object.publicNote = message.publicNote; + return object; + }; + + /** + * Converts this BurnFromPoolEvent to JSON. + * @function toJSON + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent + * @instance + * @returns {Object.} JSON object + */ + BurnFromPoolEvent.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + return BurnFromPoolEvent; + })(); + GetGroupActionsResponseV0.GroupActionEvent = (function() { /** @@ -99369,6 +99879,8 @@ $root.org = (function() { * @property {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IEmergencyActionEvent|null} [emergencyAction] TokenEvent emergencyAction * @property {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ITokenConfigUpdateEvent|null} [tokenConfigUpdate] TokenEvent tokenConfigUpdate * @property {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IUpdateDirectPurchasePriceEvent|null} [updatePrice] TokenEvent updatePrice + * @property {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IMintToPoolEvent|null} [mintToPool] TokenEvent mintToPool + * @property {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IBurnFromPoolEvent|null} [burnFromPool] TokenEvent burnFromPool */ /** @@ -99450,17 +99962,33 @@ $root.org = (function() { */ TokenEvent.prototype.updatePrice = null; + /** + * TokenEvent mintToPool. + * @member {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IMintToPoolEvent|null|undefined} mintToPool + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent + * @instance + */ + TokenEvent.prototype.mintToPool = null; + + /** + * TokenEvent burnFromPool. + * @member {org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.IBurnFromPoolEvent|null|undefined} burnFromPool + * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent + * @instance + */ + TokenEvent.prototype.burnFromPool = null; + // OneOf field names bound to virtual getters and setters var $oneOfFields; /** * TokenEvent type. - * @member {"mint"|"burn"|"freeze"|"unfreeze"|"destroyFrozenFunds"|"emergencyAction"|"tokenConfigUpdate"|"updatePrice"|undefined} type + * @member {"mint"|"burn"|"freeze"|"unfreeze"|"destroyFrozenFunds"|"emergencyAction"|"tokenConfigUpdate"|"updatePrice"|"mintToPool"|"burnFromPool"|undefined} type * @memberof org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent * @instance */ Object.defineProperty(TokenEvent.prototype, "type", { - get: $util.oneOfGetter($oneOfFields = ["mint", "burn", "freeze", "unfreeze", "destroyFrozenFunds", "emergencyAction", "tokenConfigUpdate", "updatePrice"]), + get: $util.oneOfGetter($oneOfFields = ["mint", "burn", "freeze", "unfreeze", "destroyFrozenFunds", "emergencyAction", "tokenConfigUpdate", "updatePrice", "mintToPool", "burnFromPool"]), set: $util.oneOfSetter($oneOfFields) }); @@ -99504,6 +100032,10 @@ $root.org = (function() { $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenConfigUpdateEvent.encode(message.tokenConfigUpdate, writer.uint32(/* id 7, wireType 2 =*/58).fork()).ldelim(); if (message.updatePrice != null && Object.hasOwnProperty.call(message, "updatePrice")) $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.encode(message.updatePrice, writer.uint32(/* id 8, wireType 2 =*/66).fork()).ldelim(); + if (message.mintToPool != null && Object.hasOwnProperty.call(message, "mintToPool")) + $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.encode(message.mintToPool, writer.uint32(/* id 9, wireType 2 =*/74).fork()).ldelim(); + if (message.burnFromPool != null && Object.hasOwnProperty.call(message, "burnFromPool")) + $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.encode(message.burnFromPool, writer.uint32(/* id 10, wireType 2 =*/82).fork()).ldelim(); return writer; }; @@ -99562,6 +100094,12 @@ $root.org = (function() { case 8: message.updatePrice = $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.decode(reader, reader.uint32()); break; + case 9: + message.mintToPool = $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.decode(reader, reader.uint32()); + break; + case 10: + message.burnFromPool = $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.decode(reader, reader.uint32()); + break; default: reader.skipType(tag & 7); break; @@ -99676,6 +100214,26 @@ $root.org = (function() { return "updatePrice." + error; } } + if (message.mintToPool != null && message.hasOwnProperty("mintToPool")) { + if (properties.type === 1) + return "type: multiple values"; + properties.type = 1; + { + var error = $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.verify(message.mintToPool); + if (error) + return "mintToPool." + error; + } + } + if (message.burnFromPool != null && message.hasOwnProperty("burnFromPool")) { + if (properties.type === 1) + return "type: multiple values"; + properties.type = 1; + { + var error = $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.verify(message.burnFromPool); + if (error) + return "burnFromPool." + error; + } + } return null; }; @@ -99731,6 +100289,16 @@ $root.org = (function() { throw TypeError(".org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.updatePrice: object expected"); message.updatePrice = $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.fromObject(object.updatePrice); } + if (object.mintToPool != null) { + if (typeof object.mintToPool !== "object") + throw TypeError(".org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.mintToPool: object expected"); + message.mintToPool = $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.fromObject(object.mintToPool); + } + if (object.burnFromPool != null) { + if (typeof object.burnFromPool !== "object") + throw TypeError(".org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.burnFromPool: object expected"); + message.burnFromPool = $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.fromObject(object.burnFromPool); + } return message; }; @@ -99787,6 +100355,16 @@ $root.org = (function() { if (options.oneofs) object.type = "updatePrice"; } + if (message.mintToPool != null && message.hasOwnProperty("mintToPool")) { + object.mintToPool = $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.toObject(message.mintToPool, options); + if (options.oneofs) + object.type = "mintToPool"; + } + if (message.burnFromPool != null && message.hasOwnProperty("burnFromPool")) { + object.burnFromPool = $root.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.toObject(message.burnFromPool, options); + if (options.oneofs) + object.type = "burnFromPool"; + } return object; }; @@ -109886,6 +110464,7 @@ $root.org = (function() { * @property {number|Long|null} [startIndex] GetShieldedEncryptedNotesRequestV0 startIndex * @property {number|null} [count] GetShieldedEncryptedNotesRequestV0 count * @property {boolean|null} [prove] GetShieldedEncryptedNotesRequestV0 prove + * @property {Uint8Array|null} [tokenId] GetShieldedEncryptedNotesRequestV0 tokenId */ /** @@ -109927,6 +110506,14 @@ $root.org = (function() { */ GetShieldedEncryptedNotesRequestV0.prototype.prove = false; + /** + * GetShieldedEncryptedNotesRequestV0 tokenId. + * @member {Uint8Array} tokenId + * @memberof org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0 + * @instance + */ + GetShieldedEncryptedNotesRequestV0.prototype.tokenId = $util.newBuffer([]); + /** * Creates a new GetShieldedEncryptedNotesRequestV0 instance using the specified properties. * @function create @@ -109957,6 +110544,8 @@ $root.org = (function() { writer.uint32(/* id 2, wireType 0 =*/16).uint32(message.count); if (message.prove != null && Object.hasOwnProperty.call(message, "prove")) writer.uint32(/* id 3, wireType 0 =*/24).bool(message.prove); + if (message.tokenId != null && Object.hasOwnProperty.call(message, "tokenId")) + writer.uint32(/* id 4, wireType 2 =*/34).bytes(message.tokenId); return writer; }; @@ -110000,6 +110589,9 @@ $root.org = (function() { case 3: message.prove = reader.bool(); break; + case 4: + message.tokenId = reader.bytes(); + break; default: reader.skipType(tag & 7); break; @@ -110044,6 +110636,9 @@ $root.org = (function() { if (message.prove != null && message.hasOwnProperty("prove")) if (typeof message.prove !== "boolean") return "prove: boolean expected"; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + if (!(message.tokenId && typeof message.tokenId.length === "number" || $util.isString(message.tokenId))) + return "tokenId: buffer expected"; return null; }; @@ -110072,6 +110667,11 @@ $root.org = (function() { message.count = object.count >>> 0; if (object.prove != null) message.prove = Boolean(object.prove); + if (object.tokenId != null) + if (typeof object.tokenId === "string") + $util.base64.decode(object.tokenId, message.tokenId = $util.newBuffer($util.base64.length(object.tokenId)), 0); + else if (object.tokenId.length >= 0) + message.tokenId = object.tokenId; return message; }; @@ -110096,6 +110696,13 @@ $root.org = (function() { object.startIndex = options.longs === String ? "0" : 0; object.count = 0; object.prove = false; + if (options.bytes === String) + object.tokenId = ""; + else { + object.tokenId = []; + if (options.bytes !== Array) + object.tokenId = $util.newBuffer(object.tokenId); + } } if (message.startIndex != null && message.hasOwnProperty("startIndex")) if (typeof message.startIndex === "number") @@ -110106,6 +110713,8 @@ $root.org = (function() { object.count = message.count; if (message.prove != null && message.hasOwnProperty("prove")) object.prove = message.prove; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + object.tokenId = options.bytes === String ? $util.base64.encode(message.tokenId, 0, message.tokenId.length) : options.bytes === Array ? Array.prototype.slice.call(message.tokenId) : message.tokenId; return object; }; @@ -111323,6 +111932,7 @@ $root.org = (function() { * @memberof org.dash.platform.dapi.v0.GetShieldedAnchorsRequest * @interface IGetShieldedAnchorsRequestV0 * @property {boolean|null} [prove] GetShieldedAnchorsRequestV0 prove + * @property {Uint8Array|null} [tokenId] GetShieldedAnchorsRequestV0 tokenId */ /** @@ -111348,6 +111958,14 @@ $root.org = (function() { */ GetShieldedAnchorsRequestV0.prototype.prove = false; + /** + * GetShieldedAnchorsRequestV0 tokenId. + * @member {Uint8Array} tokenId + * @memberof org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0 + * @instance + */ + GetShieldedAnchorsRequestV0.prototype.tokenId = $util.newBuffer([]); + /** * Creates a new GetShieldedAnchorsRequestV0 instance using the specified properties. * @function create @@ -111374,6 +111992,8 @@ $root.org = (function() { writer = $Writer.create(); if (message.prove != null && Object.hasOwnProperty.call(message, "prove")) writer.uint32(/* id 1, wireType 0 =*/8).bool(message.prove); + if (message.tokenId != null && Object.hasOwnProperty.call(message, "tokenId")) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.tokenId); return writer; }; @@ -111411,6 +112031,9 @@ $root.org = (function() { case 1: message.prove = reader.bool(); break; + case 2: + message.tokenId = reader.bytes(); + break; default: reader.skipType(tag & 7); break; @@ -111449,6 +112072,9 @@ $root.org = (function() { if (message.prove != null && message.hasOwnProperty("prove")) if (typeof message.prove !== "boolean") return "prove: boolean expected"; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + if (!(message.tokenId && typeof message.tokenId.length === "number" || $util.isString(message.tokenId))) + return "tokenId: buffer expected"; return null; }; @@ -111466,6 +112092,11 @@ $root.org = (function() { var message = new $root.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0(); if (object.prove != null) message.prove = Boolean(object.prove); + if (object.tokenId != null) + if (typeof object.tokenId === "string") + $util.base64.decode(object.tokenId, message.tokenId = $util.newBuffer($util.base64.length(object.tokenId)), 0); + else if (object.tokenId.length >= 0) + message.tokenId = object.tokenId; return message; }; @@ -111482,10 +112113,20 @@ $root.org = (function() { if (!options) options = {}; var object = {}; - if (options.defaults) + if (options.defaults) { object.prove = false; + if (options.bytes === String) + object.tokenId = ""; + else { + object.tokenId = []; + if (options.bytes !== Array) + object.tokenId = $util.newBuffer(object.tokenId); + } + } if (message.prove != null && message.hasOwnProperty("prove")) object.prove = message.prove; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + object.tokenId = options.bytes === String ? $util.base64.encode(message.tokenId, 0, message.tokenId.length) : options.bytes === Array ? Array.prototype.slice.call(message.tokenId) : message.tokenId; return object; }; @@ -112411,6 +113052,7 @@ $root.org = (function() { * @memberof org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest * @interface IGetMostRecentShieldedAnchorRequestV0 * @property {boolean|null} [prove] GetMostRecentShieldedAnchorRequestV0 prove + * @property {Uint8Array|null} [tokenId] GetMostRecentShieldedAnchorRequestV0 tokenId */ /** @@ -112436,6 +113078,14 @@ $root.org = (function() { */ GetMostRecentShieldedAnchorRequestV0.prototype.prove = false; + /** + * GetMostRecentShieldedAnchorRequestV0 tokenId. + * @member {Uint8Array} tokenId + * @memberof org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0 + * @instance + */ + GetMostRecentShieldedAnchorRequestV0.prototype.tokenId = $util.newBuffer([]); + /** * Creates a new GetMostRecentShieldedAnchorRequestV0 instance using the specified properties. * @function create @@ -112462,6 +113112,8 @@ $root.org = (function() { writer = $Writer.create(); if (message.prove != null && Object.hasOwnProperty.call(message, "prove")) writer.uint32(/* id 1, wireType 0 =*/8).bool(message.prove); + if (message.tokenId != null && Object.hasOwnProperty.call(message, "tokenId")) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.tokenId); return writer; }; @@ -112499,6 +113151,9 @@ $root.org = (function() { case 1: message.prove = reader.bool(); break; + case 2: + message.tokenId = reader.bytes(); + break; default: reader.skipType(tag & 7); break; @@ -112537,6 +113192,9 @@ $root.org = (function() { if (message.prove != null && message.hasOwnProperty("prove")) if (typeof message.prove !== "boolean") return "prove: boolean expected"; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + if (!(message.tokenId && typeof message.tokenId.length === "number" || $util.isString(message.tokenId))) + return "tokenId: buffer expected"; return null; }; @@ -112554,6 +113212,11 @@ $root.org = (function() { var message = new $root.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0(); if (object.prove != null) message.prove = Boolean(object.prove); + if (object.tokenId != null) + if (typeof object.tokenId === "string") + $util.base64.decode(object.tokenId, message.tokenId = $util.newBuffer($util.base64.length(object.tokenId)), 0); + else if (object.tokenId.length >= 0) + message.tokenId = object.tokenId; return message; }; @@ -112570,10 +113233,20 @@ $root.org = (function() { if (!options) options = {}; var object = {}; - if (options.defaults) + if (options.defaults) { object.prove = false; + if (options.bytes === String) + object.tokenId = ""; + else { + object.tokenId = []; + if (options.bytes !== Array) + object.tokenId = $util.newBuffer(object.tokenId); + } + } if (message.prove != null && message.hasOwnProperty("prove")) object.prove = message.prove; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + object.tokenId = options.bytes === String ? $util.base64.encode(message.tokenId, 0, message.tokenId.length) : options.bytes === Array ? Array.prototype.slice.call(message.tokenId) : message.tokenId; return object; }; @@ -113290,6 +113963,7 @@ $root.org = (function() { * @memberof org.dash.platform.dapi.v0.GetShieldedPoolStateRequest * @interface IGetShieldedPoolStateRequestV0 * @property {boolean|null} [prove] GetShieldedPoolStateRequestV0 prove + * @property {Uint8Array|null} [tokenId] GetShieldedPoolStateRequestV0 tokenId */ /** @@ -113315,6 +113989,14 @@ $root.org = (function() { */ GetShieldedPoolStateRequestV0.prototype.prove = false; + /** + * GetShieldedPoolStateRequestV0 tokenId. + * @member {Uint8Array} tokenId + * @memberof org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0 + * @instance + */ + GetShieldedPoolStateRequestV0.prototype.tokenId = $util.newBuffer([]); + /** * Creates a new GetShieldedPoolStateRequestV0 instance using the specified properties. * @function create @@ -113341,6 +114023,8 @@ $root.org = (function() { writer = $Writer.create(); if (message.prove != null && Object.hasOwnProperty.call(message, "prove")) writer.uint32(/* id 1, wireType 0 =*/8).bool(message.prove); + if (message.tokenId != null && Object.hasOwnProperty.call(message, "tokenId")) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.tokenId); return writer; }; @@ -113378,6 +114062,9 @@ $root.org = (function() { case 1: message.prove = reader.bool(); break; + case 2: + message.tokenId = reader.bytes(); + break; default: reader.skipType(tag & 7); break; @@ -113416,6 +114103,9 @@ $root.org = (function() { if (message.prove != null && message.hasOwnProperty("prove")) if (typeof message.prove !== "boolean") return "prove: boolean expected"; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + if (!(message.tokenId && typeof message.tokenId.length === "number" || $util.isString(message.tokenId))) + return "tokenId: buffer expected"; return null; }; @@ -113433,6 +114123,11 @@ $root.org = (function() { var message = new $root.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0(); if (object.prove != null) message.prove = Boolean(object.prove); + if (object.tokenId != null) + if (typeof object.tokenId === "string") + $util.base64.decode(object.tokenId, message.tokenId = $util.newBuffer($util.base64.length(object.tokenId)), 0); + else if (object.tokenId.length >= 0) + message.tokenId = object.tokenId; return message; }; @@ -113449,10 +114144,20 @@ $root.org = (function() { if (!options) options = {}; var object = {}; - if (options.defaults) + if (options.defaults) { object.prove = false; + if (options.bytes === String) + object.tokenId = ""; + else { + object.tokenId = []; + if (options.bytes !== Array) + object.tokenId = $util.newBuffer(object.tokenId); + } + } if (message.prove != null && message.hasOwnProperty("prove")) object.prove = message.prove; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + object.tokenId = options.bytes === String ? $util.base64.encode(message.tokenId, 0, message.tokenId.length) : options.bytes === Array ? Array.prototype.slice.call(message.tokenId) : message.tokenId; return object; }; @@ -114176,6 +114881,7 @@ $root.org = (function() { * @memberof org.dash.platform.dapi.v0.GetShieldedNotesCountRequest * @interface IGetShieldedNotesCountRequestV0 * @property {boolean|null} [prove] GetShieldedNotesCountRequestV0 prove + * @property {Uint8Array|null} [tokenId] GetShieldedNotesCountRequestV0 tokenId */ /** @@ -114201,6 +114907,14 @@ $root.org = (function() { */ GetShieldedNotesCountRequestV0.prototype.prove = false; + /** + * GetShieldedNotesCountRequestV0 tokenId. + * @member {Uint8Array} tokenId + * @memberof org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0 + * @instance + */ + GetShieldedNotesCountRequestV0.prototype.tokenId = $util.newBuffer([]); + /** * Creates a new GetShieldedNotesCountRequestV0 instance using the specified properties. * @function create @@ -114227,6 +114941,8 @@ $root.org = (function() { writer = $Writer.create(); if (message.prove != null && Object.hasOwnProperty.call(message, "prove")) writer.uint32(/* id 1, wireType 0 =*/8).bool(message.prove); + if (message.tokenId != null && Object.hasOwnProperty.call(message, "tokenId")) + writer.uint32(/* id 2, wireType 2 =*/18).bytes(message.tokenId); return writer; }; @@ -114264,6 +114980,9 @@ $root.org = (function() { case 1: message.prove = reader.bool(); break; + case 2: + message.tokenId = reader.bytes(); + break; default: reader.skipType(tag & 7); break; @@ -114302,6 +115021,9 @@ $root.org = (function() { if (message.prove != null && message.hasOwnProperty("prove")) if (typeof message.prove !== "boolean") return "prove: boolean expected"; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + if (!(message.tokenId && typeof message.tokenId.length === "number" || $util.isString(message.tokenId))) + return "tokenId: buffer expected"; return null; }; @@ -114319,6 +115041,11 @@ $root.org = (function() { var message = new $root.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0(); if (object.prove != null) message.prove = Boolean(object.prove); + if (object.tokenId != null) + if (typeof object.tokenId === "string") + $util.base64.decode(object.tokenId, message.tokenId = $util.newBuffer($util.base64.length(object.tokenId)), 0); + else if (object.tokenId.length >= 0) + message.tokenId = object.tokenId; return message; }; @@ -114335,10 +115062,20 @@ $root.org = (function() { if (!options) options = {}; var object = {}; - if (options.defaults) + if (options.defaults) { object.prove = false; + if (options.bytes === String) + object.tokenId = ""; + else { + object.tokenId = []; + if (options.bytes !== Array) + object.tokenId = $util.newBuffer(object.tokenId); + } + } if (message.prove != null && message.hasOwnProperty("prove")) object.prove = message.prove; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + object.tokenId = options.bytes === String ? $util.base64.encode(message.tokenId, 0, message.tokenId.length) : options.bytes === Array ? Array.prototype.slice.call(message.tokenId) : message.tokenId; return object; }; @@ -115063,6 +115800,7 @@ $root.org = (function() { * @interface IGetShieldedNullifiersRequestV0 * @property {Array.|null} [nullifiers] GetShieldedNullifiersRequestV0 nullifiers * @property {boolean|null} [prove] GetShieldedNullifiersRequestV0 prove + * @property {Uint8Array|null} [tokenId] GetShieldedNullifiersRequestV0 tokenId */ /** @@ -115097,6 +115835,14 @@ $root.org = (function() { */ GetShieldedNullifiersRequestV0.prototype.prove = false; + /** + * GetShieldedNullifiersRequestV0 tokenId. + * @member {Uint8Array} tokenId + * @memberof org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0 + * @instance + */ + GetShieldedNullifiersRequestV0.prototype.tokenId = $util.newBuffer([]); + /** * Creates a new GetShieldedNullifiersRequestV0 instance using the specified properties. * @function create @@ -115126,6 +115872,8 @@ $root.org = (function() { writer.uint32(/* id 1, wireType 2 =*/10).bytes(message.nullifiers[i]); if (message.prove != null && Object.hasOwnProperty.call(message, "prove")) writer.uint32(/* id 2, wireType 0 =*/16).bool(message.prove); + if (message.tokenId != null && Object.hasOwnProperty.call(message, "tokenId")) + writer.uint32(/* id 3, wireType 2 =*/26).bytes(message.tokenId); return writer; }; @@ -115168,6 +115916,9 @@ $root.org = (function() { case 2: message.prove = reader.bool(); break; + case 3: + message.tokenId = reader.bytes(); + break; default: reader.skipType(tag & 7); break; @@ -115213,6 +115964,9 @@ $root.org = (function() { if (message.prove != null && message.hasOwnProperty("prove")) if (typeof message.prove !== "boolean") return "prove: boolean expected"; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + if (!(message.tokenId && typeof message.tokenId.length === "number" || $util.isString(message.tokenId))) + return "tokenId: buffer expected"; return null; }; @@ -115240,6 +115994,11 @@ $root.org = (function() { } if (object.prove != null) message.prove = Boolean(object.prove); + if (object.tokenId != null) + if (typeof object.tokenId === "string") + $util.base64.decode(object.tokenId, message.tokenId = $util.newBuffer($util.base64.length(object.tokenId)), 0); + else if (object.tokenId.length >= 0) + message.tokenId = object.tokenId; return message; }; @@ -115258,8 +116017,16 @@ $root.org = (function() { var object = {}; if (options.arrays || options.defaults) object.nullifiers = []; - if (options.defaults) + if (options.defaults) { object.prove = false; + if (options.bytes === String) + object.tokenId = ""; + else { + object.tokenId = []; + if (options.bytes !== Array) + object.tokenId = $util.newBuffer(object.tokenId); + } + } if (message.nullifiers && message.nullifiers.length) { object.nullifiers = []; for (var j = 0; j < message.nullifiers.length; ++j) @@ -115267,6 +116034,8 @@ $root.org = (function() { } if (message.prove != null && message.hasOwnProperty("prove")) object.prove = message.prove; + if (message.tokenId != null && message.hasOwnProperty("tokenId")) + object.tokenId = options.bytes === String ? $util.base64.encode(message.tokenId, 0, message.tokenId.length) : options.bytes === Array ? Array.prototype.slice.call(message.tokenId) : message.tokenId; return object; }; diff --git a/packages/dapi-grpc/clients/platform/v0/nodejs/platform_protoc.js b/packages/dapi-grpc/clients/platform/v0/nodejs/platform_protoc.js index 8fe2b482632..d2e380a4f8a 100644 --- a/packages/dapi-grpc/clients/platform/v0/nodejs/platform_protoc.js +++ b/packages/dapi-grpc/clients/platform/v0/nodejs/platform_protoc.js @@ -354,6 +354,7 @@ goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsRequest.Versio goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnEvent', null, { proto }); +goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ContractEvent', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ContractEvent.TypeCase', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ContractUpdateEvent', null, { proto }); @@ -369,6 +370,7 @@ goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGr goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionEvent.EventTypeCase', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActions', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintEvent', null, { proto }); +goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.PersonalEncryptedNote', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ResultCase', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.SharedEncryptedNote', null, { proto }); @@ -8984,6 +8986,48 @@ if (goog.DEBUG && !COMPILED) { */ proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.PricingSchedule.displayName = 'proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.PricingSchedule'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.displayName = 'proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.displayName = 'proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent'; +} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -97785,6 +97829,470 @@ proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.toObject = function(opt_includeInstance) { + return proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.toObject = function(includeInstance, msg) { + var f, obj = { + amount: jspb.Message.getFieldWithDefault(msg, 1, 0), + actionsDigest: msg.getActionsDigest_asB64(), + publicNote: jspb.Message.getFieldWithDefault(msg, 3, "") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent; + return proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {number} */ (reader.readUint64()); + msg.setAmount(value); + break; + case 2: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setActionsDigest(value); + break; + case 3: + var value = /** @type {string} */ (reader.readString()); + msg.setPublicNote(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getAmount(); + if (f !== 0) { + writer.writeUint64( + 1, + f + ); + } + f = message.getActionsDigest_asU8(); + if (f.length > 0) { + writer.writeBytes( + 2, + f + ); + } + f = /** @type {string} */ (jspb.Message.getField(message, 3)); + if (f != null) { + writer.writeString( + 3, + f + ); + } +}; + + +/** + * optional uint64 amount = 1; + * @return {number} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.getAmount = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} returns this + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.setAmount = function(value) { + return jspb.Message.setProto3IntField(this, 1, value); +}; + + +/** + * optional bytes actions_digest = 2; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.getActionsDigest = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * optional bytes actions_digest = 2; + * This is a type-conversion wrapper around `getActionsDigest()` + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.getActionsDigest_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getActionsDigest())); +}; + + +/** + * optional bytes actions_digest = 2; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getActionsDigest()` + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.getActionsDigest_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getActionsDigest())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} returns this + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.setActionsDigest = function(value) { + return jspb.Message.setProto3BytesField(this, 2, value); +}; + + +/** + * optional string public_note = 3; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.getPublicNote = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "")); +}; + + +/** + * @param {string} value + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} returns this + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.setPublicNote = function(value) { + return jspb.Message.setField(this, 3, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} returns this + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.clearPublicNote = function() { + return jspb.Message.setField(this, 3, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.hasPublicNote = function() { + return jspb.Message.getField(this, 3) != null; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.toObject = function(opt_includeInstance) { + return proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.toObject = function(includeInstance, msg) { + var f, obj = { + amount: jspb.Message.getFieldWithDefault(msg, 1, 0), + actionsDigest: msg.getActionsDigest_asB64(), + publicNote: jspb.Message.getFieldWithDefault(msg, 3, "") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent; + return proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {number} */ (reader.readUint64()); + msg.setAmount(value); + break; + case 2: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setActionsDigest(value); + break; + case 3: + var value = /** @type {string} */ (reader.readString()); + msg.setPublicNote(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getAmount(); + if (f !== 0) { + writer.writeUint64( + 1, + f + ); + } + f = message.getActionsDigest_asU8(); + if (f.length > 0) { + writer.writeBytes( + 2, + f + ); + } + f = /** @type {string} */ (jspb.Message.getField(message, 3)); + if (f != null) { + writer.writeString( + 3, + f + ); + } +}; + + +/** + * optional uint64 amount = 1; + * @return {number} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.getAmount = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} returns this + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.setAmount = function(value) { + return jspb.Message.setProto3IntField(this, 1, value); +}; + + +/** + * optional bytes actions_digest = 2; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.getActionsDigest = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * optional bytes actions_digest = 2; + * This is a type-conversion wrapper around `getActionsDigest()` + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.getActionsDigest_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getActionsDigest())); +}; + + +/** + * optional bytes actions_digest = 2; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getActionsDigest()` + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.getActionsDigest_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getActionsDigest())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} returns this + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.setActionsDigest = function(value) { + return jspb.Message.setProto3BytesField(this, 2, value); +}; + + +/** + * optional string public_note = 3; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.getPublicNote = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "")); +}; + + +/** + * @param {string} value + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} returns this + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.setPublicNote = function(value) { + return jspb.Message.setField(this, 3, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} returns this + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.clearPublicNote = function() { + return jspb.Message.setField(this, 3, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.hasPublicNote = function() { + return jspb.Message.getField(this, 3) != null; +}; + + + /** * Oneof group definitions for this message. Each group defines the field * numbers belonging to that group. When of these fields' value is set, all @@ -98733,7 +99241,7 @@ proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV * @private {!Array>} * @const */ -proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.oneofGroups_ = [[1,2,3,4,5,6,7,8]]; +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.oneofGroups_ = [[1,2,3,4,5,6,7,8,9,10]]; /** * @enum {number} @@ -98747,7 +99255,9 @@ proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV DESTROY_FROZEN_FUNDS: 5, EMERGENCY_ACTION: 6, TOKEN_CONFIG_UPDATE: 7, - UPDATE_PRICE: 8 + UPDATE_PRICE: 8, + MINT_TO_POOL: 9, + BURN_FROM_POOL: 10 }; /** @@ -98795,7 +99305,9 @@ proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV destroyFrozenFunds: (f = msg.getDestroyFrozenFunds()) && proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.DestroyFrozenFundsEvent.toObject(includeInstance, f), emergencyAction: (f = msg.getEmergencyAction()) && proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.EmergencyActionEvent.toObject(includeInstance, f), tokenConfigUpdate: (f = msg.getTokenConfigUpdate()) && proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenConfigUpdateEvent.toObject(includeInstance, f), - updatePrice: (f = msg.getUpdatePrice()) && proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.toObject(includeInstance, f) + updatePrice: (f = msg.getUpdatePrice()) && proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.toObject(includeInstance, f), + mintToPool: (f = msg.getMintToPool()) && proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.toObject(includeInstance, f), + burnFromPool: (f = msg.getBurnFromPool()) && proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.toObject(includeInstance, f) }; if (includeInstance) { @@ -98872,6 +99384,16 @@ proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV reader.readMessage(value,proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.deserializeBinaryFromReader); msg.setUpdatePrice(value); break; + case 9: + var value = new proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent; + reader.readMessage(value,proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.deserializeBinaryFromReader); + msg.setMintToPool(value); + break; + case 10: + var value = new proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent; + reader.readMessage(value,proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.deserializeBinaryFromReader); + msg.setBurnFromPool(value); + break; default: reader.skipField(); break; @@ -98965,6 +99487,22 @@ proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.serializeBinaryToWriter ); } + f = message.getMintToPool(); + if (f != null) { + writer.writeMessage( + 9, + f, + proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.serializeBinaryToWriter + ); + } + f = message.getBurnFromPool(); + if (f != null) { + writer.writeMessage( + 10, + f, + proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.serializeBinaryToWriter + ); + } }; @@ -99264,6 +99802,80 @@ proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV }; +/** + * optional MintToPoolEvent mint_to_pool = 9; + * @return {?proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.prototype.getMintToPool = function() { + return /** @type{?proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} */ ( + jspb.Message.getWrapperField(this, proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent, 9)); +}; + + +/** + * @param {?proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent|undefined} value + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent} returns this +*/ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.prototype.setMintToPool = function(value) { + return jspb.Message.setOneofWrapperField(this, 9, proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.oneofGroups_[0], value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent} returns this + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.prototype.clearMintToPool = function() { + return this.setMintToPool(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.prototype.hasMintToPool = function() { + return jspb.Message.getField(this, 9) != null; +}; + + +/** + * optional BurnFromPoolEvent burn_from_pool = 10; + * @return {?proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.prototype.getBurnFromPool = function() { + return /** @type{?proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} */ ( + jspb.Message.getWrapperField(this, proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent, 10)); +}; + + +/** + * @param {?proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent|undefined} value + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent} returns this +*/ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.prototype.setBurnFromPool = function(value) { + return jspb.Message.setOneofWrapperField(this, 10, proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.oneofGroups_[0], value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent} returns this + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.prototype.clearBurnFromPool = function() { + return this.setBurnFromPool(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.prototype.hasBurnFromPool = function() { + return jspb.Message.getField(this, 10) != null; +}; + + @@ -108004,7 +108616,8 @@ proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncr var f, obj = { startIndex: jspb.Message.getFieldWithDefault(msg, 1, 0), count: jspb.Message.getFieldWithDefault(msg, 2, 0), - prove: jspb.Message.getBooleanFieldWithDefault(msg, 3, false) + prove: jspb.Message.getBooleanFieldWithDefault(msg, 3, false), + tokenId: msg.getTokenId_asB64() }; if (includeInstance) { @@ -108053,6 +108666,10 @@ proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncr var value = /** @type {boolean} */ (reader.readBool()); msg.setProve(value); break; + case 4: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setTokenId(value); + break; default: reader.skipField(); break; @@ -108103,6 +108720,13 @@ proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncr f ); } + f = /** @type {!(string|Uint8Array)} */ (jspb.Message.getField(message, 4)); + if (f != null) { + writer.writeBytes( + 4, + f + ); + } }; @@ -108160,6 +108784,66 @@ proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncr }; +/** + * optional bytes token_id = 4; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0.prototype.getTokenId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, "")); +}; + + +/** + * optional bytes token_id = 4; + * This is a type-conversion wrapper around `getTokenId()` + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0.prototype.getTokenId_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getTokenId())); +}; + + +/** + * optional bytes token_id = 4; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getTokenId()` + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0.prototype.getTokenId_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getTokenId())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0.prototype.setTokenId = function(value) { + return jspb.Message.setField(this, 4, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0.prototype.clearTokenId = function() { + return jspb.Message.setField(this, 4, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0.prototype.hasTokenId = function() { + return jspb.Message.getField(this, 4) != null; +}; + + /** * optional GetShieldedEncryptedNotesRequestV0 v0 = 1; * @return {?proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0} @@ -109299,7 +109983,8 @@ proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequ */ proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0.toObject = function(includeInstance, msg) { var f, obj = { - prove: jspb.Message.getBooleanFieldWithDefault(msg, 1, false) + prove: jspb.Message.getBooleanFieldWithDefault(msg, 1, false), + tokenId: msg.getTokenId_asB64() }; if (includeInstance) { @@ -109340,6 +110025,10 @@ proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequ var value = /** @type {boolean} */ (reader.readBool()); msg.setProve(value); break; + case 2: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setTokenId(value); + break; default: reader.skipField(); break; @@ -109376,6 +110065,13 @@ proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequ f ); } + f = /** @type {!(string|Uint8Array)} */ (jspb.Message.getField(message, 2)); + if (f != null) { + writer.writeBytes( + 2, + f + ); + } }; @@ -109397,6 +110093,66 @@ proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequ }; +/** + * optional bytes token_id = 2; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0.prototype.getTokenId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * optional bytes token_id = 2; + * This is a type-conversion wrapper around `getTokenId()` + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0.prototype.getTokenId_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getTokenId())); +}; + + +/** + * optional bytes token_id = 2; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getTokenId()` + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0.prototype.getTokenId_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getTokenId())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0.prototype.setTokenId = function(value) { + return jspb.Message.setField(this, 2, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0.prototype.clearTokenId = function() { + return jspb.Message.setField(this, 2, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0.prototype.hasTokenId = function() { + return jspb.Message.getField(this, 2) != null; +}; + + /** * optional GetShieldedAnchorsRequestV0 v0 = 1; * @return {?proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0} @@ -110240,7 +110996,8 @@ proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecent */ proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0.toObject = function(includeInstance, msg) { var f, obj = { - prove: jspb.Message.getBooleanFieldWithDefault(msg, 1, false) + prove: jspb.Message.getBooleanFieldWithDefault(msg, 1, false), + tokenId: msg.getTokenId_asB64() }; if (includeInstance) { @@ -110281,6 +111038,10 @@ proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecent var value = /** @type {boolean} */ (reader.readBool()); msg.setProve(value); break; + case 2: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setTokenId(value); + break; default: reader.skipField(); break; @@ -110317,6 +111078,13 @@ proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecent f ); } + f = /** @type {!(string|Uint8Array)} */ (jspb.Message.getField(message, 2)); + if (f != null) { + writer.writeBytes( + 2, + f + ); + } }; @@ -110338,6 +111106,66 @@ proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecent }; +/** + * optional bytes token_id = 2; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0.prototype.getTokenId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * optional bytes token_id = 2; + * This is a type-conversion wrapper around `getTokenId()` + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0.prototype.getTokenId_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getTokenId())); +}; + + +/** + * optional bytes token_id = 2; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getTokenId()` + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0.prototype.getTokenId_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getTokenId())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0.prototype.setTokenId = function(value) { + return jspb.Message.setField(this, 2, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0.prototype.clearTokenId = function() { + return jspb.Message.setField(this, 2, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0.prototype.hasTokenId = function() { + return jspb.Message.getField(this, 2) != null; +}; + + /** * optional GetMostRecentShieldedAnchorRequestV0 v0 = 1; * @return {?proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0} @@ -111022,7 +111850,8 @@ proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolState */ proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0.toObject = function(includeInstance, msg) { var f, obj = { - prove: jspb.Message.getBooleanFieldWithDefault(msg, 1, false) + prove: jspb.Message.getBooleanFieldWithDefault(msg, 1, false), + tokenId: msg.getTokenId_asB64() }; if (includeInstance) { @@ -111063,6 +111892,10 @@ proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolState var value = /** @type {boolean} */ (reader.readBool()); msg.setProve(value); break; + case 2: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setTokenId(value); + break; default: reader.skipField(); break; @@ -111099,6 +111932,13 @@ proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolState f ); } + f = /** @type {!(string|Uint8Array)} */ (jspb.Message.getField(message, 2)); + if (f != null) { + writer.writeBytes( + 2, + f + ); + } }; @@ -111120,6 +111960,66 @@ proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolState }; +/** + * optional bytes token_id = 2; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0.prototype.getTokenId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * optional bytes token_id = 2; + * This is a type-conversion wrapper around `getTokenId()` + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0.prototype.getTokenId_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getTokenId())); +}; + + +/** + * optional bytes token_id = 2; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getTokenId()` + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0.prototype.getTokenId_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getTokenId())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0.prototype.setTokenId = function(value) { + return jspb.Message.setField(this, 2, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0.prototype.clearTokenId = function() { + return jspb.Message.setField(this, 2, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0.prototype.hasTokenId = function() { + return jspb.Message.getField(this, 2) != null; +}; + + /** * optional GetShieldedPoolStateRequestV0 v0 = 1; * @return {?proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0} @@ -111780,7 +112680,8 @@ proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCou */ proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0.toObject = function(includeInstance, msg) { var f, obj = { - prove: jspb.Message.getBooleanFieldWithDefault(msg, 1, false) + prove: jspb.Message.getBooleanFieldWithDefault(msg, 1, false), + tokenId: msg.getTokenId_asB64() }; if (includeInstance) { @@ -111821,6 +112722,10 @@ proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCou var value = /** @type {boolean} */ (reader.readBool()); msg.setProve(value); break; + case 2: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setTokenId(value); + break; default: reader.skipField(); break; @@ -111857,6 +112762,13 @@ proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCou f ); } + f = /** @type {!(string|Uint8Array)} */ (jspb.Message.getField(message, 2)); + if (f != null) { + writer.writeBytes( + 2, + f + ); + } }; @@ -111878,6 +112790,66 @@ proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCou }; +/** + * optional bytes token_id = 2; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0.prototype.getTokenId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * optional bytes token_id = 2; + * This is a type-conversion wrapper around `getTokenId()` + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0.prototype.getTokenId_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getTokenId())); +}; + + +/** + * optional bytes token_id = 2; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getTokenId()` + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0.prototype.getTokenId_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getTokenId())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0.prototype.setTokenId = function(value) { + return jspb.Message.setField(this, 2, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0.prototype.clearTokenId = function() { + return jspb.Message.setField(this, 2, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0.prototype.hasTokenId = function() { + return jspb.Message.getField(this, 2) != null; +}; + + /** * optional GetShieldedNotesCountRequestV0 v0 = 1; * @return {?proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0} @@ -112546,7 +113518,8 @@ proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifie proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0.toObject = function(includeInstance, msg) { var f, obj = { nullifiersList: msg.getNullifiersList_asB64(), - prove: jspb.Message.getBooleanFieldWithDefault(msg, 2, false) + prove: jspb.Message.getBooleanFieldWithDefault(msg, 2, false), + tokenId: msg.getTokenId_asB64() }; if (includeInstance) { @@ -112591,6 +113564,10 @@ proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifie var value = /** @type {boolean} */ (reader.readBool()); msg.setProve(value); break; + case 3: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setTokenId(value); + break; default: reader.skipField(); break; @@ -112634,6 +113611,13 @@ proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifie f ); } + f = /** @type {!(string|Uint8Array)} */ (jspb.Message.getField(message, 3)); + if (f != null) { + writer.writeBytes( + 3, + f + ); + } }; @@ -112716,6 +113700,66 @@ proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifie }; +/** + * optional bytes token_id = 3; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0.prototype.getTokenId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "")); +}; + + +/** + * optional bytes token_id = 3; + * This is a type-conversion wrapper around `getTokenId()` + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0.prototype.getTokenId_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getTokenId())); +}; + + +/** + * optional bytes token_id = 3; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getTokenId()` + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0.prototype.getTokenId_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getTokenId())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0.prototype.setTokenId = function(value) { + return jspb.Message.setField(this, 3, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0.prototype.clearTokenId = function() { + return jspb.Message.setField(this, 3, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0.prototype.hasTokenId = function() { + return jspb.Message.getField(this, 3) != null; +}; + + /** * optional GetShieldedNullifiersRequestV0 v0 = 1; * @return {?proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0} diff --git a/packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.h b/packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.h index f54c64a2361..0f400eb8083 100644 --- a/packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.h +++ b/packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.h @@ -192,6 +192,7 @@ CF_EXTERN_C_BEGIN @class GetGroupActionsRequest_StartAtActionId; @class GetGroupActionsResponse_GetGroupActionsResponseV0; @class GetGroupActionsResponse_GetGroupActionsResponseV0_BurnEvent; +@class GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent; @class GetGroupActionsResponse_GetGroupActionsResponseV0_ContractEvent; @class GetGroupActionsResponse_GetGroupActionsResponseV0_ContractUpdateEvent; @class GetGroupActionsResponse_GetGroupActionsResponseV0_DestroyFrozenFundsEvent; @@ -203,6 +204,7 @@ CF_EXTERN_C_BEGIN @class GetGroupActionsResponse_GetGroupActionsResponseV0_GroupActionEvent; @class GetGroupActionsResponse_GetGroupActionsResponseV0_GroupActions; @class GetGroupActionsResponse_GetGroupActionsResponseV0_MintEvent; +@class GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent; @class GetGroupActionsResponse_GetGroupActionsResponseV0_TokenConfigUpdateEvent; @class GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent; @class GetGroupActionsResponse_GetGroupActionsResponseV0_UnfreezeEvent; @@ -11567,6 +11569,58 @@ GPB_FINAL @interface GetGroupActionsResponse_GetGroupActionsResponseV0_UpdateDir @end +#pragma mark - GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent + +typedef GPB_ENUM(GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent_FieldNumber) { + GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent_FieldNumber_Amount = 1, + GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent_FieldNumber_ActionsDigest = 2, + GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent_FieldNumber_PublicNote = 3, +}; + +/** + * Mint straight into the token's shielded pool (protocol version 14+) + **/ +GPB_FINAL @interface GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent : GPBMessage + +/** Amount minted into the pool */ +@property(nonatomic, readwrite) uint64_t amount; + +/** Digest of the Orchard actions every signer commits to */ +@property(nonatomic, readwrite, copy, null_resettable) NSData *actionsDigest; + +/** Public note */ +@property(nonatomic, readwrite, copy, null_resettable) NSString *publicNote; +/** Test to see if @c publicNote has been set. */ +@property(nonatomic, readwrite) BOOL hasPublicNote; + +@end + +#pragma mark - GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent + +typedef GPB_ENUM(GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent_FieldNumber) { + GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent_FieldNumber_Amount = 1, + GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent_FieldNumber_ActionsDigest = 2, + GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent_FieldNumber_PublicNote = 3, +}; + +/** + * Burn of notes held in the token's shielded pool (protocol version 14+) + **/ +GPB_FINAL @interface GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent : GPBMessage + +/** Amount destroyed */ +@property(nonatomic, readwrite) uint64_t amount; + +/** Digest of the Orchard actions every signer commits to */ +@property(nonatomic, readwrite, copy, null_resettable) NSData *actionsDigest; + +/** Public note */ +@property(nonatomic, readwrite, copy, null_resettable) NSString *publicNote; +/** Test to see if @c publicNote has been set. */ +@property(nonatomic, readwrite) BOOL hasPublicNote; + +@end + #pragma mark - GetGroupActionsResponse_GetGroupActionsResponseV0_GroupActionEvent typedef GPB_ENUM(GetGroupActionsResponse_GetGroupActionsResponseV0_GroupActionEvent_FieldNumber) { @@ -11688,6 +11742,8 @@ typedef GPB_ENUM(GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent_Fi GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent_FieldNumber_EmergencyAction = 6, GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent_FieldNumber_TokenConfigUpdate = 7, GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent_FieldNumber_UpdatePrice = 8, + GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent_FieldNumber_MintToPool = 9, + GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent_FieldNumber_BurnFromPool = 10, }; typedef GPB_ENUM(GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent_Type_OneOfCase) { @@ -11700,6 +11756,8 @@ typedef GPB_ENUM(GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent_Ty GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent_Type_OneOfCase_EmergencyAction = 6, GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent_Type_OneOfCase_TokenConfigUpdate = 7, GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent_Type_OneOfCase_UpdatePrice = 8, + GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent_Type_OneOfCase_MintToPool = 9, + GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent_Type_OneOfCase_BurnFromPool = 10, }; /** @@ -11733,6 +11791,12 @@ GPB_FINAL @interface GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEven /** Updating the token direct selling price */ @property(nonatomic, readwrite, strong, null_resettable) GetGroupActionsResponse_GetGroupActionsResponseV0_UpdateDirectPurchasePriceEvent *updatePrice; +/** Mint into the token shielded pool */ +@property(nonatomic, readwrite, strong, null_resettable) GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent *mintToPool; + +/** Burn from the token shielded pool */ +@property(nonatomic, readwrite, strong, null_resettable) GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent *burnFromPool; + @end /** @@ -12693,6 +12757,11 @@ typedef GPB_ENUM(GetShieldedEncryptedNotesRequest_Version_OneOfCase) { GetShieldedEncryptedNotesRequest_Version_OneOfCase_V0 = 1, }; +/** + * Every shielded pool query below targets the credit shielded pool unless `token_id` is set, + * in which case it targets that token's own shielded pool (protocol version 15+; the 32-byte + * token id). Responses have the same shape for both pools. + **/ GPB_FINAL @interface GetShieldedEncryptedNotesRequest : GPBMessage @property(nonatomic, readonly) GetShieldedEncryptedNotesRequest_Version_OneOfCase versionOneOfCase; @@ -12712,6 +12781,7 @@ typedef GPB_ENUM(GetShieldedEncryptedNotesRequest_GetShieldedEncryptedNotesReque GetShieldedEncryptedNotesRequest_GetShieldedEncryptedNotesRequestV0_FieldNumber_StartIndex = 1, GetShieldedEncryptedNotesRequest_GetShieldedEncryptedNotesRequestV0_FieldNumber_Count = 2, GetShieldedEncryptedNotesRequest_GetShieldedEncryptedNotesRequestV0_FieldNumber_Prove = 3, + GetShieldedEncryptedNotesRequest_GetShieldedEncryptedNotesRequestV0_FieldNumber_TokenId = 4, }; GPB_FINAL @interface GetShieldedEncryptedNotesRequest_GetShieldedEncryptedNotesRequestV0 : GPBMessage @@ -12722,6 +12792,11 @@ GPB_FINAL @interface GetShieldedEncryptedNotesRequest_GetShieldedEncryptedNotesR @property(nonatomic, readwrite) BOOL prove; +/** target a token's shielded pool instead of the credit pool */ +@property(nonatomic, readwrite, copy, null_resettable) NSData *tokenId; +/** Test to see if @c tokenId has been set. */ +@property(nonatomic, readwrite) BOOL hasTokenId; + @end #pragma mark - GetShieldedEncryptedNotesResponse @@ -12848,12 +12923,18 @@ void GetShieldedAnchorsRequest_ClearVersionOneOfCase(GetShieldedAnchorsRequest * typedef GPB_ENUM(GetShieldedAnchorsRequest_GetShieldedAnchorsRequestV0_FieldNumber) { GetShieldedAnchorsRequest_GetShieldedAnchorsRequestV0_FieldNumber_Prove = 1, + GetShieldedAnchorsRequest_GetShieldedAnchorsRequestV0_FieldNumber_TokenId = 2, }; GPB_FINAL @interface GetShieldedAnchorsRequest_GetShieldedAnchorsRequestV0 : GPBMessage @property(nonatomic, readwrite) BOOL prove; +/** target a token's shielded pool instead of the credit pool */ +@property(nonatomic, readwrite, copy, null_resettable) NSData *tokenId; +/** Test to see if @c tokenId has been set. */ +@property(nonatomic, readwrite) BOOL hasTokenId; + @end #pragma mark - GetShieldedAnchorsResponse @@ -12955,12 +13036,18 @@ void GetMostRecentShieldedAnchorRequest_ClearVersionOneOfCase(GetMostRecentShiel typedef GPB_ENUM(GetMostRecentShieldedAnchorRequest_GetMostRecentShieldedAnchorRequestV0_FieldNumber) { GetMostRecentShieldedAnchorRequest_GetMostRecentShieldedAnchorRequestV0_FieldNumber_Prove = 1, + GetMostRecentShieldedAnchorRequest_GetMostRecentShieldedAnchorRequestV0_FieldNumber_TokenId = 2, }; GPB_FINAL @interface GetMostRecentShieldedAnchorRequest_GetMostRecentShieldedAnchorRequestV0 : GPBMessage @property(nonatomic, readwrite) BOOL prove; +/** target a token's shielded pool instead of the credit pool */ +@property(nonatomic, readwrite, copy, null_resettable) NSData *tokenId; +/** Test to see if @c tokenId has been set. */ +@property(nonatomic, readwrite) BOOL hasTokenId; + @end #pragma mark - GetMostRecentShieldedAnchorResponse @@ -13048,12 +13135,18 @@ void GetShieldedPoolStateRequest_ClearVersionOneOfCase(GetShieldedPoolStateReque typedef GPB_ENUM(GetShieldedPoolStateRequest_GetShieldedPoolStateRequestV0_FieldNumber) { GetShieldedPoolStateRequest_GetShieldedPoolStateRequestV0_FieldNumber_Prove = 1, + GetShieldedPoolStateRequest_GetShieldedPoolStateRequestV0_FieldNumber_TokenId = 2, }; GPB_FINAL @interface GetShieldedPoolStateRequest_GetShieldedPoolStateRequestV0 : GPBMessage @property(nonatomic, readwrite) BOOL prove; +/** target a token's shielded pool instead of the credit pool */ +@property(nonatomic, readwrite, copy, null_resettable) NSData *tokenId; +/** Test to see if @c tokenId has been set. */ +@property(nonatomic, readwrite) BOOL hasTokenId; + @end #pragma mark - GetShieldedPoolStateResponse @@ -13150,12 +13243,18 @@ void GetShieldedNotesCountRequest_ClearVersionOneOfCase(GetShieldedNotesCountReq typedef GPB_ENUM(GetShieldedNotesCountRequest_GetShieldedNotesCountRequestV0_FieldNumber) { GetShieldedNotesCountRequest_GetShieldedNotesCountRequestV0_FieldNumber_Prove = 1, + GetShieldedNotesCountRequest_GetShieldedNotesCountRequestV0_FieldNumber_TokenId = 2, }; GPB_FINAL @interface GetShieldedNotesCountRequest_GetShieldedNotesCountRequestV0 : GPBMessage @property(nonatomic, readwrite) BOOL prove; +/** target a token's shielded pool instead of the credit pool */ +@property(nonatomic, readwrite, copy, null_resettable) NSData *tokenId; +/** Test to see if @c tokenId has been set. */ +@property(nonatomic, readwrite) BOOL hasTokenId; + @end #pragma mark - GetShieldedNotesCountResponse @@ -13244,6 +13343,7 @@ void GetShieldedNullifiersRequest_ClearVersionOneOfCase(GetShieldedNullifiersReq typedef GPB_ENUM(GetShieldedNullifiersRequest_GetShieldedNullifiersRequestV0_FieldNumber) { GetShieldedNullifiersRequest_GetShieldedNullifiersRequestV0_FieldNumber_NullifiersArray = 1, GetShieldedNullifiersRequest_GetShieldedNullifiersRequestV0_FieldNumber_Prove = 2, + GetShieldedNullifiersRequest_GetShieldedNullifiersRequestV0_FieldNumber_TokenId = 3, }; GPB_FINAL @interface GetShieldedNullifiersRequest_GetShieldedNullifiersRequestV0 : GPBMessage @@ -13254,6 +13354,11 @@ GPB_FINAL @interface GetShieldedNullifiersRequest_GetShieldedNullifiersRequestV0 @property(nonatomic, readwrite) BOOL prove; +/** target a token's shielded pool instead of the credit pool */ +@property(nonatomic, readwrite, copy, null_resettable) NSData *tokenId; +/** Test to see if @c tokenId has been set. */ +@property(nonatomic, readwrite) BOOL hasTokenId; + @end #pragma mark - GetShieldedNullifiersResponse diff --git a/packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.m b/packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.m index 838e7fcfe14..7ae9d104a7e 100644 --- a/packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.m +++ b/packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.m @@ -250,6 +250,7 @@ GPBObjCClassDeclaration(GetGroupActionsResponse); GPBObjCClassDeclaration(GetGroupActionsResponse_GetGroupActionsResponseV0); GPBObjCClassDeclaration(GetGroupActionsResponse_GetGroupActionsResponseV0_BurnEvent); +GPBObjCClassDeclaration(GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent); GPBObjCClassDeclaration(GetGroupActionsResponse_GetGroupActionsResponseV0_ContractEvent); GPBObjCClassDeclaration(GetGroupActionsResponse_GetGroupActionsResponseV0_ContractUpdateEvent); GPBObjCClassDeclaration(GetGroupActionsResponse_GetGroupActionsResponseV0_DestroyFrozenFundsEvent); @@ -261,6 +262,7 @@ GPBObjCClassDeclaration(GetGroupActionsResponse_GetGroupActionsResponseV0_GroupActionEvent); GPBObjCClassDeclaration(GetGroupActionsResponse_GetGroupActionsResponseV0_GroupActions); GPBObjCClassDeclaration(GetGroupActionsResponse_GetGroupActionsResponseV0_MintEvent); +GPBObjCClassDeclaration(GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent); GPBObjCClassDeclaration(GetGroupActionsResponse_GetGroupActionsResponseV0_TokenConfigUpdateEvent); GPBObjCClassDeclaration(GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent); GPBObjCClassDeclaration(GetGroupActionsResponse_GetGroupActionsResponseV0_UnfreezeEvent); @@ -26911,6 +26913,142 @@ + (GPBDescriptor *)descriptor { @end +#pragma mark - GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent + +@implementation GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent + +@dynamic amount; +@dynamic actionsDigest; +@dynamic hasPublicNote, publicNote; + +typedef struct GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent__storage_ { + uint32_t _has_storage_[1]; + NSData *actionsDigest; + NSString *publicNote; + uint64_t amount; +} GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "amount", + .dataTypeSpecific.clazz = Nil, + .number = GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent_FieldNumber_Amount, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent__storage_, amount), + .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldClearHasIvarOnZero), + .dataType = GPBDataTypeUInt64, + }, + { + .name = "actionsDigest", + .dataTypeSpecific.clazz = Nil, + .number = GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent_FieldNumber_ActionsDigest, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent__storage_, actionsDigest), + .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldClearHasIvarOnZero), + .dataType = GPBDataTypeBytes, + }, + { + .name = "publicNote", + .dataTypeSpecific.clazz = Nil, + .number = GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent_FieldNumber_PublicNote, + .hasIndex = 2, + .offset = (uint32_t)offsetof(GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent__storage_, publicNote), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent class] + rootClass:[PlatformRoot class] + file:PlatformRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent__storage_) + flags:(GPBDescriptorInitializationFlags)(GPBDescriptorInitializationFlag_UsesClassRefs | GPBDescriptorInitializationFlag_Proto3OptionalKnown)]; + [localDescriptor setupContainingMessageClass:GPBObjCClass(GetGroupActionsResponse_GetGroupActionsResponseV0)]; + #if defined(DEBUG) && DEBUG + NSAssert(descriptor == nil, @"Startup recursed!"); + #endif // DEBUG + descriptor = localDescriptor; + } + return descriptor; +} + +@end + +#pragma mark - GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent + +@implementation GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent + +@dynamic amount; +@dynamic actionsDigest; +@dynamic hasPublicNote, publicNote; + +typedef struct GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent__storage_ { + uint32_t _has_storage_[1]; + NSData *actionsDigest; + NSString *publicNote; + uint64_t amount; +} GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "amount", + .dataTypeSpecific.clazz = Nil, + .number = GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent_FieldNumber_Amount, + .hasIndex = 0, + .offset = (uint32_t)offsetof(GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent__storage_, amount), + .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldClearHasIvarOnZero), + .dataType = GPBDataTypeUInt64, + }, + { + .name = "actionsDigest", + .dataTypeSpecific.clazz = Nil, + .number = GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent_FieldNumber_ActionsDigest, + .hasIndex = 1, + .offset = (uint32_t)offsetof(GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent__storage_, actionsDigest), + .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldClearHasIvarOnZero), + .dataType = GPBDataTypeBytes, + }, + { + .name = "publicNote", + .dataTypeSpecific.clazz = Nil, + .number = GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent_FieldNumber_PublicNote, + .hasIndex = 2, + .offset = (uint32_t)offsetof(GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent__storage_, publicNote), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent class] + rootClass:[PlatformRoot class] + file:PlatformRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent__storage_) + flags:(GPBDescriptorInitializationFlags)(GPBDescriptorInitializationFlag_UsesClassRefs | GPBDescriptorInitializationFlag_Proto3OptionalKnown)]; + [localDescriptor setupContainingMessageClass:GPBObjCClass(GetGroupActionsResponse_GetGroupActionsResponseV0)]; + #if defined(DEBUG) && DEBUG + NSAssert(descriptor == nil, @"Startup recursed!"); + #endif // DEBUG + descriptor = localDescriptor; + } + return descriptor; +} + +@end + #pragma mark - GetGroupActionsResponse_GetGroupActionsResponseV0_GroupActionEvent @implementation GetGroupActionsResponse_GetGroupActionsResponseV0_GroupActionEvent @@ -27212,6 +27350,8 @@ @implementation GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent @dynamic emergencyAction; @dynamic tokenConfigUpdate; @dynamic updatePrice; +@dynamic mintToPool; +@dynamic burnFromPool; typedef struct GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent__storage_ { uint32_t _has_storage_[2]; @@ -27223,6 +27363,8 @@ @implementation GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent GetGroupActionsResponse_GetGroupActionsResponseV0_EmergencyActionEvent *emergencyAction; GetGroupActionsResponse_GetGroupActionsResponseV0_TokenConfigUpdateEvent *tokenConfigUpdate; GetGroupActionsResponse_GetGroupActionsResponseV0_UpdateDirectPurchasePriceEvent *updatePrice; + GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent *mintToPool; + GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent *burnFromPool; } GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent__storage_; // This method is threadsafe because it is initially called @@ -27303,6 +27445,24 @@ + (GPBDescriptor *)descriptor { .flags = GPBFieldOptional, .dataType = GPBDataTypeMessage, }, + { + .name = "mintToPool", + .dataTypeSpecific.clazz = GPBObjCClass(GetGroupActionsResponse_GetGroupActionsResponseV0_MintToPoolEvent), + .number = GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent_FieldNumber_MintToPool, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent__storage_, mintToPool), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, + { + .name = "burnFromPool", + .dataTypeSpecific.clazz = GPBObjCClass(GetGroupActionsResponse_GetGroupActionsResponseV0_BurnFromPoolEvent), + .number = GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent_FieldNumber_BurnFromPool, + .hasIndex = -1, + .offset = (uint32_t)offsetof(GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent__storage_, burnFromPool), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeMessage, + }, }; GPBDescriptor *localDescriptor = [GPBDescriptor allocDescriptorForClass:[GetGroupActionsResponse_GetGroupActionsResponseV0_TokenEvent class] @@ -30005,10 +30165,12 @@ @implementation GetShieldedEncryptedNotesRequest_GetShieldedEncryptedNotesReques @dynamic startIndex; @dynamic count; @dynamic prove; +@dynamic hasTokenId, tokenId; typedef struct GetShieldedEncryptedNotesRequest_GetShieldedEncryptedNotesRequestV0__storage_ { uint32_t _has_storage_[1]; uint32_t count; + NSData *tokenId; uint64_t startIndex; } GetShieldedEncryptedNotesRequest_GetShieldedEncryptedNotesRequestV0__storage_; @@ -30045,6 +30207,15 @@ + (GPBDescriptor *)descriptor { .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldClearHasIvarOnZero), .dataType = GPBDataTypeBool, }, + { + .name = "tokenId", + .dataTypeSpecific.clazz = Nil, + .number = GetShieldedEncryptedNotesRequest_GetShieldedEncryptedNotesRequestV0_FieldNumber_TokenId, + .hasIndex = 4, + .offset = (uint32_t)offsetof(GetShieldedEncryptedNotesRequest_GetShieldedEncryptedNotesRequestV0__storage_, tokenId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, }; GPBDescriptor *localDescriptor = [GPBDescriptor allocDescriptorForClass:[GetShieldedEncryptedNotesRequest_GetShieldedEncryptedNotesRequestV0 class] @@ -30389,9 +30560,11 @@ void GetShieldedAnchorsRequest_ClearVersionOneOfCase(GetShieldedAnchorsRequest * @implementation GetShieldedAnchorsRequest_GetShieldedAnchorsRequestV0 @dynamic prove; +@dynamic hasTokenId, tokenId; typedef struct GetShieldedAnchorsRequest_GetShieldedAnchorsRequestV0__storage_ { uint32_t _has_storage_[1]; + NSData *tokenId; } GetShieldedAnchorsRequest_GetShieldedAnchorsRequestV0__storage_; // This method is threadsafe because it is initially called @@ -30409,6 +30582,15 @@ + (GPBDescriptor *)descriptor { .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldClearHasIvarOnZero), .dataType = GPBDataTypeBool, }, + { + .name = "tokenId", + .dataTypeSpecific.clazz = Nil, + .number = GetShieldedAnchorsRequest_GetShieldedAnchorsRequestV0_FieldNumber_TokenId, + .hasIndex = 2, + .offset = (uint32_t)offsetof(GetShieldedAnchorsRequest_GetShieldedAnchorsRequestV0__storage_, tokenId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, }; GPBDescriptor *localDescriptor = [GPBDescriptor allocDescriptorForClass:[GetShieldedAnchorsRequest_GetShieldedAnchorsRequestV0 class] @@ -30674,9 +30856,11 @@ void GetMostRecentShieldedAnchorRequest_ClearVersionOneOfCase(GetMostRecentShiel @implementation GetMostRecentShieldedAnchorRequest_GetMostRecentShieldedAnchorRequestV0 @dynamic prove; +@dynamic hasTokenId, tokenId; typedef struct GetMostRecentShieldedAnchorRequest_GetMostRecentShieldedAnchorRequestV0__storage_ { uint32_t _has_storage_[1]; + NSData *tokenId; } GetMostRecentShieldedAnchorRequest_GetMostRecentShieldedAnchorRequestV0__storage_; // This method is threadsafe because it is initially called @@ -30694,6 +30878,15 @@ + (GPBDescriptor *)descriptor { .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldClearHasIvarOnZero), .dataType = GPBDataTypeBool, }, + { + .name = "tokenId", + .dataTypeSpecific.clazz = Nil, + .number = GetMostRecentShieldedAnchorRequest_GetMostRecentShieldedAnchorRequestV0_FieldNumber_TokenId, + .hasIndex = 2, + .offset = (uint32_t)offsetof(GetMostRecentShieldedAnchorRequest_GetMostRecentShieldedAnchorRequestV0__storage_, tokenId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, }; GPBDescriptor *localDescriptor = [GPBDescriptor allocDescriptorForClass:[GetMostRecentShieldedAnchorRequest_GetMostRecentShieldedAnchorRequestV0 class] @@ -30913,9 +31106,11 @@ void GetShieldedPoolStateRequest_ClearVersionOneOfCase(GetShieldedPoolStateReque @implementation GetShieldedPoolStateRequest_GetShieldedPoolStateRequestV0 @dynamic prove; +@dynamic hasTokenId, tokenId; typedef struct GetShieldedPoolStateRequest_GetShieldedPoolStateRequestV0__storage_ { uint32_t _has_storage_[1]; + NSData *tokenId; } GetShieldedPoolStateRequest_GetShieldedPoolStateRequestV0__storage_; // This method is threadsafe because it is initially called @@ -30933,6 +31128,15 @@ + (GPBDescriptor *)descriptor { .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldClearHasIvarOnZero), .dataType = GPBDataTypeBool, }, + { + .name = "tokenId", + .dataTypeSpecific.clazz = Nil, + .number = GetShieldedPoolStateRequest_GetShieldedPoolStateRequestV0_FieldNumber_TokenId, + .hasIndex = 2, + .offset = (uint32_t)offsetof(GetShieldedPoolStateRequest_GetShieldedPoolStateRequestV0__storage_, tokenId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, }; GPBDescriptor *localDescriptor = [GPBDescriptor allocDescriptorForClass:[GetShieldedPoolStateRequest_GetShieldedPoolStateRequestV0 class] @@ -31152,9 +31356,11 @@ void GetShieldedNotesCountRequest_ClearVersionOneOfCase(GetShieldedNotesCountReq @implementation GetShieldedNotesCountRequest_GetShieldedNotesCountRequestV0 @dynamic prove; +@dynamic hasTokenId, tokenId; typedef struct GetShieldedNotesCountRequest_GetShieldedNotesCountRequestV0__storage_ { uint32_t _has_storage_[1]; + NSData *tokenId; } GetShieldedNotesCountRequest_GetShieldedNotesCountRequestV0__storage_; // This method is threadsafe because it is initially called @@ -31172,6 +31378,15 @@ + (GPBDescriptor *)descriptor { .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldClearHasIvarOnZero), .dataType = GPBDataTypeBool, }, + { + .name = "tokenId", + .dataTypeSpecific.clazz = Nil, + .number = GetShieldedNotesCountRequest_GetShieldedNotesCountRequestV0_FieldNumber_TokenId, + .hasIndex = 2, + .offset = (uint32_t)offsetof(GetShieldedNotesCountRequest_GetShieldedNotesCountRequestV0__storage_, tokenId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, }; GPBDescriptor *localDescriptor = [GPBDescriptor allocDescriptorForClass:[GetShieldedNotesCountRequest_GetShieldedNotesCountRequestV0 class] @@ -31392,10 +31607,12 @@ @implementation GetShieldedNullifiersRequest_GetShieldedNullifiersRequestV0 @dynamic nullifiersArray, nullifiersArray_Count; @dynamic prove; +@dynamic hasTokenId, tokenId; typedef struct GetShieldedNullifiersRequest_GetShieldedNullifiersRequestV0__storage_ { uint32_t _has_storage_[1]; NSMutableArray *nullifiersArray; + NSData *tokenId; } GetShieldedNullifiersRequest_GetShieldedNullifiersRequestV0__storage_; // This method is threadsafe because it is initially called @@ -31422,6 +31639,15 @@ + (GPBDescriptor *)descriptor { .flags = (GPBFieldFlags)(GPBFieldOptional | GPBFieldClearHasIvarOnZero), .dataType = GPBDataTypeBool, }, + { + .name = "tokenId", + .dataTypeSpecific.clazz = Nil, + .number = GetShieldedNullifiersRequest_GetShieldedNullifiersRequestV0_FieldNumber_TokenId, + .hasIndex = 2, + .offset = (uint32_t)offsetof(GetShieldedNullifiersRequest_GetShieldedNullifiersRequestV0__storage_, tokenId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeBytes, + }, }; GPBDescriptor *localDescriptor = [GPBDescriptor allocDescriptorForClass:[GetShieldedNullifiersRequest_GetShieldedNullifiersRequestV0 class] diff --git a/packages/dapi-grpc/clients/platform/v0/python/platform_pb2.py b/packages/dapi-grpc/clients/platform/v0/python/platform_pb2.py index 56f0877d1ad..ad9b17ceb5c 100644 --- a/packages/dapi-grpc/clients/platform/v0/python/platform_pb2.py +++ b/packages/dapi-grpc/clients/platform/v0/python/platform_pb2.py @@ -23,7 +23,7 @@ syntax='proto3', serialized_options=None, create_key=_descriptor._internal_create_key, - serialized_pb=b'\n\x0eplatform.proto\x12\x19org.dash.platform.dapi.v0\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x81\x01\n\x05Proof\x12\x15\n\rgrovedb_proof\x18\x01 \x01(\x0c\x12\x13\n\x0bquorum_hash\x18\x02 \x01(\x0c\x12\x11\n\tsignature\x18\x03 \x01(\x0c\x12\r\n\x05round\x18\x04 \x01(\r\x12\x15\n\rblock_id_hash\x18\x05 \x01(\x0c\x12\x13\n\x0bquorum_type\x18\x06 \x01(\r\"\x98\x01\n\x10ResponseMetadata\x12\x12\n\x06height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12 \n\x18\x63ore_chain_locked_height\x18\x02 \x01(\r\x12\r\n\x05\x65poch\x18\x03 \x01(\r\x12\x13\n\x07time_ms\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x18\n\x10protocol_version\x18\x05 \x01(\r\x12\x10\n\x08\x63hain_id\x18\x06 \x01(\t\"L\n\x1dStateTransitionBroadcastError\x12\x0c\n\x04\x63ode\x18\x01 \x01(\r\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\";\n\x1f\x42roadcastStateTransitionRequest\x12\x18\n\x10state_transition\x18\x01 \x01(\x0c\"\"\n BroadcastStateTransitionResponse\"\xa4\x01\n\x12GetIdentityRequest\x12P\n\x02v0\x18\x01 \x01(\x0b\x32\x42.org.dash.platform.dapi.v0.GetIdentityRequest.GetIdentityRequestV0H\x00\x1a\x31\n\x14GetIdentityRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xc1\x01\n\x17GetIdentityNonceRequest\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetIdentityNonceRequest.GetIdentityNonceRequestV0H\x00\x1a?\n\x19GetIdentityNonceRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf6\x01\n\x1fGetIdentityContractNonceRequest\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetIdentityContractNonceRequest.GetIdentityContractNonceRequestV0H\x00\x1a\\\n!GetIdentityContractNonceRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x13\n\x0b\x63ontract_id\x18\x02 \x01(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xc0\x01\n\x19GetIdentityBalanceRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetIdentityBalanceRequest.GetIdentityBalanceRequestV0H\x00\x1a\x38\n\x1bGetIdentityBalanceRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xec\x01\n$GetIdentityBalanceAndRevisionRequest\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionRequest.GetIdentityBalanceAndRevisionRequestV0H\x00\x1a\x43\n&GetIdentityBalanceAndRevisionRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9e\x02\n\x13GetIdentityResponse\x12R\n\x02v0\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetIdentityResponse.GetIdentityResponseV0H\x00\x1a\xa7\x01\n\x15GetIdentityResponseV0\x12\x12\n\x08identity\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xbc\x02\n\x18GetIdentityNonceResponse\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetIdentityNonceResponse.GetIdentityNonceResponseV0H\x00\x1a\xb6\x01\n\x1aGetIdentityNonceResponseV0\x12\x1c\n\x0eidentity_nonce\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xe5\x02\n GetIdentityContractNonceResponse\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetIdentityContractNonceResponse.GetIdentityContractNonceResponseV0H\x00\x1a\xc7\x01\n\"GetIdentityContractNonceResponseV0\x12%\n\x17identity_contract_nonce\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\x8e\x02\n&GetIdentityKeysRemainingBudgetsRequest\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsRequest.GetIdentityKeysRemainingBudgetsRequestV0H\x00\x1a_\n(GetIdentityKeysRemainingBudgetsRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x0f\n\x07key_ids\x18\x02 \x03(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\x96\x06\n\'GetIdentityKeysRemainingBudgetsResponse\x12z\n\x02v0\x18\x01 \x01(\x0b\x32l.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsResponse.GetIdentityKeysRemainingBudgetsResponseV0H\x00\x1a\xe3\x04\n)GetIdentityKeysRemainingBudgetsResponseV0\x12\xa4\x01\n\x16keys_remaining_budgets\x18\x01 \x01(\x0b\x32\x81\x01.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsResponse.GetIdentityKeysRemainingBudgetsResponseV0.KeysRemainingBudgetsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x61\n\x17KeyRemainingBudgetEntry\x12\x0e\n\x06key_id\x18\x01 \x01(\r\x12!\n\x10remaining_budget\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x42\x13\n\x11_remaining_budget\x1a\xaf\x01\n\x14KeysRemainingBudgets\x12\x96\x01\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsResponse.GetIdentityKeysRemainingBudgetsResponseV0.KeyRemainingBudgetEntryB\x08\n\x06resultB\t\n\x07version\"\xbd\x02\n\x1aGetIdentityBalanceResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetIdentityBalanceResponse.GetIdentityBalanceResponseV0H\x00\x1a\xb1\x01\n\x1cGetIdentityBalanceResponseV0\x12\x15\n\x07\x62\x61lance\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xb1\x04\n%GetIdentityBalanceAndRevisionResponse\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionResponse.GetIdentityBalanceAndRevisionResponseV0H\x00\x1a\x84\x03\n\'GetIdentityBalanceAndRevisionResponseV0\x12\x9b\x01\n\x14\x62\x61lance_and_revision\x18\x01 \x01(\x0b\x32{.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionResponse.GetIdentityBalanceAndRevisionResponseV0.BalanceAndRevisionH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a?\n\x12\x42\x61lanceAndRevision\x12\x13\n\x07\x62\x61lance\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x14\n\x08revision\x18\x02 \x01(\x04\x42\x02\x30\x01\x42\x08\n\x06resultB\t\n\x07version\"\xd1\x01\n\x0eKeyRequestType\x12\x36\n\x08\x61ll_keys\x18\x01 \x01(\x0b\x32\".org.dash.platform.dapi.v0.AllKeysH\x00\x12@\n\rspecific_keys\x18\x02 \x01(\x0b\x32\'.org.dash.platform.dapi.v0.SpecificKeysH\x00\x12:\n\nsearch_key\x18\x03 \x01(\x0b\x32$.org.dash.platform.dapi.v0.SearchKeyH\x00\x42\t\n\x07request\"\t\n\x07\x41llKeys\"\x1f\n\x0cSpecificKeys\x12\x0f\n\x07key_ids\x18\x01 \x03(\r\"\xb6\x01\n\tSearchKey\x12I\n\x0bpurpose_map\x18\x01 \x03(\x0b\x32\x34.org.dash.platform.dapi.v0.SearchKey.PurposeMapEntry\x1a^\n\x0fPurposeMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12:\n\x05value\x18\x02 \x01(\x0b\x32+.org.dash.platform.dapi.v0.SecurityLevelMap:\x02\x38\x01\"\xbf\x02\n\x10SecurityLevelMap\x12]\n\x12security_level_map\x18\x01 \x03(\x0b\x32\x41.org.dash.platform.dapi.v0.SecurityLevelMap.SecurityLevelMapEntry\x1aw\n\x15SecurityLevelMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12M\n\x05value\x18\x02 \x01(\x0e\x32>.org.dash.platform.dapi.v0.SecurityLevelMap.KeyKindRequestType:\x02\x38\x01\"S\n\x12KeyKindRequestType\x12\x1f\n\x1b\x43URRENT_KEY_OF_KIND_REQUEST\x10\x00\x12\x1c\n\x18\x41LL_KEYS_OF_KIND_REQUEST\x10\x01\"\xda\x02\n\x16GetIdentityKeysRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetIdentityKeysRequest.GetIdentityKeysRequestV0H\x00\x1a\xda\x01\n\x18GetIdentityKeysRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12?\n\x0crequest_type\x18\x02 \x01(\x0b\x32).org.dash.platform.dapi.v0.KeyRequestType\x12+\n\x05limit\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\t\n\x07version\"\x99\x03\n\x17GetIdentityKeysResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetIdentityKeysResponse.GetIdentityKeysResponseV0H\x00\x1a\x96\x02\n\x19GetIdentityKeysResponseV0\x12\x61\n\x04keys\x18\x01 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetIdentityKeysResponse.GetIdentityKeysResponseV0.KeysH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1a\n\x04Keys\x12\x12\n\nkeys_bytes\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\xef\x02\n GetIdentitiesContractKeysRequest\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetIdentitiesContractKeysRequest.GetIdentitiesContractKeysRequestV0H\x00\x1a\xd1\x01\n\"GetIdentitiesContractKeysRequestV0\x12\x16\n\x0eidentities_ids\x18\x01 \x03(\x0c\x12\x13\n\x0b\x63ontract_id\x18\x02 \x01(\x0c\x12\x1f\n\x12\x64ocument_type_name\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x37\n\x08purposes\x18\x04 \x03(\x0e\x32%.org.dash.platform.dapi.v0.KeyPurpose\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x15\n\x13_document_type_nameB\t\n\x07version\"\xdf\x06\n!GetIdentitiesContractKeysResponse\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0H\x00\x1a\xbe\x05\n#GetIdentitiesContractKeysResponseV0\x12\x8a\x01\n\x0fidentities_keys\x18\x01 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0.IdentitiesKeysH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aY\n\x0bPurposeKeys\x12\x36\n\x07purpose\x18\x01 \x01(\x0e\x32%.org.dash.platform.dapi.v0.KeyPurpose\x12\x12\n\nkeys_bytes\x18\x02 \x03(\x0c\x1a\x9f\x01\n\x0cIdentityKeys\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12z\n\x04keys\x18\x02 \x03(\x0b\x32l.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0.PurposeKeys\x1a\x90\x01\n\x0eIdentitiesKeys\x12~\n\x07\x65ntries\x18\x01 \x03(\x0b\x32m.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0.IdentityKeysB\x08\n\x06resultB\t\n\x07version\"\xa4\x02\n*GetEvonodesProposedEpochBlocksByIdsRequest\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByIdsRequest.GetEvonodesProposedEpochBlocksByIdsRequestV0H\x00\x1ah\n,GetEvonodesProposedEpochBlocksByIdsRequestV0\x12\x12\n\x05\x65poch\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x0b\n\x03ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\x08\n\x06_epochB\t\n\x07version\"\x92\x06\n&GetEvonodesProposedEpochBlocksResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse.GetEvonodesProposedEpochBlocksResponseV0H\x00\x1a\xe2\x04\n(GetEvonodesProposedEpochBlocksResponseV0\x12\xb1\x01\n#evonodes_proposed_block_counts_info\x18\x01 \x01(\x0b\x32\x81\x01.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse.GetEvonodesProposedEpochBlocksResponseV0.EvonodesProposedBlocksH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a?\n\x15\x45vonodeProposedBlocks\x12\x13\n\x0bpro_tx_hash\x18\x01 \x01(\x0c\x12\x11\n\x05\x63ount\x18\x02 \x01(\x04\x42\x02\x30\x01\x1a\xc4\x01\n\x16\x45vonodesProposedBlocks\x12\xa9\x01\n\x1e\x65vonodes_proposed_block_counts\x18\x01 \x03(\x0b\x32\x80\x01.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse.GetEvonodesProposedEpochBlocksResponseV0.EvonodeProposedBlocksB\x08\n\x06resultB\t\n\x07version\"\xf2\x02\n,GetEvonodesProposedEpochBlocksByRangeRequest\x12\x84\x01\n\x02v0\x18\x01 \x01(\x0b\x32v.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByRangeRequest.GetEvonodesProposedEpochBlocksByRangeRequestV0H\x00\x1a\xaf\x01\n.GetEvonodesProposedEpochBlocksByRangeRequestV0\x12\x12\n\x05\x65poch\x18\x01 \x01(\rH\x01\x88\x01\x01\x12\x12\n\x05limit\x18\x02 \x01(\rH\x02\x88\x01\x01\x12\x15\n\x0bstart_after\x18\x03 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x04 \x01(\x0cH\x00\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x07\n\x05startB\x08\n\x06_epochB\x08\n\x06_limitB\t\n\x07version\"\xcd\x01\n\x1cGetIdentitiesBalancesRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetIdentitiesBalancesRequest.GetIdentitiesBalancesRequestV0H\x00\x1a<\n\x1eGetIdentitiesBalancesRequestV0\x12\x0b\n\x03ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9f\x05\n\x1dGetIdentitiesBalancesResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse.GetIdentitiesBalancesResponseV0H\x00\x1a\x8a\x04\n\x1fGetIdentitiesBalancesResponseV0\x12\x8a\x01\n\x13identities_balances\x18\x01 \x01(\x0b\x32k.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse.GetIdentitiesBalancesResponseV0.IdentitiesBalancesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aL\n\x0fIdentityBalance\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x18\n\x07\x62\x61lance\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x42\n\n\x08_balance\x1a\x8f\x01\n\x12IdentitiesBalances\x12y\n\x07\x65ntries\x18\x01 \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse.GetIdentitiesBalancesResponseV0.IdentityBalanceB\x08\n\x06resultB\t\n\x07version\"\xb4\x01\n\x16GetDataContractRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetDataContractRequest.GetDataContractRequestV0H\x00\x1a\x35\n\x18GetDataContractRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xb3\x02\n\x17GetDataContractResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetDataContractResponse.GetDataContractResponseV0H\x00\x1a\xb0\x01\n\x19GetDataContractResponseV0\x12\x17\n\rdata_contract\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\x8c\x02\n%GetDataContractsLatestVersionsRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetDataContractsLatestVersionsRequest.GetDataContractsLatestVersionsRequestV0H\x00\x1a`\n\'GetDataContractsLatestVersionsRequestV0\x12\x0b\n\x03ids\x18\x01 \x03(\x0c\x12\x19\n\x11include_contracts\x18\x02 \x01(\x08\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xfa\x05\n&GetDataContractsLatestVersionsResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetDataContractsLatestVersionsResponse.GetDataContractsLatestVersionsResponseV0H\x00\x1a\x84\x01\n\x1e\x44\x61taContractLatestVersionEntry\x12\x12\n\nidentifier\x18\x01 \x01(\x0c\x12\x14\n\x07version\x18\x02 \x01(\rH\x00\x88\x01\x01\x12\x1a\n\rdata_contract\x18\x03 \x01(\x0cH\x01\x88\x01\x01\x42\n\n\x08_versionB\x10\n\x0e_data_contract\x1a\x90\x01\n\x1b\x44\x61taContractsLatestVersions\x12q\n\x07\x65ntries\x18\x01 \x03(\x0b\x32`.org.dash.platform.dapi.v0.GetDataContractsLatestVersionsResponse.DataContractLatestVersionEntry\x1a\xb0\x02\n(GetDataContractsLatestVersionsResponseV0\x12\x87\x01\n\x1e\x64\x61ta_contracts_latest_versions\x18\x01 \x01(\x0b\x32].org.dash.platform.dapi.v0.GetDataContractsLatestVersionsResponse.DataContractsLatestVersionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"R\n\x1f\x43ontractGroupDocumentTypeMember\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\"G\n\x18\x43ontractGroupTokenMember\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x16\n\x0etoken_position\x18\x02 \x01(\r\"\xd7\x01\n\x1bGetContractGroupInfoRequest\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetContractGroupInfoRequest.GetContractGroupInfoRequestV0H\x00\x1aI\n\x1dGetContractGroupInfoRequestV0\x12\x19\n\x11\x63ontract_group_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x98\x04\n\x1cGetContractGroupInfoResponse\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetContractGroupInfoResponse.GetContractGroupInfoResponseV0H\x00\x1a~\n\x11\x43ontractGroupInfo\x12\x10\n\x08owner_id\x18\x01 \x01(\x0c\x12\x11\n\tadmin_ids\x18\x02 \x03(\x0c\x12\x11\n\x04name\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x01\x88\x01\x01\x42\x07\n\x05_nameB\x0e\n\x0c_description\x1a\x86\x02\n\x1eGetContractGroupInfoResponseV0\x12h\n\x13\x63ontract_group_info\x18\x01 \x01(\x0b\x32I.org.dash.platform.dapi.v0.GetContractGroupInfoResponse.ContractGroupInfoH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xf8\x06\n\x1eGetContractGroupMembersRequest\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetContractGroupMembersRequest.GetContractGroupMembersRequestV0H\x00\x1a@\n\x14\x43ontractMembersQuery\x12\x18\n\x0bstart_after\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x42\x0e\n\x0c_start_after\x1a\x80\x01\n\x18\x44ocumentTypeMembersQuery\x12T\n\x0bstart_after\x18\x01 \x01(\x0b\x32:.org.dash.platform.dapi.v0.ContractGroupDocumentTypeMemberH\x00\x88\x01\x01\x42\x0e\n\x0c_start_after\x1ar\n\x11TokenMembersQuery\x12M\n\x0bstart_after\x18\x01 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractGroupTokenMemberH\x00\x88\x01\x01\x42\x0e\n\x0c_start_after\x1a\xa7\x03\n GetContractGroupMembersRequestV0\x12\x19\n\x11\x63ontract_group_id\x18\x01 \x01(\x0c\x12\x63\n\tcontracts\x18\x02 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetContractGroupMembersRequest.ContractMembersQueryH\x00\x12l\n\x0e\x64ocument_types\x18\x03 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetContractGroupMembersRequest.DocumentTypeMembersQueryH\x00\x12]\n\x06tokens\x18\x04 \x01(\x0b\x32K.org.dash.platform.dapi.v0.GetContractGroupMembersRequest.TokenMembersQueryH\x00\x12\x12\n\x05limit\x18\x05 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x06 \x01(\x08\x42\t\n\x07membersB\x08\n\x06_limitB\t\n\x07version\"\xc9\x06\n\x1fGetContractGroupMembersResponse\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetContractGroupMembersResponse.GetContractGroupMembersResponseV0H\x00\x1a\'\n\x0f\x43ontractMembers\x12\x14\n\x0c\x63ontract_ids\x18\x01 \x03(\x0c\x1ai\n\x13\x44ocumentTypeMembers\x12R\n\x0e\x64ocument_types\x18\x01 \x03(\x0b\x32:.org.dash.platform.dapi.v0.ContractGroupDocumentTypeMember\x1aS\n\x0cTokenMembers\x12\x43\n\x06tokens\x18\x01 \x03(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractGroupTokenMember\x1a\xc5\x03\n!GetContractGroupMembersResponseV0\x12_\n\tcontracts\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetContractGroupMembersResponse.ContractMembersH\x00\x12h\n\x0e\x64ocument_types\x18\x02 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetContractGroupMembersResponse.DocumentTypeMembersH\x00\x12Y\n\x06tokens\x18\x03 \x01(\x0b\x32G.org.dash.platform.dapi.v0.GetContractGroupMembersResponse.TokenMembersH\x00\x12\x31\n\x05proof\x18\x04 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x05 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"M\n\x1a\x43ontractModerationDocument\x12\x1a\n\x12\x64ocument_type_name\x18\x01 \x01(\t\x12\x13\n\x0b\x64ocument_id\x18\x02 \x01(\x0c\"\x8e\x01\n\x18\x43ontractModerationReason\x12\x11\n\x04\x63ode\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x0c\n\x04text\x18\x02 \x01(\t\x12H\n\tdocuments\x18\x03 \x03(\x0b\x32\x35.org.dash.platform.dapi.v0.ContractModerationDocumentB\x07\n\x05_code\"i\n\x0f\x43ontractWarning\x12\x11\n\twarned_at\x18\x01 \x01(\x04\x12\x43\n\x06reason\x18\x02 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractModerationReason\"\xc5\x02\n\"GetContractModerationStatusRequest\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetContractModerationStatusRequest.GetContractModerationStatusRequestV0H\x00\x1a\xa1\x01\n$GetContractModerationStatusRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x13\n\x0bidentity_id\x18\x02 \x01(\x0c\x12@\n\x05lists\x18\x03 \x03(\x0e\x32\x31.org.dash.platform.dapi.v0.ContractModerationList\x12\r\n\x05prove\x18\x04 \x01(\x08\x42\t\n\x07version\"\xec\x06\n#GetContractModerationStatusResponse\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetContractModerationStatusResponse.GetContractModerationStatusResponseV0H\x00\x1a\xb4\x03\n\x18\x43ontractModerationStatus\x12\x13\n\x06\x62\x61nned\x18\x01 \x01(\x08H\x00\x88\x01\x01\x12\x1c\n\x0fsuspended_until\x18\x02 \x01(\x04H\x01\x88\x01\x01\x12@\n\x05lists\x18\x03 \x03(\x0e\x32\x31.org.dash.platform.dapi.v0.ContractModerationList\x12L\n\nban_reason\x18\x04 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractModerationReasonH\x02\x88\x01\x01\x12S\n\x11suspension_reason\x18\x05 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractModerationReasonH\x03\x88\x01\x01\x12<\n\x08warnings\x18\x06 \x03(\x0b\x32*.org.dash.platform.dapi.v0.ContractWarningB\t\n\x07_bannedB\x12\n\x10_suspended_untilB\r\n\x0b_ban_reasonB\x14\n\x12_suspension_reason\x1a\x8e\x02\n%GetContractModerationStatusResponseV0\x12i\n\x06status\x18\x01 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetContractModerationStatusResponse.ContractModerationStatusH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xfb\x02\n#GetContractModerationEntriesRequest\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetContractModerationEntriesRequest.GetContractModerationEntriesRequestV0H\x00\x1a\xd4\x01\n%GetContractModerationEntriesRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12?\n\x04list\x18\x02 \x01(\x0e\x32\x31.org.dash.platform.dapi.v0.ContractModerationList\x12\x18\n\x0bstart_after\x18\x03 \x01(\x0cH\x00\x88\x01\x01\x12\x12\n\x05limit\x18\x04 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x0e\n\x0c_start_afterB\x08\n\x06_limitB\t\n\x07version\"\x96\x06\n$GetContractModerationEntriesResponse\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetContractModerationEntriesResponse.GetContractModerationEntriesResponseV0H\x00\x1a\xcf\x01\n\x17\x43ontractModerationEntry\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x12\n\x05until\x18\x02 \x01(\x04H\x00\x88\x01\x01\x12\x43\n\x06reason\x18\x03 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractModerationReason\x12<\n\x08warnings\x18\x04 \x03(\x0b\x32*.org.dash.platform.dapi.v0.ContractWarningB\x08\n\x06_until\x1a\x85\x01\n\x19\x43ontractModerationEntries\x12h\n\x07\x65ntries\x18\x01 \x03(\x0b\x32W.org.dash.platform.dapi.v0.GetContractModerationEntriesResponse.ContractModerationEntry\x1a\x92\x02\n&GetContractModerationEntriesResponseV0\x12l\n\x07\x65ntries\x18\x01 \x01(\x0b\x32Y.org.dash.platform.dapi.v0.GetContractModerationEntriesResponse.ContractModerationEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc3\x04\n\"GetContractDocumentRemovalsRequest\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetContractDocumentRemovalsRequest.GetContractDocumentRemovalsRequestV0H\x00\x1a#\n\x0b\x44ocumentIds\x12\x14\n\x0c\x64ocument_ids\x18\x01 \x03(\x0c\x1aN\n\x04Page\x12\x18\n\x0bstart_after\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x12\n\x05limit\x18\x02 \x01(\rH\x01\x88\x01\x01\x42\x0e\n\x0c_start_afterB\x08\n\x06_limit\x1a\xaa\x02\n$GetContractDocumentRemovalsRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x61\n\x0c\x64ocument_ids\x18\x03 \x01(\x0b\x32I.org.dash.platform.dapi.v0.GetContractDocumentRemovalsRequest.DocumentIdsH\x00\x12R\n\x04page\x18\x04 \x01(\x0b\x32\x42.org.dash.platform.dapi.v0.GetContractDocumentRemovalsRequest.PageH\x00\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x0b\n\tselectionB\t\n\x07version\"\xcb\x07\n#GetContractDocumentRemovalsResponse\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetContractDocumentRemovalsResponse.GetContractDocumentRemovalsResponseV0H\x00\x1aH\n\x1b\x43ontractDocumentRestoration\x12\x14\n\x0cmoderator_id\x18\x01 \x01(\x0c\x12\x13\n\x0brestored_at\x18\x02 \x01(\x04\x1a\xc0\x02\n\x17\x43ontractDocumentRemoval\x12\x13\n\x0b\x64ocument_id\x18\x01 \x01(\x0c\x12\x19\n\x11\x64ocument_owner_id\x18\x02 \x01(\x0c\x12\x14\n\x0cmoderator_id\x18\x03 \x01(\x0c\x12\x12\n\nremoved_at\x18\x04 \x01(\x04\x12\x43\n\x06reason\x18\x05 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractModerationReason\x12\x15\n\rdocument_hash\x18\x06 \x01(\x0c\x12o\n\x0brestoration\x18\x07 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetContractDocumentRemovalsResponse.ContractDocumentRestoration\x1a\x84\x01\n\x18\x43ontractDocumentRemovals\x12h\n\x08removals\x18\x01 \x03(\x0b\x32V.org.dash.platform.dapi.v0.GetContractDocumentRemovalsResponse.ContractDocumentRemoval\x1a\x90\x02\n%GetContractDocumentRemovalsResponseV0\x12k\n\x08removals\x18\x01 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetContractDocumentRemovalsResponse.ContractDocumentRemovalsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc9\x01\n\x19GetContractFeePotsRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetContractFeePotsRequest.GetContractFeePotsRequestV0H\x00\x1a\x41\n\x1bGetContractFeePotsRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9f\x06\n\x1aGetContractFeePotsResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetContractFeePotsResponse.GetContractFeePotsResponseV0H\x00\x1aR\n\x17\x43ontractFeePotLastClaim\x12\r\n\x05\x65poch\x18\x01 \x01(\r\x12\x13\n\x07time_ms\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x13\n\x0b\x63laimant_id\x18\x03 \x01(\x0c\x1a\x88\x01\n\x0e\x43ontractFeePot\x12\x13\n\x07\x63redits\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x61\n\nlast_claim\x18\x02 \x01(\x0b\x32M.org.dash.platform.dapi.v0.GetContractFeePotsResponse.ContractFeePotLastClaim\x1a\xc0\x01\n\x0f\x43ontractFeePots\x12S\n\x05owner\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetContractFeePotsResponse.ContractFeePot\x12X\n\nmoderators\x18\x02 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetContractFeePotsResponse.ContractFeePot\x1a\xf1\x01\n\x1cGetContractFeePotsResponseV0\x12U\n\x04pots\x18\x01 \x01(\x0b\x32\x45.org.dash.platform.dapi.v0.GetContractFeePotsResponse.ContractFeePotsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xf1\x01\n#GetContractGroupsForContractRequest\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetContractGroupsForContractRequest.GetContractGroupsForContractRequestV0H\x00\x1aK\n%GetContractGroupsForContractRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf5\x06\n$GetContractGroupsForContractResponse\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetContractGroupsForContractResponse.GetContractGroupsForContractResponseV0H\x00\x1aQ\n\x17\x44ocumentTypeMemberships\x12\x1a\n\x12\x64ocument_type_name\x18\x01 \x01(\t\x12\x1a\n\x12\x63ontract_group_ids\x18\x02 \x03(\x0c\x1a\x46\n\x10TokenMemberships\x12\x16\n\x0etoken_position\x18\x01 \x01(\r\x12\x1a\n\x12\x63ontract_group_ids\x18\x02 \x03(\x0c\x1a\x89\x02\n\x18\x43ontractGroupMemberships\x12\x1a\n\x12\x63ontract_group_ids\x18\x01 \x03(\x0c\x12o\n\x0e\x64ocument_types\x18\x02 \x03(\x0b\x32W.org.dash.platform.dapi.v0.GetContractGroupsForContractResponse.DocumentTypeMemberships\x12`\n\x06tokens\x18\x03 \x03(\x0b\x32P.org.dash.platform.dapi.v0.GetContractGroupsForContractResponse.TokenMemberships\x1a\xa4\x02\n&GetContractGroupsForContractResponseV0\x12~\n\x1a\x63ontract_group_memberships\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetContractGroupsForContractResponse.ContractGroupMembershipsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xb9\x01\n\x17GetDataContractsRequest\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetDataContractsRequest.GetDataContractsRequestV0H\x00\x1a\x37\n\x19GetDataContractsRequestV0\x12\x0b\n\x03ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xad\x02\n\x1eGetDataContractsByRangeRequest\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetDataContractsByRangeRequest.GetDataContractsByRangeRequestV0H\x00\x1a\x95\x01\n GetDataContractsByRangeRequestV0\x12\x12\n\x05limit\x18\x01 \x01(\rH\x01\x88\x01\x01\x12\x15\n\x0bstart_after\x18\x02 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x03 \x01(\x0cH\x00\x12\x10\n\x08ids_only\x18\x04 \x01(\x08\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x07\n\x05startB\x08\n\x06_limitB\t\n\x07version\"\xcf\x04\n\x18GetDataContractsResponse\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetDataContractsResponse.GetDataContractsResponseV0H\x00\x1a[\n\x11\x44\x61taContractEntry\x12\x12\n\nidentifier\x18\x01 \x01(\x0c\x12\x32\n\rdata_contract\x18\x02 \x01(\x0b\x32\x1b.google.protobuf.BytesValue\x1au\n\rDataContracts\x12\x64\n\x15\x64\x61ta_contract_entries\x18\x01 \x03(\x0b\x32\x45.org.dash.platform.dapi.v0.GetDataContractsResponse.DataContractEntry\x1a\xf5\x01\n\x1aGetDataContractsResponseV0\x12[\n\x0e\x64\x61ta_contracts\x18\x01 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDataContractsResponse.DataContractsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc5\x02\n\x1dGetDataContractHistoryRequest\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetDataContractHistoryRequest.GetDataContractHistoryRequestV0H\x00\x1a\xb0\x01\n\x1fGetDataContractHistoryRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12+\n\x05limit\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x17\n\x0bstart_at_ms\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\t\n\x07version\"\xb2\x05\n\x1eGetDataContractHistoryResponse\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetDataContractHistoryResponse.GetDataContractHistoryResponseV0H\x00\x1a\x9a\x04\n GetDataContractHistoryResponseV0\x12\x8f\x01\n\x15\x64\x61ta_contract_history\x18\x01 \x01(\x0b\x32n.org.dash.platform.dapi.v0.GetDataContractHistoryResponse.GetDataContractHistoryResponseV0.DataContractHistoryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a;\n\x18\x44\x61taContractHistoryEntry\x12\x10\n\x04\x64\x61te\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05value\x18\x02 \x01(\x0c\x1a\xaa\x01\n\x13\x44\x61taContractHistory\x12\x92\x01\n\x15\x64\x61ta_contract_entries\x18\x01 \x03(\x0b\x32s.org.dash.platform.dapi.v0.GetDataContractHistoryResponse.GetDataContractHistoryResponseV0.DataContractHistoryEntryB\x08\n\x06resultB\t\n\x07version\"\xe0\x1f\n\x13GetDocumentsRequest\x12R\n\x02v0\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV0H\x00\x12R\n\x02v1\x18\x02 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1H\x00\x1a\xfe\x02\n\x12\x44ocumentFieldValue\x12\x14\n\nbool_value\x18\x01 \x01(\x08H\x00\x12\x19\n\x0bint64_value\x18\x02 \x01(\x12\x42\x02\x30\x01H\x00\x12\x1a\n\x0cuint64_value\x18\x03 \x01(\x04\x42\x02\x30\x01H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12\x0e\n\x04text\x18\x05 \x01(\tH\x00\x12\x15\n\x0b\x62ytes_value\x18\x06 \x01(\x0cH\x00\x12[\n\x04list\x18\x07 \x01(\x0b\x32K.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue.ValueListH\x00\x12\x14\n\nnull_value\x18\x08 \x01(\x08H\x00\x1a^\n\tValueList\x12Q\n\x06values\x18\x01 \x03(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValueB\t\n\x07variant\x1a\xe2\x02\n\x12TimeRangeSelection\x12\\\n\x08selector\x18\x01 \x01(\x0e\x32J.org.dash.platform.dapi.v0.GetDocumentsRequest.TimeRangeSelection.Selector\x12\x19\n\x08start_ms\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x12T\n\x04grid\x18\x03 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetDocumentsRequest.TimeRangeSelection.Grid\x1a>\n\x04Grid\x12\x11\n\x05range\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x10\n\x04step\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x11\n\x05phase\x18\x03 \x01(\x04\x42\x02\x30\x01\"0\n\x08Selector\x12\n\n\x06NEWEST\x10\x00\x12\n\n\x06OLDEST\x10\x01\x12\x0c\n\x08\x42Y_START\x10\x02\x42\x0b\n\t_start_ms\x1a\x95\x02\n\x0bWhereClause\x12\r\n\x05\x66ield\x18\x01 \x01(\t\x12N\n\x08operator\x18\x02 \x01(\x0e\x32<.org.dash.platform.dapi.v0.GetDocumentsRequest.WhereOperator\x12P\n\x05value\x18\x03 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue\x12U\n\ntime_range\x18\x04 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.TimeRangeSelection\x1a\xa4\x01\n\x0fHavingAggregate\x12Y\n\x08\x66unction\x18\x01 \x01(\x0e\x32G.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregate.Function\x12\r\n\x05\x66ield\x18\x02 \x01(\t\"\'\n\x08\x46unction\x12\t\n\x05\x43OUNT\x10\x00\x12\x07\n\x03SUM\x10\x01\x12\x07\n\x03\x41VG\x10\x02\x1a\xf9\x03\n\x0cHavingClause\x12Q\n\taggregate\x18\x01 \x01(\x0b\x32>.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregate\x12V\n\x08operator\x18\x02 \x01(\x0e\x32\x44.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.Operator\x12R\n\x05value\x18\x03 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValueH\x00\"\xe0\x01\n\x08Operator\x12\t\n\x05\x45QUAL\x10\x00\x12\r\n\tNOT_EQUAL\x10\x01\x12\x10\n\x0cGREATER_THAN\x10\x02\x12\x1a\n\x16GREATER_THAN_OR_EQUALS\x10\x03\x12\r\n\tLESS_THAN\x10\x04\x12\x17\n\x13LESS_THAN_OR_EQUALS\x10\x05\x12\x0b\n\x07\x42\x45TWEEN\x10\x06\x12\x1a\n\x16\x42\x45TWEEN_EXCLUDE_BOUNDS\x10\x07\x12\x18\n\x14\x42\x45TWEEN_EXCLUDE_LEFT\x10\x08\x12\x19\n\x15\x42\x45TWEEN_EXCLUDE_RIGHT\x10\t\x12\x06\n\x02IN\x10\nB\x07\n\x05right\x1a\x90\x01\n\x0bOrderClause\x12\x0f\n\x05\x66ield\x18\x01 \x01(\tH\x00\x12S\n\taggregate\x18\x03 \x01(\x0b\x32>.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregateH\x00\x12\x11\n\tascending\x18\x02 \x01(\x08\x42\x08\n\x06target\x1a\xbb\x01\n\x15GetDocumentsRequestV0\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x15\n\rdocument_type\x18\x02 \x01(\t\x12\r\n\x05where\x18\x03 \x01(\x0c\x12\x10\n\x08order_by\x18\x04 \x01(\x0c\x12\r\n\x05limit\x18\x05 \x01(\r\x12\x15\n\x0bstart_after\x18\x06 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x07 \x01(\x0cH\x00\x12\r\n\x05prove\x18\x08 \x01(\x08\x42\x07\n\x05start\x1a\xa6\x0c\n\x15GetDocumentsRequestV1\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x15\n\rdocument_type\x18\x02 \x01(\t\x12Q\n\rwhere_clauses\x18\x03 \x03(\x0b\x32:.org.dash.platform.dapi.v0.GetDocumentsRequest.WhereClause\x12L\n\x08order_by\x18\x04 \x03(\x0b\x32:.org.dash.platform.dapi.v0.GetDocumentsRequest.OrderClause\x12\x12\n\x05limit\x18\x05 \x01(\rH\x01\x88\x01\x01\x12\x15\n\x0bstart_after\x18\x06 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x07 \x01(\x0cH\x00\x12\r\n\x05prove\x18\x08 \x01(\x08\x12\\\n\x07selects\x18\t \x03(\x0b\x32K.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.Select\x12\x10\n\x08group_by\x18\n \x03(\t\x12K\n\x06having\x18\x0b \x03(\x0b\x32;.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause\x12\x13\n\x06offset\x18\x0c \x01(\rH\x02\x88\x01\x01\x12\x61\n\x07\x63hained\x18\r \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.ChainedJoin\x12\x62\n\x0bsub_queries\x18\x0e \x03(\x0b\x32M.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.SubQuery\x1a\xc9\x01\n\x06Select\x12\x66\n\x08\x66unction\x18\x01 \x01(\x0e\x32T.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.Select.Function\x12\r\n\x05\x66ield\x18\x02 \x01(\t\"H\n\x08\x46unction\x12\r\n\tDOCUMENTS\x10\x00\x12\t\n\x05\x43OUNT\x10\x01\x12\x07\n\x03SUM\x10\x02\x12\x07\n\x03\x41VG\x10\x03\x12\x07\n\x03MIN\x10\x04\x12\x07\n\x03MAX\x10\x05\x1a\x41\n\x0b\x43hainedJoin\x12\x15\n\rjoin_property\x18\x01 \x01(\t\x12\x1b\n\x13outer_document_type\x18\x02 \x01(\t\x1a\xa6\x04\n\x08SubQuery\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x15\n\rdocument_type\x18\x02 \x01(\t\x12Q\n\rwhere_clauses\x18\x03 \x03(\x0b\x32:.org.dash.platform.dapi.v0.GetDocumentsRequest.WhereClause\x12L\n\x08order_by\x18\x04 \x03(\x0b\x32:.org.dash.platform.dapi.v0.GetDocumentsRequest.OrderClause\x12\x12\n\x05limit\x18\x05 \x01(\rH\x00\x88\x01\x01\x12`\n\x04kind\x18\x06 \x01(\x0e\x32R.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.SubQuery.Kind\x12\x63\n\x04\x62ind\x18\x07 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.SubQuery.Binding\x1a\x41\n\x07\x42inding\x12\x0e\n\x06source\x18\x01 \x01(\r\x12\x17\n\x0fsource_property\x18\x02 \x01(\t\x12\r\n\x05\x66ield\x18\x03 \x01(\t\" \n\x04Kind\x12\r\n\tDOCUMENTS\x10\x00\x12\t\n\x05\x43OUNT\x10\x01\x42\x08\n\x06_limitB\x07\n\x05startB\x08\n\x06_limitB\t\n\x07_offset\"\xfa\x01\n\rWhereOperator\x12\t\n\x05\x45QUAL\x10\x00\x12\x10\n\x0cGREATER_THAN\x10\x01\x12\x1a\n\x16GREATER_THAN_OR_EQUALS\x10\x02\x12\r\n\tLESS_THAN\x10\x03\x12\x17\n\x13LESS_THAN_OR_EQUALS\x10\x04\x12\x0b\n\x07\x42\x45TWEEN\x10\x05\x12\x1a\n\x16\x42\x45TWEEN_EXCLUDE_BOUNDS\x10\x06\x12\x18\n\x14\x42\x45TWEEN_EXCLUDE_LEFT\x10\x07\x12\x19\n\x15\x42\x45TWEEN_EXCLUDE_RIGHT\x10\x08\x12\x06\n\x02IN\x10\t\x12\x0f\n\x0bSTARTS_WITH\x10\n\x12\x11\n\rIN_TIME_RANGE\x10\x0b\x42\t\n\x07version\"\xf2\x1b\n\x14GetDocumentsResponse\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV0H\x00\x12T\n\x02v1\x18\x02 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1H\x00\x1a\x9b\x02\n\x16GetDocumentsResponseV0\x12\x65\n\tdocuments\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV0.DocumentsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1e\n\tDocuments\x12\x11\n\tdocuments\x18\x01 \x03(\x0c\x42\x08\n\x06result\x1a\x84\x18\n\x16GetDocumentsResponseV1\x12\x61\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultDataH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1e\n\tDocuments\x12\x11\n\tdocuments\x18\x01 \x03(\x0c\x1aL\n\nCountEntry\x12\x13\n\x06in_key\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x0b\n\x03key\x18\x02 \x01(\x0c\x12\x11\n\x05\x63ount\x18\x03 \x01(\x04\x42\x02\x30\x01\x42\t\n\x07_in_key\x1ar\n\x0c\x43ountEntries\x12\x62\n\x07\x65ntries\x18\x01 \x03(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountEntry\x1a\xa0\x01\n\x0c\x43ountResults\x12\x1d\n\x0f\x61ggregate_count\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x66\n\x07\x65ntries\x18\x02 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountEntriesH\x00\x42\t\n\x07variant\x1aH\n\x08SumEntry\x12\x13\n\x06in_key\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x0b\n\x03key\x18\x02 \x01(\x0c\x12\x0f\n\x03sum\x18\x03 \x01(\x12\x42\x02\x30\x01\x42\t\n\x07_in_key\x1an\n\nSumEntries\x12`\n\x07\x65ntries\x18\x01 \x03(\x0b\x32O.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumEntry\x1a\x9a\x01\n\nSumResults\x12\x1b\n\raggregate_sum\x18\x01 \x01(\x12\x42\x02\x30\x01H\x00\x12\x64\n\x07\x65ntries\x18\x02 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumEntriesH\x00\x42\t\n\x07variant\x1a_\n\x0c\x41verageEntry\x12\x13\n\x06in_key\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x0b\n\x03key\x18\x02 \x01(\x0c\x12\x11\n\x05\x63ount\x18\x03 \x01(\x04\x42\x02\x30\x01\x12\x0f\n\x03sum\x18\x04 \x01(\x12\x42\x02\x30\x01\x42\t\n\x07_in_key\x1av\n\x0e\x41verageEntries\x12\x64\n\x07\x65ntries\x18\x01 \x03(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageEntry\x1a\x36\n\x10\x41verageAggregate\x12\x11\n\x05\x63ount\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x0f\n\x03sum\x18\x02 \x01(\x12\x42\x02\x30\x01\x1a\xfb\x01\n\x0e\x41verageResults\x12t\n\x11\x61ggregate_average\x18\x01 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageAggregateH\x00\x12h\n\x07\x65ntries\x18\x02 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageEntriesH\x00\x42\t\n\x07variant\x1az\n\x0bRankedEntry\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\x13\n\x05\x63ount\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12\x11\n\x03sum\x18\x03 \x01(\x12\x42\x02\x30\x01H\x00\x12\r\n\x03\x61vg\x18\x04 \x01(\x01H\x00\x12\x13\n\x06in_key\x18\x05 \x01(\x0cH\x01\x88\x01\x01\x42\x07\n\x05valueB\t\n\x07_in_key\x1a\x9a\x01\n\rRankedEntries\x12\x63\n\x07\x65ntries\x18\x01 \x03(\x0b\x32R.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry\x12\x18\n\x07skipped\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x42\n\n\x08_skipped\x1a\xf7\x05\n\nResultData\x12\x65\n\tdocuments\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.DocumentsH\x00\x12\x65\n\x06\x63ounts\x18\x02 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResultsH\x00\x12\x61\n\x04sums\x18\x03 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumResultsH\x00\x12i\n\x08\x61verages\x18\x04 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResultsH\x00\x12\x66\n\x06ranked\x18\x05 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntriesH\x00\x12j\n\x07\x63hained\x18\x06 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ChainedDocumentsH\x00\x12n\n\tcomposite\x18\x07 \x01(\x0b\x32Y.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CompositeDocumentsH\x00\x42\t\n\x07variant\x1a_\n\x10\x43hainedDocuments\x12\x17\n\x0finner_documents\x18\x01 \x03(\x0c\x12\x17\n\x0fouter_documents\x18\x02 \x03(\x0c\x12\x19\n\x11missing_outer_ids\x18\x03 \x03(\x0c\x1a\xab\x03\n\x12\x43ompositeDocuments\x12\x16\n\x0epage_documents\x18\x01 \x03(\x0c\x12}\n\x0bsub_results\x18\x02 \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CompositeDocuments.SubQueryResult\x1a\xfd\x01\n\x0eSubQueryResult\x12\x65\n\tdocuments\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.DocumentsH\x00\x12\x65\n\x06\x63ounts\x18\x02 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountEntriesH\x00\x12\x13\n\x0bmissing_ids\x18\x03 \x03(\x0c\x42\x08\n\x06resultB\x08\n\x06resultB\t\n\x07version\"\xf4\x02\n\x19GetDocumentHistoryRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentHistoryRequest.GetDocumentHistoryRequestV0H\x00\x1a\xeb\x01\n\x1bGetDocumentHistoryRequestV0\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x13\n\x0b\x64ocument_id\x18\x03 \x01(\x0c\x12+\n\x05limit\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x17\n\x0bstart_at_ms\x18\x06 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x07 \x01(\x08\x42\t\n\x07version\"\xf7\x04\n\x1aGetDocumentHistoryResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetDocumentHistoryResponse.GetDocumentHistoryResponseV0H\x00\x1a\xeb\x03\n\x1cGetDocumentHistoryResponseV0\x12~\n\x10\x64ocument_history\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetDocumentHistoryResponse.GetDocumentHistoryResponseV0.DocumentHistoryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x37\n\x14\x44ocumentHistoryEntry\x12\x10\n\x04\x64\x61te\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05value\x18\x02 \x01(\x0c\x1a\x95\x01\n\x0f\x44ocumentHistory\x12\x81\x01\n\x10\x64ocument_entries\x18\x01 \x03(\x0b\x32g.org.dash.platform.dapi.v0.GetDocumentHistoryResponse.GetDocumentHistoryResponseV0.DocumentHistoryEntryB\x08\n\x06resultB\t\n\x07version\"\xed\x01\n!GetIdentityByPublicKeyHashRequest\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashRequest.GetIdentityByPublicKeyHashRequestV0H\x00\x1aM\n#GetIdentityByPublicKeyHashRequestV0\x12\x17\n\x0fpublic_key_hash\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xda\x02\n\"GetIdentityByPublicKeyHashResponse\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashResponse.GetIdentityByPublicKeyHashResponseV0H\x00\x1a\xb6\x01\n$GetIdentityByPublicKeyHashResponseV0\x12\x12\n\x08identity\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xbd\x02\n*GetIdentityByNonUniquePublicKeyHashRequest\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashRequest.GetIdentityByNonUniquePublicKeyHashRequestV0H\x00\x1a\x80\x01\n,GetIdentityByNonUniquePublicKeyHashRequestV0\x12\x17\n\x0fpublic_key_hash\x18\x01 \x01(\x0c\x12\x18\n\x0bstart_after\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\x0e\n\x0c_start_afterB\t\n\x07version\"\xd6\x06\n+GetIdentityByNonUniquePublicKeyHashResponse\x12\x82\x01\n\x02v0\x18\x01 \x01(\x0b\x32t.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse.GetIdentityByNonUniquePublicKeyHashResponseV0H\x00\x1a\x96\x05\n-GetIdentityByNonUniquePublicKeyHashResponseV0\x12\x9a\x01\n\x08identity\x18\x01 \x01(\x0b\x32\x85\x01.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse.GetIdentityByNonUniquePublicKeyHashResponseV0.IdentityResponseH\x00\x12\x9d\x01\n\x05proof\x18\x02 \x01(\x0b\x32\x8b\x01.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse.GetIdentityByNonUniquePublicKeyHashResponseV0.IdentityProvedResponseH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x36\n\x10IdentityResponse\x12\x15\n\x08identity\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x42\x0b\n\t_identity\x1a\xa6\x01\n\x16IdentityProvedResponse\x12P\n&grovedb_identity_public_key_hash_proof\x18\x01 \x01(\x0b\x32 .org.dash.platform.dapi.v0.Proof\x12!\n\x14identity_proof_bytes\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x42\x17\n\x15_identity_proof_bytesB\x08\n\x06resultB\t\n\x07version\"\xfb\x01\n#WaitForStateTransitionResultRequest\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.WaitForStateTransitionResultRequest.WaitForStateTransitionResultRequestV0H\x00\x1aU\n%WaitForStateTransitionResultRequestV0\x12\x1d\n\x15state_transition_hash\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x99\x03\n$WaitForStateTransitionResultResponse\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.WaitForStateTransitionResultResponse.WaitForStateTransitionResultResponseV0H\x00\x1a\xef\x01\n&WaitForStateTransitionResultResponseV0\x12I\n\x05\x65rror\x18\x01 \x01(\x0b\x32\x38.org.dash.platform.dapi.v0.StateTransitionBroadcastErrorH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc4\x01\n\x19GetConsensusParamsRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetConsensusParamsRequest.GetConsensusParamsRequestV0H\x00\x1a<\n\x1bGetConsensusParamsRequestV0\x12\x0e\n\x06height\x18\x01 \x01(\x05\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9c\x04\n\x1aGetConsensusParamsResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetConsensusParamsResponse.GetConsensusParamsResponseV0H\x00\x1aP\n\x14\x43onsensusParamsBlock\x12\x11\n\tmax_bytes\x18\x01 \x01(\t\x12\x0f\n\x07max_gas\x18\x02 \x01(\t\x12\x14\n\x0ctime_iota_ms\x18\x03 \x01(\t\x1a\x62\n\x17\x43onsensusParamsEvidence\x12\x1a\n\x12max_age_num_blocks\x18\x01 \x01(\t\x12\x18\n\x10max_age_duration\x18\x02 \x01(\t\x12\x11\n\tmax_bytes\x18\x03 \x01(\t\x1a\xda\x01\n\x1cGetConsensusParamsResponseV0\x12Y\n\x05\x62lock\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetConsensusParamsResponse.ConsensusParamsBlock\x12_\n\x08\x65vidence\x18\x02 \x01(\x0b\x32M.org.dash.platform.dapi.v0.GetConsensusParamsResponse.ConsensusParamsEvidenceB\t\n\x07version\"\xe4\x01\n%GetProtocolVersionUpgradeStateRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateRequest.GetProtocolVersionUpgradeStateRequestV0H\x00\x1a\x38\n\'GetProtocolVersionUpgradeStateRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xb5\x05\n&GetProtocolVersionUpgradeStateResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse.GetProtocolVersionUpgradeStateResponseV0H\x00\x1a\x85\x04\n(GetProtocolVersionUpgradeStateResponseV0\x12\x87\x01\n\x08versions\x18\x01 \x01(\x0b\x32s.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse.GetProtocolVersionUpgradeStateResponseV0.VersionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x96\x01\n\x08Versions\x12\x89\x01\n\x08versions\x18\x01 \x03(\x0b\x32w.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse.GetProtocolVersionUpgradeStateResponseV0.VersionEntry\x1a:\n\x0cVersionEntry\x12\x16\n\x0eversion_number\x18\x01 \x01(\r\x12\x12\n\nvote_count\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xa3\x02\n*GetProtocolVersionUpgradeVoteStatusRequest\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusRequest.GetProtocolVersionUpgradeVoteStatusRequestV0H\x00\x1ag\n,GetProtocolVersionUpgradeVoteStatusRequestV0\x12\x19\n\x11start_pro_tx_hash\x18\x01 \x01(\x0c\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xef\x05\n+GetProtocolVersionUpgradeVoteStatusResponse\x12\x82\x01\n\x02v0\x18\x01 \x01(\x0b\x32t.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse.GetProtocolVersionUpgradeVoteStatusResponseV0H\x00\x1a\xaf\x04\n-GetProtocolVersionUpgradeVoteStatusResponseV0\x12\x98\x01\n\x08versions\x18\x01 \x01(\x0b\x32\x83\x01.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse.GetProtocolVersionUpgradeVoteStatusResponseV0.VersionSignalsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xaf\x01\n\x0eVersionSignals\x12\x9c\x01\n\x0fversion_signals\x18\x01 \x03(\x0b\x32\x82\x01.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse.GetProtocolVersionUpgradeVoteStatusResponseV0.VersionSignal\x1a\x35\n\rVersionSignal\x12\x13\n\x0bpro_tx_hash\x18\x01 \x01(\x0c\x12\x0f\n\x07version\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xf5\x01\n\x14GetEpochsInfoRequest\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetEpochsInfoRequest.GetEpochsInfoRequestV0H\x00\x1a|\n\x16GetEpochsInfoRequestV0\x12\x31\n\x0bstart_epoch\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\x11\n\tascending\x18\x03 \x01(\x08\x12\r\n\x05prove\x18\x04 \x01(\x08\x42\t\n\x07version\"\x99\x05\n\x15GetEpochsInfoResponse\x12V\n\x02v0\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetEpochsInfoResponse.GetEpochsInfoResponseV0H\x00\x1a\x9c\x04\n\x17GetEpochsInfoResponseV0\x12\x65\n\x06\x65pochs\x18\x01 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetEpochsInfoResponse.GetEpochsInfoResponseV0.EpochInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1au\n\nEpochInfos\x12g\n\x0b\x65poch_infos\x18\x01 \x03(\x0b\x32R.org.dash.platform.dapi.v0.GetEpochsInfoResponse.GetEpochsInfoResponseV0.EpochInfo\x1a\xa6\x01\n\tEpochInfo\x12\x0e\n\x06number\x18\x01 \x01(\r\x12\x1e\n\x12\x66irst_block_height\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x1f\n\x17\x66irst_core_block_height\x18\x03 \x01(\r\x12\x16\n\nstart_time\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x16\n\x0e\x66\x65\x65_multiplier\x18\x05 \x01(\x01\x12\x18\n\x10protocol_version\x18\x06 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xbf\x02\n\x1dGetFinalizedEpochInfosRequest\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetFinalizedEpochInfosRequest.GetFinalizedEpochInfosRequestV0H\x00\x1a\xaa\x01\n\x1fGetFinalizedEpochInfosRequestV0\x12\x19\n\x11start_epoch_index\x18\x01 \x01(\r\x12\"\n\x1astart_epoch_index_included\x18\x02 \x01(\x08\x12\x17\n\x0f\x65nd_epoch_index\x18\x03 \x01(\r\x12 \n\x18\x65nd_epoch_index_included\x18\x04 \x01(\x08\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\t\n\x07version\"\xbd\t\n\x1eGetFinalizedEpochInfosResponse\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0H\x00\x1a\xa5\x08\n GetFinalizedEpochInfosResponseV0\x12\x80\x01\n\x06\x65pochs\x18\x01 \x01(\x0b\x32n.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0.FinalizedEpochInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xa4\x01\n\x13\x46inalizedEpochInfos\x12\x8c\x01\n\x15\x66inalized_epoch_infos\x18\x01 \x03(\x0b\x32m.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0.FinalizedEpochInfo\x1a\x9f\x04\n\x12\x46inalizedEpochInfo\x12\x0e\n\x06number\x18\x01 \x01(\r\x12\x1e\n\x12\x66irst_block_height\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x1f\n\x17\x66irst_core_block_height\x18\x03 \x01(\r\x12\x1c\n\x10\x66irst_block_time\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x16\n\x0e\x66\x65\x65_multiplier\x18\x05 \x01(\x01\x12\x18\n\x10protocol_version\x18\x06 \x01(\r\x12!\n\x15total_blocks_in_epoch\x18\x07 \x01(\x04\x42\x02\x30\x01\x12*\n\"next_epoch_start_core_block_height\x18\x08 \x01(\r\x12!\n\x15total_processing_fees\x18\t \x01(\x04\x42\x02\x30\x01\x12*\n\x1etotal_distributed_storage_fees\x18\n \x01(\x04\x42\x02\x30\x01\x12&\n\x1atotal_created_storage_fees\x18\x0b \x01(\x04\x42\x02\x30\x01\x12\x1e\n\x12\x63ore_block_rewards\x18\x0c \x01(\x04\x42\x02\x30\x01\x12\x81\x01\n\x0f\x62lock_proposers\x18\r \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0.BlockProposer\x1a\x39\n\rBlockProposer\x12\x13\n\x0bproposer_id\x18\x01 \x01(\x0c\x12\x13\n\x0b\x62lock_count\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xde\x04\n\x1cGetContestedResourcesRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetContestedResourcesRequest.GetContestedResourcesRequestV0H\x00\x1a\xcc\x03\n\x1eGetContestedResourcesRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x12\n\nindex_name\x18\x03 \x01(\t\x12\x1a\n\x12start_index_values\x18\x04 \x03(\x0c\x12\x18\n\x10\x65nd_index_values\x18\x05 \x03(\x0c\x12\x89\x01\n\x13start_at_value_info\x18\x06 \x01(\x0b\x32g.org.dash.platform.dapi.v0.GetContestedResourcesRequest.GetContestedResourcesRequestV0.StartAtValueInfoH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x07 \x01(\rH\x01\x88\x01\x01\x12\x17\n\x0forder_ascending\x18\x08 \x01(\x08\x12\r\n\x05prove\x18\t \x01(\x08\x1a\x45\n\x10StartAtValueInfo\x12\x13\n\x0bstart_value\x18\x01 \x01(\x0c\x12\x1c\n\x14start_value_included\x18\x02 \x01(\x08\x42\x16\n\x14_start_at_value_infoB\x08\n\x06_countB\t\n\x07version\"\x88\x04\n\x1dGetContestedResourcesResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetContestedResourcesResponse.GetContestedResourcesResponseV0H\x00\x1a\xf3\x02\n\x1fGetContestedResourcesResponseV0\x12\x95\x01\n\x19\x63ontested_resource_values\x18\x01 \x01(\x0b\x32p.org.dash.platform.dapi.v0.GetContestedResourcesResponse.GetContestedResourcesResponseV0.ContestedResourceValuesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a<\n\x17\x43ontestedResourceValues\x12!\n\x19\x63ontested_resource_values\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\xd2\x05\n\x1cGetVotePollsByEndDateRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest.GetVotePollsByEndDateRequestV0H\x00\x1a\xc0\x04\n\x1eGetVotePollsByEndDateRequestV0\x12\x84\x01\n\x0fstart_time_info\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest.GetVotePollsByEndDateRequestV0.StartAtTimeInfoH\x00\x88\x01\x01\x12\x80\x01\n\rend_time_info\x18\x02 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest.GetVotePollsByEndDateRequestV0.EndAtTimeInfoH\x01\x88\x01\x01\x12\x12\n\x05limit\x18\x03 \x01(\rH\x02\x88\x01\x01\x12\x13\n\x06offset\x18\x04 \x01(\rH\x03\x88\x01\x01\x12\x11\n\tascending\x18\x05 \x01(\x08\x12\r\n\x05prove\x18\x06 \x01(\x08\x1aI\n\x0fStartAtTimeInfo\x12\x19\n\rstart_time_ms\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1b\n\x13start_time_included\x18\x02 \x01(\x08\x1a\x43\n\rEndAtTimeInfo\x12\x17\n\x0b\x65nd_time_ms\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x19\n\x11\x65nd_time_included\x18\x02 \x01(\x08\x42\x12\n\x10_start_time_infoB\x10\n\x0e_end_time_infoB\x08\n\x06_limitB\t\n\x07_offsetB\t\n\x07version\"\x83\x06\n\x1dGetVotePollsByEndDateResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse.GetVotePollsByEndDateResponseV0H\x00\x1a\xee\x04\n\x1fGetVotePollsByEndDateResponseV0\x12\x9c\x01\n\x18vote_polls_by_timestamps\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse.GetVotePollsByEndDateResponseV0.SerializedVotePollsByTimestampsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aV\n\x1eSerializedVotePollsByTimestamp\x12\x15\n\ttimestamp\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1d\n\x15serialized_vote_polls\x18\x02 \x03(\x0c\x1a\xd7\x01\n\x1fSerializedVotePollsByTimestamps\x12\x99\x01\n\x18vote_polls_by_timestamps\x18\x01 \x03(\x0b\x32w.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse.GetVotePollsByEndDateResponseV0.SerializedVotePollsByTimestamp\x12\x18\n\x10\x66inished_results\x18\x02 \x01(\x08\x42\x08\n\x06resultB\t\n\x07version\"\xff\x06\n$GetContestedResourceVoteStateRequest\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest.GetContestedResourceVoteStateRequestV0H\x00\x1a\xd5\x05\n&GetContestedResourceVoteStateRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x12\n\nindex_name\x18\x03 \x01(\t\x12\x14\n\x0cindex_values\x18\x04 \x03(\x0c\x12\x86\x01\n\x0bresult_type\x18\x05 \x01(\x0e\x32q.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest.GetContestedResourceVoteStateRequestV0.ResultType\x12\x36\n.allow_include_locked_and_abstaining_vote_tally\x18\x06 \x01(\x08\x12\xa3\x01\n\x18start_at_identifier_info\x18\x07 \x01(\x0b\x32|.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest.GetContestedResourceVoteStateRequestV0.StartAtIdentifierInfoH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x08 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\t \x01(\x08\x1aT\n\x15StartAtIdentifierInfo\x12\x18\n\x10start_identifier\x18\x01 \x01(\x0c\x12!\n\x19start_identifier_included\x18\x02 \x01(\x08\"I\n\nResultType\x12\r\n\tDOCUMENTS\x10\x00\x12\x0e\n\nVOTE_TALLY\x10\x01\x12\x1c\n\x18\x44OCUMENTS_AND_VOTE_TALLY\x10\x02\x42\x1b\n\x19_start_at_identifier_infoB\x08\n\x06_countB\t\n\x07version\"\x94\x0c\n%GetContestedResourceVoteStateResponse\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0H\x00\x1a\xe7\n\n\'GetContestedResourceVoteStateResponseV0\x12\xae\x01\n\x1d\x63ontested_resource_contenders\x18\x01 \x01(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.ContestedResourceContendersH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xda\x03\n\x10\x46inishedVoteInfo\x12\xad\x01\n\x15\x66inished_vote_outcome\x18\x01 \x01(\x0e\x32\x8d\x01.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.FinishedVoteInfo.FinishedVoteOutcome\x12\x1f\n\x12won_by_identity_id\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x12$\n\x18\x66inished_at_block_height\x18\x03 \x01(\x04\x42\x02\x30\x01\x12%\n\x1d\x66inished_at_core_block_height\x18\x04 \x01(\r\x12%\n\x19\x66inished_at_block_time_ms\x18\x05 \x01(\x04\x42\x02\x30\x01\x12\x19\n\x11\x66inished_at_epoch\x18\x06 \x01(\r\"O\n\x13\x46inishedVoteOutcome\x12\x14\n\x10TOWARDS_IDENTITY\x10\x00\x12\n\n\x06LOCKED\x10\x01\x12\x16\n\x12NO_PREVIOUS_WINNER\x10\x02\x42\x15\n\x13_won_by_identity_id\x1a\xc4\x03\n\x1b\x43ontestedResourceContenders\x12\x86\x01\n\ncontenders\x18\x01 \x03(\x0b\x32r.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.Contender\x12\x1f\n\x12\x61\x62stain_vote_tally\x18\x02 \x01(\rH\x00\x88\x01\x01\x12\x1c\n\x0flock_vote_tally\x18\x03 \x01(\rH\x01\x88\x01\x01\x12\x9a\x01\n\x12\x66inished_vote_info\x18\x04 \x01(\x0b\x32y.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.FinishedVoteInfoH\x02\x88\x01\x01\x42\x15\n\x13_abstain_vote_tallyB\x12\n\x10_lock_vote_tallyB\x15\n\x13_finished_vote_info\x1ak\n\tContender\x12\x12\n\nidentifier\x18\x01 \x01(\x0c\x12\x17\n\nvote_count\x18\x02 \x01(\rH\x00\x88\x01\x01\x12\x15\n\x08\x64ocument\x18\x03 \x01(\x0cH\x01\x88\x01\x01\x42\r\n\x0b_vote_countB\x0b\n\t_documentB\x08\n\x06resultB\t\n\x07version\"\xd5\x05\n,GetContestedResourceVotersForIdentityRequest\x12\x84\x01\n\x02v0\x18\x01 \x01(\x0b\x32v.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityRequest.GetContestedResourceVotersForIdentityRequestV0H\x00\x1a\x92\x04\n.GetContestedResourceVotersForIdentityRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x12\n\nindex_name\x18\x03 \x01(\t\x12\x14\n\x0cindex_values\x18\x04 \x03(\x0c\x12\x15\n\rcontestant_id\x18\x05 \x01(\x0c\x12\xb4\x01\n\x18start_at_identifier_info\x18\x06 \x01(\x0b\x32\x8c\x01.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityRequest.GetContestedResourceVotersForIdentityRequestV0.StartAtIdentifierInfoH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x07 \x01(\rH\x01\x88\x01\x01\x12\x17\n\x0forder_ascending\x18\x08 \x01(\x08\x12\r\n\x05prove\x18\t \x01(\x08\x1aT\n\x15StartAtIdentifierInfo\x12\x18\n\x10start_identifier\x18\x01 \x01(\x0c\x12!\n\x19start_identifier_included\x18\x02 \x01(\x08\x42\x1b\n\x19_start_at_identifier_infoB\x08\n\x06_countB\t\n\x07version\"\xf1\x04\n-GetContestedResourceVotersForIdentityResponse\x12\x86\x01\n\x02v0\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityResponse.GetContestedResourceVotersForIdentityResponseV0H\x00\x1a\xab\x03\n/GetContestedResourceVotersForIdentityResponseV0\x12\xb6\x01\n\x19\x63ontested_resource_voters\x18\x01 \x01(\x0b\x32\x90\x01.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityResponse.GetContestedResourceVotersForIdentityResponseV0.ContestedResourceVotersH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x43\n\x17\x43ontestedResourceVoters\x12\x0e\n\x06voters\x18\x01 \x03(\x0c\x12\x18\n\x10\x66inished_results\x18\x02 \x01(\x08\x42\x08\n\x06resultB\t\n\x07version\"\xad\x05\n(GetContestedResourceIdentityVotesRequest\x12|\n\x02v0\x18\x01 \x01(\x0b\x32n.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesRequest.GetContestedResourceIdentityVotesRequestV0H\x00\x1a\xf7\x03\n*GetContestedResourceIdentityVotesRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12+\n\x05limit\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x17\n\x0forder_ascending\x18\x04 \x01(\x08\x12\xae\x01\n\x1astart_at_vote_poll_id_info\x18\x05 \x01(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesRequest.GetContestedResourceIdentityVotesRequestV0.StartAtVotePollIdInfoH\x00\x88\x01\x01\x12\r\n\x05prove\x18\x06 \x01(\x08\x1a\x61\n\x15StartAtVotePollIdInfo\x12 \n\x18start_at_poll_identifier\x18\x01 \x01(\x0c\x12&\n\x1estart_poll_identifier_included\x18\x02 \x01(\x08\x42\x1d\n\x1b_start_at_vote_poll_id_infoB\t\n\x07version\"\xc8\n\n)GetContestedResourceIdentityVotesResponse\x12~\n\x02v0\x18\x01 \x01(\x0b\x32p.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0H\x00\x1a\x8f\t\n+GetContestedResourceIdentityVotesResponseV0\x12\xa1\x01\n\x05votes\x18\x01 \x01(\x0b\x32\x8f\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ContestedResourceIdentityVotesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xf7\x01\n\x1e\x43ontestedResourceIdentityVotes\x12\xba\x01\n!contested_resource_identity_votes\x18\x01 \x03(\x0b\x32\x8e\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ContestedResourceIdentityVote\x12\x18\n\x10\x66inished_results\x18\x02 \x01(\x08\x1a\xad\x02\n\x12ResourceVoteChoice\x12\xad\x01\n\x10vote_choice_type\x18\x01 \x01(\x0e\x32\x92\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ResourceVoteChoice.VoteChoiceType\x12\x18\n\x0bidentity_id\x18\x02 \x01(\x0cH\x00\x88\x01\x01\"=\n\x0eVoteChoiceType\x12\x14\n\x10TOWARDS_IDENTITY\x10\x00\x12\x0b\n\x07\x41\x42STAIN\x10\x01\x12\x08\n\x04LOCK\x10\x02\x42\x0e\n\x0c_identity_id\x1a\x95\x02\n\x1d\x43ontestedResourceIdentityVote\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\'\n\x1fserialized_index_storage_values\x18\x03 \x03(\x0c\x12\x99\x01\n\x0bvote_choice\x18\x04 \x01(\x0b\x32\x83\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ResourceVoteChoiceB\x08\n\x06resultB\t\n\x07version\"\xf0\x01\n%GetPrefundedSpecializedBalanceRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceRequest.GetPrefundedSpecializedBalanceRequestV0H\x00\x1a\x44\n\'GetPrefundedSpecializedBalanceRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xed\x02\n&GetPrefundedSpecializedBalanceResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceResponse.GetPrefundedSpecializedBalanceResponseV0H\x00\x1a\xbd\x01\n(GetPrefundedSpecializedBalanceResponseV0\x12\x15\n\x07\x62\x61lance\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xd0\x01\n GetTotalCreditsInPlatformRequest\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformRequest.GetTotalCreditsInPlatformRequestV0H\x00\x1a\x33\n\"GetTotalCreditsInPlatformRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xd9\x02\n!GetTotalCreditsInPlatformResponse\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformResponse.GetTotalCreditsInPlatformResponseV0H\x00\x1a\xb8\x01\n#GetTotalCreditsInPlatformResponseV0\x12\x15\n\x07\x63redits\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc4\x01\n\x16GetPathElementsRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetPathElementsRequest.GetPathElementsRequestV0H\x00\x1a\x45\n\x18GetPathElementsRequestV0\x12\x0c\n\x04path\x18\x01 \x03(\x0c\x12\x0c\n\x04keys\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xa3\x03\n\x17GetPathElementsResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetPathElementsResponse.GetPathElementsResponseV0H\x00\x1a\xa0\x02\n\x19GetPathElementsResponseV0\x12i\n\x08\x65lements\x18\x01 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetPathElementsResponse.GetPathElementsResponseV0.ElementsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1c\n\x08\x45lements\x12\x10\n\x08\x65lements\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\x81\x01\n\x10GetStatusRequest\x12L\n\x02v0\x18\x01 \x01(\x0b\x32>.org.dash.platform.dapi.v0.GetStatusRequest.GetStatusRequestV0H\x00\x1a\x14\n\x12GetStatusRequestV0B\t\n\x07version\"\xe4\x10\n\x11GetStatusResponse\x12N\n\x02v0\x18\x01 \x01(\x0b\x32@.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0H\x00\x1a\xf3\x0f\n\x13GetStatusResponseV0\x12Y\n\x07version\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version\x12S\n\x04node\x18\x02 \x01(\x0b\x32\x45.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Node\x12U\n\x05\x63hain\x18\x03 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Chain\x12Y\n\x07network\x18\x04 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Network\x12^\n\nstate_sync\x18\x05 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.StateSync\x12S\n\x04time\x18\x06 \x01(\x0b\x32\x45.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Time\x1a\x82\x05\n\x07Version\x12\x63\n\x08software\x18\x01 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Software\x12\x63\n\x08protocol\x18\x02 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Protocol\x1a^\n\x08Software\x12\x0c\n\x04\x64\x61pi\x18\x01 \x01(\t\x12\x12\n\x05\x64rive\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x17\n\ntenderdash\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\x08\n\x06_driveB\r\n\x0b_tenderdash\x1a\xcc\x02\n\x08Protocol\x12p\n\ntenderdash\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Protocol.Tenderdash\x12\x66\n\x05\x64rive\x18\x02 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Protocol.Drive\x1a(\n\nTenderdash\x12\x0b\n\x03p2p\x18\x01 \x01(\r\x12\r\n\x05\x62lock\x18\x02 \x01(\r\x1a<\n\x05\x44rive\x12\x0e\n\x06latest\x18\x03 \x01(\r\x12\x0f\n\x07\x63urrent\x18\x04 \x01(\r\x12\x12\n\nnext_epoch\x18\x05 \x01(\r\x1a\x7f\n\x04Time\x12\x11\n\x05local\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x16\n\x05\x62lock\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x12\x18\n\x07genesis\x18\x03 \x01(\x04\x42\x02\x30\x01H\x01\x88\x01\x01\x12\x12\n\x05\x65poch\x18\x04 \x01(\rH\x02\x88\x01\x01\x42\x08\n\x06_blockB\n\n\x08_genesisB\x08\n\x06_epoch\x1a<\n\x04Node\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\x18\n\x0bpro_tx_hash\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x42\x0e\n\x0c_pro_tx_hash\x1a\xb3\x02\n\x05\x43hain\x12\x13\n\x0b\x63\x61tching_up\x18\x01 \x01(\x08\x12\x19\n\x11latest_block_hash\x18\x02 \x01(\x0c\x12\x17\n\x0flatest_app_hash\x18\x03 \x01(\x0c\x12\x1f\n\x13latest_block_height\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x1b\n\x13\x65\x61rliest_block_hash\x18\x05 \x01(\x0c\x12\x19\n\x11\x65\x61rliest_app_hash\x18\x06 \x01(\x0c\x12!\n\x15\x65\x61rliest_block_height\x18\x07 \x01(\x04\x42\x02\x30\x01\x12!\n\x15max_peer_block_height\x18\t \x01(\x04\x42\x02\x30\x01\x12%\n\x18\x63ore_chain_locked_height\x18\n \x01(\rH\x00\x88\x01\x01\x42\x1b\n\x19_core_chain_locked_height\x1a\x43\n\x07Network\x12\x10\n\x08\x63hain_id\x18\x01 \x01(\t\x12\x13\n\x0bpeers_count\x18\x02 \x01(\r\x12\x11\n\tlistening\x18\x03 \x01(\x08\x1a\x85\x02\n\tStateSync\x12\x1d\n\x11total_synced_time\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1a\n\x0eremaining_time\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x17\n\x0ftotal_snapshots\x18\x03 \x01(\r\x12\"\n\x16\x63hunk_process_avg_time\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x1b\n\x0fsnapshot_height\x18\x05 \x01(\x04\x42\x02\x30\x01\x12!\n\x15snapshot_chunks_count\x18\x06 \x01(\x04\x42\x02\x30\x01\x12\x1d\n\x11\x62\x61\x63kfilled_blocks\x18\x07 \x01(\x04\x42\x02\x30\x01\x12!\n\x15\x62\x61\x63kfill_blocks_total\x18\x08 \x01(\x04\x42\x02\x30\x01\x42\t\n\x07version\"\xb1\x01\n\x1cGetCurrentQuorumsInfoRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoRequest.GetCurrentQuorumsInfoRequestV0H\x00\x1a \n\x1eGetCurrentQuorumsInfoRequestV0B\t\n\x07version\"\xa1\x05\n\x1dGetCurrentQuorumsInfoResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse.GetCurrentQuorumsInfoResponseV0H\x00\x1a\x46\n\x0bValidatorV0\x12\x13\n\x0bpro_tx_hash\x18\x01 \x01(\x0c\x12\x0f\n\x07node_ip\x18\x02 \x01(\t\x12\x11\n\tis_banned\x18\x03 \x01(\x08\x1a\xaf\x01\n\x0eValidatorSetV0\x12\x13\n\x0bquorum_hash\x18\x01 \x01(\x0c\x12\x13\n\x0b\x63ore_height\x18\x02 \x01(\r\x12U\n\x07members\x18\x03 \x03(\x0b\x32\x44.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse.ValidatorV0\x12\x1c\n\x14threshold_public_key\x18\x04 \x01(\x0c\x1a\x92\x02\n\x1fGetCurrentQuorumsInfoResponseV0\x12\x15\n\rquorum_hashes\x18\x01 \x03(\x0c\x12\x1b\n\x13\x63urrent_quorum_hash\x18\x02 \x01(\x0c\x12_\n\x0evalidator_sets\x18\x03 \x03(\x0b\x32G.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse.ValidatorSetV0\x12\x1b\n\x13last_block_proposer\x18\x04 \x01(\x0c\x12=\n\x08metadata\x18\x05 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\t\n\x07version\"\xf4\x01\n\x1fGetIdentityTokenBalancesRequest\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetIdentityTokenBalancesRequest.GetIdentityTokenBalancesRequestV0H\x00\x1aZ\n!GetIdentityTokenBalancesRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x11\n\ttoken_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xad\x05\n GetIdentityTokenBalancesResponse\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse.GetIdentityTokenBalancesResponseV0H\x00\x1a\x8f\x04\n\"GetIdentityTokenBalancesResponseV0\x12\x86\x01\n\x0etoken_balances\x18\x01 \x01(\x0b\x32l.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse.GetIdentityTokenBalancesResponseV0.TokenBalancesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aG\n\x11TokenBalanceEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x14\n\x07\x62\x61lance\x18\x02 \x01(\x04H\x00\x88\x01\x01\x42\n\n\x08_balance\x1a\x9a\x01\n\rTokenBalances\x12\x88\x01\n\x0etoken_balances\x18\x01 \x03(\x0b\x32p.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse.GetIdentityTokenBalancesResponseV0.TokenBalanceEntryB\x08\n\x06resultB\t\n\x07version\"\xfc\x01\n!GetIdentitiesTokenBalancesRequest\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesRequest.GetIdentitiesTokenBalancesRequestV0H\x00\x1a\\\n#GetIdentitiesTokenBalancesRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x14\n\x0cidentity_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xf2\x05\n\"GetIdentitiesTokenBalancesResponse\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse.GetIdentitiesTokenBalancesResponseV0H\x00\x1a\xce\x04\n$GetIdentitiesTokenBalancesResponseV0\x12\x9b\x01\n\x17identity_token_balances\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse.GetIdentitiesTokenBalancesResponseV0.IdentityTokenBalancesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aR\n\x19IdentityTokenBalanceEntry\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x14\n\x07\x62\x61lance\x18\x02 \x01(\x04H\x00\x88\x01\x01\x42\n\n\x08_balance\x1a\xb7\x01\n\x15IdentityTokenBalances\x12\x9d\x01\n\x17identity_token_balances\x18\x01 \x03(\x0b\x32|.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse.GetIdentitiesTokenBalancesResponseV0.IdentityTokenBalanceEntryB\x08\n\x06resultB\t\n\x07version\"\xe8\x01\n\x1cGetIdentityTokenInfosRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetIdentityTokenInfosRequest.GetIdentityTokenInfosRequestV0H\x00\x1aW\n\x1eGetIdentityTokenInfosRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x11\n\ttoken_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\x98\x06\n\x1dGetIdentityTokenInfosResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0H\x00\x1a\x83\x05\n\x1fGetIdentityTokenInfosResponseV0\x12z\n\x0btoken_infos\x18\x01 \x01(\x0b\x32\x63.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0.TokenInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a(\n\x16TokenIdentityInfoEntry\x12\x0e\n\x06\x66rozen\x18\x01 \x01(\x08\x1a\xb0\x01\n\x0eTokenInfoEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x82\x01\n\x04info\x18\x02 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0.TokenIdentityInfoEntryH\x00\x88\x01\x01\x42\x07\n\x05_info\x1a\x8a\x01\n\nTokenInfos\x12|\n\x0btoken_infos\x18\x01 \x03(\x0b\x32g.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0.TokenInfoEntryB\x08\n\x06resultB\t\n\x07version\"\xf0\x01\n\x1eGetIdentitiesTokenInfosRequest\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosRequest.GetIdentitiesTokenInfosRequestV0H\x00\x1aY\n GetIdentitiesTokenInfosRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x14\n\x0cidentity_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xca\x06\n\x1fGetIdentitiesTokenInfosResponse\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0H\x00\x1a\xaf\x05\n!GetIdentitiesTokenInfosResponseV0\x12\x8f\x01\n\x14identity_token_infos\x18\x01 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0.IdentityTokenInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a(\n\x16TokenIdentityInfoEntry\x12\x0e\n\x06\x66rozen\x18\x01 \x01(\x08\x1a\xb7\x01\n\x0eTokenInfoEntry\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x86\x01\n\x04info\x18\x02 \x01(\x0b\x32s.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0.TokenIdentityInfoEntryH\x00\x88\x01\x01\x42\x07\n\x05_info\x1a\x97\x01\n\x12IdentityTokenInfos\x12\x80\x01\n\x0btoken_infos\x18\x01 \x03(\x0b\x32k.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0.TokenInfoEntryB\x08\n\x06resultB\t\n\x07version\"\xbf\x01\n\x17GetTokenStatusesRequest\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetTokenStatusesRequest.GetTokenStatusesRequestV0H\x00\x1a=\n\x19GetTokenStatusesRequestV0\x12\x11\n\ttoken_ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xe7\x04\n\x18GetTokenStatusesResponse\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetTokenStatusesResponse.GetTokenStatusesResponseV0H\x00\x1a\xe1\x03\n\x1aGetTokenStatusesResponseV0\x12v\n\x0etoken_statuses\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetTokenStatusesResponse.GetTokenStatusesResponseV0.TokenStatusesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x44\n\x10TokenStatusEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x13\n\x06paused\x18\x02 \x01(\x08H\x00\x88\x01\x01\x42\t\n\x07_paused\x1a\x88\x01\n\rTokenStatuses\x12w\n\x0etoken_statuses\x18\x01 \x03(\x0b\x32_.org.dash.platform.dapi.v0.GetTokenStatusesResponse.GetTokenStatusesResponseV0.TokenStatusEntryB\x08\n\x06resultB\t\n\x07version\"\xef\x01\n#GetTokenDirectPurchasePricesRequest\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesRequest.GetTokenDirectPurchasePricesRequestV0H\x00\x1aI\n%GetTokenDirectPurchasePricesRequestV0\x12\x11\n\ttoken_ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x8b\t\n$GetTokenDirectPurchasePricesResponse\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0H\x00\x1a\xe1\x07\n&GetTokenDirectPurchasePricesResponseV0\x12\xa9\x01\n\x1ctoken_direct_purchase_prices\x18\x01 \x01(\x0b\x32\x80\x01.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.TokenDirectPurchasePricesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x33\n\x10PriceForQuantity\x12\x10\n\x08quantity\x18\x01 \x01(\x04\x12\r\n\x05price\x18\x02 \x01(\x04\x1a\xa7\x01\n\x0fPricingSchedule\x12\x93\x01\n\x12price_for_quantity\x18\x01 \x03(\x0b\x32w.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.PriceForQuantity\x1a\xe4\x01\n\x1dTokenDirectPurchasePriceEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x15\n\x0b\x66ixed_price\x18\x02 \x01(\x04H\x00\x12\x90\x01\n\x0evariable_price\x18\x03 \x01(\x0b\x32v.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.PricingScheduleH\x00\x42\x07\n\x05price\x1a\xc8\x01\n\x19TokenDirectPurchasePrices\x12\xaa\x01\n\x1btoken_direct_purchase_price\x18\x01 \x03(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.TokenDirectPurchasePriceEntryB\x08\n\x06resultB\t\n\x07version\"\xce\x01\n\x1bGetTokenContractInfoRequest\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetTokenContractInfoRequest.GetTokenContractInfoRequestV0H\x00\x1a@\n\x1dGetTokenContractInfoRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xfb\x03\n\x1cGetTokenContractInfoResponse\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetTokenContractInfoResponse.GetTokenContractInfoResponseV0H\x00\x1a\xe9\x02\n\x1eGetTokenContractInfoResponseV0\x12|\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32l.org.dash.platform.dapi.v0.GetTokenContractInfoResponse.GetTokenContractInfoResponseV0.TokenContractInfoDataH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aM\n\x15TokenContractInfoData\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17token_contract_position\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xef\x04\n)GetTokenPreProgrammedDistributionsRequest\x12~\n\x02v0\x18\x01 \x01(\x0b\x32p.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsRequest.GetTokenPreProgrammedDistributionsRequestV0H\x00\x1a\xb6\x03\n+GetTokenPreProgrammedDistributionsRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x98\x01\n\rstart_at_info\x18\x02 \x01(\x0b\x32|.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsRequest.GetTokenPreProgrammedDistributionsRequestV0.StartAtInfoH\x00\x88\x01\x01\x12\x12\n\x05limit\x18\x03 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x04 \x01(\x08\x1a\x9a\x01\n\x0bStartAtInfo\x12\x15\n\rstart_time_ms\x18\x01 \x01(\x04\x12\x1c\n\x0fstart_recipient\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x12%\n\x18start_recipient_included\x18\x03 \x01(\x08H\x01\x88\x01\x01\x42\x12\n\x10_start_recipientB\x1b\n\x19_start_recipient_includedB\x10\n\x0e_start_at_infoB\x08\n\x06_limitB\t\n\x07version\"\xec\x07\n*GetTokenPreProgrammedDistributionsResponse\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0H\x00\x1a\xaf\x06\n,GetTokenPreProgrammedDistributionsResponseV0\x12\xa5\x01\n\x13token_distributions\x18\x01 \x01(\x0b\x32\x85\x01.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0.TokenDistributionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a>\n\x16TokenDistributionEntry\x12\x14\n\x0crecipient_id\x18\x01 \x01(\x0c\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x04\x1a\xd4\x01\n\x1bTokenTimedDistributionEntry\x12\x11\n\ttimestamp\x18\x01 \x01(\x04\x12\xa1\x01\n\rdistributions\x18\x02 \x03(\x0b\x32\x89\x01.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0.TokenDistributionEntry\x1a\xc3\x01\n\x12TokenDistributions\x12\xac\x01\n\x13token_distributions\x18\x01 \x03(\x0b\x32\x8e\x01.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0.TokenTimedDistributionEntryB\x08\n\x06resultB\t\n\x07version\"\x82\x04\n-GetTokenPerpetualDistributionLastClaimRequest\x12\x86\x01\n\x02v0\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimRequest.GetTokenPerpetualDistributionLastClaimRequestV0H\x00\x1aI\n\x11\x43ontractTokenInfo\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17token_contract_position\x18\x02 \x01(\r\x1a\xf1\x01\n/GetTokenPerpetualDistributionLastClaimRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12v\n\rcontract_info\x18\x02 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimRequest.ContractTokenInfoH\x00\x88\x01\x01\x12\x13\n\x0bidentity_id\x18\x04 \x01(\x0c\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x10\n\x0e_contract_infoB\t\n\x07version\"\x93\x05\n.GetTokenPerpetualDistributionLastClaimResponse\x12\x88\x01\n\x02v0\x18\x01 \x01(\x0b\x32z.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimResponse.GetTokenPerpetualDistributionLastClaimResponseV0H\x00\x1a\xca\x03\n0GetTokenPerpetualDistributionLastClaimResponseV0\x12\x9f\x01\n\nlast_claim\x18\x01 \x01(\x0b\x32\x88\x01.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimResponse.GetTokenPerpetualDistributionLastClaimResponseV0.LastClaimInfoH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1ax\n\rLastClaimInfo\x12\x1a\n\x0ctimestamp_ms\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x1a\n\x0c\x62lock_height\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12\x0f\n\x05\x65poch\x18\x03 \x01(\rH\x00\x12\x13\n\traw_bytes\x18\x04 \x01(\x0cH\x00\x42\t\n\x07paid_atB\x08\n\x06resultB\t\n\x07version\"\xca\x01\n\x1aGetTokenTotalSupplyRequest\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetTokenTotalSupplyRequest.GetTokenTotalSupplyRequestV0H\x00\x1a?\n\x1cGetTokenTotalSupplyRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xaf\x04\n\x1bGetTokenTotalSupplyResponse\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetTokenTotalSupplyResponse.GetTokenTotalSupplyResponseV0H\x00\x1a\xa0\x03\n\x1dGetTokenTotalSupplyResponseV0\x12\x88\x01\n\x12token_total_supply\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetTokenTotalSupplyResponse.GetTokenTotalSupplyResponseV0.TokenTotalSupplyEntryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1ax\n\x15TokenTotalSupplyEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x30\n(total_aggregated_amount_in_user_accounts\x18\x02 \x01(\x04\x12\x1b\n\x13total_system_amount\x18\x03 \x01(\x04\x42\x08\n\x06resultB\t\n\x07version\"\xd2\x01\n\x13GetGroupInfoRequest\x12R\n\x02v0\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetGroupInfoRequest.GetGroupInfoRequestV0H\x00\x1a\\\n\x15GetGroupInfoRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17group_contract_position\x18\x02 \x01(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xd4\x05\n\x14GetGroupInfoResponse\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0H\x00\x1a\xda\x04\n\x16GetGroupInfoResponseV0\x12\x66\n\ngroup_info\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0.GroupInfoH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x04 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x34\n\x10GroupMemberEntry\x12\x11\n\tmember_id\x18\x01 \x01(\x0c\x12\r\n\x05power\x18\x02 \x01(\r\x1a\x98\x01\n\x0eGroupInfoEntry\x12h\n\x07members\x18\x01 \x03(\x0b\x32W.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0.GroupMemberEntry\x12\x1c\n\x14group_required_power\x18\x02 \x01(\r\x1a\x8a\x01\n\tGroupInfo\x12n\n\ngroup_info\x18\x01 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0.GroupInfoEntryH\x00\x88\x01\x01\x42\r\n\x0b_group_infoB\x08\n\x06resultB\t\n\x07version\"\xed\x03\n\x14GetGroupInfosRequest\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetGroupInfosRequest.GetGroupInfosRequestV0H\x00\x1au\n\x1cStartAtGroupContractPosition\x12%\n\x1dstart_group_contract_position\x18\x01 \x01(\r\x12.\n&start_group_contract_position_included\x18\x02 \x01(\x08\x1a\xfc\x01\n\x16GetGroupInfosRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12{\n start_at_group_contract_position\x18\x02 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetGroupInfosRequest.StartAtGroupContractPositionH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x03 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x04 \x01(\x08\x42#\n!_start_at_group_contract_positionB\x08\n\x06_countB\t\n\x07version\"\xff\x05\n\x15GetGroupInfosResponse\x12V\n\x02v0\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0H\x00\x1a\x82\x05\n\x17GetGroupInfosResponseV0\x12j\n\x0bgroup_infos\x18\x01 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0.GroupInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x04 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x34\n\x10GroupMemberEntry\x12\x11\n\tmember_id\x18\x01 \x01(\x0c\x12\r\n\x05power\x18\x02 \x01(\r\x1a\xc3\x01\n\x16GroupPositionInfoEntry\x12\x1f\n\x17group_contract_position\x18\x01 \x01(\r\x12j\n\x07members\x18\x02 \x03(\x0b\x32Y.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0.GroupMemberEntry\x12\x1c\n\x14group_required_power\x18\x03 \x01(\r\x1a\x82\x01\n\nGroupInfos\x12t\n\x0bgroup_infos\x18\x01 \x03(\x0b\x32_.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0.GroupPositionInfoEntryB\x08\n\x06resultB\t\n\x07version\"\xbe\x04\n\x16GetGroupActionsRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetGroupActionsRequest.GetGroupActionsRequestV0H\x00\x1aL\n\x0fStartAtActionId\x12\x17\n\x0fstart_action_id\x18\x01 \x01(\x0c\x12 \n\x18start_action_id_included\x18\x02 \x01(\x08\x1a\xc8\x02\n\x18GetGroupActionsRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17group_contract_position\x18\x02 \x01(\r\x12N\n\x06status\x18\x03 \x01(\x0e\x32>.org.dash.platform.dapi.v0.GetGroupActionsRequest.ActionStatus\x12\x62\n\x12start_at_action_id\x18\x04 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetGroupActionsRequest.StartAtActionIdH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x05 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x06 \x01(\x08\x42\x15\n\x13_start_at_action_idB\x08\n\x06_count\"&\n\x0c\x41\x63tionStatus\x12\n\n\x06\x41\x43TIVE\x10\x00\x12\n\n\x06\x43LOSED\x10\x01\x42\t\n\x07version\"\xd6\x1e\n\x17GetGroupActionsResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0H\x00\x1a\xd3\x1d\n\x19GetGroupActionsResponseV0\x12r\n\rgroup_actions\x18\x01 \x01(\x0b\x32Y.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a[\n\tMintEvent\x12\x0e\n\x06\x61mount\x18\x01 \x01(\x04\x12\x14\n\x0crecipient_id\x18\x02 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a[\n\tBurnEvent\x12\x0e\n\x06\x61mount\x18\x01 \x01(\x04\x12\x14\n\x0c\x62urn_from_id\x18\x02 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1aJ\n\x0b\x46reezeEvent\x12\x11\n\tfrozen_id\x18\x01 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1aL\n\rUnfreezeEvent\x12\x11\n\tfrozen_id\x18\x01 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\x66\n\x17\x44\x65stroyFrozenFundsEvent\x12\x11\n\tfrozen_id\x18\x01 \x01(\x0c\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x04\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\x64\n\x13SharedEncryptedNote\x12\x18\n\x10sender_key_index\x18\x01 \x01(\r\x12\x1b\n\x13recipient_key_index\x18\x02 \x01(\r\x12\x16\n\x0e\x65ncrypted_data\x18\x03 \x01(\x0c\x1a{\n\x15PersonalEncryptedNote\x12!\n\x19root_encryption_key_index\x18\x01 \x01(\r\x12\'\n\x1f\x64\x65rivation_encryption_key_index\x18\x02 \x01(\r\x12\x16\n\x0e\x65ncrypted_data\x18\x03 \x01(\x0c\x1a\xe9\x01\n\x14\x45mergencyActionEvent\x12\x81\x01\n\x0b\x61\x63tion_type\x18\x01 \x01(\x0e\x32l.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.EmergencyActionEvent.ActionType\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\"#\n\nActionType\x12\t\n\x05PAUSE\x10\x00\x12\n\n\x06RESUME\x10\x01\x42\x0e\n\x0c_public_note\x1a\x64\n\x16TokenConfigUpdateEvent\x12 \n\x18token_config_update_item\x18\x01 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\xe6\x03\n\x1eUpdateDirectPurchasePriceEvent\x12\x15\n\x0b\x66ixed_price\x18\x01 \x01(\x04H\x00\x12\x95\x01\n\x0evariable_price\x18\x02 \x01(\x0b\x32{.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.PricingScheduleH\x00\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x01\x88\x01\x01\x1a\x33\n\x10PriceForQuantity\x12\x10\n\x08quantity\x18\x01 \x01(\x04\x12\r\n\x05price\x18\x02 \x01(\x04\x1a\xac\x01\n\x0fPricingSchedule\x12\x98\x01\n\x12price_for_quantity\x18\x01 \x03(\x0b\x32|.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.PriceForQuantityB\x07\n\x05priceB\x0e\n\x0c_public_note\x1a\xfc\x02\n\x10GroupActionEvent\x12n\n\x0btoken_event\x18\x01 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEventH\x00\x12t\n\x0e\x64ocument_event\x18\x02 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.DocumentEventH\x00\x12t\n\x0e\x63ontract_event\x18\x03 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ContractEventH\x00\x42\x0c\n\nevent_type\x1a\x8b\x01\n\rDocumentEvent\x12r\n\x06\x63reate\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.DocumentCreateEventH\x00\x42\x06\n\x04type\x1a/\n\x13\x44ocumentCreateEvent\x12\x18\n\x10\x63reated_document\x18\x01 \x01(\x0c\x1a/\n\x13\x43ontractUpdateEvent\x12\x18\n\x10updated_contract\x18\x01 \x01(\x0c\x1a\x8b\x01\n\rContractEvent\x12r\n\x06update\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ContractUpdateEventH\x00\x42\x06\n\x04type\x1a\xd1\x07\n\nTokenEvent\x12\x66\n\x04mint\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintEventH\x00\x12\x66\n\x04\x62urn\x18\x02 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnEventH\x00\x12j\n\x06\x66reeze\x18\x03 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.FreezeEventH\x00\x12n\n\x08unfreeze\x18\x04 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UnfreezeEventH\x00\x12\x84\x01\n\x14\x64\x65stroy_frozen_funds\x18\x05 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.DestroyFrozenFundsEventH\x00\x12}\n\x10\x65mergency_action\x18\x06 \x01(\x0b\x32\x61.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.EmergencyActionEventH\x00\x12\x82\x01\n\x13token_config_update\x18\x07 \x01(\x0b\x32\x63.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenConfigUpdateEventH\x00\x12\x83\x01\n\x0cupdate_price\x18\x08 \x01(\x0b\x32k.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEventH\x00\x42\x06\n\x04type\x1a\x93\x01\n\x10GroupActionEntry\x12\x11\n\taction_id\x18\x01 \x01(\x0c\x12l\n\x05\x65vent\x18\x02 \x01(\x0b\x32].org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionEvent\x1a\x84\x01\n\x0cGroupActions\x12t\n\rgroup_actions\x18\x01 \x03(\x0b\x32].org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionEntryB\x08\n\x06resultB\t\n\x07version\"\x88\x03\n\x1cGetGroupActionSignersRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetGroupActionSignersRequest.GetGroupActionSignersRequestV0H\x00\x1a\xce\x01\n\x1eGetGroupActionSignersRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17group_contract_position\x18\x02 \x01(\r\x12T\n\x06status\x18\x03 \x01(\x0e\x32\x44.org.dash.platform.dapi.v0.GetGroupActionSignersRequest.ActionStatus\x12\x11\n\taction_id\x18\x04 \x01(\x0c\x12\r\n\x05prove\x18\x05 \x01(\x08\"&\n\x0c\x41\x63tionStatus\x12\n\n\x06\x41\x43TIVE\x10\x00\x12\n\n\x06\x43LOSED\x10\x01\x42\t\n\x07version\"\x8b\x05\n\x1dGetGroupActionSignersResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetGroupActionSignersResponse.GetGroupActionSignersResponseV0H\x00\x1a\xf6\x03\n\x1fGetGroupActionSignersResponseV0\x12\x8b\x01\n\x14group_action_signers\x18\x01 \x01(\x0b\x32k.org.dash.platform.dapi.v0.GetGroupActionSignersResponse.GetGroupActionSignersResponseV0.GroupActionSignersH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x35\n\x11GroupActionSigner\x12\x11\n\tsigner_id\x18\x01 \x01(\x0c\x12\r\n\x05power\x18\x02 \x01(\r\x1a\x91\x01\n\x12GroupActionSigners\x12{\n\x07signers\x18\x01 \x03(\x0b\x32j.org.dash.platform.dapi.v0.GetGroupActionSignersResponse.GetGroupActionSignersResponseV0.GroupActionSignerB\x08\n\x06resultB\t\n\x07version\"\xb5\x01\n\x15GetAddressInfoRequest\x12V\n\x02v0\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetAddressInfoRequest.GetAddressInfoRequestV0H\x00\x1a\x39\n\x17GetAddressInfoRequestV0\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x85\x01\n\x10\x41\x64\x64ressInfoEntry\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12J\n\x11\x62\x61lance_and_nonce\x18\x02 \x01(\x0b\x32*.org.dash.platform.dapi.v0.BalanceAndNonceH\x00\x88\x01\x01\x42\x14\n\x12_balance_and_nonce\"1\n\x0f\x42\x61lanceAndNonce\x12\x0f\n\x07\x62\x61lance\x18\x01 \x01(\x04\x12\r\n\x05nonce\x18\x02 \x01(\r\"_\n\x12\x41\x64\x64ressInfoEntries\x12I\n\x14\x61\x64\x64ress_info_entries\x18\x01 \x03(\x0b\x32+.org.dash.platform.dapi.v0.AddressInfoEntry\"m\n\x14\x41\x64\x64ressBalanceChange\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12\x19\n\x0bset_balance\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12\x1c\n\x0e\x61\x64\x64_to_balance\x18\x03 \x01(\x04\x42\x02\x30\x01H\x00\x42\x0b\n\toperation\"x\n\x1a\x42lockAddressBalanceChanges\x12\x18\n\x0c\x62lock_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12@\n\x07\x63hanges\x18\x02 \x03(\x0b\x32/.org.dash.platform.dapi.v0.AddressBalanceChange\"k\n\x1b\x41\x64\x64ressBalanceUpdateEntries\x12L\n\rblock_changes\x18\x01 \x03(\x0b\x32\x35.org.dash.platform.dapi.v0.BlockAddressBalanceChanges\"\xe1\x02\n\x16GetAddressInfoResponse\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetAddressInfoResponse.GetAddressInfoResponseV0H\x00\x1a\xe1\x01\n\x18GetAddressInfoResponseV0\x12I\n\x12\x61\x64\x64ress_info_entry\x18\x01 \x01(\x0b\x32+.org.dash.platform.dapi.v0.AddressInfoEntryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc3\x01\n\x18GetAddressesInfosRequest\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetAddressesInfosRequest.GetAddressesInfosRequestV0H\x00\x1a>\n\x1aGetAddressesInfosRequestV0\x12\x11\n\taddresses\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf1\x02\n\x19GetAddressesInfosResponse\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetAddressesInfosResponse.GetAddressesInfosResponseV0H\x00\x1a\xe8\x01\n\x1bGetAddressesInfosResponseV0\x12M\n\x14\x61\x64\x64ress_info_entries\x18\x01 \x01(\x0b\x32-.org.dash.platform.dapi.v0.AddressInfoEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xb5\x01\n\x1dGetAddressesTrunkStateRequest\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetAddressesTrunkStateRequest.GetAddressesTrunkStateRequestV0H\x00\x1a!\n\x1fGetAddressesTrunkStateRequestV0B\t\n\x07version\"\xaa\x02\n\x1eGetAddressesTrunkStateResponse\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetAddressesTrunkStateResponse.GetAddressesTrunkStateResponseV0H\x00\x1a\x92\x01\n GetAddressesTrunkStateResponseV0\x12/\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.Proof\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\t\n\x07version\"\xf0\x01\n\x1eGetAddressesBranchStateRequest\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetAddressesBranchStateRequest.GetAddressesBranchStateRequestV0H\x00\x1aY\n GetAddressesBranchStateRequestV0\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\r\n\x05\x64\x65pth\x18\x02 \x01(\r\x12\x19\n\x11\x63heckpoint_height\x18\x03 \x01(\x04\x42\t\n\x07version\"\xd1\x01\n\x1fGetAddressesBranchStateResponse\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetAddressesBranchStateResponse.GetAddressesBranchStateResponseV0H\x00\x1a\x37\n!GetAddressesBranchStateResponseV0\x12\x12\n\nmerk_proof\x18\x02 \x01(\x0c\x42\t\n\x07version\"\x9e\x02\n%GetRecentAddressBalanceChangesRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesRequest.GetRecentAddressBalanceChangesRequestV0H\x00\x1ar\n\'GetRecentAddressBalanceChangesRequestV0\x12\x18\n\x0cstart_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x02 \x01(\x08\x12\x1e\n\x16start_height_exclusive\x18\x03 \x01(\x08\x42\t\n\x07version\"\xb8\x03\n&GetRecentAddressBalanceChangesResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesResponse.GetRecentAddressBalanceChangesResponseV0H\x00\x1a\x88\x02\n(GetRecentAddressBalanceChangesResponseV0\x12`\n\x1e\x61\x64\x64ress_balance_update_entries\x18\x01 \x01(\x0b\x32\x36.org.dash.platform.dapi.v0.AddressBalanceUpdateEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"G\n\x16\x42lockHeightCreditEntry\x12\x18\n\x0c\x62lock_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x13\n\x07\x63redits\x18\x02 \x01(\x04\x42\x02\x30\x01\"\xb0\x01\n\x1d\x43ompactedAddressBalanceChange\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12\x19\n\x0bset_credits\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12V\n\x19\x61\x64\x64_to_credits_operations\x18\x03 \x01(\x0b\x32\x31.org.dash.platform.dapi.v0.AddToCreditsOperationsH\x00\x42\x0b\n\toperation\"\\\n\x16\x41\x64\x64ToCreditsOperations\x12\x42\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x31.org.dash.platform.dapi.v0.BlockHeightCreditEntry\"\xae\x01\n#CompactedBlockAddressBalanceChanges\x12\x1e\n\x12start_block_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1c\n\x10\x65nd_block_height\x18\x02 \x01(\x04\x42\x02\x30\x01\x12I\n\x07\x63hanges\x18\x03 \x03(\x0b\x32\x38.org.dash.platform.dapi.v0.CompactedAddressBalanceChange\"\x87\x01\n$CompactedAddressBalanceUpdateEntries\x12_\n\x17\x63ompacted_block_changes\x18\x01 \x03(\x0b\x32>.org.dash.platform.dapi.v0.CompactedBlockAddressBalanceChanges\"\xa9\x02\n.GetRecentCompactedAddressBalanceChangesRequest\x12\x88\x01\n\x02v0\x18\x01 \x01(\x0b\x32z.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesRequest.GetRecentCompactedAddressBalanceChangesRequestV0H\x00\x1a\x61\n0GetRecentCompactedAddressBalanceChangesRequestV0\x12\x1e\n\x12start_block_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf0\x03\n/GetRecentCompactedAddressBalanceChangesResponse\x12\x8a\x01\n\x02v0\x18\x01 \x01(\x0b\x32|.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesResponse.GetRecentCompactedAddressBalanceChangesResponseV0H\x00\x1a\xa4\x02\n1GetRecentCompactedAddressBalanceChangesResponseV0\x12s\n(compacted_address_balance_update_entries\x18\x01 \x01(\x0b\x32?.org.dash.platform.dapi.v0.CompactedAddressBalanceUpdateEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xf4\x01\n GetShieldedEncryptedNotesRequest\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0H\x00\x1aW\n\"GetShieldedEncryptedNotesRequestV0\x12\x13\n\x0bstart_index\x18\x01 \x01(\x04\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xbc\x05\n!GetShieldedEncryptedNotesResponse\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse.GetShieldedEncryptedNotesResponseV0H\x00\x1a\x9b\x04\n#GetShieldedEncryptedNotesResponseV0\x12\x8a\x01\n\x0f\x65ncrypted_notes\x18\x01 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse.GetShieldedEncryptedNotesResponseV0.EncryptedNotesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aW\n\rEncryptedNote\x12\x11\n\tnullifier\x18\x01 \x01(\x0c\x12\x0b\n\x03\x63mx\x18\x02 \x01(\x0c\x12\x16\n\x0e\x65ncrypted_note\x18\x03 \x01(\x0c\x12\x0e\n\x06\x63v_net\x18\x04 \x01(\x0c\x1a\x91\x01\n\x0e\x45ncryptedNotes\x12\x7f\n\x07\x65ntries\x18\x01 \x03(\x0b\x32n.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse.GetShieldedEncryptedNotesResponseV0.EncryptedNoteB\x08\n\x06resultB\t\n\x07version\"\xb4\x01\n\x19GetShieldedAnchorsRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0H\x00\x1a,\n\x1bGetShieldedAnchorsRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xb1\x03\n\x1aGetShieldedAnchorsResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetShieldedAnchorsResponse.GetShieldedAnchorsResponseV0H\x00\x1a\xa5\x02\n\x1cGetShieldedAnchorsResponseV0\x12m\n\x07\x61nchors\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetShieldedAnchorsResponse.GetShieldedAnchorsResponseV0.AnchorsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1a\n\x07\x41nchors\x12\x0f\n\x07\x61nchors\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\xd8\x01\n\"GetMostRecentShieldedAnchorRequest\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0H\x00\x1a\x35\n$GetMostRecentShieldedAnchorRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xdc\x02\n#GetMostRecentShieldedAnchorResponse\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorResponse.GetMostRecentShieldedAnchorResponseV0H\x00\x1a\xb5\x01\n%GetMostRecentShieldedAnchorResponseV0\x12\x10\n\x06\x61nchor\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xbc\x01\n\x1bGetShieldedPoolStateRequest\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0H\x00\x1a.\n\x1dGetShieldedPoolStateRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xcb\x02\n\x1cGetShieldedPoolStateResponse\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetShieldedPoolStateResponse.GetShieldedPoolStateResponseV0H\x00\x1a\xb9\x01\n\x1eGetShieldedPoolStateResponseV0\x12\x1b\n\rtotal_balance\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc0\x01\n\x1cGetShieldedNotesCountRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0H\x00\x1a/\n\x1eGetShieldedNotesCountRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xd3\x02\n\x1dGetShieldedNotesCountResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetShieldedNotesCountResponse.GetShieldedNotesCountResponseV0H\x00\x1a\xbe\x01\n\x1fGetShieldedNotesCountResponseV0\x12\x1f\n\x11total_notes_count\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xd4\x01\n\x1cGetShieldedNullifiersRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0H\x00\x1a\x43\n\x1eGetShieldedNullifiersRequestV0\x12\x12\n\nnullifiers\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x86\x05\n\x1dGetShieldedNullifiersResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetShieldedNullifiersResponse.GetShieldedNullifiersResponseV0H\x00\x1a\xf1\x03\n\x1fGetShieldedNullifiersResponseV0\x12\x88\x01\n\x12nullifier_statuses\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetShieldedNullifiersResponse.GetShieldedNullifiersResponseV0.NullifierStatusesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x36\n\x0fNullifierStatus\x12\x11\n\tnullifier\x18\x01 \x01(\x0c\x12\x10\n\x08is_spent\x18\x02 \x01(\x08\x1a\x8e\x01\n\x11NullifierStatuses\x12y\n\x07\x65ntries\x18\x01 \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetShieldedNullifiersResponse.GetShieldedNullifiersResponseV0.NullifierStatusB\x08\n\x06resultB\t\n\x07version*Z\n\nKeyPurpose\x12\x12\n\x0e\x41UTHENTICATION\x10\x00\x12\x0e\n\nENCRYPTION\x10\x01\x12\x0e\n\nDECRYPTION\x10\x02\x12\x0c\n\x08TRANSFER\x10\x03\x12\n\n\x06VOTING\x10\x05*\xb9\x01\n\x16\x43ontractModerationList\x12(\n$CONTRACT_MODERATION_LIST_UNSPECIFIED\x10\x00\x12$\n CONTRACT_MODERATION_LIST_BANLIST\x10\x01\x12(\n$CONTRACT_MODERATION_LIST_SUSPENSIONS\x10\x02\x12%\n!CONTRACT_MODERATION_LIST_WARNINGS\x10\x03\x32\xd0P\n\x08Platform\x12\x93\x01\n\x18\x62roadcastStateTransition\x12:.org.dash.platform.dapi.v0.BroadcastStateTransitionRequest\x1a;.org.dash.platform.dapi.v0.BroadcastStateTransitionResponse\x12l\n\x0bgetIdentity\x12-.org.dash.platform.dapi.v0.GetIdentityRequest\x1a..org.dash.platform.dapi.v0.GetIdentityResponse\x12x\n\x0fgetIdentityKeys\x12\x31.org.dash.platform.dapi.v0.GetIdentityKeysRequest\x1a\x32.org.dash.platform.dapi.v0.GetIdentityKeysResponse\x12\x96\x01\n\x19getIdentitiesContractKeys\x12;.org.dash.platform.dapi.v0.GetIdentitiesContractKeysRequest\x1a<.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse\x12{\n\x10getIdentityNonce\x12\x32.org.dash.platform.dapi.v0.GetIdentityNonceRequest\x1a\x33.org.dash.platform.dapi.v0.GetIdentityNonceResponse\x12\x93\x01\n\x18getIdentityContractNonce\x12:.org.dash.platform.dapi.v0.GetIdentityContractNonceRequest\x1a;.org.dash.platform.dapi.v0.GetIdentityContractNonceResponse\x12\xa8\x01\n\x1fgetIdentityKeysRemainingBudgets\x12\x41.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsRequest\x1a\x42.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsResponse\x12\x81\x01\n\x12getIdentityBalance\x12\x34.org.dash.platform.dapi.v0.GetIdentityBalanceRequest\x1a\x35.org.dash.platform.dapi.v0.GetIdentityBalanceResponse\x12\x8a\x01\n\x15getIdentitiesBalances\x12\x37.org.dash.platform.dapi.v0.GetIdentitiesBalancesRequest\x1a\x38.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse\x12\xa2\x01\n\x1dgetIdentityBalanceAndRevision\x12?.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionRequest\x1a@.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionResponse\x12\xaf\x01\n#getEvonodesProposedEpochBlocksByIds\x12\x45.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByIdsRequest\x1a\x41.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse\x12\xb3\x01\n%getEvonodesProposedEpochBlocksByRange\x12G.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByRangeRequest\x1a\x41.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse\x12x\n\x0fgetDataContract\x12\x31.org.dash.platform.dapi.v0.GetDataContractRequest\x1a\x32.org.dash.platform.dapi.v0.GetDataContractResponse\x12\x8d\x01\n\x16getDataContractHistory\x12\x38.org.dash.platform.dapi.v0.GetDataContractHistoryRequest\x1a\x39.org.dash.platform.dapi.v0.GetDataContractHistoryResponse\x12\xa5\x01\n\x1egetDataContractsLatestVersions\x12@.org.dash.platform.dapi.v0.GetDataContractsLatestVersionsRequest\x1a\x41.org.dash.platform.dapi.v0.GetDataContractsLatestVersionsResponse\x12{\n\x10getDataContracts\x12\x32.org.dash.platform.dapi.v0.GetDataContractsRequest\x1a\x33.org.dash.platform.dapi.v0.GetDataContractsResponse\x12\x89\x01\n\x17getDataContractsByRange\x12\x39.org.dash.platform.dapi.v0.GetDataContractsByRangeRequest\x1a\x33.org.dash.platform.dapi.v0.GetDataContractsResponse\x12\x87\x01\n\x14getContractGroupInfo\x12\x36.org.dash.platform.dapi.v0.GetContractGroupInfoRequest\x1a\x37.org.dash.platform.dapi.v0.GetContractGroupInfoResponse\x12\x90\x01\n\x17getContractGroupMembers\x12\x39.org.dash.platform.dapi.v0.GetContractGroupMembersRequest\x1a:.org.dash.platform.dapi.v0.GetContractGroupMembersResponse\x12\x9f\x01\n\x1cgetContractGroupsForContract\x12>.org.dash.platform.dapi.v0.GetContractGroupsForContractRequest\x1a?.org.dash.platform.dapi.v0.GetContractGroupsForContractResponse\x12\x9c\x01\n\x1bgetContractModerationStatus\x12=.org.dash.platform.dapi.v0.GetContractModerationStatusRequest\x1a>.org.dash.platform.dapi.v0.GetContractModerationStatusResponse\x12\x9f\x01\n\x1cgetContractModerationEntries\x12>.org.dash.platform.dapi.v0.GetContractModerationEntriesRequest\x1a?.org.dash.platform.dapi.v0.GetContractModerationEntriesResponse\x12\x9c\x01\n\x1bgetContractDocumentRemovals\x12=.org.dash.platform.dapi.v0.GetContractDocumentRemovalsRequest\x1a>.org.dash.platform.dapi.v0.GetContractDocumentRemovalsResponse\x12\x81\x01\n\x12getContractFeePots\x12\x34.org.dash.platform.dapi.v0.GetContractFeePotsRequest\x1a\x35.org.dash.platform.dapi.v0.GetContractFeePotsResponse\x12\x81\x01\n\x12getDocumentHistory\x12\x34.org.dash.platform.dapi.v0.GetDocumentHistoryRequest\x1a\x35.org.dash.platform.dapi.v0.GetDocumentHistoryResponse\x12o\n\x0cgetDocuments\x12..org.dash.platform.dapi.v0.GetDocumentsRequest\x1a/.org.dash.platform.dapi.v0.GetDocumentsResponse\x12\x99\x01\n\x1agetIdentityByPublicKeyHash\x12<.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashRequest\x1a=.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashResponse\x12\xb4\x01\n#getIdentityByNonUniquePublicKeyHash\x12\x45.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashRequest\x1a\x46.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse\x12\x9f\x01\n\x1cwaitForStateTransitionResult\x12>.org.dash.platform.dapi.v0.WaitForStateTransitionResultRequest\x1a?.org.dash.platform.dapi.v0.WaitForStateTransitionResultResponse\x12\x81\x01\n\x12getConsensusParams\x12\x34.org.dash.platform.dapi.v0.GetConsensusParamsRequest\x1a\x35.org.dash.platform.dapi.v0.GetConsensusParamsResponse\x12\xa5\x01\n\x1egetProtocolVersionUpgradeState\x12@.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateRequest\x1a\x41.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse\x12\xb4\x01\n#getProtocolVersionUpgradeVoteStatus\x12\x45.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusRequest\x1a\x46.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse\x12r\n\rgetEpochsInfo\x12/.org.dash.platform.dapi.v0.GetEpochsInfoRequest\x1a\x30.org.dash.platform.dapi.v0.GetEpochsInfoResponse\x12\x8d\x01\n\x16getFinalizedEpochInfos\x12\x38.org.dash.platform.dapi.v0.GetFinalizedEpochInfosRequest\x1a\x39.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse\x12\x8a\x01\n\x15getContestedResources\x12\x37.org.dash.platform.dapi.v0.GetContestedResourcesRequest\x1a\x38.org.dash.platform.dapi.v0.GetContestedResourcesResponse\x12\xa2\x01\n\x1dgetContestedResourceVoteState\x12?.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest\x1a@.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse\x12\xba\x01\n%getContestedResourceVotersForIdentity\x12G.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityRequest\x1aH.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityResponse\x12\xae\x01\n!getContestedResourceIdentityVotes\x12\x43.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesRequest\x1a\x44.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse\x12\x8a\x01\n\x15getVotePollsByEndDate\x12\x37.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest\x1a\x38.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse\x12\xa5\x01\n\x1egetPrefundedSpecializedBalance\x12@.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceRequest\x1a\x41.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceResponse\x12\x96\x01\n\x19getTotalCreditsInPlatform\x12;.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformRequest\x1a<.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformResponse\x12x\n\x0fgetPathElements\x12\x31.org.dash.platform.dapi.v0.GetPathElementsRequest\x1a\x32.org.dash.platform.dapi.v0.GetPathElementsResponse\x12\x66\n\tgetStatus\x12+.org.dash.platform.dapi.v0.GetStatusRequest\x1a,.org.dash.platform.dapi.v0.GetStatusResponse\x12\x8a\x01\n\x15getCurrentQuorumsInfo\x12\x37.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoRequest\x1a\x38.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse\x12\x93\x01\n\x18getIdentityTokenBalances\x12:.org.dash.platform.dapi.v0.GetIdentityTokenBalancesRequest\x1a;.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse\x12\x99\x01\n\x1agetIdentitiesTokenBalances\x12<.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesRequest\x1a=.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse\x12\x8a\x01\n\x15getIdentityTokenInfos\x12\x37.org.dash.platform.dapi.v0.GetIdentityTokenInfosRequest\x1a\x38.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse\x12\x90\x01\n\x17getIdentitiesTokenInfos\x12\x39.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosRequest\x1a:.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse\x12{\n\x10getTokenStatuses\x12\x32.org.dash.platform.dapi.v0.GetTokenStatusesRequest\x1a\x33.org.dash.platform.dapi.v0.GetTokenStatusesResponse\x12\x9f\x01\n\x1cgetTokenDirectPurchasePrices\x12>.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesRequest\x1a?.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse\x12\x87\x01\n\x14getTokenContractInfo\x12\x36.org.dash.platform.dapi.v0.GetTokenContractInfoRequest\x1a\x37.org.dash.platform.dapi.v0.GetTokenContractInfoResponse\x12\xb1\x01\n\"getTokenPreProgrammedDistributions\x12\x44.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsRequest\x1a\x45.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse\x12\xbd\x01\n&getTokenPerpetualDistributionLastClaim\x12H.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimRequest\x1aI.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimResponse\x12\x84\x01\n\x13getTokenTotalSupply\x12\x35.org.dash.platform.dapi.v0.GetTokenTotalSupplyRequest\x1a\x36.org.dash.platform.dapi.v0.GetTokenTotalSupplyResponse\x12o\n\x0cgetGroupInfo\x12..org.dash.platform.dapi.v0.GetGroupInfoRequest\x1a/.org.dash.platform.dapi.v0.GetGroupInfoResponse\x12r\n\rgetGroupInfos\x12/.org.dash.platform.dapi.v0.GetGroupInfosRequest\x1a\x30.org.dash.platform.dapi.v0.GetGroupInfosResponse\x12x\n\x0fgetGroupActions\x12\x31.org.dash.platform.dapi.v0.GetGroupActionsRequest\x1a\x32.org.dash.platform.dapi.v0.GetGroupActionsResponse\x12\x8a\x01\n\x15getGroupActionSigners\x12\x37.org.dash.platform.dapi.v0.GetGroupActionSignersRequest\x1a\x38.org.dash.platform.dapi.v0.GetGroupActionSignersResponse\x12u\n\x0egetAddressInfo\x12\x30.org.dash.platform.dapi.v0.GetAddressInfoRequest\x1a\x31.org.dash.platform.dapi.v0.GetAddressInfoResponse\x12~\n\x11getAddressesInfos\x12\x33.org.dash.platform.dapi.v0.GetAddressesInfosRequest\x1a\x34.org.dash.platform.dapi.v0.GetAddressesInfosResponse\x12\x8d\x01\n\x16getAddressesTrunkState\x12\x38.org.dash.platform.dapi.v0.GetAddressesTrunkStateRequest\x1a\x39.org.dash.platform.dapi.v0.GetAddressesTrunkStateResponse\x12\x90\x01\n\x17getAddressesBranchState\x12\x39.org.dash.platform.dapi.v0.GetAddressesBranchStateRequest\x1a:.org.dash.platform.dapi.v0.GetAddressesBranchStateResponse\x12\xa5\x01\n\x1egetRecentAddressBalanceChanges\x12@.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesRequest\x1a\x41.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesResponse\x12\xc0\x01\n\'getRecentCompactedAddressBalanceChanges\x12I.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesRequest\x1aJ.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesResponse\x12\x96\x01\n\x19getShieldedEncryptedNotes\x12;.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest\x1a<.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse\x12\x81\x01\n\x12getShieldedAnchors\x12\x34.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest\x1a\x35.org.dash.platform.dapi.v0.GetShieldedAnchorsResponse\x12\x9c\x01\n\x1bgetMostRecentShieldedAnchor\x12=.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest\x1a>.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorResponse\x12\x87\x01\n\x14getShieldedPoolState\x12\x36.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest\x1a\x37.org.dash.platform.dapi.v0.GetShieldedPoolStateResponse\x12\x8a\x01\n\x15getShieldedNotesCount\x12\x37.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest\x1a\x38.org.dash.platform.dapi.v0.GetShieldedNotesCountResponse\x12\x8a\x01\n\x15getShieldedNullifiers\x12\x37.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest\x1a\x38.org.dash.platform.dapi.v0.GetShieldedNullifiersResponseb\x06proto3' + serialized_pb=b'\n\x0eplatform.proto\x12\x19org.dash.platform.dapi.v0\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x81\x01\n\x05Proof\x12\x15\n\rgrovedb_proof\x18\x01 \x01(\x0c\x12\x13\n\x0bquorum_hash\x18\x02 \x01(\x0c\x12\x11\n\tsignature\x18\x03 \x01(\x0c\x12\r\n\x05round\x18\x04 \x01(\r\x12\x15\n\rblock_id_hash\x18\x05 \x01(\x0c\x12\x13\n\x0bquorum_type\x18\x06 \x01(\r\"\x98\x01\n\x10ResponseMetadata\x12\x12\n\x06height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12 \n\x18\x63ore_chain_locked_height\x18\x02 \x01(\r\x12\r\n\x05\x65poch\x18\x03 \x01(\r\x12\x13\n\x07time_ms\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x18\n\x10protocol_version\x18\x05 \x01(\r\x12\x10\n\x08\x63hain_id\x18\x06 \x01(\t\"L\n\x1dStateTransitionBroadcastError\x12\x0c\n\x04\x63ode\x18\x01 \x01(\r\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\";\n\x1f\x42roadcastStateTransitionRequest\x12\x18\n\x10state_transition\x18\x01 \x01(\x0c\"\"\n BroadcastStateTransitionResponse\"\xa4\x01\n\x12GetIdentityRequest\x12P\n\x02v0\x18\x01 \x01(\x0b\x32\x42.org.dash.platform.dapi.v0.GetIdentityRequest.GetIdentityRequestV0H\x00\x1a\x31\n\x14GetIdentityRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xc1\x01\n\x17GetIdentityNonceRequest\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetIdentityNonceRequest.GetIdentityNonceRequestV0H\x00\x1a?\n\x19GetIdentityNonceRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf6\x01\n\x1fGetIdentityContractNonceRequest\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetIdentityContractNonceRequest.GetIdentityContractNonceRequestV0H\x00\x1a\\\n!GetIdentityContractNonceRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x13\n\x0b\x63ontract_id\x18\x02 \x01(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xc0\x01\n\x19GetIdentityBalanceRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetIdentityBalanceRequest.GetIdentityBalanceRequestV0H\x00\x1a\x38\n\x1bGetIdentityBalanceRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xec\x01\n$GetIdentityBalanceAndRevisionRequest\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionRequest.GetIdentityBalanceAndRevisionRequestV0H\x00\x1a\x43\n&GetIdentityBalanceAndRevisionRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9e\x02\n\x13GetIdentityResponse\x12R\n\x02v0\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetIdentityResponse.GetIdentityResponseV0H\x00\x1a\xa7\x01\n\x15GetIdentityResponseV0\x12\x12\n\x08identity\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xbc\x02\n\x18GetIdentityNonceResponse\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetIdentityNonceResponse.GetIdentityNonceResponseV0H\x00\x1a\xb6\x01\n\x1aGetIdentityNonceResponseV0\x12\x1c\n\x0eidentity_nonce\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xe5\x02\n GetIdentityContractNonceResponse\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetIdentityContractNonceResponse.GetIdentityContractNonceResponseV0H\x00\x1a\xc7\x01\n\"GetIdentityContractNonceResponseV0\x12%\n\x17identity_contract_nonce\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\x8e\x02\n&GetIdentityKeysRemainingBudgetsRequest\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsRequest.GetIdentityKeysRemainingBudgetsRequestV0H\x00\x1a_\n(GetIdentityKeysRemainingBudgetsRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x0f\n\x07key_ids\x18\x02 \x03(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\x96\x06\n\'GetIdentityKeysRemainingBudgetsResponse\x12z\n\x02v0\x18\x01 \x01(\x0b\x32l.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsResponse.GetIdentityKeysRemainingBudgetsResponseV0H\x00\x1a\xe3\x04\n)GetIdentityKeysRemainingBudgetsResponseV0\x12\xa4\x01\n\x16keys_remaining_budgets\x18\x01 \x01(\x0b\x32\x81\x01.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsResponse.GetIdentityKeysRemainingBudgetsResponseV0.KeysRemainingBudgetsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x61\n\x17KeyRemainingBudgetEntry\x12\x0e\n\x06key_id\x18\x01 \x01(\r\x12!\n\x10remaining_budget\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x42\x13\n\x11_remaining_budget\x1a\xaf\x01\n\x14KeysRemainingBudgets\x12\x96\x01\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsResponse.GetIdentityKeysRemainingBudgetsResponseV0.KeyRemainingBudgetEntryB\x08\n\x06resultB\t\n\x07version\"\xbd\x02\n\x1aGetIdentityBalanceResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetIdentityBalanceResponse.GetIdentityBalanceResponseV0H\x00\x1a\xb1\x01\n\x1cGetIdentityBalanceResponseV0\x12\x15\n\x07\x62\x61lance\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xb1\x04\n%GetIdentityBalanceAndRevisionResponse\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionResponse.GetIdentityBalanceAndRevisionResponseV0H\x00\x1a\x84\x03\n\'GetIdentityBalanceAndRevisionResponseV0\x12\x9b\x01\n\x14\x62\x61lance_and_revision\x18\x01 \x01(\x0b\x32{.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionResponse.GetIdentityBalanceAndRevisionResponseV0.BalanceAndRevisionH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a?\n\x12\x42\x61lanceAndRevision\x12\x13\n\x07\x62\x61lance\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x14\n\x08revision\x18\x02 \x01(\x04\x42\x02\x30\x01\x42\x08\n\x06resultB\t\n\x07version\"\xd1\x01\n\x0eKeyRequestType\x12\x36\n\x08\x61ll_keys\x18\x01 \x01(\x0b\x32\".org.dash.platform.dapi.v0.AllKeysH\x00\x12@\n\rspecific_keys\x18\x02 \x01(\x0b\x32\'.org.dash.platform.dapi.v0.SpecificKeysH\x00\x12:\n\nsearch_key\x18\x03 \x01(\x0b\x32$.org.dash.platform.dapi.v0.SearchKeyH\x00\x42\t\n\x07request\"\t\n\x07\x41llKeys\"\x1f\n\x0cSpecificKeys\x12\x0f\n\x07key_ids\x18\x01 \x03(\r\"\xb6\x01\n\tSearchKey\x12I\n\x0bpurpose_map\x18\x01 \x03(\x0b\x32\x34.org.dash.platform.dapi.v0.SearchKey.PurposeMapEntry\x1a^\n\x0fPurposeMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12:\n\x05value\x18\x02 \x01(\x0b\x32+.org.dash.platform.dapi.v0.SecurityLevelMap:\x02\x38\x01\"\xbf\x02\n\x10SecurityLevelMap\x12]\n\x12security_level_map\x18\x01 \x03(\x0b\x32\x41.org.dash.platform.dapi.v0.SecurityLevelMap.SecurityLevelMapEntry\x1aw\n\x15SecurityLevelMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12M\n\x05value\x18\x02 \x01(\x0e\x32>.org.dash.platform.dapi.v0.SecurityLevelMap.KeyKindRequestType:\x02\x38\x01\"S\n\x12KeyKindRequestType\x12\x1f\n\x1b\x43URRENT_KEY_OF_KIND_REQUEST\x10\x00\x12\x1c\n\x18\x41LL_KEYS_OF_KIND_REQUEST\x10\x01\"\xda\x02\n\x16GetIdentityKeysRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetIdentityKeysRequest.GetIdentityKeysRequestV0H\x00\x1a\xda\x01\n\x18GetIdentityKeysRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12?\n\x0crequest_type\x18\x02 \x01(\x0b\x32).org.dash.platform.dapi.v0.KeyRequestType\x12+\n\x05limit\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\t\n\x07version\"\x99\x03\n\x17GetIdentityKeysResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetIdentityKeysResponse.GetIdentityKeysResponseV0H\x00\x1a\x96\x02\n\x19GetIdentityKeysResponseV0\x12\x61\n\x04keys\x18\x01 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetIdentityKeysResponse.GetIdentityKeysResponseV0.KeysH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1a\n\x04Keys\x12\x12\n\nkeys_bytes\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\xef\x02\n GetIdentitiesContractKeysRequest\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetIdentitiesContractKeysRequest.GetIdentitiesContractKeysRequestV0H\x00\x1a\xd1\x01\n\"GetIdentitiesContractKeysRequestV0\x12\x16\n\x0eidentities_ids\x18\x01 \x03(\x0c\x12\x13\n\x0b\x63ontract_id\x18\x02 \x01(\x0c\x12\x1f\n\x12\x64ocument_type_name\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x37\n\x08purposes\x18\x04 \x03(\x0e\x32%.org.dash.platform.dapi.v0.KeyPurpose\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x15\n\x13_document_type_nameB\t\n\x07version\"\xdf\x06\n!GetIdentitiesContractKeysResponse\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0H\x00\x1a\xbe\x05\n#GetIdentitiesContractKeysResponseV0\x12\x8a\x01\n\x0fidentities_keys\x18\x01 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0.IdentitiesKeysH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aY\n\x0bPurposeKeys\x12\x36\n\x07purpose\x18\x01 \x01(\x0e\x32%.org.dash.platform.dapi.v0.KeyPurpose\x12\x12\n\nkeys_bytes\x18\x02 \x03(\x0c\x1a\x9f\x01\n\x0cIdentityKeys\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12z\n\x04keys\x18\x02 \x03(\x0b\x32l.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0.PurposeKeys\x1a\x90\x01\n\x0eIdentitiesKeys\x12~\n\x07\x65ntries\x18\x01 \x03(\x0b\x32m.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0.IdentityKeysB\x08\n\x06resultB\t\n\x07version\"\xa4\x02\n*GetEvonodesProposedEpochBlocksByIdsRequest\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByIdsRequest.GetEvonodesProposedEpochBlocksByIdsRequestV0H\x00\x1ah\n,GetEvonodesProposedEpochBlocksByIdsRequestV0\x12\x12\n\x05\x65poch\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x0b\n\x03ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\x08\n\x06_epochB\t\n\x07version\"\x92\x06\n&GetEvonodesProposedEpochBlocksResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse.GetEvonodesProposedEpochBlocksResponseV0H\x00\x1a\xe2\x04\n(GetEvonodesProposedEpochBlocksResponseV0\x12\xb1\x01\n#evonodes_proposed_block_counts_info\x18\x01 \x01(\x0b\x32\x81\x01.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse.GetEvonodesProposedEpochBlocksResponseV0.EvonodesProposedBlocksH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a?\n\x15\x45vonodeProposedBlocks\x12\x13\n\x0bpro_tx_hash\x18\x01 \x01(\x0c\x12\x11\n\x05\x63ount\x18\x02 \x01(\x04\x42\x02\x30\x01\x1a\xc4\x01\n\x16\x45vonodesProposedBlocks\x12\xa9\x01\n\x1e\x65vonodes_proposed_block_counts\x18\x01 \x03(\x0b\x32\x80\x01.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse.GetEvonodesProposedEpochBlocksResponseV0.EvonodeProposedBlocksB\x08\n\x06resultB\t\n\x07version\"\xf2\x02\n,GetEvonodesProposedEpochBlocksByRangeRequest\x12\x84\x01\n\x02v0\x18\x01 \x01(\x0b\x32v.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByRangeRequest.GetEvonodesProposedEpochBlocksByRangeRequestV0H\x00\x1a\xaf\x01\n.GetEvonodesProposedEpochBlocksByRangeRequestV0\x12\x12\n\x05\x65poch\x18\x01 \x01(\rH\x01\x88\x01\x01\x12\x12\n\x05limit\x18\x02 \x01(\rH\x02\x88\x01\x01\x12\x15\n\x0bstart_after\x18\x03 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x04 \x01(\x0cH\x00\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x07\n\x05startB\x08\n\x06_epochB\x08\n\x06_limitB\t\n\x07version\"\xcd\x01\n\x1cGetIdentitiesBalancesRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetIdentitiesBalancesRequest.GetIdentitiesBalancesRequestV0H\x00\x1a<\n\x1eGetIdentitiesBalancesRequestV0\x12\x0b\n\x03ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9f\x05\n\x1dGetIdentitiesBalancesResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse.GetIdentitiesBalancesResponseV0H\x00\x1a\x8a\x04\n\x1fGetIdentitiesBalancesResponseV0\x12\x8a\x01\n\x13identities_balances\x18\x01 \x01(\x0b\x32k.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse.GetIdentitiesBalancesResponseV0.IdentitiesBalancesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aL\n\x0fIdentityBalance\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x18\n\x07\x62\x61lance\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x42\n\n\x08_balance\x1a\x8f\x01\n\x12IdentitiesBalances\x12y\n\x07\x65ntries\x18\x01 \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse.GetIdentitiesBalancesResponseV0.IdentityBalanceB\x08\n\x06resultB\t\n\x07version\"\xb4\x01\n\x16GetDataContractRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetDataContractRequest.GetDataContractRequestV0H\x00\x1a\x35\n\x18GetDataContractRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xb3\x02\n\x17GetDataContractResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetDataContractResponse.GetDataContractResponseV0H\x00\x1a\xb0\x01\n\x19GetDataContractResponseV0\x12\x17\n\rdata_contract\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\x8c\x02\n%GetDataContractsLatestVersionsRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetDataContractsLatestVersionsRequest.GetDataContractsLatestVersionsRequestV0H\x00\x1a`\n\'GetDataContractsLatestVersionsRequestV0\x12\x0b\n\x03ids\x18\x01 \x03(\x0c\x12\x19\n\x11include_contracts\x18\x02 \x01(\x08\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xfa\x05\n&GetDataContractsLatestVersionsResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetDataContractsLatestVersionsResponse.GetDataContractsLatestVersionsResponseV0H\x00\x1a\x84\x01\n\x1e\x44\x61taContractLatestVersionEntry\x12\x12\n\nidentifier\x18\x01 \x01(\x0c\x12\x14\n\x07version\x18\x02 \x01(\rH\x00\x88\x01\x01\x12\x1a\n\rdata_contract\x18\x03 \x01(\x0cH\x01\x88\x01\x01\x42\n\n\x08_versionB\x10\n\x0e_data_contract\x1a\x90\x01\n\x1b\x44\x61taContractsLatestVersions\x12q\n\x07\x65ntries\x18\x01 \x03(\x0b\x32`.org.dash.platform.dapi.v0.GetDataContractsLatestVersionsResponse.DataContractLatestVersionEntry\x1a\xb0\x02\n(GetDataContractsLatestVersionsResponseV0\x12\x87\x01\n\x1e\x64\x61ta_contracts_latest_versions\x18\x01 \x01(\x0b\x32].org.dash.platform.dapi.v0.GetDataContractsLatestVersionsResponse.DataContractsLatestVersionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"R\n\x1f\x43ontractGroupDocumentTypeMember\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\"G\n\x18\x43ontractGroupTokenMember\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x16\n\x0etoken_position\x18\x02 \x01(\r\"\xd7\x01\n\x1bGetContractGroupInfoRequest\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetContractGroupInfoRequest.GetContractGroupInfoRequestV0H\x00\x1aI\n\x1dGetContractGroupInfoRequestV0\x12\x19\n\x11\x63ontract_group_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x98\x04\n\x1cGetContractGroupInfoResponse\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetContractGroupInfoResponse.GetContractGroupInfoResponseV0H\x00\x1a~\n\x11\x43ontractGroupInfo\x12\x10\n\x08owner_id\x18\x01 \x01(\x0c\x12\x11\n\tadmin_ids\x18\x02 \x03(\x0c\x12\x11\n\x04name\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x01\x88\x01\x01\x42\x07\n\x05_nameB\x0e\n\x0c_description\x1a\x86\x02\n\x1eGetContractGroupInfoResponseV0\x12h\n\x13\x63ontract_group_info\x18\x01 \x01(\x0b\x32I.org.dash.platform.dapi.v0.GetContractGroupInfoResponse.ContractGroupInfoH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xf8\x06\n\x1eGetContractGroupMembersRequest\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetContractGroupMembersRequest.GetContractGroupMembersRequestV0H\x00\x1a@\n\x14\x43ontractMembersQuery\x12\x18\n\x0bstart_after\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x42\x0e\n\x0c_start_after\x1a\x80\x01\n\x18\x44ocumentTypeMembersQuery\x12T\n\x0bstart_after\x18\x01 \x01(\x0b\x32:.org.dash.platform.dapi.v0.ContractGroupDocumentTypeMemberH\x00\x88\x01\x01\x42\x0e\n\x0c_start_after\x1ar\n\x11TokenMembersQuery\x12M\n\x0bstart_after\x18\x01 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractGroupTokenMemberH\x00\x88\x01\x01\x42\x0e\n\x0c_start_after\x1a\xa7\x03\n GetContractGroupMembersRequestV0\x12\x19\n\x11\x63ontract_group_id\x18\x01 \x01(\x0c\x12\x63\n\tcontracts\x18\x02 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetContractGroupMembersRequest.ContractMembersQueryH\x00\x12l\n\x0e\x64ocument_types\x18\x03 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetContractGroupMembersRequest.DocumentTypeMembersQueryH\x00\x12]\n\x06tokens\x18\x04 \x01(\x0b\x32K.org.dash.platform.dapi.v0.GetContractGroupMembersRequest.TokenMembersQueryH\x00\x12\x12\n\x05limit\x18\x05 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x06 \x01(\x08\x42\t\n\x07membersB\x08\n\x06_limitB\t\n\x07version\"\xc9\x06\n\x1fGetContractGroupMembersResponse\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetContractGroupMembersResponse.GetContractGroupMembersResponseV0H\x00\x1a\'\n\x0f\x43ontractMembers\x12\x14\n\x0c\x63ontract_ids\x18\x01 \x03(\x0c\x1ai\n\x13\x44ocumentTypeMembers\x12R\n\x0e\x64ocument_types\x18\x01 \x03(\x0b\x32:.org.dash.platform.dapi.v0.ContractGroupDocumentTypeMember\x1aS\n\x0cTokenMembers\x12\x43\n\x06tokens\x18\x01 \x03(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractGroupTokenMember\x1a\xc5\x03\n!GetContractGroupMembersResponseV0\x12_\n\tcontracts\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetContractGroupMembersResponse.ContractMembersH\x00\x12h\n\x0e\x64ocument_types\x18\x02 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetContractGroupMembersResponse.DocumentTypeMembersH\x00\x12Y\n\x06tokens\x18\x03 \x01(\x0b\x32G.org.dash.platform.dapi.v0.GetContractGroupMembersResponse.TokenMembersH\x00\x12\x31\n\x05proof\x18\x04 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x05 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"M\n\x1a\x43ontractModerationDocument\x12\x1a\n\x12\x64ocument_type_name\x18\x01 \x01(\t\x12\x13\n\x0b\x64ocument_id\x18\x02 \x01(\x0c\"\x8e\x01\n\x18\x43ontractModerationReason\x12\x11\n\x04\x63ode\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x0c\n\x04text\x18\x02 \x01(\t\x12H\n\tdocuments\x18\x03 \x03(\x0b\x32\x35.org.dash.platform.dapi.v0.ContractModerationDocumentB\x07\n\x05_code\"i\n\x0f\x43ontractWarning\x12\x11\n\twarned_at\x18\x01 \x01(\x04\x12\x43\n\x06reason\x18\x02 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractModerationReason\"\xc5\x02\n\"GetContractModerationStatusRequest\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetContractModerationStatusRequest.GetContractModerationStatusRequestV0H\x00\x1a\xa1\x01\n$GetContractModerationStatusRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x13\n\x0bidentity_id\x18\x02 \x01(\x0c\x12@\n\x05lists\x18\x03 \x03(\x0e\x32\x31.org.dash.platform.dapi.v0.ContractModerationList\x12\r\n\x05prove\x18\x04 \x01(\x08\x42\t\n\x07version\"\xec\x06\n#GetContractModerationStatusResponse\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetContractModerationStatusResponse.GetContractModerationStatusResponseV0H\x00\x1a\xb4\x03\n\x18\x43ontractModerationStatus\x12\x13\n\x06\x62\x61nned\x18\x01 \x01(\x08H\x00\x88\x01\x01\x12\x1c\n\x0fsuspended_until\x18\x02 \x01(\x04H\x01\x88\x01\x01\x12@\n\x05lists\x18\x03 \x03(\x0e\x32\x31.org.dash.platform.dapi.v0.ContractModerationList\x12L\n\nban_reason\x18\x04 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractModerationReasonH\x02\x88\x01\x01\x12S\n\x11suspension_reason\x18\x05 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractModerationReasonH\x03\x88\x01\x01\x12<\n\x08warnings\x18\x06 \x03(\x0b\x32*.org.dash.platform.dapi.v0.ContractWarningB\t\n\x07_bannedB\x12\n\x10_suspended_untilB\r\n\x0b_ban_reasonB\x14\n\x12_suspension_reason\x1a\x8e\x02\n%GetContractModerationStatusResponseV0\x12i\n\x06status\x18\x01 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetContractModerationStatusResponse.ContractModerationStatusH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xfb\x02\n#GetContractModerationEntriesRequest\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetContractModerationEntriesRequest.GetContractModerationEntriesRequestV0H\x00\x1a\xd4\x01\n%GetContractModerationEntriesRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12?\n\x04list\x18\x02 \x01(\x0e\x32\x31.org.dash.platform.dapi.v0.ContractModerationList\x12\x18\n\x0bstart_after\x18\x03 \x01(\x0cH\x00\x88\x01\x01\x12\x12\n\x05limit\x18\x04 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x0e\n\x0c_start_afterB\x08\n\x06_limitB\t\n\x07version\"\x96\x06\n$GetContractModerationEntriesResponse\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetContractModerationEntriesResponse.GetContractModerationEntriesResponseV0H\x00\x1a\xcf\x01\n\x17\x43ontractModerationEntry\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x12\n\x05until\x18\x02 \x01(\x04H\x00\x88\x01\x01\x12\x43\n\x06reason\x18\x03 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractModerationReason\x12<\n\x08warnings\x18\x04 \x03(\x0b\x32*.org.dash.platform.dapi.v0.ContractWarningB\x08\n\x06_until\x1a\x85\x01\n\x19\x43ontractModerationEntries\x12h\n\x07\x65ntries\x18\x01 \x03(\x0b\x32W.org.dash.platform.dapi.v0.GetContractModerationEntriesResponse.ContractModerationEntry\x1a\x92\x02\n&GetContractModerationEntriesResponseV0\x12l\n\x07\x65ntries\x18\x01 \x01(\x0b\x32Y.org.dash.platform.dapi.v0.GetContractModerationEntriesResponse.ContractModerationEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc3\x04\n\"GetContractDocumentRemovalsRequest\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetContractDocumentRemovalsRequest.GetContractDocumentRemovalsRequestV0H\x00\x1a#\n\x0b\x44ocumentIds\x12\x14\n\x0c\x64ocument_ids\x18\x01 \x03(\x0c\x1aN\n\x04Page\x12\x18\n\x0bstart_after\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x12\n\x05limit\x18\x02 \x01(\rH\x01\x88\x01\x01\x42\x0e\n\x0c_start_afterB\x08\n\x06_limit\x1a\xaa\x02\n$GetContractDocumentRemovalsRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x61\n\x0c\x64ocument_ids\x18\x03 \x01(\x0b\x32I.org.dash.platform.dapi.v0.GetContractDocumentRemovalsRequest.DocumentIdsH\x00\x12R\n\x04page\x18\x04 \x01(\x0b\x32\x42.org.dash.platform.dapi.v0.GetContractDocumentRemovalsRequest.PageH\x00\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x0b\n\tselectionB\t\n\x07version\"\xcb\x07\n#GetContractDocumentRemovalsResponse\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetContractDocumentRemovalsResponse.GetContractDocumentRemovalsResponseV0H\x00\x1aH\n\x1b\x43ontractDocumentRestoration\x12\x14\n\x0cmoderator_id\x18\x01 \x01(\x0c\x12\x13\n\x0brestored_at\x18\x02 \x01(\x04\x1a\xc0\x02\n\x17\x43ontractDocumentRemoval\x12\x13\n\x0b\x64ocument_id\x18\x01 \x01(\x0c\x12\x19\n\x11\x64ocument_owner_id\x18\x02 \x01(\x0c\x12\x14\n\x0cmoderator_id\x18\x03 \x01(\x0c\x12\x12\n\nremoved_at\x18\x04 \x01(\x04\x12\x43\n\x06reason\x18\x05 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractModerationReason\x12\x15\n\rdocument_hash\x18\x06 \x01(\x0c\x12o\n\x0brestoration\x18\x07 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetContractDocumentRemovalsResponse.ContractDocumentRestoration\x1a\x84\x01\n\x18\x43ontractDocumentRemovals\x12h\n\x08removals\x18\x01 \x03(\x0b\x32V.org.dash.platform.dapi.v0.GetContractDocumentRemovalsResponse.ContractDocumentRemoval\x1a\x90\x02\n%GetContractDocumentRemovalsResponseV0\x12k\n\x08removals\x18\x01 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetContractDocumentRemovalsResponse.ContractDocumentRemovalsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc9\x01\n\x19GetContractFeePotsRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetContractFeePotsRequest.GetContractFeePotsRequestV0H\x00\x1a\x41\n\x1bGetContractFeePotsRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9f\x06\n\x1aGetContractFeePotsResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetContractFeePotsResponse.GetContractFeePotsResponseV0H\x00\x1aR\n\x17\x43ontractFeePotLastClaim\x12\r\n\x05\x65poch\x18\x01 \x01(\r\x12\x13\n\x07time_ms\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x13\n\x0b\x63laimant_id\x18\x03 \x01(\x0c\x1a\x88\x01\n\x0e\x43ontractFeePot\x12\x13\n\x07\x63redits\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x61\n\nlast_claim\x18\x02 \x01(\x0b\x32M.org.dash.platform.dapi.v0.GetContractFeePotsResponse.ContractFeePotLastClaim\x1a\xc0\x01\n\x0f\x43ontractFeePots\x12S\n\x05owner\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetContractFeePotsResponse.ContractFeePot\x12X\n\nmoderators\x18\x02 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetContractFeePotsResponse.ContractFeePot\x1a\xf1\x01\n\x1cGetContractFeePotsResponseV0\x12U\n\x04pots\x18\x01 \x01(\x0b\x32\x45.org.dash.platform.dapi.v0.GetContractFeePotsResponse.ContractFeePotsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xf1\x01\n#GetContractGroupsForContractRequest\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetContractGroupsForContractRequest.GetContractGroupsForContractRequestV0H\x00\x1aK\n%GetContractGroupsForContractRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf5\x06\n$GetContractGroupsForContractResponse\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetContractGroupsForContractResponse.GetContractGroupsForContractResponseV0H\x00\x1aQ\n\x17\x44ocumentTypeMemberships\x12\x1a\n\x12\x64ocument_type_name\x18\x01 \x01(\t\x12\x1a\n\x12\x63ontract_group_ids\x18\x02 \x03(\x0c\x1a\x46\n\x10TokenMemberships\x12\x16\n\x0etoken_position\x18\x01 \x01(\r\x12\x1a\n\x12\x63ontract_group_ids\x18\x02 \x03(\x0c\x1a\x89\x02\n\x18\x43ontractGroupMemberships\x12\x1a\n\x12\x63ontract_group_ids\x18\x01 \x03(\x0c\x12o\n\x0e\x64ocument_types\x18\x02 \x03(\x0b\x32W.org.dash.platform.dapi.v0.GetContractGroupsForContractResponse.DocumentTypeMemberships\x12`\n\x06tokens\x18\x03 \x03(\x0b\x32P.org.dash.platform.dapi.v0.GetContractGroupsForContractResponse.TokenMemberships\x1a\xa4\x02\n&GetContractGroupsForContractResponseV0\x12~\n\x1a\x63ontract_group_memberships\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetContractGroupsForContractResponse.ContractGroupMembershipsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xb9\x01\n\x17GetDataContractsRequest\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetDataContractsRequest.GetDataContractsRequestV0H\x00\x1a\x37\n\x19GetDataContractsRequestV0\x12\x0b\n\x03ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xad\x02\n\x1eGetDataContractsByRangeRequest\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetDataContractsByRangeRequest.GetDataContractsByRangeRequestV0H\x00\x1a\x95\x01\n GetDataContractsByRangeRequestV0\x12\x12\n\x05limit\x18\x01 \x01(\rH\x01\x88\x01\x01\x12\x15\n\x0bstart_after\x18\x02 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x03 \x01(\x0cH\x00\x12\x10\n\x08ids_only\x18\x04 \x01(\x08\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x07\n\x05startB\x08\n\x06_limitB\t\n\x07version\"\xcf\x04\n\x18GetDataContractsResponse\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetDataContractsResponse.GetDataContractsResponseV0H\x00\x1a[\n\x11\x44\x61taContractEntry\x12\x12\n\nidentifier\x18\x01 \x01(\x0c\x12\x32\n\rdata_contract\x18\x02 \x01(\x0b\x32\x1b.google.protobuf.BytesValue\x1au\n\rDataContracts\x12\x64\n\x15\x64\x61ta_contract_entries\x18\x01 \x03(\x0b\x32\x45.org.dash.platform.dapi.v0.GetDataContractsResponse.DataContractEntry\x1a\xf5\x01\n\x1aGetDataContractsResponseV0\x12[\n\x0e\x64\x61ta_contracts\x18\x01 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDataContractsResponse.DataContractsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc5\x02\n\x1dGetDataContractHistoryRequest\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetDataContractHistoryRequest.GetDataContractHistoryRequestV0H\x00\x1a\xb0\x01\n\x1fGetDataContractHistoryRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12+\n\x05limit\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x17\n\x0bstart_at_ms\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\t\n\x07version\"\xb2\x05\n\x1eGetDataContractHistoryResponse\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetDataContractHistoryResponse.GetDataContractHistoryResponseV0H\x00\x1a\x9a\x04\n GetDataContractHistoryResponseV0\x12\x8f\x01\n\x15\x64\x61ta_contract_history\x18\x01 \x01(\x0b\x32n.org.dash.platform.dapi.v0.GetDataContractHistoryResponse.GetDataContractHistoryResponseV0.DataContractHistoryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a;\n\x18\x44\x61taContractHistoryEntry\x12\x10\n\x04\x64\x61te\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05value\x18\x02 \x01(\x0c\x1a\xaa\x01\n\x13\x44\x61taContractHistory\x12\x92\x01\n\x15\x64\x61ta_contract_entries\x18\x01 \x03(\x0b\x32s.org.dash.platform.dapi.v0.GetDataContractHistoryResponse.GetDataContractHistoryResponseV0.DataContractHistoryEntryB\x08\n\x06resultB\t\n\x07version\"\xe0\x1f\n\x13GetDocumentsRequest\x12R\n\x02v0\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV0H\x00\x12R\n\x02v1\x18\x02 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1H\x00\x1a\xfe\x02\n\x12\x44ocumentFieldValue\x12\x14\n\nbool_value\x18\x01 \x01(\x08H\x00\x12\x19\n\x0bint64_value\x18\x02 \x01(\x12\x42\x02\x30\x01H\x00\x12\x1a\n\x0cuint64_value\x18\x03 \x01(\x04\x42\x02\x30\x01H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12\x0e\n\x04text\x18\x05 \x01(\tH\x00\x12\x15\n\x0b\x62ytes_value\x18\x06 \x01(\x0cH\x00\x12[\n\x04list\x18\x07 \x01(\x0b\x32K.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue.ValueListH\x00\x12\x14\n\nnull_value\x18\x08 \x01(\x08H\x00\x1a^\n\tValueList\x12Q\n\x06values\x18\x01 \x03(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValueB\t\n\x07variant\x1a\xe2\x02\n\x12TimeRangeSelection\x12\\\n\x08selector\x18\x01 \x01(\x0e\x32J.org.dash.platform.dapi.v0.GetDocumentsRequest.TimeRangeSelection.Selector\x12\x19\n\x08start_ms\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x12T\n\x04grid\x18\x03 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetDocumentsRequest.TimeRangeSelection.Grid\x1a>\n\x04Grid\x12\x11\n\x05range\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x10\n\x04step\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x11\n\x05phase\x18\x03 \x01(\x04\x42\x02\x30\x01\"0\n\x08Selector\x12\n\n\x06NEWEST\x10\x00\x12\n\n\x06OLDEST\x10\x01\x12\x0c\n\x08\x42Y_START\x10\x02\x42\x0b\n\t_start_ms\x1a\x95\x02\n\x0bWhereClause\x12\r\n\x05\x66ield\x18\x01 \x01(\t\x12N\n\x08operator\x18\x02 \x01(\x0e\x32<.org.dash.platform.dapi.v0.GetDocumentsRequest.WhereOperator\x12P\n\x05value\x18\x03 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue\x12U\n\ntime_range\x18\x04 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.TimeRangeSelection\x1a\xa4\x01\n\x0fHavingAggregate\x12Y\n\x08\x66unction\x18\x01 \x01(\x0e\x32G.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregate.Function\x12\r\n\x05\x66ield\x18\x02 \x01(\t\"\'\n\x08\x46unction\x12\t\n\x05\x43OUNT\x10\x00\x12\x07\n\x03SUM\x10\x01\x12\x07\n\x03\x41VG\x10\x02\x1a\xf9\x03\n\x0cHavingClause\x12Q\n\taggregate\x18\x01 \x01(\x0b\x32>.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregate\x12V\n\x08operator\x18\x02 \x01(\x0e\x32\x44.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.Operator\x12R\n\x05value\x18\x03 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValueH\x00\"\xe0\x01\n\x08Operator\x12\t\n\x05\x45QUAL\x10\x00\x12\r\n\tNOT_EQUAL\x10\x01\x12\x10\n\x0cGREATER_THAN\x10\x02\x12\x1a\n\x16GREATER_THAN_OR_EQUALS\x10\x03\x12\r\n\tLESS_THAN\x10\x04\x12\x17\n\x13LESS_THAN_OR_EQUALS\x10\x05\x12\x0b\n\x07\x42\x45TWEEN\x10\x06\x12\x1a\n\x16\x42\x45TWEEN_EXCLUDE_BOUNDS\x10\x07\x12\x18\n\x14\x42\x45TWEEN_EXCLUDE_LEFT\x10\x08\x12\x19\n\x15\x42\x45TWEEN_EXCLUDE_RIGHT\x10\t\x12\x06\n\x02IN\x10\nB\x07\n\x05right\x1a\x90\x01\n\x0bOrderClause\x12\x0f\n\x05\x66ield\x18\x01 \x01(\tH\x00\x12S\n\taggregate\x18\x03 \x01(\x0b\x32>.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregateH\x00\x12\x11\n\tascending\x18\x02 \x01(\x08\x42\x08\n\x06target\x1a\xbb\x01\n\x15GetDocumentsRequestV0\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x15\n\rdocument_type\x18\x02 \x01(\t\x12\r\n\x05where\x18\x03 \x01(\x0c\x12\x10\n\x08order_by\x18\x04 \x01(\x0c\x12\r\n\x05limit\x18\x05 \x01(\r\x12\x15\n\x0bstart_after\x18\x06 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x07 \x01(\x0cH\x00\x12\r\n\x05prove\x18\x08 \x01(\x08\x42\x07\n\x05start\x1a\xa6\x0c\n\x15GetDocumentsRequestV1\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x15\n\rdocument_type\x18\x02 \x01(\t\x12Q\n\rwhere_clauses\x18\x03 \x03(\x0b\x32:.org.dash.platform.dapi.v0.GetDocumentsRequest.WhereClause\x12L\n\x08order_by\x18\x04 \x03(\x0b\x32:.org.dash.platform.dapi.v0.GetDocumentsRequest.OrderClause\x12\x12\n\x05limit\x18\x05 \x01(\rH\x01\x88\x01\x01\x12\x15\n\x0bstart_after\x18\x06 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x07 \x01(\x0cH\x00\x12\r\n\x05prove\x18\x08 \x01(\x08\x12\\\n\x07selects\x18\t \x03(\x0b\x32K.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.Select\x12\x10\n\x08group_by\x18\n \x03(\t\x12K\n\x06having\x18\x0b \x03(\x0b\x32;.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause\x12\x13\n\x06offset\x18\x0c \x01(\rH\x02\x88\x01\x01\x12\x61\n\x07\x63hained\x18\r \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.ChainedJoin\x12\x62\n\x0bsub_queries\x18\x0e \x03(\x0b\x32M.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.SubQuery\x1a\xc9\x01\n\x06Select\x12\x66\n\x08\x66unction\x18\x01 \x01(\x0e\x32T.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.Select.Function\x12\r\n\x05\x66ield\x18\x02 \x01(\t\"H\n\x08\x46unction\x12\r\n\tDOCUMENTS\x10\x00\x12\t\n\x05\x43OUNT\x10\x01\x12\x07\n\x03SUM\x10\x02\x12\x07\n\x03\x41VG\x10\x03\x12\x07\n\x03MIN\x10\x04\x12\x07\n\x03MAX\x10\x05\x1a\x41\n\x0b\x43hainedJoin\x12\x15\n\rjoin_property\x18\x01 \x01(\t\x12\x1b\n\x13outer_document_type\x18\x02 \x01(\t\x1a\xa6\x04\n\x08SubQuery\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x15\n\rdocument_type\x18\x02 \x01(\t\x12Q\n\rwhere_clauses\x18\x03 \x03(\x0b\x32:.org.dash.platform.dapi.v0.GetDocumentsRequest.WhereClause\x12L\n\x08order_by\x18\x04 \x03(\x0b\x32:.org.dash.platform.dapi.v0.GetDocumentsRequest.OrderClause\x12\x12\n\x05limit\x18\x05 \x01(\rH\x00\x88\x01\x01\x12`\n\x04kind\x18\x06 \x01(\x0e\x32R.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.SubQuery.Kind\x12\x63\n\x04\x62ind\x18\x07 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.SubQuery.Binding\x1a\x41\n\x07\x42inding\x12\x0e\n\x06source\x18\x01 \x01(\r\x12\x17\n\x0fsource_property\x18\x02 \x01(\t\x12\r\n\x05\x66ield\x18\x03 \x01(\t\" \n\x04Kind\x12\r\n\tDOCUMENTS\x10\x00\x12\t\n\x05\x43OUNT\x10\x01\x42\x08\n\x06_limitB\x07\n\x05startB\x08\n\x06_limitB\t\n\x07_offset\"\xfa\x01\n\rWhereOperator\x12\t\n\x05\x45QUAL\x10\x00\x12\x10\n\x0cGREATER_THAN\x10\x01\x12\x1a\n\x16GREATER_THAN_OR_EQUALS\x10\x02\x12\r\n\tLESS_THAN\x10\x03\x12\x17\n\x13LESS_THAN_OR_EQUALS\x10\x04\x12\x0b\n\x07\x42\x45TWEEN\x10\x05\x12\x1a\n\x16\x42\x45TWEEN_EXCLUDE_BOUNDS\x10\x06\x12\x18\n\x14\x42\x45TWEEN_EXCLUDE_LEFT\x10\x07\x12\x19\n\x15\x42\x45TWEEN_EXCLUDE_RIGHT\x10\x08\x12\x06\n\x02IN\x10\t\x12\x0f\n\x0bSTARTS_WITH\x10\n\x12\x11\n\rIN_TIME_RANGE\x10\x0b\x42\t\n\x07version\"\xf2\x1b\n\x14GetDocumentsResponse\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV0H\x00\x12T\n\x02v1\x18\x02 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1H\x00\x1a\x9b\x02\n\x16GetDocumentsResponseV0\x12\x65\n\tdocuments\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV0.DocumentsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1e\n\tDocuments\x12\x11\n\tdocuments\x18\x01 \x03(\x0c\x42\x08\n\x06result\x1a\x84\x18\n\x16GetDocumentsResponseV1\x12\x61\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultDataH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1e\n\tDocuments\x12\x11\n\tdocuments\x18\x01 \x03(\x0c\x1aL\n\nCountEntry\x12\x13\n\x06in_key\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x0b\n\x03key\x18\x02 \x01(\x0c\x12\x11\n\x05\x63ount\x18\x03 \x01(\x04\x42\x02\x30\x01\x42\t\n\x07_in_key\x1ar\n\x0c\x43ountEntries\x12\x62\n\x07\x65ntries\x18\x01 \x03(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountEntry\x1a\xa0\x01\n\x0c\x43ountResults\x12\x1d\n\x0f\x61ggregate_count\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x66\n\x07\x65ntries\x18\x02 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountEntriesH\x00\x42\t\n\x07variant\x1aH\n\x08SumEntry\x12\x13\n\x06in_key\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x0b\n\x03key\x18\x02 \x01(\x0c\x12\x0f\n\x03sum\x18\x03 \x01(\x12\x42\x02\x30\x01\x42\t\n\x07_in_key\x1an\n\nSumEntries\x12`\n\x07\x65ntries\x18\x01 \x03(\x0b\x32O.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumEntry\x1a\x9a\x01\n\nSumResults\x12\x1b\n\raggregate_sum\x18\x01 \x01(\x12\x42\x02\x30\x01H\x00\x12\x64\n\x07\x65ntries\x18\x02 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumEntriesH\x00\x42\t\n\x07variant\x1a_\n\x0c\x41verageEntry\x12\x13\n\x06in_key\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x0b\n\x03key\x18\x02 \x01(\x0c\x12\x11\n\x05\x63ount\x18\x03 \x01(\x04\x42\x02\x30\x01\x12\x0f\n\x03sum\x18\x04 \x01(\x12\x42\x02\x30\x01\x42\t\n\x07_in_key\x1av\n\x0e\x41verageEntries\x12\x64\n\x07\x65ntries\x18\x01 \x03(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageEntry\x1a\x36\n\x10\x41verageAggregate\x12\x11\n\x05\x63ount\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x0f\n\x03sum\x18\x02 \x01(\x12\x42\x02\x30\x01\x1a\xfb\x01\n\x0e\x41verageResults\x12t\n\x11\x61ggregate_average\x18\x01 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageAggregateH\x00\x12h\n\x07\x65ntries\x18\x02 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageEntriesH\x00\x42\t\n\x07variant\x1az\n\x0bRankedEntry\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\x13\n\x05\x63ount\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12\x11\n\x03sum\x18\x03 \x01(\x12\x42\x02\x30\x01H\x00\x12\r\n\x03\x61vg\x18\x04 \x01(\x01H\x00\x12\x13\n\x06in_key\x18\x05 \x01(\x0cH\x01\x88\x01\x01\x42\x07\n\x05valueB\t\n\x07_in_key\x1a\x9a\x01\n\rRankedEntries\x12\x63\n\x07\x65ntries\x18\x01 \x03(\x0b\x32R.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry\x12\x18\n\x07skipped\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x42\n\n\x08_skipped\x1a\xf7\x05\n\nResultData\x12\x65\n\tdocuments\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.DocumentsH\x00\x12\x65\n\x06\x63ounts\x18\x02 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResultsH\x00\x12\x61\n\x04sums\x18\x03 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumResultsH\x00\x12i\n\x08\x61verages\x18\x04 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResultsH\x00\x12\x66\n\x06ranked\x18\x05 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntriesH\x00\x12j\n\x07\x63hained\x18\x06 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ChainedDocumentsH\x00\x12n\n\tcomposite\x18\x07 \x01(\x0b\x32Y.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CompositeDocumentsH\x00\x42\t\n\x07variant\x1a_\n\x10\x43hainedDocuments\x12\x17\n\x0finner_documents\x18\x01 \x03(\x0c\x12\x17\n\x0fouter_documents\x18\x02 \x03(\x0c\x12\x19\n\x11missing_outer_ids\x18\x03 \x03(\x0c\x1a\xab\x03\n\x12\x43ompositeDocuments\x12\x16\n\x0epage_documents\x18\x01 \x03(\x0c\x12}\n\x0bsub_results\x18\x02 \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CompositeDocuments.SubQueryResult\x1a\xfd\x01\n\x0eSubQueryResult\x12\x65\n\tdocuments\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.DocumentsH\x00\x12\x65\n\x06\x63ounts\x18\x02 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountEntriesH\x00\x12\x13\n\x0bmissing_ids\x18\x03 \x03(\x0c\x42\x08\n\x06resultB\x08\n\x06resultB\t\n\x07version\"\xf4\x02\n\x19GetDocumentHistoryRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentHistoryRequest.GetDocumentHistoryRequestV0H\x00\x1a\xeb\x01\n\x1bGetDocumentHistoryRequestV0\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x13\n\x0b\x64ocument_id\x18\x03 \x01(\x0c\x12+\n\x05limit\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x17\n\x0bstart_at_ms\x18\x06 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x07 \x01(\x08\x42\t\n\x07version\"\xf7\x04\n\x1aGetDocumentHistoryResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetDocumentHistoryResponse.GetDocumentHistoryResponseV0H\x00\x1a\xeb\x03\n\x1cGetDocumentHistoryResponseV0\x12~\n\x10\x64ocument_history\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetDocumentHistoryResponse.GetDocumentHistoryResponseV0.DocumentHistoryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x37\n\x14\x44ocumentHistoryEntry\x12\x10\n\x04\x64\x61te\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05value\x18\x02 \x01(\x0c\x1a\x95\x01\n\x0f\x44ocumentHistory\x12\x81\x01\n\x10\x64ocument_entries\x18\x01 \x03(\x0b\x32g.org.dash.platform.dapi.v0.GetDocumentHistoryResponse.GetDocumentHistoryResponseV0.DocumentHistoryEntryB\x08\n\x06resultB\t\n\x07version\"\xed\x01\n!GetIdentityByPublicKeyHashRequest\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashRequest.GetIdentityByPublicKeyHashRequestV0H\x00\x1aM\n#GetIdentityByPublicKeyHashRequestV0\x12\x17\n\x0fpublic_key_hash\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xda\x02\n\"GetIdentityByPublicKeyHashResponse\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashResponse.GetIdentityByPublicKeyHashResponseV0H\x00\x1a\xb6\x01\n$GetIdentityByPublicKeyHashResponseV0\x12\x12\n\x08identity\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xbd\x02\n*GetIdentityByNonUniquePublicKeyHashRequest\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashRequest.GetIdentityByNonUniquePublicKeyHashRequestV0H\x00\x1a\x80\x01\n,GetIdentityByNonUniquePublicKeyHashRequestV0\x12\x17\n\x0fpublic_key_hash\x18\x01 \x01(\x0c\x12\x18\n\x0bstart_after\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\x0e\n\x0c_start_afterB\t\n\x07version\"\xd6\x06\n+GetIdentityByNonUniquePublicKeyHashResponse\x12\x82\x01\n\x02v0\x18\x01 \x01(\x0b\x32t.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse.GetIdentityByNonUniquePublicKeyHashResponseV0H\x00\x1a\x96\x05\n-GetIdentityByNonUniquePublicKeyHashResponseV0\x12\x9a\x01\n\x08identity\x18\x01 \x01(\x0b\x32\x85\x01.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse.GetIdentityByNonUniquePublicKeyHashResponseV0.IdentityResponseH\x00\x12\x9d\x01\n\x05proof\x18\x02 \x01(\x0b\x32\x8b\x01.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse.GetIdentityByNonUniquePublicKeyHashResponseV0.IdentityProvedResponseH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x36\n\x10IdentityResponse\x12\x15\n\x08identity\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x42\x0b\n\t_identity\x1a\xa6\x01\n\x16IdentityProvedResponse\x12P\n&grovedb_identity_public_key_hash_proof\x18\x01 \x01(\x0b\x32 .org.dash.platform.dapi.v0.Proof\x12!\n\x14identity_proof_bytes\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x42\x17\n\x15_identity_proof_bytesB\x08\n\x06resultB\t\n\x07version\"\xfb\x01\n#WaitForStateTransitionResultRequest\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.WaitForStateTransitionResultRequest.WaitForStateTransitionResultRequestV0H\x00\x1aU\n%WaitForStateTransitionResultRequestV0\x12\x1d\n\x15state_transition_hash\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x99\x03\n$WaitForStateTransitionResultResponse\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.WaitForStateTransitionResultResponse.WaitForStateTransitionResultResponseV0H\x00\x1a\xef\x01\n&WaitForStateTransitionResultResponseV0\x12I\n\x05\x65rror\x18\x01 \x01(\x0b\x32\x38.org.dash.platform.dapi.v0.StateTransitionBroadcastErrorH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc4\x01\n\x19GetConsensusParamsRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetConsensusParamsRequest.GetConsensusParamsRequestV0H\x00\x1a<\n\x1bGetConsensusParamsRequestV0\x12\x0e\n\x06height\x18\x01 \x01(\x05\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9c\x04\n\x1aGetConsensusParamsResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetConsensusParamsResponse.GetConsensusParamsResponseV0H\x00\x1aP\n\x14\x43onsensusParamsBlock\x12\x11\n\tmax_bytes\x18\x01 \x01(\t\x12\x0f\n\x07max_gas\x18\x02 \x01(\t\x12\x14\n\x0ctime_iota_ms\x18\x03 \x01(\t\x1a\x62\n\x17\x43onsensusParamsEvidence\x12\x1a\n\x12max_age_num_blocks\x18\x01 \x01(\t\x12\x18\n\x10max_age_duration\x18\x02 \x01(\t\x12\x11\n\tmax_bytes\x18\x03 \x01(\t\x1a\xda\x01\n\x1cGetConsensusParamsResponseV0\x12Y\n\x05\x62lock\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetConsensusParamsResponse.ConsensusParamsBlock\x12_\n\x08\x65vidence\x18\x02 \x01(\x0b\x32M.org.dash.platform.dapi.v0.GetConsensusParamsResponse.ConsensusParamsEvidenceB\t\n\x07version\"\xe4\x01\n%GetProtocolVersionUpgradeStateRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateRequest.GetProtocolVersionUpgradeStateRequestV0H\x00\x1a\x38\n\'GetProtocolVersionUpgradeStateRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xb5\x05\n&GetProtocolVersionUpgradeStateResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse.GetProtocolVersionUpgradeStateResponseV0H\x00\x1a\x85\x04\n(GetProtocolVersionUpgradeStateResponseV0\x12\x87\x01\n\x08versions\x18\x01 \x01(\x0b\x32s.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse.GetProtocolVersionUpgradeStateResponseV0.VersionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x96\x01\n\x08Versions\x12\x89\x01\n\x08versions\x18\x01 \x03(\x0b\x32w.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse.GetProtocolVersionUpgradeStateResponseV0.VersionEntry\x1a:\n\x0cVersionEntry\x12\x16\n\x0eversion_number\x18\x01 \x01(\r\x12\x12\n\nvote_count\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xa3\x02\n*GetProtocolVersionUpgradeVoteStatusRequest\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusRequest.GetProtocolVersionUpgradeVoteStatusRequestV0H\x00\x1ag\n,GetProtocolVersionUpgradeVoteStatusRequestV0\x12\x19\n\x11start_pro_tx_hash\x18\x01 \x01(\x0c\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xef\x05\n+GetProtocolVersionUpgradeVoteStatusResponse\x12\x82\x01\n\x02v0\x18\x01 \x01(\x0b\x32t.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse.GetProtocolVersionUpgradeVoteStatusResponseV0H\x00\x1a\xaf\x04\n-GetProtocolVersionUpgradeVoteStatusResponseV0\x12\x98\x01\n\x08versions\x18\x01 \x01(\x0b\x32\x83\x01.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse.GetProtocolVersionUpgradeVoteStatusResponseV0.VersionSignalsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xaf\x01\n\x0eVersionSignals\x12\x9c\x01\n\x0fversion_signals\x18\x01 \x03(\x0b\x32\x82\x01.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse.GetProtocolVersionUpgradeVoteStatusResponseV0.VersionSignal\x1a\x35\n\rVersionSignal\x12\x13\n\x0bpro_tx_hash\x18\x01 \x01(\x0c\x12\x0f\n\x07version\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xf5\x01\n\x14GetEpochsInfoRequest\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetEpochsInfoRequest.GetEpochsInfoRequestV0H\x00\x1a|\n\x16GetEpochsInfoRequestV0\x12\x31\n\x0bstart_epoch\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\x11\n\tascending\x18\x03 \x01(\x08\x12\r\n\x05prove\x18\x04 \x01(\x08\x42\t\n\x07version\"\x99\x05\n\x15GetEpochsInfoResponse\x12V\n\x02v0\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetEpochsInfoResponse.GetEpochsInfoResponseV0H\x00\x1a\x9c\x04\n\x17GetEpochsInfoResponseV0\x12\x65\n\x06\x65pochs\x18\x01 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetEpochsInfoResponse.GetEpochsInfoResponseV0.EpochInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1au\n\nEpochInfos\x12g\n\x0b\x65poch_infos\x18\x01 \x03(\x0b\x32R.org.dash.platform.dapi.v0.GetEpochsInfoResponse.GetEpochsInfoResponseV0.EpochInfo\x1a\xa6\x01\n\tEpochInfo\x12\x0e\n\x06number\x18\x01 \x01(\r\x12\x1e\n\x12\x66irst_block_height\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x1f\n\x17\x66irst_core_block_height\x18\x03 \x01(\r\x12\x16\n\nstart_time\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x16\n\x0e\x66\x65\x65_multiplier\x18\x05 \x01(\x01\x12\x18\n\x10protocol_version\x18\x06 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xbf\x02\n\x1dGetFinalizedEpochInfosRequest\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetFinalizedEpochInfosRequest.GetFinalizedEpochInfosRequestV0H\x00\x1a\xaa\x01\n\x1fGetFinalizedEpochInfosRequestV0\x12\x19\n\x11start_epoch_index\x18\x01 \x01(\r\x12\"\n\x1astart_epoch_index_included\x18\x02 \x01(\x08\x12\x17\n\x0f\x65nd_epoch_index\x18\x03 \x01(\r\x12 \n\x18\x65nd_epoch_index_included\x18\x04 \x01(\x08\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\t\n\x07version\"\xbd\t\n\x1eGetFinalizedEpochInfosResponse\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0H\x00\x1a\xa5\x08\n GetFinalizedEpochInfosResponseV0\x12\x80\x01\n\x06\x65pochs\x18\x01 \x01(\x0b\x32n.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0.FinalizedEpochInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xa4\x01\n\x13\x46inalizedEpochInfos\x12\x8c\x01\n\x15\x66inalized_epoch_infos\x18\x01 \x03(\x0b\x32m.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0.FinalizedEpochInfo\x1a\x9f\x04\n\x12\x46inalizedEpochInfo\x12\x0e\n\x06number\x18\x01 \x01(\r\x12\x1e\n\x12\x66irst_block_height\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x1f\n\x17\x66irst_core_block_height\x18\x03 \x01(\r\x12\x1c\n\x10\x66irst_block_time\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x16\n\x0e\x66\x65\x65_multiplier\x18\x05 \x01(\x01\x12\x18\n\x10protocol_version\x18\x06 \x01(\r\x12!\n\x15total_blocks_in_epoch\x18\x07 \x01(\x04\x42\x02\x30\x01\x12*\n\"next_epoch_start_core_block_height\x18\x08 \x01(\r\x12!\n\x15total_processing_fees\x18\t \x01(\x04\x42\x02\x30\x01\x12*\n\x1etotal_distributed_storage_fees\x18\n \x01(\x04\x42\x02\x30\x01\x12&\n\x1atotal_created_storage_fees\x18\x0b \x01(\x04\x42\x02\x30\x01\x12\x1e\n\x12\x63ore_block_rewards\x18\x0c \x01(\x04\x42\x02\x30\x01\x12\x81\x01\n\x0f\x62lock_proposers\x18\r \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0.BlockProposer\x1a\x39\n\rBlockProposer\x12\x13\n\x0bproposer_id\x18\x01 \x01(\x0c\x12\x13\n\x0b\x62lock_count\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xde\x04\n\x1cGetContestedResourcesRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetContestedResourcesRequest.GetContestedResourcesRequestV0H\x00\x1a\xcc\x03\n\x1eGetContestedResourcesRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x12\n\nindex_name\x18\x03 \x01(\t\x12\x1a\n\x12start_index_values\x18\x04 \x03(\x0c\x12\x18\n\x10\x65nd_index_values\x18\x05 \x03(\x0c\x12\x89\x01\n\x13start_at_value_info\x18\x06 \x01(\x0b\x32g.org.dash.platform.dapi.v0.GetContestedResourcesRequest.GetContestedResourcesRequestV0.StartAtValueInfoH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x07 \x01(\rH\x01\x88\x01\x01\x12\x17\n\x0forder_ascending\x18\x08 \x01(\x08\x12\r\n\x05prove\x18\t \x01(\x08\x1a\x45\n\x10StartAtValueInfo\x12\x13\n\x0bstart_value\x18\x01 \x01(\x0c\x12\x1c\n\x14start_value_included\x18\x02 \x01(\x08\x42\x16\n\x14_start_at_value_infoB\x08\n\x06_countB\t\n\x07version\"\x88\x04\n\x1dGetContestedResourcesResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetContestedResourcesResponse.GetContestedResourcesResponseV0H\x00\x1a\xf3\x02\n\x1fGetContestedResourcesResponseV0\x12\x95\x01\n\x19\x63ontested_resource_values\x18\x01 \x01(\x0b\x32p.org.dash.platform.dapi.v0.GetContestedResourcesResponse.GetContestedResourcesResponseV0.ContestedResourceValuesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a<\n\x17\x43ontestedResourceValues\x12!\n\x19\x63ontested_resource_values\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\xd2\x05\n\x1cGetVotePollsByEndDateRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest.GetVotePollsByEndDateRequestV0H\x00\x1a\xc0\x04\n\x1eGetVotePollsByEndDateRequestV0\x12\x84\x01\n\x0fstart_time_info\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest.GetVotePollsByEndDateRequestV0.StartAtTimeInfoH\x00\x88\x01\x01\x12\x80\x01\n\rend_time_info\x18\x02 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest.GetVotePollsByEndDateRequestV0.EndAtTimeInfoH\x01\x88\x01\x01\x12\x12\n\x05limit\x18\x03 \x01(\rH\x02\x88\x01\x01\x12\x13\n\x06offset\x18\x04 \x01(\rH\x03\x88\x01\x01\x12\x11\n\tascending\x18\x05 \x01(\x08\x12\r\n\x05prove\x18\x06 \x01(\x08\x1aI\n\x0fStartAtTimeInfo\x12\x19\n\rstart_time_ms\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1b\n\x13start_time_included\x18\x02 \x01(\x08\x1a\x43\n\rEndAtTimeInfo\x12\x17\n\x0b\x65nd_time_ms\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x19\n\x11\x65nd_time_included\x18\x02 \x01(\x08\x42\x12\n\x10_start_time_infoB\x10\n\x0e_end_time_infoB\x08\n\x06_limitB\t\n\x07_offsetB\t\n\x07version\"\x83\x06\n\x1dGetVotePollsByEndDateResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse.GetVotePollsByEndDateResponseV0H\x00\x1a\xee\x04\n\x1fGetVotePollsByEndDateResponseV0\x12\x9c\x01\n\x18vote_polls_by_timestamps\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse.GetVotePollsByEndDateResponseV0.SerializedVotePollsByTimestampsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aV\n\x1eSerializedVotePollsByTimestamp\x12\x15\n\ttimestamp\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1d\n\x15serialized_vote_polls\x18\x02 \x03(\x0c\x1a\xd7\x01\n\x1fSerializedVotePollsByTimestamps\x12\x99\x01\n\x18vote_polls_by_timestamps\x18\x01 \x03(\x0b\x32w.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse.GetVotePollsByEndDateResponseV0.SerializedVotePollsByTimestamp\x12\x18\n\x10\x66inished_results\x18\x02 \x01(\x08\x42\x08\n\x06resultB\t\n\x07version\"\xff\x06\n$GetContestedResourceVoteStateRequest\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest.GetContestedResourceVoteStateRequestV0H\x00\x1a\xd5\x05\n&GetContestedResourceVoteStateRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x12\n\nindex_name\x18\x03 \x01(\t\x12\x14\n\x0cindex_values\x18\x04 \x03(\x0c\x12\x86\x01\n\x0bresult_type\x18\x05 \x01(\x0e\x32q.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest.GetContestedResourceVoteStateRequestV0.ResultType\x12\x36\n.allow_include_locked_and_abstaining_vote_tally\x18\x06 \x01(\x08\x12\xa3\x01\n\x18start_at_identifier_info\x18\x07 \x01(\x0b\x32|.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest.GetContestedResourceVoteStateRequestV0.StartAtIdentifierInfoH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x08 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\t \x01(\x08\x1aT\n\x15StartAtIdentifierInfo\x12\x18\n\x10start_identifier\x18\x01 \x01(\x0c\x12!\n\x19start_identifier_included\x18\x02 \x01(\x08\"I\n\nResultType\x12\r\n\tDOCUMENTS\x10\x00\x12\x0e\n\nVOTE_TALLY\x10\x01\x12\x1c\n\x18\x44OCUMENTS_AND_VOTE_TALLY\x10\x02\x42\x1b\n\x19_start_at_identifier_infoB\x08\n\x06_countB\t\n\x07version\"\x94\x0c\n%GetContestedResourceVoteStateResponse\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0H\x00\x1a\xe7\n\n\'GetContestedResourceVoteStateResponseV0\x12\xae\x01\n\x1d\x63ontested_resource_contenders\x18\x01 \x01(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.ContestedResourceContendersH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xda\x03\n\x10\x46inishedVoteInfo\x12\xad\x01\n\x15\x66inished_vote_outcome\x18\x01 \x01(\x0e\x32\x8d\x01.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.FinishedVoteInfo.FinishedVoteOutcome\x12\x1f\n\x12won_by_identity_id\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x12$\n\x18\x66inished_at_block_height\x18\x03 \x01(\x04\x42\x02\x30\x01\x12%\n\x1d\x66inished_at_core_block_height\x18\x04 \x01(\r\x12%\n\x19\x66inished_at_block_time_ms\x18\x05 \x01(\x04\x42\x02\x30\x01\x12\x19\n\x11\x66inished_at_epoch\x18\x06 \x01(\r\"O\n\x13\x46inishedVoteOutcome\x12\x14\n\x10TOWARDS_IDENTITY\x10\x00\x12\n\n\x06LOCKED\x10\x01\x12\x16\n\x12NO_PREVIOUS_WINNER\x10\x02\x42\x15\n\x13_won_by_identity_id\x1a\xc4\x03\n\x1b\x43ontestedResourceContenders\x12\x86\x01\n\ncontenders\x18\x01 \x03(\x0b\x32r.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.Contender\x12\x1f\n\x12\x61\x62stain_vote_tally\x18\x02 \x01(\rH\x00\x88\x01\x01\x12\x1c\n\x0flock_vote_tally\x18\x03 \x01(\rH\x01\x88\x01\x01\x12\x9a\x01\n\x12\x66inished_vote_info\x18\x04 \x01(\x0b\x32y.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.FinishedVoteInfoH\x02\x88\x01\x01\x42\x15\n\x13_abstain_vote_tallyB\x12\n\x10_lock_vote_tallyB\x15\n\x13_finished_vote_info\x1ak\n\tContender\x12\x12\n\nidentifier\x18\x01 \x01(\x0c\x12\x17\n\nvote_count\x18\x02 \x01(\rH\x00\x88\x01\x01\x12\x15\n\x08\x64ocument\x18\x03 \x01(\x0cH\x01\x88\x01\x01\x42\r\n\x0b_vote_countB\x0b\n\t_documentB\x08\n\x06resultB\t\n\x07version\"\xd5\x05\n,GetContestedResourceVotersForIdentityRequest\x12\x84\x01\n\x02v0\x18\x01 \x01(\x0b\x32v.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityRequest.GetContestedResourceVotersForIdentityRequestV0H\x00\x1a\x92\x04\n.GetContestedResourceVotersForIdentityRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x12\n\nindex_name\x18\x03 \x01(\t\x12\x14\n\x0cindex_values\x18\x04 \x03(\x0c\x12\x15\n\rcontestant_id\x18\x05 \x01(\x0c\x12\xb4\x01\n\x18start_at_identifier_info\x18\x06 \x01(\x0b\x32\x8c\x01.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityRequest.GetContestedResourceVotersForIdentityRequestV0.StartAtIdentifierInfoH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x07 \x01(\rH\x01\x88\x01\x01\x12\x17\n\x0forder_ascending\x18\x08 \x01(\x08\x12\r\n\x05prove\x18\t \x01(\x08\x1aT\n\x15StartAtIdentifierInfo\x12\x18\n\x10start_identifier\x18\x01 \x01(\x0c\x12!\n\x19start_identifier_included\x18\x02 \x01(\x08\x42\x1b\n\x19_start_at_identifier_infoB\x08\n\x06_countB\t\n\x07version\"\xf1\x04\n-GetContestedResourceVotersForIdentityResponse\x12\x86\x01\n\x02v0\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityResponse.GetContestedResourceVotersForIdentityResponseV0H\x00\x1a\xab\x03\n/GetContestedResourceVotersForIdentityResponseV0\x12\xb6\x01\n\x19\x63ontested_resource_voters\x18\x01 \x01(\x0b\x32\x90\x01.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityResponse.GetContestedResourceVotersForIdentityResponseV0.ContestedResourceVotersH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x43\n\x17\x43ontestedResourceVoters\x12\x0e\n\x06voters\x18\x01 \x03(\x0c\x12\x18\n\x10\x66inished_results\x18\x02 \x01(\x08\x42\x08\n\x06resultB\t\n\x07version\"\xad\x05\n(GetContestedResourceIdentityVotesRequest\x12|\n\x02v0\x18\x01 \x01(\x0b\x32n.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesRequest.GetContestedResourceIdentityVotesRequestV0H\x00\x1a\xf7\x03\n*GetContestedResourceIdentityVotesRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12+\n\x05limit\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x17\n\x0forder_ascending\x18\x04 \x01(\x08\x12\xae\x01\n\x1astart_at_vote_poll_id_info\x18\x05 \x01(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesRequest.GetContestedResourceIdentityVotesRequestV0.StartAtVotePollIdInfoH\x00\x88\x01\x01\x12\r\n\x05prove\x18\x06 \x01(\x08\x1a\x61\n\x15StartAtVotePollIdInfo\x12 \n\x18start_at_poll_identifier\x18\x01 \x01(\x0c\x12&\n\x1estart_poll_identifier_included\x18\x02 \x01(\x08\x42\x1d\n\x1b_start_at_vote_poll_id_infoB\t\n\x07version\"\xc8\n\n)GetContestedResourceIdentityVotesResponse\x12~\n\x02v0\x18\x01 \x01(\x0b\x32p.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0H\x00\x1a\x8f\t\n+GetContestedResourceIdentityVotesResponseV0\x12\xa1\x01\n\x05votes\x18\x01 \x01(\x0b\x32\x8f\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ContestedResourceIdentityVotesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xf7\x01\n\x1e\x43ontestedResourceIdentityVotes\x12\xba\x01\n!contested_resource_identity_votes\x18\x01 \x03(\x0b\x32\x8e\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ContestedResourceIdentityVote\x12\x18\n\x10\x66inished_results\x18\x02 \x01(\x08\x1a\xad\x02\n\x12ResourceVoteChoice\x12\xad\x01\n\x10vote_choice_type\x18\x01 \x01(\x0e\x32\x92\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ResourceVoteChoice.VoteChoiceType\x12\x18\n\x0bidentity_id\x18\x02 \x01(\x0cH\x00\x88\x01\x01\"=\n\x0eVoteChoiceType\x12\x14\n\x10TOWARDS_IDENTITY\x10\x00\x12\x0b\n\x07\x41\x42STAIN\x10\x01\x12\x08\n\x04LOCK\x10\x02\x42\x0e\n\x0c_identity_id\x1a\x95\x02\n\x1d\x43ontestedResourceIdentityVote\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\'\n\x1fserialized_index_storage_values\x18\x03 \x03(\x0c\x12\x99\x01\n\x0bvote_choice\x18\x04 \x01(\x0b\x32\x83\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ResourceVoteChoiceB\x08\n\x06resultB\t\n\x07version\"\xf0\x01\n%GetPrefundedSpecializedBalanceRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceRequest.GetPrefundedSpecializedBalanceRequestV0H\x00\x1a\x44\n\'GetPrefundedSpecializedBalanceRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xed\x02\n&GetPrefundedSpecializedBalanceResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceResponse.GetPrefundedSpecializedBalanceResponseV0H\x00\x1a\xbd\x01\n(GetPrefundedSpecializedBalanceResponseV0\x12\x15\n\x07\x62\x61lance\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xd0\x01\n GetTotalCreditsInPlatformRequest\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformRequest.GetTotalCreditsInPlatformRequestV0H\x00\x1a\x33\n\"GetTotalCreditsInPlatformRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xd9\x02\n!GetTotalCreditsInPlatformResponse\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformResponse.GetTotalCreditsInPlatformResponseV0H\x00\x1a\xb8\x01\n#GetTotalCreditsInPlatformResponseV0\x12\x15\n\x07\x63redits\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc4\x01\n\x16GetPathElementsRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetPathElementsRequest.GetPathElementsRequestV0H\x00\x1a\x45\n\x18GetPathElementsRequestV0\x12\x0c\n\x04path\x18\x01 \x03(\x0c\x12\x0c\n\x04keys\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xa3\x03\n\x17GetPathElementsResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetPathElementsResponse.GetPathElementsResponseV0H\x00\x1a\xa0\x02\n\x19GetPathElementsResponseV0\x12i\n\x08\x65lements\x18\x01 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetPathElementsResponse.GetPathElementsResponseV0.ElementsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1c\n\x08\x45lements\x12\x10\n\x08\x65lements\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\x81\x01\n\x10GetStatusRequest\x12L\n\x02v0\x18\x01 \x01(\x0b\x32>.org.dash.platform.dapi.v0.GetStatusRequest.GetStatusRequestV0H\x00\x1a\x14\n\x12GetStatusRequestV0B\t\n\x07version\"\xe4\x10\n\x11GetStatusResponse\x12N\n\x02v0\x18\x01 \x01(\x0b\x32@.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0H\x00\x1a\xf3\x0f\n\x13GetStatusResponseV0\x12Y\n\x07version\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version\x12S\n\x04node\x18\x02 \x01(\x0b\x32\x45.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Node\x12U\n\x05\x63hain\x18\x03 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Chain\x12Y\n\x07network\x18\x04 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Network\x12^\n\nstate_sync\x18\x05 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.StateSync\x12S\n\x04time\x18\x06 \x01(\x0b\x32\x45.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Time\x1a\x82\x05\n\x07Version\x12\x63\n\x08software\x18\x01 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Software\x12\x63\n\x08protocol\x18\x02 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Protocol\x1a^\n\x08Software\x12\x0c\n\x04\x64\x61pi\x18\x01 \x01(\t\x12\x12\n\x05\x64rive\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x17\n\ntenderdash\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\x08\n\x06_driveB\r\n\x0b_tenderdash\x1a\xcc\x02\n\x08Protocol\x12p\n\ntenderdash\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Protocol.Tenderdash\x12\x66\n\x05\x64rive\x18\x02 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Protocol.Drive\x1a(\n\nTenderdash\x12\x0b\n\x03p2p\x18\x01 \x01(\r\x12\r\n\x05\x62lock\x18\x02 \x01(\r\x1a<\n\x05\x44rive\x12\x0e\n\x06latest\x18\x03 \x01(\r\x12\x0f\n\x07\x63urrent\x18\x04 \x01(\r\x12\x12\n\nnext_epoch\x18\x05 \x01(\r\x1a\x7f\n\x04Time\x12\x11\n\x05local\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x16\n\x05\x62lock\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x12\x18\n\x07genesis\x18\x03 \x01(\x04\x42\x02\x30\x01H\x01\x88\x01\x01\x12\x12\n\x05\x65poch\x18\x04 \x01(\rH\x02\x88\x01\x01\x42\x08\n\x06_blockB\n\n\x08_genesisB\x08\n\x06_epoch\x1a<\n\x04Node\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\x18\n\x0bpro_tx_hash\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x42\x0e\n\x0c_pro_tx_hash\x1a\xb3\x02\n\x05\x43hain\x12\x13\n\x0b\x63\x61tching_up\x18\x01 \x01(\x08\x12\x19\n\x11latest_block_hash\x18\x02 \x01(\x0c\x12\x17\n\x0flatest_app_hash\x18\x03 \x01(\x0c\x12\x1f\n\x13latest_block_height\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x1b\n\x13\x65\x61rliest_block_hash\x18\x05 \x01(\x0c\x12\x19\n\x11\x65\x61rliest_app_hash\x18\x06 \x01(\x0c\x12!\n\x15\x65\x61rliest_block_height\x18\x07 \x01(\x04\x42\x02\x30\x01\x12!\n\x15max_peer_block_height\x18\t \x01(\x04\x42\x02\x30\x01\x12%\n\x18\x63ore_chain_locked_height\x18\n \x01(\rH\x00\x88\x01\x01\x42\x1b\n\x19_core_chain_locked_height\x1a\x43\n\x07Network\x12\x10\n\x08\x63hain_id\x18\x01 \x01(\t\x12\x13\n\x0bpeers_count\x18\x02 \x01(\r\x12\x11\n\tlistening\x18\x03 \x01(\x08\x1a\x85\x02\n\tStateSync\x12\x1d\n\x11total_synced_time\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1a\n\x0eremaining_time\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x17\n\x0ftotal_snapshots\x18\x03 \x01(\r\x12\"\n\x16\x63hunk_process_avg_time\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x1b\n\x0fsnapshot_height\x18\x05 \x01(\x04\x42\x02\x30\x01\x12!\n\x15snapshot_chunks_count\x18\x06 \x01(\x04\x42\x02\x30\x01\x12\x1d\n\x11\x62\x61\x63kfilled_blocks\x18\x07 \x01(\x04\x42\x02\x30\x01\x12!\n\x15\x62\x61\x63kfill_blocks_total\x18\x08 \x01(\x04\x42\x02\x30\x01\x42\t\n\x07version\"\xb1\x01\n\x1cGetCurrentQuorumsInfoRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoRequest.GetCurrentQuorumsInfoRequestV0H\x00\x1a \n\x1eGetCurrentQuorumsInfoRequestV0B\t\n\x07version\"\xa1\x05\n\x1dGetCurrentQuorumsInfoResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse.GetCurrentQuorumsInfoResponseV0H\x00\x1a\x46\n\x0bValidatorV0\x12\x13\n\x0bpro_tx_hash\x18\x01 \x01(\x0c\x12\x0f\n\x07node_ip\x18\x02 \x01(\t\x12\x11\n\tis_banned\x18\x03 \x01(\x08\x1a\xaf\x01\n\x0eValidatorSetV0\x12\x13\n\x0bquorum_hash\x18\x01 \x01(\x0c\x12\x13\n\x0b\x63ore_height\x18\x02 \x01(\r\x12U\n\x07members\x18\x03 \x03(\x0b\x32\x44.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse.ValidatorV0\x12\x1c\n\x14threshold_public_key\x18\x04 \x01(\x0c\x1a\x92\x02\n\x1fGetCurrentQuorumsInfoResponseV0\x12\x15\n\rquorum_hashes\x18\x01 \x03(\x0c\x12\x1b\n\x13\x63urrent_quorum_hash\x18\x02 \x01(\x0c\x12_\n\x0evalidator_sets\x18\x03 \x03(\x0b\x32G.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse.ValidatorSetV0\x12\x1b\n\x13last_block_proposer\x18\x04 \x01(\x0c\x12=\n\x08metadata\x18\x05 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\t\n\x07version\"\xf4\x01\n\x1fGetIdentityTokenBalancesRequest\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetIdentityTokenBalancesRequest.GetIdentityTokenBalancesRequestV0H\x00\x1aZ\n!GetIdentityTokenBalancesRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x11\n\ttoken_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xad\x05\n GetIdentityTokenBalancesResponse\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse.GetIdentityTokenBalancesResponseV0H\x00\x1a\x8f\x04\n\"GetIdentityTokenBalancesResponseV0\x12\x86\x01\n\x0etoken_balances\x18\x01 \x01(\x0b\x32l.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse.GetIdentityTokenBalancesResponseV0.TokenBalancesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aG\n\x11TokenBalanceEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x14\n\x07\x62\x61lance\x18\x02 \x01(\x04H\x00\x88\x01\x01\x42\n\n\x08_balance\x1a\x9a\x01\n\rTokenBalances\x12\x88\x01\n\x0etoken_balances\x18\x01 \x03(\x0b\x32p.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse.GetIdentityTokenBalancesResponseV0.TokenBalanceEntryB\x08\n\x06resultB\t\n\x07version\"\xfc\x01\n!GetIdentitiesTokenBalancesRequest\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesRequest.GetIdentitiesTokenBalancesRequestV0H\x00\x1a\\\n#GetIdentitiesTokenBalancesRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x14\n\x0cidentity_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xf2\x05\n\"GetIdentitiesTokenBalancesResponse\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse.GetIdentitiesTokenBalancesResponseV0H\x00\x1a\xce\x04\n$GetIdentitiesTokenBalancesResponseV0\x12\x9b\x01\n\x17identity_token_balances\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse.GetIdentitiesTokenBalancesResponseV0.IdentityTokenBalancesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aR\n\x19IdentityTokenBalanceEntry\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x14\n\x07\x62\x61lance\x18\x02 \x01(\x04H\x00\x88\x01\x01\x42\n\n\x08_balance\x1a\xb7\x01\n\x15IdentityTokenBalances\x12\x9d\x01\n\x17identity_token_balances\x18\x01 \x03(\x0b\x32|.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse.GetIdentitiesTokenBalancesResponseV0.IdentityTokenBalanceEntryB\x08\n\x06resultB\t\n\x07version\"\xe8\x01\n\x1cGetIdentityTokenInfosRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetIdentityTokenInfosRequest.GetIdentityTokenInfosRequestV0H\x00\x1aW\n\x1eGetIdentityTokenInfosRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x11\n\ttoken_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\x98\x06\n\x1dGetIdentityTokenInfosResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0H\x00\x1a\x83\x05\n\x1fGetIdentityTokenInfosResponseV0\x12z\n\x0btoken_infos\x18\x01 \x01(\x0b\x32\x63.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0.TokenInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a(\n\x16TokenIdentityInfoEntry\x12\x0e\n\x06\x66rozen\x18\x01 \x01(\x08\x1a\xb0\x01\n\x0eTokenInfoEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x82\x01\n\x04info\x18\x02 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0.TokenIdentityInfoEntryH\x00\x88\x01\x01\x42\x07\n\x05_info\x1a\x8a\x01\n\nTokenInfos\x12|\n\x0btoken_infos\x18\x01 \x03(\x0b\x32g.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0.TokenInfoEntryB\x08\n\x06resultB\t\n\x07version\"\xf0\x01\n\x1eGetIdentitiesTokenInfosRequest\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosRequest.GetIdentitiesTokenInfosRequestV0H\x00\x1aY\n GetIdentitiesTokenInfosRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x14\n\x0cidentity_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xca\x06\n\x1fGetIdentitiesTokenInfosResponse\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0H\x00\x1a\xaf\x05\n!GetIdentitiesTokenInfosResponseV0\x12\x8f\x01\n\x14identity_token_infos\x18\x01 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0.IdentityTokenInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a(\n\x16TokenIdentityInfoEntry\x12\x0e\n\x06\x66rozen\x18\x01 \x01(\x08\x1a\xb7\x01\n\x0eTokenInfoEntry\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x86\x01\n\x04info\x18\x02 \x01(\x0b\x32s.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0.TokenIdentityInfoEntryH\x00\x88\x01\x01\x42\x07\n\x05_info\x1a\x97\x01\n\x12IdentityTokenInfos\x12\x80\x01\n\x0btoken_infos\x18\x01 \x03(\x0b\x32k.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0.TokenInfoEntryB\x08\n\x06resultB\t\n\x07version\"\xbf\x01\n\x17GetTokenStatusesRequest\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetTokenStatusesRequest.GetTokenStatusesRequestV0H\x00\x1a=\n\x19GetTokenStatusesRequestV0\x12\x11\n\ttoken_ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xe7\x04\n\x18GetTokenStatusesResponse\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetTokenStatusesResponse.GetTokenStatusesResponseV0H\x00\x1a\xe1\x03\n\x1aGetTokenStatusesResponseV0\x12v\n\x0etoken_statuses\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetTokenStatusesResponse.GetTokenStatusesResponseV0.TokenStatusesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x44\n\x10TokenStatusEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x13\n\x06paused\x18\x02 \x01(\x08H\x00\x88\x01\x01\x42\t\n\x07_paused\x1a\x88\x01\n\rTokenStatuses\x12w\n\x0etoken_statuses\x18\x01 \x03(\x0b\x32_.org.dash.platform.dapi.v0.GetTokenStatusesResponse.GetTokenStatusesResponseV0.TokenStatusEntryB\x08\n\x06resultB\t\n\x07version\"\xef\x01\n#GetTokenDirectPurchasePricesRequest\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesRequest.GetTokenDirectPurchasePricesRequestV0H\x00\x1aI\n%GetTokenDirectPurchasePricesRequestV0\x12\x11\n\ttoken_ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x8b\t\n$GetTokenDirectPurchasePricesResponse\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0H\x00\x1a\xe1\x07\n&GetTokenDirectPurchasePricesResponseV0\x12\xa9\x01\n\x1ctoken_direct_purchase_prices\x18\x01 \x01(\x0b\x32\x80\x01.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.TokenDirectPurchasePricesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x33\n\x10PriceForQuantity\x12\x10\n\x08quantity\x18\x01 \x01(\x04\x12\r\n\x05price\x18\x02 \x01(\x04\x1a\xa7\x01\n\x0fPricingSchedule\x12\x93\x01\n\x12price_for_quantity\x18\x01 \x03(\x0b\x32w.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.PriceForQuantity\x1a\xe4\x01\n\x1dTokenDirectPurchasePriceEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x15\n\x0b\x66ixed_price\x18\x02 \x01(\x04H\x00\x12\x90\x01\n\x0evariable_price\x18\x03 \x01(\x0b\x32v.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.PricingScheduleH\x00\x42\x07\n\x05price\x1a\xc8\x01\n\x19TokenDirectPurchasePrices\x12\xaa\x01\n\x1btoken_direct_purchase_price\x18\x01 \x03(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.TokenDirectPurchasePriceEntryB\x08\n\x06resultB\t\n\x07version\"\xce\x01\n\x1bGetTokenContractInfoRequest\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetTokenContractInfoRequest.GetTokenContractInfoRequestV0H\x00\x1a@\n\x1dGetTokenContractInfoRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xfb\x03\n\x1cGetTokenContractInfoResponse\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetTokenContractInfoResponse.GetTokenContractInfoResponseV0H\x00\x1a\xe9\x02\n\x1eGetTokenContractInfoResponseV0\x12|\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32l.org.dash.platform.dapi.v0.GetTokenContractInfoResponse.GetTokenContractInfoResponseV0.TokenContractInfoDataH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aM\n\x15TokenContractInfoData\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17token_contract_position\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xef\x04\n)GetTokenPreProgrammedDistributionsRequest\x12~\n\x02v0\x18\x01 \x01(\x0b\x32p.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsRequest.GetTokenPreProgrammedDistributionsRequestV0H\x00\x1a\xb6\x03\n+GetTokenPreProgrammedDistributionsRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x98\x01\n\rstart_at_info\x18\x02 \x01(\x0b\x32|.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsRequest.GetTokenPreProgrammedDistributionsRequestV0.StartAtInfoH\x00\x88\x01\x01\x12\x12\n\x05limit\x18\x03 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x04 \x01(\x08\x1a\x9a\x01\n\x0bStartAtInfo\x12\x15\n\rstart_time_ms\x18\x01 \x01(\x04\x12\x1c\n\x0fstart_recipient\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x12%\n\x18start_recipient_included\x18\x03 \x01(\x08H\x01\x88\x01\x01\x42\x12\n\x10_start_recipientB\x1b\n\x19_start_recipient_includedB\x10\n\x0e_start_at_infoB\x08\n\x06_limitB\t\n\x07version\"\xec\x07\n*GetTokenPreProgrammedDistributionsResponse\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0H\x00\x1a\xaf\x06\n,GetTokenPreProgrammedDistributionsResponseV0\x12\xa5\x01\n\x13token_distributions\x18\x01 \x01(\x0b\x32\x85\x01.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0.TokenDistributionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a>\n\x16TokenDistributionEntry\x12\x14\n\x0crecipient_id\x18\x01 \x01(\x0c\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x04\x1a\xd4\x01\n\x1bTokenTimedDistributionEntry\x12\x11\n\ttimestamp\x18\x01 \x01(\x04\x12\xa1\x01\n\rdistributions\x18\x02 \x03(\x0b\x32\x89\x01.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0.TokenDistributionEntry\x1a\xc3\x01\n\x12TokenDistributions\x12\xac\x01\n\x13token_distributions\x18\x01 \x03(\x0b\x32\x8e\x01.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0.TokenTimedDistributionEntryB\x08\n\x06resultB\t\n\x07version\"\x82\x04\n-GetTokenPerpetualDistributionLastClaimRequest\x12\x86\x01\n\x02v0\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimRequest.GetTokenPerpetualDistributionLastClaimRequestV0H\x00\x1aI\n\x11\x43ontractTokenInfo\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17token_contract_position\x18\x02 \x01(\r\x1a\xf1\x01\n/GetTokenPerpetualDistributionLastClaimRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12v\n\rcontract_info\x18\x02 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimRequest.ContractTokenInfoH\x00\x88\x01\x01\x12\x13\n\x0bidentity_id\x18\x04 \x01(\x0c\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x10\n\x0e_contract_infoB\t\n\x07version\"\x93\x05\n.GetTokenPerpetualDistributionLastClaimResponse\x12\x88\x01\n\x02v0\x18\x01 \x01(\x0b\x32z.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimResponse.GetTokenPerpetualDistributionLastClaimResponseV0H\x00\x1a\xca\x03\n0GetTokenPerpetualDistributionLastClaimResponseV0\x12\x9f\x01\n\nlast_claim\x18\x01 \x01(\x0b\x32\x88\x01.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimResponse.GetTokenPerpetualDistributionLastClaimResponseV0.LastClaimInfoH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1ax\n\rLastClaimInfo\x12\x1a\n\x0ctimestamp_ms\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x1a\n\x0c\x62lock_height\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12\x0f\n\x05\x65poch\x18\x03 \x01(\rH\x00\x12\x13\n\traw_bytes\x18\x04 \x01(\x0cH\x00\x42\t\n\x07paid_atB\x08\n\x06resultB\t\n\x07version\"\xca\x01\n\x1aGetTokenTotalSupplyRequest\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetTokenTotalSupplyRequest.GetTokenTotalSupplyRequestV0H\x00\x1a?\n\x1cGetTokenTotalSupplyRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xaf\x04\n\x1bGetTokenTotalSupplyResponse\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetTokenTotalSupplyResponse.GetTokenTotalSupplyResponseV0H\x00\x1a\xa0\x03\n\x1dGetTokenTotalSupplyResponseV0\x12\x88\x01\n\x12token_total_supply\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetTokenTotalSupplyResponse.GetTokenTotalSupplyResponseV0.TokenTotalSupplyEntryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1ax\n\x15TokenTotalSupplyEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x30\n(total_aggregated_amount_in_user_accounts\x18\x02 \x01(\x04\x12\x1b\n\x13total_system_amount\x18\x03 \x01(\x04\x42\x08\n\x06resultB\t\n\x07version\"\xd2\x01\n\x13GetGroupInfoRequest\x12R\n\x02v0\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetGroupInfoRequest.GetGroupInfoRequestV0H\x00\x1a\\\n\x15GetGroupInfoRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17group_contract_position\x18\x02 \x01(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xd4\x05\n\x14GetGroupInfoResponse\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0H\x00\x1a\xda\x04\n\x16GetGroupInfoResponseV0\x12\x66\n\ngroup_info\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0.GroupInfoH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x04 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x34\n\x10GroupMemberEntry\x12\x11\n\tmember_id\x18\x01 \x01(\x0c\x12\r\n\x05power\x18\x02 \x01(\r\x1a\x98\x01\n\x0eGroupInfoEntry\x12h\n\x07members\x18\x01 \x03(\x0b\x32W.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0.GroupMemberEntry\x12\x1c\n\x14group_required_power\x18\x02 \x01(\r\x1a\x8a\x01\n\tGroupInfo\x12n\n\ngroup_info\x18\x01 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0.GroupInfoEntryH\x00\x88\x01\x01\x42\r\n\x0b_group_infoB\x08\n\x06resultB\t\n\x07version\"\xed\x03\n\x14GetGroupInfosRequest\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetGroupInfosRequest.GetGroupInfosRequestV0H\x00\x1au\n\x1cStartAtGroupContractPosition\x12%\n\x1dstart_group_contract_position\x18\x01 \x01(\r\x12.\n&start_group_contract_position_included\x18\x02 \x01(\x08\x1a\xfc\x01\n\x16GetGroupInfosRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12{\n start_at_group_contract_position\x18\x02 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetGroupInfosRequest.StartAtGroupContractPositionH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x03 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x04 \x01(\x08\x42#\n!_start_at_group_contract_positionB\x08\n\x06_countB\t\n\x07version\"\xff\x05\n\x15GetGroupInfosResponse\x12V\n\x02v0\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0H\x00\x1a\x82\x05\n\x17GetGroupInfosResponseV0\x12j\n\x0bgroup_infos\x18\x01 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0.GroupInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x04 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x34\n\x10GroupMemberEntry\x12\x11\n\tmember_id\x18\x01 \x01(\x0c\x12\r\n\x05power\x18\x02 \x01(\r\x1a\xc3\x01\n\x16GroupPositionInfoEntry\x12\x1f\n\x17group_contract_position\x18\x01 \x01(\r\x12j\n\x07members\x18\x02 \x03(\x0b\x32Y.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0.GroupMemberEntry\x12\x1c\n\x14group_required_power\x18\x03 \x01(\r\x1a\x82\x01\n\nGroupInfos\x12t\n\x0bgroup_infos\x18\x01 \x03(\x0b\x32_.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0.GroupPositionInfoEntryB\x08\n\x06resultB\t\n\x07version\"\xbe\x04\n\x16GetGroupActionsRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetGroupActionsRequest.GetGroupActionsRequestV0H\x00\x1aL\n\x0fStartAtActionId\x12\x17\n\x0fstart_action_id\x18\x01 \x01(\x0c\x12 \n\x18start_action_id_included\x18\x02 \x01(\x08\x1a\xc8\x02\n\x18GetGroupActionsRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17group_contract_position\x18\x02 \x01(\r\x12N\n\x06status\x18\x03 \x01(\x0e\x32>.org.dash.platform.dapi.v0.GetGroupActionsRequest.ActionStatus\x12\x62\n\x12start_at_action_id\x18\x04 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetGroupActionsRequest.StartAtActionIdH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x05 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x06 \x01(\x08\x42\x15\n\x13_start_at_action_idB\x08\n\x06_count\"&\n\x0c\x41\x63tionStatus\x12\n\n\x06\x41\x43TIVE\x10\x00\x12\n\n\x06\x43LOSED\x10\x01\x42\t\n\x07version\"\x92\"\n\x17GetGroupActionsResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0H\x00\x1a\x8f!\n\x19GetGroupActionsResponseV0\x12r\n\rgroup_actions\x18\x01 \x01(\x0b\x32Y.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a[\n\tMintEvent\x12\x0e\n\x06\x61mount\x18\x01 \x01(\x04\x12\x14\n\x0crecipient_id\x18\x02 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a[\n\tBurnEvent\x12\x0e\n\x06\x61mount\x18\x01 \x01(\x04\x12\x14\n\x0c\x62urn_from_id\x18\x02 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1aJ\n\x0b\x46reezeEvent\x12\x11\n\tfrozen_id\x18\x01 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1aL\n\rUnfreezeEvent\x12\x11\n\tfrozen_id\x18\x01 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\x66\n\x17\x44\x65stroyFrozenFundsEvent\x12\x11\n\tfrozen_id\x18\x01 \x01(\x0c\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x04\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\x64\n\x13SharedEncryptedNote\x12\x18\n\x10sender_key_index\x18\x01 \x01(\r\x12\x1b\n\x13recipient_key_index\x18\x02 \x01(\r\x12\x16\n\x0e\x65ncrypted_data\x18\x03 \x01(\x0c\x1a{\n\x15PersonalEncryptedNote\x12!\n\x19root_encryption_key_index\x18\x01 \x01(\r\x12\'\n\x1f\x64\x65rivation_encryption_key_index\x18\x02 \x01(\r\x12\x16\n\x0e\x65ncrypted_data\x18\x03 \x01(\x0c\x1a\xe9\x01\n\x14\x45mergencyActionEvent\x12\x81\x01\n\x0b\x61\x63tion_type\x18\x01 \x01(\x0e\x32l.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.EmergencyActionEvent.ActionType\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\"#\n\nActionType\x12\t\n\x05PAUSE\x10\x00\x12\n\n\x06RESUME\x10\x01\x42\x0e\n\x0c_public_note\x1a\x64\n\x16TokenConfigUpdateEvent\x12 \n\x18token_config_update_item\x18\x01 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\xe6\x03\n\x1eUpdateDirectPurchasePriceEvent\x12\x15\n\x0b\x66ixed_price\x18\x01 \x01(\x04H\x00\x12\x95\x01\n\x0evariable_price\x18\x02 \x01(\x0b\x32{.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.PricingScheduleH\x00\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x01\x88\x01\x01\x1a\x33\n\x10PriceForQuantity\x12\x10\n\x08quantity\x18\x01 \x01(\x04\x12\r\n\x05price\x18\x02 \x01(\x04\x1a\xac\x01\n\x0fPricingSchedule\x12\x98\x01\n\x12price_for_quantity\x18\x01 \x03(\x0b\x32|.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.PriceForQuantityB\x07\n\x05priceB\x0e\n\x0c_public_note\x1a\x63\n\x0fMintToPoolEvent\x12\x0e\n\x06\x61mount\x18\x01 \x01(\x04\x12\x16\n\x0e\x61\x63tions_digest\x18\x02 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\x65\n\x11\x42urnFromPoolEvent\x12\x0e\n\x06\x61mount\x18\x01 \x01(\x04\x12\x16\n\x0e\x61\x63tions_digest\x18\x02 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\xfc\x02\n\x10GroupActionEvent\x12n\n\x0btoken_event\x18\x01 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEventH\x00\x12t\n\x0e\x64ocument_event\x18\x02 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.DocumentEventH\x00\x12t\n\x0e\x63ontract_event\x18\x03 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ContractEventH\x00\x42\x0c\n\nevent_type\x1a\x8b\x01\n\rDocumentEvent\x12r\n\x06\x63reate\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.DocumentCreateEventH\x00\x42\x06\n\x04type\x1a/\n\x13\x44ocumentCreateEvent\x12\x18\n\x10\x63reated_document\x18\x01 \x01(\x0c\x1a/\n\x13\x43ontractUpdateEvent\x12\x18\n\x10updated_contract\x18\x01 \x01(\x0c\x1a\x8b\x01\n\rContractEvent\x12r\n\x06update\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ContractUpdateEventH\x00\x42\x06\n\x04type\x1a\xc1\t\n\nTokenEvent\x12\x66\n\x04mint\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintEventH\x00\x12\x66\n\x04\x62urn\x18\x02 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnEventH\x00\x12j\n\x06\x66reeze\x18\x03 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.FreezeEventH\x00\x12n\n\x08unfreeze\x18\x04 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UnfreezeEventH\x00\x12\x84\x01\n\x14\x64\x65stroy_frozen_funds\x18\x05 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.DestroyFrozenFundsEventH\x00\x12}\n\x10\x65mergency_action\x18\x06 \x01(\x0b\x32\x61.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.EmergencyActionEventH\x00\x12\x82\x01\n\x13token_config_update\x18\x07 \x01(\x0b\x32\x63.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenConfigUpdateEventH\x00\x12\x83\x01\n\x0cupdate_price\x18\x08 \x01(\x0b\x32k.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEventH\x00\x12t\n\x0cmint_to_pool\x18\t \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEventH\x00\x12x\n\x0e\x62urn_from_pool\x18\n \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEventH\x00\x42\x06\n\x04type\x1a\x93\x01\n\x10GroupActionEntry\x12\x11\n\taction_id\x18\x01 \x01(\x0c\x12l\n\x05\x65vent\x18\x02 \x01(\x0b\x32].org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionEvent\x1a\x84\x01\n\x0cGroupActions\x12t\n\rgroup_actions\x18\x01 \x03(\x0b\x32].org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionEntryB\x08\n\x06resultB\t\n\x07version\"\x88\x03\n\x1cGetGroupActionSignersRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetGroupActionSignersRequest.GetGroupActionSignersRequestV0H\x00\x1a\xce\x01\n\x1eGetGroupActionSignersRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17group_contract_position\x18\x02 \x01(\r\x12T\n\x06status\x18\x03 \x01(\x0e\x32\x44.org.dash.platform.dapi.v0.GetGroupActionSignersRequest.ActionStatus\x12\x11\n\taction_id\x18\x04 \x01(\x0c\x12\r\n\x05prove\x18\x05 \x01(\x08\"&\n\x0c\x41\x63tionStatus\x12\n\n\x06\x41\x43TIVE\x10\x00\x12\n\n\x06\x43LOSED\x10\x01\x42\t\n\x07version\"\x8b\x05\n\x1dGetGroupActionSignersResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetGroupActionSignersResponse.GetGroupActionSignersResponseV0H\x00\x1a\xf6\x03\n\x1fGetGroupActionSignersResponseV0\x12\x8b\x01\n\x14group_action_signers\x18\x01 \x01(\x0b\x32k.org.dash.platform.dapi.v0.GetGroupActionSignersResponse.GetGroupActionSignersResponseV0.GroupActionSignersH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x35\n\x11GroupActionSigner\x12\x11\n\tsigner_id\x18\x01 \x01(\x0c\x12\r\n\x05power\x18\x02 \x01(\r\x1a\x91\x01\n\x12GroupActionSigners\x12{\n\x07signers\x18\x01 \x03(\x0b\x32j.org.dash.platform.dapi.v0.GetGroupActionSignersResponse.GetGroupActionSignersResponseV0.GroupActionSignerB\x08\n\x06resultB\t\n\x07version\"\xb5\x01\n\x15GetAddressInfoRequest\x12V\n\x02v0\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetAddressInfoRequest.GetAddressInfoRequestV0H\x00\x1a\x39\n\x17GetAddressInfoRequestV0\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x85\x01\n\x10\x41\x64\x64ressInfoEntry\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12J\n\x11\x62\x61lance_and_nonce\x18\x02 \x01(\x0b\x32*.org.dash.platform.dapi.v0.BalanceAndNonceH\x00\x88\x01\x01\x42\x14\n\x12_balance_and_nonce\"1\n\x0f\x42\x61lanceAndNonce\x12\x0f\n\x07\x62\x61lance\x18\x01 \x01(\x04\x12\r\n\x05nonce\x18\x02 \x01(\r\"_\n\x12\x41\x64\x64ressInfoEntries\x12I\n\x14\x61\x64\x64ress_info_entries\x18\x01 \x03(\x0b\x32+.org.dash.platform.dapi.v0.AddressInfoEntry\"m\n\x14\x41\x64\x64ressBalanceChange\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12\x19\n\x0bset_balance\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12\x1c\n\x0e\x61\x64\x64_to_balance\x18\x03 \x01(\x04\x42\x02\x30\x01H\x00\x42\x0b\n\toperation\"x\n\x1a\x42lockAddressBalanceChanges\x12\x18\n\x0c\x62lock_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12@\n\x07\x63hanges\x18\x02 \x03(\x0b\x32/.org.dash.platform.dapi.v0.AddressBalanceChange\"k\n\x1b\x41\x64\x64ressBalanceUpdateEntries\x12L\n\rblock_changes\x18\x01 \x03(\x0b\x32\x35.org.dash.platform.dapi.v0.BlockAddressBalanceChanges\"\xe1\x02\n\x16GetAddressInfoResponse\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetAddressInfoResponse.GetAddressInfoResponseV0H\x00\x1a\xe1\x01\n\x18GetAddressInfoResponseV0\x12I\n\x12\x61\x64\x64ress_info_entry\x18\x01 \x01(\x0b\x32+.org.dash.platform.dapi.v0.AddressInfoEntryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc3\x01\n\x18GetAddressesInfosRequest\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetAddressesInfosRequest.GetAddressesInfosRequestV0H\x00\x1a>\n\x1aGetAddressesInfosRequestV0\x12\x11\n\taddresses\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf1\x02\n\x19GetAddressesInfosResponse\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetAddressesInfosResponse.GetAddressesInfosResponseV0H\x00\x1a\xe8\x01\n\x1bGetAddressesInfosResponseV0\x12M\n\x14\x61\x64\x64ress_info_entries\x18\x01 \x01(\x0b\x32-.org.dash.platform.dapi.v0.AddressInfoEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xb5\x01\n\x1dGetAddressesTrunkStateRequest\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetAddressesTrunkStateRequest.GetAddressesTrunkStateRequestV0H\x00\x1a!\n\x1fGetAddressesTrunkStateRequestV0B\t\n\x07version\"\xaa\x02\n\x1eGetAddressesTrunkStateResponse\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetAddressesTrunkStateResponse.GetAddressesTrunkStateResponseV0H\x00\x1a\x92\x01\n GetAddressesTrunkStateResponseV0\x12/\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.Proof\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\t\n\x07version\"\xf0\x01\n\x1eGetAddressesBranchStateRequest\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetAddressesBranchStateRequest.GetAddressesBranchStateRequestV0H\x00\x1aY\n GetAddressesBranchStateRequestV0\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\r\n\x05\x64\x65pth\x18\x02 \x01(\r\x12\x19\n\x11\x63heckpoint_height\x18\x03 \x01(\x04\x42\t\n\x07version\"\xd1\x01\n\x1fGetAddressesBranchStateResponse\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetAddressesBranchStateResponse.GetAddressesBranchStateResponseV0H\x00\x1a\x37\n!GetAddressesBranchStateResponseV0\x12\x12\n\nmerk_proof\x18\x02 \x01(\x0c\x42\t\n\x07version\"\x9e\x02\n%GetRecentAddressBalanceChangesRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesRequest.GetRecentAddressBalanceChangesRequestV0H\x00\x1ar\n\'GetRecentAddressBalanceChangesRequestV0\x12\x18\n\x0cstart_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x02 \x01(\x08\x12\x1e\n\x16start_height_exclusive\x18\x03 \x01(\x08\x42\t\n\x07version\"\xb8\x03\n&GetRecentAddressBalanceChangesResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesResponse.GetRecentAddressBalanceChangesResponseV0H\x00\x1a\x88\x02\n(GetRecentAddressBalanceChangesResponseV0\x12`\n\x1e\x61\x64\x64ress_balance_update_entries\x18\x01 \x01(\x0b\x32\x36.org.dash.platform.dapi.v0.AddressBalanceUpdateEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"G\n\x16\x42lockHeightCreditEntry\x12\x18\n\x0c\x62lock_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x13\n\x07\x63redits\x18\x02 \x01(\x04\x42\x02\x30\x01\"\xb0\x01\n\x1d\x43ompactedAddressBalanceChange\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12\x19\n\x0bset_credits\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12V\n\x19\x61\x64\x64_to_credits_operations\x18\x03 \x01(\x0b\x32\x31.org.dash.platform.dapi.v0.AddToCreditsOperationsH\x00\x42\x0b\n\toperation\"\\\n\x16\x41\x64\x64ToCreditsOperations\x12\x42\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x31.org.dash.platform.dapi.v0.BlockHeightCreditEntry\"\xae\x01\n#CompactedBlockAddressBalanceChanges\x12\x1e\n\x12start_block_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1c\n\x10\x65nd_block_height\x18\x02 \x01(\x04\x42\x02\x30\x01\x12I\n\x07\x63hanges\x18\x03 \x03(\x0b\x32\x38.org.dash.platform.dapi.v0.CompactedAddressBalanceChange\"\x87\x01\n$CompactedAddressBalanceUpdateEntries\x12_\n\x17\x63ompacted_block_changes\x18\x01 \x03(\x0b\x32>.org.dash.platform.dapi.v0.CompactedBlockAddressBalanceChanges\"\xa9\x02\n.GetRecentCompactedAddressBalanceChangesRequest\x12\x88\x01\n\x02v0\x18\x01 \x01(\x0b\x32z.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesRequest.GetRecentCompactedAddressBalanceChangesRequestV0H\x00\x1a\x61\n0GetRecentCompactedAddressBalanceChangesRequestV0\x12\x1e\n\x12start_block_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf0\x03\n/GetRecentCompactedAddressBalanceChangesResponse\x12\x8a\x01\n\x02v0\x18\x01 \x01(\x0b\x32|.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesResponse.GetRecentCompactedAddressBalanceChangesResponseV0H\x00\x1a\xa4\x02\n1GetRecentCompactedAddressBalanceChangesResponseV0\x12s\n(compacted_address_balance_update_entries\x18\x01 \x01(\x0b\x32?.org.dash.platform.dapi.v0.CompactedAddressBalanceUpdateEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\x98\x02\n GetShieldedEncryptedNotesRequest\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0H\x00\x1a{\n\"GetShieldedEncryptedNotesRequestV0\x12\x13\n\x0bstart_index\x18\x01 \x01(\x04\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x12\x15\n\x08token_id\x18\x04 \x01(\x0cH\x00\x88\x01\x01\x42\x0b\n\t_token_idB\t\n\x07version\"\xbc\x05\n!GetShieldedEncryptedNotesResponse\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse.GetShieldedEncryptedNotesResponseV0H\x00\x1a\x9b\x04\n#GetShieldedEncryptedNotesResponseV0\x12\x8a\x01\n\x0f\x65ncrypted_notes\x18\x01 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse.GetShieldedEncryptedNotesResponseV0.EncryptedNotesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aW\n\rEncryptedNote\x12\x11\n\tnullifier\x18\x01 \x01(\x0c\x12\x0b\n\x03\x63mx\x18\x02 \x01(\x0c\x12\x16\n\x0e\x65ncrypted_note\x18\x03 \x01(\x0c\x12\x0e\n\x06\x63v_net\x18\x04 \x01(\x0c\x1a\x91\x01\n\x0e\x45ncryptedNotes\x12\x7f\n\x07\x65ntries\x18\x01 \x03(\x0b\x32n.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse.GetShieldedEncryptedNotesResponseV0.EncryptedNoteB\x08\n\x06resultB\t\n\x07version\"\xd8\x01\n\x19GetShieldedAnchorsRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0H\x00\x1aP\n\x1bGetShieldedAnchorsRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x12\x15\n\x08token_id\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x42\x0b\n\t_token_idB\t\n\x07version\"\xb1\x03\n\x1aGetShieldedAnchorsResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetShieldedAnchorsResponse.GetShieldedAnchorsResponseV0H\x00\x1a\xa5\x02\n\x1cGetShieldedAnchorsResponseV0\x12m\n\x07\x61nchors\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetShieldedAnchorsResponse.GetShieldedAnchorsResponseV0.AnchorsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1a\n\x07\x41nchors\x12\x0f\n\x07\x61nchors\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\xfc\x01\n\"GetMostRecentShieldedAnchorRequest\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0H\x00\x1aY\n$GetMostRecentShieldedAnchorRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x12\x15\n\x08token_id\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x42\x0b\n\t_token_idB\t\n\x07version\"\xdc\x02\n#GetMostRecentShieldedAnchorResponse\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorResponse.GetMostRecentShieldedAnchorResponseV0H\x00\x1a\xb5\x01\n%GetMostRecentShieldedAnchorResponseV0\x12\x10\n\x06\x61nchor\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xe0\x01\n\x1bGetShieldedPoolStateRequest\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0H\x00\x1aR\n\x1dGetShieldedPoolStateRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x12\x15\n\x08token_id\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x42\x0b\n\t_token_idB\t\n\x07version\"\xcb\x02\n\x1cGetShieldedPoolStateResponse\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetShieldedPoolStateResponse.GetShieldedPoolStateResponseV0H\x00\x1a\xb9\x01\n\x1eGetShieldedPoolStateResponseV0\x12\x1b\n\rtotal_balance\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xe4\x01\n\x1cGetShieldedNotesCountRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0H\x00\x1aS\n\x1eGetShieldedNotesCountRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x12\x15\n\x08token_id\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x42\x0b\n\t_token_idB\t\n\x07version\"\xd3\x02\n\x1dGetShieldedNotesCountResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetShieldedNotesCountResponse.GetShieldedNotesCountResponseV0H\x00\x1a\xbe\x01\n\x1fGetShieldedNotesCountResponseV0\x12\x1f\n\x11total_notes_count\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xf8\x01\n\x1cGetShieldedNullifiersRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0H\x00\x1ag\n\x1eGetShieldedNullifiersRequestV0\x12\x12\n\nnullifiers\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x12\x15\n\x08token_id\x18\x03 \x01(\x0cH\x00\x88\x01\x01\x42\x0b\n\t_token_idB\t\n\x07version\"\x86\x05\n\x1dGetShieldedNullifiersResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetShieldedNullifiersResponse.GetShieldedNullifiersResponseV0H\x00\x1a\xf1\x03\n\x1fGetShieldedNullifiersResponseV0\x12\x88\x01\n\x12nullifier_statuses\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetShieldedNullifiersResponse.GetShieldedNullifiersResponseV0.NullifierStatusesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x36\n\x0fNullifierStatus\x12\x11\n\tnullifier\x18\x01 \x01(\x0c\x12\x10\n\x08is_spent\x18\x02 \x01(\x08\x1a\x8e\x01\n\x11NullifierStatuses\x12y\n\x07\x65ntries\x18\x01 \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetShieldedNullifiersResponse.GetShieldedNullifiersResponseV0.NullifierStatusB\x08\n\x06resultB\t\n\x07version*Z\n\nKeyPurpose\x12\x12\n\x0e\x41UTHENTICATION\x10\x00\x12\x0e\n\nENCRYPTION\x10\x01\x12\x0e\n\nDECRYPTION\x10\x02\x12\x0c\n\x08TRANSFER\x10\x03\x12\n\n\x06VOTING\x10\x05*\xb9\x01\n\x16\x43ontractModerationList\x12(\n$CONTRACT_MODERATION_LIST_UNSPECIFIED\x10\x00\x12$\n CONTRACT_MODERATION_LIST_BANLIST\x10\x01\x12(\n$CONTRACT_MODERATION_LIST_SUSPENSIONS\x10\x02\x12%\n!CONTRACT_MODERATION_LIST_WARNINGS\x10\x03\x32\xd0P\n\x08Platform\x12\x93\x01\n\x18\x62roadcastStateTransition\x12:.org.dash.platform.dapi.v0.BroadcastStateTransitionRequest\x1a;.org.dash.platform.dapi.v0.BroadcastStateTransitionResponse\x12l\n\x0bgetIdentity\x12-.org.dash.platform.dapi.v0.GetIdentityRequest\x1a..org.dash.platform.dapi.v0.GetIdentityResponse\x12x\n\x0fgetIdentityKeys\x12\x31.org.dash.platform.dapi.v0.GetIdentityKeysRequest\x1a\x32.org.dash.platform.dapi.v0.GetIdentityKeysResponse\x12\x96\x01\n\x19getIdentitiesContractKeys\x12;.org.dash.platform.dapi.v0.GetIdentitiesContractKeysRequest\x1a<.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse\x12{\n\x10getIdentityNonce\x12\x32.org.dash.platform.dapi.v0.GetIdentityNonceRequest\x1a\x33.org.dash.platform.dapi.v0.GetIdentityNonceResponse\x12\x93\x01\n\x18getIdentityContractNonce\x12:.org.dash.platform.dapi.v0.GetIdentityContractNonceRequest\x1a;.org.dash.platform.dapi.v0.GetIdentityContractNonceResponse\x12\xa8\x01\n\x1fgetIdentityKeysRemainingBudgets\x12\x41.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsRequest\x1a\x42.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsResponse\x12\x81\x01\n\x12getIdentityBalance\x12\x34.org.dash.platform.dapi.v0.GetIdentityBalanceRequest\x1a\x35.org.dash.platform.dapi.v0.GetIdentityBalanceResponse\x12\x8a\x01\n\x15getIdentitiesBalances\x12\x37.org.dash.platform.dapi.v0.GetIdentitiesBalancesRequest\x1a\x38.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse\x12\xa2\x01\n\x1dgetIdentityBalanceAndRevision\x12?.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionRequest\x1a@.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionResponse\x12\xaf\x01\n#getEvonodesProposedEpochBlocksByIds\x12\x45.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByIdsRequest\x1a\x41.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse\x12\xb3\x01\n%getEvonodesProposedEpochBlocksByRange\x12G.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByRangeRequest\x1a\x41.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse\x12x\n\x0fgetDataContract\x12\x31.org.dash.platform.dapi.v0.GetDataContractRequest\x1a\x32.org.dash.platform.dapi.v0.GetDataContractResponse\x12\x8d\x01\n\x16getDataContractHistory\x12\x38.org.dash.platform.dapi.v0.GetDataContractHistoryRequest\x1a\x39.org.dash.platform.dapi.v0.GetDataContractHistoryResponse\x12\xa5\x01\n\x1egetDataContractsLatestVersions\x12@.org.dash.platform.dapi.v0.GetDataContractsLatestVersionsRequest\x1a\x41.org.dash.platform.dapi.v0.GetDataContractsLatestVersionsResponse\x12{\n\x10getDataContracts\x12\x32.org.dash.platform.dapi.v0.GetDataContractsRequest\x1a\x33.org.dash.platform.dapi.v0.GetDataContractsResponse\x12\x89\x01\n\x17getDataContractsByRange\x12\x39.org.dash.platform.dapi.v0.GetDataContractsByRangeRequest\x1a\x33.org.dash.platform.dapi.v0.GetDataContractsResponse\x12\x87\x01\n\x14getContractGroupInfo\x12\x36.org.dash.platform.dapi.v0.GetContractGroupInfoRequest\x1a\x37.org.dash.platform.dapi.v0.GetContractGroupInfoResponse\x12\x90\x01\n\x17getContractGroupMembers\x12\x39.org.dash.platform.dapi.v0.GetContractGroupMembersRequest\x1a:.org.dash.platform.dapi.v0.GetContractGroupMembersResponse\x12\x9f\x01\n\x1cgetContractGroupsForContract\x12>.org.dash.platform.dapi.v0.GetContractGroupsForContractRequest\x1a?.org.dash.platform.dapi.v0.GetContractGroupsForContractResponse\x12\x9c\x01\n\x1bgetContractModerationStatus\x12=.org.dash.platform.dapi.v0.GetContractModerationStatusRequest\x1a>.org.dash.platform.dapi.v0.GetContractModerationStatusResponse\x12\x9f\x01\n\x1cgetContractModerationEntries\x12>.org.dash.platform.dapi.v0.GetContractModerationEntriesRequest\x1a?.org.dash.platform.dapi.v0.GetContractModerationEntriesResponse\x12\x9c\x01\n\x1bgetContractDocumentRemovals\x12=.org.dash.platform.dapi.v0.GetContractDocumentRemovalsRequest\x1a>.org.dash.platform.dapi.v0.GetContractDocumentRemovalsResponse\x12\x81\x01\n\x12getContractFeePots\x12\x34.org.dash.platform.dapi.v0.GetContractFeePotsRequest\x1a\x35.org.dash.platform.dapi.v0.GetContractFeePotsResponse\x12\x81\x01\n\x12getDocumentHistory\x12\x34.org.dash.platform.dapi.v0.GetDocumentHistoryRequest\x1a\x35.org.dash.platform.dapi.v0.GetDocumentHistoryResponse\x12o\n\x0cgetDocuments\x12..org.dash.platform.dapi.v0.GetDocumentsRequest\x1a/.org.dash.platform.dapi.v0.GetDocumentsResponse\x12\x99\x01\n\x1agetIdentityByPublicKeyHash\x12<.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashRequest\x1a=.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashResponse\x12\xb4\x01\n#getIdentityByNonUniquePublicKeyHash\x12\x45.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashRequest\x1a\x46.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse\x12\x9f\x01\n\x1cwaitForStateTransitionResult\x12>.org.dash.platform.dapi.v0.WaitForStateTransitionResultRequest\x1a?.org.dash.platform.dapi.v0.WaitForStateTransitionResultResponse\x12\x81\x01\n\x12getConsensusParams\x12\x34.org.dash.platform.dapi.v0.GetConsensusParamsRequest\x1a\x35.org.dash.platform.dapi.v0.GetConsensusParamsResponse\x12\xa5\x01\n\x1egetProtocolVersionUpgradeState\x12@.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateRequest\x1a\x41.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse\x12\xb4\x01\n#getProtocolVersionUpgradeVoteStatus\x12\x45.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusRequest\x1a\x46.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse\x12r\n\rgetEpochsInfo\x12/.org.dash.platform.dapi.v0.GetEpochsInfoRequest\x1a\x30.org.dash.platform.dapi.v0.GetEpochsInfoResponse\x12\x8d\x01\n\x16getFinalizedEpochInfos\x12\x38.org.dash.platform.dapi.v0.GetFinalizedEpochInfosRequest\x1a\x39.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse\x12\x8a\x01\n\x15getContestedResources\x12\x37.org.dash.platform.dapi.v0.GetContestedResourcesRequest\x1a\x38.org.dash.platform.dapi.v0.GetContestedResourcesResponse\x12\xa2\x01\n\x1dgetContestedResourceVoteState\x12?.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest\x1a@.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse\x12\xba\x01\n%getContestedResourceVotersForIdentity\x12G.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityRequest\x1aH.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityResponse\x12\xae\x01\n!getContestedResourceIdentityVotes\x12\x43.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesRequest\x1a\x44.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse\x12\x8a\x01\n\x15getVotePollsByEndDate\x12\x37.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest\x1a\x38.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse\x12\xa5\x01\n\x1egetPrefundedSpecializedBalance\x12@.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceRequest\x1a\x41.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceResponse\x12\x96\x01\n\x19getTotalCreditsInPlatform\x12;.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformRequest\x1a<.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformResponse\x12x\n\x0fgetPathElements\x12\x31.org.dash.platform.dapi.v0.GetPathElementsRequest\x1a\x32.org.dash.platform.dapi.v0.GetPathElementsResponse\x12\x66\n\tgetStatus\x12+.org.dash.platform.dapi.v0.GetStatusRequest\x1a,.org.dash.platform.dapi.v0.GetStatusResponse\x12\x8a\x01\n\x15getCurrentQuorumsInfo\x12\x37.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoRequest\x1a\x38.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse\x12\x93\x01\n\x18getIdentityTokenBalances\x12:.org.dash.platform.dapi.v0.GetIdentityTokenBalancesRequest\x1a;.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse\x12\x99\x01\n\x1agetIdentitiesTokenBalances\x12<.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesRequest\x1a=.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse\x12\x8a\x01\n\x15getIdentityTokenInfos\x12\x37.org.dash.platform.dapi.v0.GetIdentityTokenInfosRequest\x1a\x38.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse\x12\x90\x01\n\x17getIdentitiesTokenInfos\x12\x39.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosRequest\x1a:.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse\x12{\n\x10getTokenStatuses\x12\x32.org.dash.platform.dapi.v0.GetTokenStatusesRequest\x1a\x33.org.dash.platform.dapi.v0.GetTokenStatusesResponse\x12\x9f\x01\n\x1cgetTokenDirectPurchasePrices\x12>.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesRequest\x1a?.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse\x12\x87\x01\n\x14getTokenContractInfo\x12\x36.org.dash.platform.dapi.v0.GetTokenContractInfoRequest\x1a\x37.org.dash.platform.dapi.v0.GetTokenContractInfoResponse\x12\xb1\x01\n\"getTokenPreProgrammedDistributions\x12\x44.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsRequest\x1a\x45.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse\x12\xbd\x01\n&getTokenPerpetualDistributionLastClaim\x12H.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimRequest\x1aI.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimResponse\x12\x84\x01\n\x13getTokenTotalSupply\x12\x35.org.dash.platform.dapi.v0.GetTokenTotalSupplyRequest\x1a\x36.org.dash.platform.dapi.v0.GetTokenTotalSupplyResponse\x12o\n\x0cgetGroupInfo\x12..org.dash.platform.dapi.v0.GetGroupInfoRequest\x1a/.org.dash.platform.dapi.v0.GetGroupInfoResponse\x12r\n\rgetGroupInfos\x12/.org.dash.platform.dapi.v0.GetGroupInfosRequest\x1a\x30.org.dash.platform.dapi.v0.GetGroupInfosResponse\x12x\n\x0fgetGroupActions\x12\x31.org.dash.platform.dapi.v0.GetGroupActionsRequest\x1a\x32.org.dash.platform.dapi.v0.GetGroupActionsResponse\x12\x8a\x01\n\x15getGroupActionSigners\x12\x37.org.dash.platform.dapi.v0.GetGroupActionSignersRequest\x1a\x38.org.dash.platform.dapi.v0.GetGroupActionSignersResponse\x12u\n\x0egetAddressInfo\x12\x30.org.dash.platform.dapi.v0.GetAddressInfoRequest\x1a\x31.org.dash.platform.dapi.v0.GetAddressInfoResponse\x12~\n\x11getAddressesInfos\x12\x33.org.dash.platform.dapi.v0.GetAddressesInfosRequest\x1a\x34.org.dash.platform.dapi.v0.GetAddressesInfosResponse\x12\x8d\x01\n\x16getAddressesTrunkState\x12\x38.org.dash.platform.dapi.v0.GetAddressesTrunkStateRequest\x1a\x39.org.dash.platform.dapi.v0.GetAddressesTrunkStateResponse\x12\x90\x01\n\x17getAddressesBranchState\x12\x39.org.dash.platform.dapi.v0.GetAddressesBranchStateRequest\x1a:.org.dash.platform.dapi.v0.GetAddressesBranchStateResponse\x12\xa5\x01\n\x1egetRecentAddressBalanceChanges\x12@.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesRequest\x1a\x41.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesResponse\x12\xc0\x01\n\'getRecentCompactedAddressBalanceChanges\x12I.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesRequest\x1aJ.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesResponse\x12\x96\x01\n\x19getShieldedEncryptedNotes\x12;.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest\x1a<.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse\x12\x81\x01\n\x12getShieldedAnchors\x12\x34.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest\x1a\x35.org.dash.platform.dapi.v0.GetShieldedAnchorsResponse\x12\x9c\x01\n\x1bgetMostRecentShieldedAnchor\x12=.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest\x1a>.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorResponse\x12\x87\x01\n\x14getShieldedPoolState\x12\x36.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest\x1a\x37.org.dash.platform.dapi.v0.GetShieldedPoolStateResponse\x12\x8a\x01\n\x15getShieldedNotesCount\x12\x37.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest\x1a\x38.org.dash.platform.dapi.v0.GetShieldedNotesCountResponse\x12\x8a\x01\n\x15getShieldedNullifiers\x12\x37.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest\x1a\x38.org.dash.platform.dapi.v0.GetShieldedNullifiersResponseb\x06proto3' , dependencies=[google_dot_protobuf_dot_wrappers__pb2.DESCRIPTOR,google_dot_protobuf_dot_struct__pb2.DESCRIPTOR,google_dot_protobuf_dot_timestamp__pb2.DESCRIPTOR,]) @@ -62,8 +62,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=79303, - serialized_end=79393, + serialized_start=79963, + serialized_end=80053, ) _sym_db.RegisterEnumDescriptor(_KEYPURPOSE) @@ -98,8 +98,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=79396, - serialized_end=79581, + serialized_start=80056, + serialized_end=80241, ) _sym_db.RegisterEnumDescriptor(_CONTRACTMODERATIONLIST) @@ -17804,6 +17804,106 @@ serialized_end=67268, ) +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTTOPOOLEVENT = _descriptor.Descriptor( + name='MintToPoolEvent', + full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='amount', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.amount', index=0, + number=1, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='actions_digest', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.actions_digest', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='public_note', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.public_note', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='_public_note', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent._public_note', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), + ], + serialized_start=67270, + serialized_end=67369, +) + +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNFROMPOOLEVENT = _descriptor.Descriptor( + name='BurnFromPoolEvent', + full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='amount', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.amount', index=0, + number=1, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='actions_digest', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.actions_digest', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='public_note', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.public_note', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='_public_note', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent._public_note', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), + ], + serialized_start=67371, + serialized_end=67472, +) + _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONEVENT = _descriptor.Descriptor( name='GroupActionEvent', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionEvent', @@ -17850,8 +17950,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=67271, - serialized_end=67651, + serialized_start=67475, + serialized_end=67855, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DOCUMENTEVENT = _descriptor.Descriptor( @@ -17886,8 +17986,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=67654, - serialized_end=67793, + serialized_start=67858, + serialized_end=67997, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DOCUMENTCREATEEVENT = _descriptor.Descriptor( @@ -17917,8 +18017,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=67795, - serialized_end=67842, + serialized_start=67999, + serialized_end=68046, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_CONTRACTUPDATEEVENT = _descriptor.Descriptor( @@ -17948,8 +18048,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=67844, - serialized_end=67891, + serialized_start=68048, + serialized_end=68095, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_CONTRACTEVENT = _descriptor.Descriptor( @@ -17984,8 +18084,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=67894, - serialized_end=68033, + serialized_start=68098, + serialized_end=68237, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT = _descriptor.Descriptor( @@ -18052,6 +18152,20 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='mint_to_pool', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.mint_to_pool', index=8, + number=9, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='burn_from_pool', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.burn_from_pool', index=9, + number=10, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -18069,8 +18183,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=68036, - serialized_end=69013, + serialized_start=68240, + serialized_end=69457, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONENTRY = _descriptor.Descriptor( @@ -18107,8 +18221,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=69016, - serialized_end=69163, + serialized_start=69460, + serialized_end=69607, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONS = _descriptor.Descriptor( @@ -18138,8 +18252,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=69166, - serialized_end=69298, + serialized_start=69610, + serialized_end=69742, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0 = _descriptor.Descriptor( @@ -18174,7 +18288,7 @@ ], extensions=[ ], - nested_types=[_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_FREEZEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UNFREEZEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DESTROYFROZENFUNDSEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_SHAREDENCRYPTEDNOTE, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_PERSONALENCRYPTEDNOTE, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_EMERGENCYACTIONEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENCONFIGUPDATEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UPDATEDIRECTPURCHASEPRICEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DOCUMENTEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DOCUMENTCREATEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_CONTRACTUPDATEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_CONTRACTEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONENTRY, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONS, ], + nested_types=[_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_FREEZEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UNFREEZEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DESTROYFROZENFUNDSEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_SHAREDENCRYPTEDNOTE, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_PERSONALENCRYPTEDNOTE, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_EMERGENCYACTIONEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENCONFIGUPDATEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UPDATEDIRECTPURCHASEPRICEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTTOPOOLEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNFROMPOOLEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DOCUMENTEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DOCUMENTCREATEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_CONTRACTUPDATEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_CONTRACTEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONENTRY, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONS, ], enum_types=[ ], serialized_options=None, @@ -18189,7 +18303,7 @@ fields=[]), ], serialized_start=65513, - serialized_end=69308, + serialized_end=69752, ) _GETGROUPACTIONSRESPONSE = _descriptor.Descriptor( @@ -18225,7 +18339,7 @@ fields=[]), ], serialized_start=65393, - serialized_end=69319, + serialized_end=69763, ) @@ -18284,8 +18398,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=69457, - serialized_end=69663, + serialized_start=69901, + serialized_end=70107, ) _GETGROUPACTIONSIGNERSREQUEST = _descriptor.Descriptor( @@ -18321,8 +18435,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=69322, - serialized_end=69714, + serialized_start=69766, + serialized_end=70158, ) @@ -18360,8 +18474,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=70146, - serialized_end=70199, + serialized_start=70590, + serialized_end=70643, ) _GETGROUPACTIONSIGNERSRESPONSE_GETGROUPACTIONSIGNERSRESPONSEV0_GROUPACTIONSIGNERS = _descriptor.Descriptor( @@ -18391,8 +18505,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=70202, - serialized_end=70347, + serialized_start=70646, + serialized_end=70791, ) _GETGROUPACTIONSIGNERSRESPONSE_GETGROUPACTIONSIGNERSRESPONSEV0 = _descriptor.Descriptor( @@ -18441,8 +18555,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=69855, - serialized_end=70357, + serialized_start=70299, + serialized_end=70801, ) _GETGROUPACTIONSIGNERSRESPONSE = _descriptor.Descriptor( @@ -18477,8 +18591,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=69717, - serialized_end=70368, + serialized_start=70161, + serialized_end=70812, ) @@ -18516,8 +18630,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=70484, - serialized_end=70541, + serialized_start=70928, + serialized_end=70985, ) _GETADDRESSINFOREQUEST = _descriptor.Descriptor( @@ -18552,8 +18666,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=70371, - serialized_end=70552, + serialized_start=70815, + serialized_end=70996, ) @@ -18596,8 +18710,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=70555, - serialized_end=70688, + serialized_start=70999, + serialized_end=71132, ) @@ -18635,8 +18749,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=70690, - serialized_end=70739, + serialized_start=71134, + serialized_end=71183, ) @@ -18667,8 +18781,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=70741, - serialized_end=70836, + serialized_start=71185, + serialized_end=71280, ) @@ -18718,8 +18832,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=70838, - serialized_end=70947, + serialized_start=71282, + serialized_end=71391, ) @@ -18757,8 +18871,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=70949, - serialized_end=71069, + serialized_start=71393, + serialized_end=71513, ) @@ -18789,8 +18903,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=71071, - serialized_end=71178, + serialized_start=71515, + serialized_end=71622, ) @@ -18840,8 +18954,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=71298, - serialized_end=71523, + serialized_start=71742, + serialized_end=71967, ) _GETADDRESSINFORESPONSE = _descriptor.Descriptor( @@ -18876,8 +18990,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=71181, - serialized_end=71534, + serialized_start=71625, + serialized_end=71978, ) @@ -18915,8 +19029,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=71659, - serialized_end=71721, + serialized_start=72103, + serialized_end=72165, ) _GETADDRESSESINFOSREQUEST = _descriptor.Descriptor( @@ -18951,8 +19065,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=71537, - serialized_end=71732, + serialized_start=71981, + serialized_end=72176, ) @@ -19002,8 +19116,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=71861, - serialized_end=72093, + serialized_start=72305, + serialized_end=72537, ) _GETADDRESSESINFOSRESPONSE = _descriptor.Descriptor( @@ -19038,8 +19152,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=71735, - serialized_end=72104, + serialized_start=72179, + serialized_end=72548, ) @@ -19063,8 +19177,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=72244, - serialized_end=72277, + serialized_start=72688, + serialized_end=72721, ) _GETADDRESSESTRUNKSTATEREQUEST = _descriptor.Descriptor( @@ -19099,8 +19213,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=72107, - serialized_end=72288, + serialized_start=72551, + serialized_end=72732, ) @@ -19138,8 +19252,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=72432, - serialized_end=72578, + serialized_start=72876, + serialized_end=73022, ) _GETADDRESSESTRUNKSTATERESPONSE = _descriptor.Descriptor( @@ -19174,8 +19288,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=72291, - serialized_end=72589, + serialized_start=72735, + serialized_end=73033, ) @@ -19220,8 +19334,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=72732, - serialized_end=72821, + serialized_start=73176, + serialized_end=73265, ) _GETADDRESSESBRANCHSTATEREQUEST = _descriptor.Descriptor( @@ -19256,8 +19370,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=72592, - serialized_end=72832, + serialized_start=73036, + serialized_end=73276, ) @@ -19288,8 +19402,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=72978, - serialized_end=73033, + serialized_start=73422, + serialized_end=73477, ) _GETADDRESSESBRANCHSTATERESPONSE = _descriptor.Descriptor( @@ -19324,8 +19438,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=72835, - serialized_end=73044, + serialized_start=73279, + serialized_end=73488, ) @@ -19370,8 +19484,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=73208, - serialized_end=73322, + serialized_start=73652, + serialized_end=73766, ) _GETRECENTADDRESSBALANCECHANGESREQUEST = _descriptor.Descriptor( @@ -19406,8 +19520,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=73047, - serialized_end=73333, + serialized_start=73491, + serialized_end=73777, ) @@ -19457,8 +19571,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=73501, - serialized_end=73765, + serialized_start=73945, + serialized_end=74209, ) _GETRECENTADDRESSBALANCECHANGESRESPONSE = _descriptor.Descriptor( @@ -19493,8 +19607,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=73336, - serialized_end=73776, + serialized_start=73780, + serialized_end=74220, ) @@ -19532,8 +19646,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=73778, - serialized_end=73849, + serialized_start=74222, + serialized_end=74293, ) @@ -19583,8 +19697,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=73852, - serialized_end=74028, + serialized_start=74296, + serialized_end=74472, ) @@ -19615,8 +19729,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=74030, - serialized_end=74122, + serialized_start=74474, + serialized_end=74566, ) @@ -19661,8 +19775,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=74125, - serialized_end=74299, + serialized_start=74569, + serialized_end=74743, ) @@ -19693,8 +19807,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=74302, - serialized_end=74437, + serialized_start=74746, + serialized_end=74881, ) @@ -19732,8 +19846,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=74629, - serialized_end=74726, + serialized_start=75073, + serialized_end=75170, ) _GETRECENTCOMPACTEDADDRESSBALANCECHANGESREQUEST = _descriptor.Descriptor( @@ -19768,8 +19882,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=74440, - serialized_end=74737, + serialized_start=74884, + serialized_end=75181, ) @@ -19819,8 +19933,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=74933, - serialized_end=75225, + serialized_start=75377, + serialized_end=75669, ) _GETRECENTCOMPACTEDADDRESSBALANCECHANGESRESPONSE = _descriptor.Descriptor( @@ -19855,8 +19969,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=74740, - serialized_end=75236, + serialized_start=75184, + serialized_end=75680, ) @@ -19889,6 +20003,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='token_id', full_name='org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0.token_id', index=3, + number=4, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -19900,9 +20021,14 @@ syntax='proto3', extension_ranges=[], oneofs=[ + _descriptor.OneofDescriptor( + name='_token_id', full_name='org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0._token_id', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), ], - serialized_start=75385, - serialized_end=75472, + serialized_start=75829, + serialized_end=75952, ) _GETSHIELDEDENCRYPTEDNOTESREQUEST = _descriptor.Descriptor( @@ -19937,8 +20063,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=75239, - serialized_end=75483, + serialized_start=75683, + serialized_end=75963, ) @@ -19990,8 +20116,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=75930, - serialized_end=76017, + serialized_start=76410, + serialized_end=76497, ) _GETSHIELDEDENCRYPTEDNOTESRESPONSE_GETSHIELDEDENCRYPTEDNOTESRESPONSEV0_ENCRYPTEDNOTES = _descriptor.Descriptor( @@ -20021,8 +20147,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=76020, - serialized_end=76165, + serialized_start=76500, + serialized_end=76645, ) _GETSHIELDEDENCRYPTEDNOTESRESPONSE_GETSHIELDEDENCRYPTEDNOTESRESPONSEV0 = _descriptor.Descriptor( @@ -20071,8 +20197,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=75636, - serialized_end=76175, + serialized_start=76116, + serialized_end=76655, ) _GETSHIELDEDENCRYPTEDNOTESRESPONSE = _descriptor.Descriptor( @@ -20107,8 +20233,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=75486, - serialized_end=76186, + serialized_start=75966, + serialized_end=76666, ) @@ -20127,6 +20253,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='token_id', full_name='org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0.token_id', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -20138,9 +20271,14 @@ syntax='proto3', extension_ranges=[], oneofs=[ + _descriptor.OneofDescriptor( + name='_token_id', full_name='org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0._token_id', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), ], - serialized_start=76314, - serialized_end=76358, + serialized_start=76794, + serialized_end=76874, ) _GETSHIELDEDANCHORSREQUEST = _descriptor.Descriptor( @@ -20175,8 +20313,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=76189, - serialized_end=76369, + serialized_start=76669, + serialized_end=76885, ) @@ -20207,8 +20345,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=76758, - serialized_end=76784, + serialized_start=77274, + serialized_end=77300, ) _GETSHIELDEDANCHORSRESPONSE_GETSHIELDEDANCHORSRESPONSEV0 = _descriptor.Descriptor( @@ -20257,8 +20395,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=76501, - serialized_end=76794, + serialized_start=77017, + serialized_end=77310, ) _GETSHIELDEDANCHORSRESPONSE = _descriptor.Descriptor( @@ -20293,8 +20431,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=76372, - serialized_end=76805, + serialized_start=76888, + serialized_end=77321, ) @@ -20313,6 +20451,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='token_id', full_name='org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0.token_id', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -20324,9 +20469,14 @@ syntax='proto3', extension_ranges=[], oneofs=[ + _descriptor.OneofDescriptor( + name='_token_id', full_name='org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0._token_id', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), ], - serialized_start=76960, - serialized_end=77013, + serialized_start=77476, + serialized_end=77565, ) _GETMOSTRECENTSHIELDEDANCHORREQUEST = _descriptor.Descriptor( @@ -20361,8 +20511,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=76808, - serialized_end=77024, + serialized_start=77324, + serialized_end=77576, ) @@ -20412,8 +20562,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=77183, - serialized_end=77364, + serialized_start=77735, + serialized_end=77916, ) _GETMOSTRECENTSHIELDEDANCHORRESPONSE = _descriptor.Descriptor( @@ -20448,8 +20598,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=77027, - serialized_end=77375, + serialized_start=77579, + serialized_end=77927, ) @@ -20468,6 +20618,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='token_id', full_name='org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0.token_id', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -20479,9 +20636,14 @@ syntax='proto3', extension_ranges=[], oneofs=[ + _descriptor.OneofDescriptor( + name='_token_id', full_name='org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0._token_id', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), ], - serialized_start=77509, - serialized_end=77555, + serialized_start=78061, + serialized_end=78143, ) _GETSHIELDEDPOOLSTATEREQUEST = _descriptor.Descriptor( @@ -20516,8 +20678,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=77378, - serialized_end=77566, + serialized_start=77930, + serialized_end=78154, ) @@ -20567,8 +20729,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=77704, - serialized_end=77889, + serialized_start=78292, + serialized_end=78477, ) _GETSHIELDEDPOOLSTATERESPONSE = _descriptor.Descriptor( @@ -20603,8 +20765,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=77569, - serialized_end=77900, + serialized_start=78157, + serialized_end=78488, ) @@ -20623,6 +20785,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='token_id', full_name='org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0.token_id', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -20634,9 +20803,14 @@ syntax='proto3', extension_ranges=[], oneofs=[ + _descriptor.OneofDescriptor( + name='_token_id', full_name='org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0._token_id', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), ], - serialized_start=78037, - serialized_end=78084, + serialized_start=78625, + serialized_end=78708, ) _GETSHIELDEDNOTESCOUNTREQUEST = _descriptor.Descriptor( @@ -20671,8 +20845,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=77903, - serialized_end=78095, + serialized_start=78491, + serialized_end=78719, ) @@ -20722,8 +20896,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=78236, - serialized_end=78426, + serialized_start=78860, + serialized_end=79050, ) _GETSHIELDEDNOTESCOUNTRESPONSE = _descriptor.Descriptor( @@ -20758,8 +20932,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=78098, - serialized_end=78437, + serialized_start=78722, + serialized_end=79061, ) @@ -20785,6 +20959,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='token_id', full_name='org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0.token_id', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -20796,9 +20977,14 @@ syntax='proto3', extension_ranges=[], oneofs=[ + _descriptor.OneofDescriptor( + name='_token_id', full_name='org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0._token_id', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), ], - serialized_start=78574, - serialized_end=78641, + serialized_start=79198, + serialized_end=79301, ) _GETSHIELDEDNULLIFIERSREQUEST = _descriptor.Descriptor( @@ -20833,8 +21019,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=78440, - serialized_end=78652, + serialized_start=79064, + serialized_end=79312, ) @@ -20872,8 +21058,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=79081, - serialized_end=79135, + serialized_start=79741, + serialized_end=79795, ) _GETSHIELDEDNULLIFIERSRESPONSE_GETSHIELDEDNULLIFIERSRESPONSEV0_NULLIFIERSTATUSES = _descriptor.Descriptor( @@ -20903,8 +21089,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=79138, - serialized_end=79280, + serialized_start=79798, + serialized_end=79940, ) _GETSHIELDEDNULLIFIERSRESPONSE_GETSHIELDEDNULLIFIERSRESPONSEV0 = _descriptor.Descriptor( @@ -20953,8 +21139,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=78793, - serialized_end=79290, + serialized_start=79453, + serialized_end=79950, ) _GETSHIELDEDNULLIFIERSRESPONSE = _descriptor.Descriptor( @@ -20989,8 +21175,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=78655, - serialized_end=79301, + serialized_start=79315, + serialized_end=79961, ) _GETIDENTITYREQUEST_GETIDENTITYREQUESTV0.containing_type = _GETIDENTITYREQUEST @@ -22748,6 +22934,14 @@ _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UPDATEDIRECTPURCHASEPRICEEVENT.oneofs_by_name['_public_note'].fields.append( _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UPDATEDIRECTPURCHASEPRICEEVENT.fields_by_name['public_note']) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UPDATEDIRECTPURCHASEPRICEEVENT.fields_by_name['public_note'].containing_oneof = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UPDATEDIRECTPURCHASEPRICEEVENT.oneofs_by_name['_public_note'] +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTTOPOOLEVENT.containing_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0 +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTTOPOOLEVENT.oneofs_by_name['_public_note'].fields.append( + _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTTOPOOLEVENT.fields_by_name['public_note']) +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTTOPOOLEVENT.fields_by_name['public_note'].containing_oneof = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTTOPOOLEVENT.oneofs_by_name['_public_note'] +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNFROMPOOLEVENT.containing_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0 +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNFROMPOOLEVENT.oneofs_by_name['_public_note'].fields.append( + _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNFROMPOOLEVENT.fields_by_name['public_note']) +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNFROMPOOLEVENT.fields_by_name['public_note'].containing_oneof = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNFROMPOOLEVENT.oneofs_by_name['_public_note'] _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONEVENT.fields_by_name['token_event'].message_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONEVENT.fields_by_name['document_event'].message_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DOCUMENTEVENT _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONEVENT.fields_by_name['contract_event'].message_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_CONTRACTEVENT @@ -22781,6 +22975,8 @@ _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['emergency_action'].message_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_EMERGENCYACTIONEVENT _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['token_config_update'].message_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENCONFIGUPDATEEVENT _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['update_price'].message_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UPDATEDIRECTPURCHASEPRICEEVENT +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['mint_to_pool'].message_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTTOPOOLEVENT +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['burn_from_pool'].message_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNFROMPOOLEVENT _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.containing_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0 _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.oneofs_by_name['type'].fields.append( _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['mint']) @@ -22806,6 +23002,12 @@ _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.oneofs_by_name['type'].fields.append( _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['update_price']) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['update_price'].containing_oneof = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.oneofs_by_name['type'] +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.oneofs_by_name['type'].fields.append( + _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['mint_to_pool']) +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['mint_to_pool'].containing_oneof = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.oneofs_by_name['type'] +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.oneofs_by_name['type'].fields.append( + _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['burn_from_pool']) +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['burn_from_pool'].containing_oneof = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.oneofs_by_name['type'] _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONENTRY.fields_by_name['event'].message_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONEVENT _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONENTRY.containing_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0 _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONS.fields_by_name['group_actions'].message_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONENTRY @@ -22970,6 +23172,9 @@ _GETRECENTCOMPACTEDADDRESSBALANCECHANGESRESPONSE.fields_by_name['v0']) _GETRECENTCOMPACTEDADDRESSBALANCECHANGESRESPONSE.fields_by_name['v0'].containing_oneof = _GETRECENTCOMPACTEDADDRESSBALANCECHANGESRESPONSE.oneofs_by_name['version'] _GETSHIELDEDENCRYPTEDNOTESREQUEST_GETSHIELDEDENCRYPTEDNOTESREQUESTV0.containing_type = _GETSHIELDEDENCRYPTEDNOTESREQUEST +_GETSHIELDEDENCRYPTEDNOTESREQUEST_GETSHIELDEDENCRYPTEDNOTESREQUESTV0.oneofs_by_name['_token_id'].fields.append( + _GETSHIELDEDENCRYPTEDNOTESREQUEST_GETSHIELDEDENCRYPTEDNOTESREQUESTV0.fields_by_name['token_id']) +_GETSHIELDEDENCRYPTEDNOTESREQUEST_GETSHIELDEDENCRYPTEDNOTESREQUESTV0.fields_by_name['token_id'].containing_oneof = _GETSHIELDEDENCRYPTEDNOTESREQUEST_GETSHIELDEDENCRYPTEDNOTESREQUESTV0.oneofs_by_name['_token_id'] _GETSHIELDEDENCRYPTEDNOTESREQUEST.fields_by_name['v0'].message_type = _GETSHIELDEDENCRYPTEDNOTESREQUEST_GETSHIELDEDENCRYPTEDNOTESREQUESTV0 _GETSHIELDEDENCRYPTEDNOTESREQUEST.oneofs_by_name['version'].fields.append( _GETSHIELDEDENCRYPTEDNOTESREQUEST.fields_by_name['v0']) @@ -22992,6 +23197,9 @@ _GETSHIELDEDENCRYPTEDNOTESRESPONSE.fields_by_name['v0']) _GETSHIELDEDENCRYPTEDNOTESRESPONSE.fields_by_name['v0'].containing_oneof = _GETSHIELDEDENCRYPTEDNOTESRESPONSE.oneofs_by_name['version'] _GETSHIELDEDANCHORSREQUEST_GETSHIELDEDANCHORSREQUESTV0.containing_type = _GETSHIELDEDANCHORSREQUEST +_GETSHIELDEDANCHORSREQUEST_GETSHIELDEDANCHORSREQUESTV0.oneofs_by_name['_token_id'].fields.append( + _GETSHIELDEDANCHORSREQUEST_GETSHIELDEDANCHORSREQUESTV0.fields_by_name['token_id']) +_GETSHIELDEDANCHORSREQUEST_GETSHIELDEDANCHORSREQUESTV0.fields_by_name['token_id'].containing_oneof = _GETSHIELDEDANCHORSREQUEST_GETSHIELDEDANCHORSREQUESTV0.oneofs_by_name['_token_id'] _GETSHIELDEDANCHORSREQUEST.fields_by_name['v0'].message_type = _GETSHIELDEDANCHORSREQUEST_GETSHIELDEDANCHORSREQUESTV0 _GETSHIELDEDANCHORSREQUEST.oneofs_by_name['version'].fields.append( _GETSHIELDEDANCHORSREQUEST.fields_by_name['v0']) @@ -23012,6 +23220,9 @@ _GETSHIELDEDANCHORSRESPONSE.fields_by_name['v0']) _GETSHIELDEDANCHORSRESPONSE.fields_by_name['v0'].containing_oneof = _GETSHIELDEDANCHORSRESPONSE.oneofs_by_name['version'] _GETMOSTRECENTSHIELDEDANCHORREQUEST_GETMOSTRECENTSHIELDEDANCHORREQUESTV0.containing_type = _GETMOSTRECENTSHIELDEDANCHORREQUEST +_GETMOSTRECENTSHIELDEDANCHORREQUEST_GETMOSTRECENTSHIELDEDANCHORREQUESTV0.oneofs_by_name['_token_id'].fields.append( + _GETMOSTRECENTSHIELDEDANCHORREQUEST_GETMOSTRECENTSHIELDEDANCHORREQUESTV0.fields_by_name['token_id']) +_GETMOSTRECENTSHIELDEDANCHORREQUEST_GETMOSTRECENTSHIELDEDANCHORREQUESTV0.fields_by_name['token_id'].containing_oneof = _GETMOSTRECENTSHIELDEDANCHORREQUEST_GETMOSTRECENTSHIELDEDANCHORREQUESTV0.oneofs_by_name['_token_id'] _GETMOSTRECENTSHIELDEDANCHORREQUEST.fields_by_name['v0'].message_type = _GETMOSTRECENTSHIELDEDANCHORREQUEST_GETMOSTRECENTSHIELDEDANCHORREQUESTV0 _GETMOSTRECENTSHIELDEDANCHORREQUEST.oneofs_by_name['version'].fields.append( _GETMOSTRECENTSHIELDEDANCHORREQUEST.fields_by_name['v0']) @@ -23030,6 +23241,9 @@ _GETMOSTRECENTSHIELDEDANCHORRESPONSE.fields_by_name['v0']) _GETMOSTRECENTSHIELDEDANCHORRESPONSE.fields_by_name['v0'].containing_oneof = _GETMOSTRECENTSHIELDEDANCHORRESPONSE.oneofs_by_name['version'] _GETSHIELDEDPOOLSTATEREQUEST_GETSHIELDEDPOOLSTATEREQUESTV0.containing_type = _GETSHIELDEDPOOLSTATEREQUEST +_GETSHIELDEDPOOLSTATEREQUEST_GETSHIELDEDPOOLSTATEREQUESTV0.oneofs_by_name['_token_id'].fields.append( + _GETSHIELDEDPOOLSTATEREQUEST_GETSHIELDEDPOOLSTATEREQUESTV0.fields_by_name['token_id']) +_GETSHIELDEDPOOLSTATEREQUEST_GETSHIELDEDPOOLSTATEREQUESTV0.fields_by_name['token_id'].containing_oneof = _GETSHIELDEDPOOLSTATEREQUEST_GETSHIELDEDPOOLSTATEREQUESTV0.oneofs_by_name['_token_id'] _GETSHIELDEDPOOLSTATEREQUEST.fields_by_name['v0'].message_type = _GETSHIELDEDPOOLSTATEREQUEST_GETSHIELDEDPOOLSTATEREQUESTV0 _GETSHIELDEDPOOLSTATEREQUEST.oneofs_by_name['version'].fields.append( _GETSHIELDEDPOOLSTATEREQUEST.fields_by_name['v0']) @@ -23048,6 +23262,9 @@ _GETSHIELDEDPOOLSTATERESPONSE.fields_by_name['v0']) _GETSHIELDEDPOOLSTATERESPONSE.fields_by_name['v0'].containing_oneof = _GETSHIELDEDPOOLSTATERESPONSE.oneofs_by_name['version'] _GETSHIELDEDNOTESCOUNTREQUEST_GETSHIELDEDNOTESCOUNTREQUESTV0.containing_type = _GETSHIELDEDNOTESCOUNTREQUEST +_GETSHIELDEDNOTESCOUNTREQUEST_GETSHIELDEDNOTESCOUNTREQUESTV0.oneofs_by_name['_token_id'].fields.append( + _GETSHIELDEDNOTESCOUNTREQUEST_GETSHIELDEDNOTESCOUNTREQUESTV0.fields_by_name['token_id']) +_GETSHIELDEDNOTESCOUNTREQUEST_GETSHIELDEDNOTESCOUNTREQUESTV0.fields_by_name['token_id'].containing_oneof = _GETSHIELDEDNOTESCOUNTREQUEST_GETSHIELDEDNOTESCOUNTREQUESTV0.oneofs_by_name['_token_id'] _GETSHIELDEDNOTESCOUNTREQUEST.fields_by_name['v0'].message_type = _GETSHIELDEDNOTESCOUNTREQUEST_GETSHIELDEDNOTESCOUNTREQUESTV0 _GETSHIELDEDNOTESCOUNTREQUEST.oneofs_by_name['version'].fields.append( _GETSHIELDEDNOTESCOUNTREQUEST.fields_by_name['v0']) @@ -23066,6 +23283,9 @@ _GETSHIELDEDNOTESCOUNTRESPONSE.fields_by_name['v0']) _GETSHIELDEDNOTESCOUNTRESPONSE.fields_by_name['v0'].containing_oneof = _GETSHIELDEDNOTESCOUNTRESPONSE.oneofs_by_name['version'] _GETSHIELDEDNULLIFIERSREQUEST_GETSHIELDEDNULLIFIERSREQUESTV0.containing_type = _GETSHIELDEDNULLIFIERSREQUEST +_GETSHIELDEDNULLIFIERSREQUEST_GETSHIELDEDNULLIFIERSREQUESTV0.oneofs_by_name['_token_id'].fields.append( + _GETSHIELDEDNULLIFIERSREQUEST_GETSHIELDEDNULLIFIERSREQUESTV0.fields_by_name['token_id']) +_GETSHIELDEDNULLIFIERSREQUEST_GETSHIELDEDNULLIFIERSREQUESTV0.fields_by_name['token_id'].containing_oneof = _GETSHIELDEDNULLIFIERSREQUEST_GETSHIELDEDNULLIFIERSREQUESTV0.oneofs_by_name['_token_id'] _GETSHIELDEDNULLIFIERSREQUEST.fields_by_name['v0'].message_type = _GETSHIELDEDNULLIFIERSREQUEST_GETSHIELDEDNULLIFIERSREQUESTV0 _GETSHIELDEDNULLIFIERSREQUEST.oneofs_by_name['version'].fields.append( _GETSHIELDEDNULLIFIERSREQUEST.fields_by_name['v0']) @@ -26265,6 +26485,20 @@ }) , + 'MintToPoolEvent' : _reflection.GeneratedProtocolMessageType('MintToPoolEvent', (_message.Message,), { + 'DESCRIPTOR' : _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTTOPOOLEVENT, + '__module__' : 'platform_pb2' + # @@protoc_insertion_point(class_scope:org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent) + }) + , + + 'BurnFromPoolEvent' : _reflection.GeneratedProtocolMessageType('BurnFromPoolEvent', (_message.Message,), { + 'DESCRIPTOR' : _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNFROMPOOLEVENT, + '__module__' : 'platform_pb2' + # @@protoc_insertion_point(class_scope:org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent) + }) + , + 'GroupActionEvent' : _reflection.GeneratedProtocolMessageType('GroupActionEvent', (_message.Message,), { 'DESCRIPTOR' : _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONEVENT, '__module__' : 'platform_pb2' @@ -26343,6 +26577,8 @@ _sym_db.RegisterMessage(GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent) _sym_db.RegisterMessage(GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.PriceForQuantity) _sym_db.RegisterMessage(GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.PricingSchedule) +_sym_db.RegisterMessage(GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent) +_sym_db.RegisterMessage(GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent) _sym_db.RegisterMessage(GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionEvent) _sym_db.RegisterMessage(GetGroupActionsResponse.GetGroupActionsResponseV0.DocumentEvent) _sym_db.RegisterMessage(GetGroupActionsResponse.GetGroupActionsResponseV0.DocumentCreateEvent) @@ -26962,8 +27198,8 @@ index=0, serialized_options=None, create_key=_descriptor._internal_create_key, - serialized_start=79584, - serialized_end=89904, + serialized_start=80244, + serialized_end=90564, methods=[ _descriptor.MethodDescriptor( name='broadcastStateTransition', diff --git a/packages/dapi-grpc/clients/platform/v0/web/platform_pb.d.ts b/packages/dapi-grpc/clients/platform/v0/web/platform_pb.d.ts index f0dfa62cdb8..f0a99b589e1 100644 --- a/packages/dapi-grpc/clients/platform/v0/web/platform_pb.d.ts +++ b/packages/dapi-grpc/clients/platform/v0/web/platform_pb.d.ts @@ -12641,6 +12641,70 @@ export namespace GetGroupActionsResponse { } } + export class MintToPoolEvent extends jspb.Message { + getAmount(): number; + setAmount(value: number): void; + + getActionsDigest(): Uint8Array | string; + getActionsDigest_asU8(): Uint8Array; + getActionsDigest_asB64(): string; + setActionsDigest(value: Uint8Array | string): void; + + hasPublicNote(): boolean; + clearPublicNote(): void; + getPublicNote(): string; + setPublicNote(value: string): void; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): MintToPoolEvent.AsObject; + static toObject(includeInstance: boolean, msg: MintToPoolEvent): MintToPoolEvent.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: MintToPoolEvent, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): MintToPoolEvent; + static deserializeBinaryFromReader(message: MintToPoolEvent, reader: jspb.BinaryReader): MintToPoolEvent; + } + + export namespace MintToPoolEvent { + export type AsObject = { + amount: number, + actionsDigest: Uint8Array | string, + publicNote: string, + } + } + + export class BurnFromPoolEvent extends jspb.Message { + getAmount(): number; + setAmount(value: number): void; + + getActionsDigest(): Uint8Array | string; + getActionsDigest_asU8(): Uint8Array; + getActionsDigest_asB64(): string; + setActionsDigest(value: Uint8Array | string): void; + + hasPublicNote(): boolean; + clearPublicNote(): void; + getPublicNote(): string; + setPublicNote(value: string): void; + + serializeBinary(): Uint8Array; + toObject(includeInstance?: boolean): BurnFromPoolEvent.AsObject; + static toObject(includeInstance: boolean, msg: BurnFromPoolEvent): BurnFromPoolEvent.AsObject; + static extensions: {[key: number]: jspb.ExtensionFieldInfo}; + static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo}; + static serializeBinaryToWriter(message: BurnFromPoolEvent, writer: jspb.BinaryWriter): void; + static deserializeBinary(bytes: Uint8Array): BurnFromPoolEvent; + static deserializeBinaryFromReader(message: BurnFromPoolEvent, reader: jspb.BinaryReader): BurnFromPoolEvent; + } + + export namespace BurnFromPoolEvent { + export type AsObject = { + amount: number, + actionsDigest: Uint8Array | string, + publicNote: string, + } + } + export class GroupActionEvent extends jspb.Message { hasTokenEvent(): boolean; clearTokenEvent(): void; @@ -12824,6 +12888,16 @@ export namespace GetGroupActionsResponse { getUpdatePrice(): GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent | undefined; setUpdatePrice(value?: GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent): void; + hasMintToPool(): boolean; + clearMintToPool(): void; + getMintToPool(): GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent | undefined; + setMintToPool(value?: GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent): void; + + hasBurnFromPool(): boolean; + clearBurnFromPool(): void; + getBurnFromPool(): GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent | undefined; + setBurnFromPool(value?: GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent): void; + getTypeCase(): TokenEvent.TypeCase; serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): TokenEvent.AsObject; @@ -12845,6 +12919,8 @@ export namespace GetGroupActionsResponse { emergencyAction?: GetGroupActionsResponse.GetGroupActionsResponseV0.EmergencyActionEvent.AsObject, tokenConfigUpdate?: GetGroupActionsResponse.GetGroupActionsResponseV0.TokenConfigUpdateEvent.AsObject, updatePrice?: GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.AsObject, + mintToPool?: GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.AsObject, + burnFromPool?: GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.AsObject, } export enum TypeCase { @@ -12857,6 +12933,8 @@ export namespace GetGroupActionsResponse { EMERGENCY_ACTION = 6, TOKEN_CONFIG_UPDATE = 7, UPDATE_PRICE = 8, + MINT_TO_POOL = 9, + BURN_FROM_POOL = 10, } } @@ -14151,6 +14229,13 @@ export namespace GetShieldedEncryptedNotesRequest { getProve(): boolean; setProve(value: boolean): void; + hasTokenId(): boolean; + clearTokenId(): void; + getTokenId(): Uint8Array | string; + getTokenId_asU8(): Uint8Array; + getTokenId_asB64(): string; + setTokenId(value: Uint8Array | string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): GetShieldedEncryptedNotesRequestV0.AsObject; static toObject(includeInstance: boolean, msg: GetShieldedEncryptedNotesRequestV0): GetShieldedEncryptedNotesRequestV0.AsObject; @@ -14166,6 +14251,7 @@ export namespace GetShieldedEncryptedNotesRequest { startIndex: number, count: number, prove: boolean, + tokenId: Uint8Array | string, } } @@ -14332,6 +14418,13 @@ export namespace GetShieldedAnchorsRequest { getProve(): boolean; setProve(value: boolean): void; + hasTokenId(): boolean; + clearTokenId(): void; + getTokenId(): Uint8Array | string; + getTokenId_asU8(): Uint8Array; + getTokenId_asB64(): string; + setTokenId(value: Uint8Array | string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): GetShieldedAnchorsRequestV0.AsObject; static toObject(includeInstance: boolean, msg: GetShieldedAnchorsRequestV0): GetShieldedAnchorsRequestV0.AsObject; @@ -14345,6 +14438,7 @@ export namespace GetShieldedAnchorsRequest { export namespace GetShieldedAnchorsRequestV0 { export type AsObject = { prove: boolean, + tokenId: Uint8Array | string, } } @@ -14473,6 +14567,13 @@ export namespace GetMostRecentShieldedAnchorRequest { getProve(): boolean; setProve(value: boolean): void; + hasTokenId(): boolean; + clearTokenId(): void; + getTokenId(): Uint8Array | string; + getTokenId_asU8(): Uint8Array; + getTokenId_asB64(): string; + setTokenId(value: Uint8Array | string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): GetMostRecentShieldedAnchorRequestV0.AsObject; static toObject(includeInstance: boolean, msg: GetMostRecentShieldedAnchorRequestV0): GetMostRecentShieldedAnchorRequestV0.AsObject; @@ -14486,6 +14587,7 @@ export namespace GetMostRecentShieldedAnchorRequest { export namespace GetMostRecentShieldedAnchorRequestV0 { export type AsObject = { prove: boolean, + tokenId: Uint8Array | string, } } @@ -14592,6 +14694,13 @@ export namespace GetShieldedPoolStateRequest { getProve(): boolean; setProve(value: boolean): void; + hasTokenId(): boolean; + clearTokenId(): void; + getTokenId(): Uint8Array | string; + getTokenId_asU8(): Uint8Array; + getTokenId_asB64(): string; + setTokenId(value: Uint8Array | string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): GetShieldedPoolStateRequestV0.AsObject; static toObject(includeInstance: boolean, msg: GetShieldedPoolStateRequestV0): GetShieldedPoolStateRequestV0.AsObject; @@ -14605,6 +14714,7 @@ export namespace GetShieldedPoolStateRequest { export namespace GetShieldedPoolStateRequestV0 { export type AsObject = { prove: boolean, + tokenId: Uint8Array | string, } } @@ -14709,6 +14819,13 @@ export namespace GetShieldedNotesCountRequest { getProve(): boolean; setProve(value: boolean): void; + hasTokenId(): boolean; + clearTokenId(): void; + getTokenId(): Uint8Array | string; + getTokenId_asU8(): Uint8Array; + getTokenId_asB64(): string; + setTokenId(value: Uint8Array | string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): GetShieldedNotesCountRequestV0.AsObject; static toObject(includeInstance: boolean, msg: GetShieldedNotesCountRequestV0): GetShieldedNotesCountRequestV0.AsObject; @@ -14722,6 +14839,7 @@ export namespace GetShieldedNotesCountRequest { export namespace GetShieldedNotesCountRequestV0 { export type AsObject = { prove: boolean, + tokenId: Uint8Array | string, } } @@ -14833,6 +14951,13 @@ export namespace GetShieldedNullifiersRequest { getProve(): boolean; setProve(value: boolean): void; + hasTokenId(): boolean; + clearTokenId(): void; + getTokenId(): Uint8Array | string; + getTokenId_asU8(): Uint8Array; + getTokenId_asB64(): string; + setTokenId(value: Uint8Array | string): void; + serializeBinary(): Uint8Array; toObject(includeInstance?: boolean): GetShieldedNullifiersRequestV0.AsObject; static toObject(includeInstance: boolean, msg: GetShieldedNullifiersRequestV0): GetShieldedNullifiersRequestV0.AsObject; @@ -14847,6 +14972,7 @@ export namespace GetShieldedNullifiersRequest { export type AsObject = { nullifiersList: Array, prove: boolean, + tokenId: Uint8Array | string, } } diff --git a/packages/dapi-grpc/clients/platform/v0/web/platform_pb.js b/packages/dapi-grpc/clients/platform/v0/web/platform_pb.js index 8fe2b482632..d2e380a4f8a 100644 --- a/packages/dapi-grpc/clients/platform/v0/web/platform_pb.js +++ b/packages/dapi-grpc/clients/platform/v0/web/platform_pb.js @@ -354,6 +354,7 @@ goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsRequest.Versio goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnEvent', null, { proto }); +goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ContractEvent', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ContractEvent.TypeCase', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ContractUpdateEvent', null, { proto }); @@ -369,6 +370,7 @@ goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGr goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionEvent.EventTypeCase', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActions', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintEvent', null, { proto }); +goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.PersonalEncryptedNote', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ResultCase', null, { proto }); goog.exportSymbol('proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.SharedEncryptedNote', null, { proto }); @@ -8984,6 +8986,48 @@ if (goog.DEBUG && !COMPILED) { */ proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.PricingSchedule.displayName = 'proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.PricingSchedule'; } +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.displayName = 'proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent'; +} +/** + * Generated by JsPbCodeGenerator. + * @param {Array=} opt_data Optional initial data array, typically from a + * server response, or constructed directly in Javascript. The array is used + * in place and becomes part of the constructed object. It is not cloned. + * If no data is provided, the constructed object will be empty, but still + * valid. + * @extends {jspb.Message} + * @constructor + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent = function(opt_data) { + jspb.Message.initialize(this, opt_data, 0, -1, null, null); +}; +goog.inherits(proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent, jspb.Message); +if (goog.DEBUG && !COMPILED) { + /** + * @public + * @override + */ + proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.displayName = 'proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent'; +} /** * Generated by JsPbCodeGenerator. * @param {Array=} opt_data Optional initial data array, typically from a @@ -97785,6 +97829,470 @@ proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.toObject = function(opt_includeInstance) { + return proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.toObject = function(includeInstance, msg) { + var f, obj = { + amount: jspb.Message.getFieldWithDefault(msg, 1, 0), + actionsDigest: msg.getActionsDigest_asB64(), + publicNote: jspb.Message.getFieldWithDefault(msg, 3, "") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent; + return proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {number} */ (reader.readUint64()); + msg.setAmount(value); + break; + case 2: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setActionsDigest(value); + break; + case 3: + var value = /** @type {string} */ (reader.readString()); + msg.setPublicNote(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getAmount(); + if (f !== 0) { + writer.writeUint64( + 1, + f + ); + } + f = message.getActionsDigest_asU8(); + if (f.length > 0) { + writer.writeBytes( + 2, + f + ); + } + f = /** @type {string} */ (jspb.Message.getField(message, 3)); + if (f != null) { + writer.writeString( + 3, + f + ); + } +}; + + +/** + * optional uint64 amount = 1; + * @return {number} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.getAmount = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} returns this + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.setAmount = function(value) { + return jspb.Message.setProto3IntField(this, 1, value); +}; + + +/** + * optional bytes actions_digest = 2; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.getActionsDigest = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * optional bytes actions_digest = 2; + * This is a type-conversion wrapper around `getActionsDigest()` + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.getActionsDigest_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getActionsDigest())); +}; + + +/** + * optional bytes actions_digest = 2; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getActionsDigest()` + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.getActionsDigest_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getActionsDigest())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} returns this + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.setActionsDigest = function(value) { + return jspb.Message.setProto3BytesField(this, 2, value); +}; + + +/** + * optional string public_note = 3; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.getPublicNote = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "")); +}; + + +/** + * @param {string} value + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} returns this + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.setPublicNote = function(value) { + return jspb.Message.setField(this, 3, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} returns this + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.clearPublicNote = function() { + return jspb.Message.setField(this, 3, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.prototype.hasPublicNote = function() { + return jspb.Message.getField(this, 3) != null; +}; + + + + + +if (jspb.Message.GENERATE_TO_OBJECT) { +/** + * Creates an object representation of this proto. + * Field names that are reserved in JavaScript and will be renamed to pb_name. + * Optional fields that are not set will be set to undefined. + * To access a reserved field use, foo.pb_, eg, foo.pb_default. + * For the list of reserved names please see: + * net/proto2/compiler/js/internal/generator.cc#kKeyword. + * @param {boolean=} opt_includeInstance Deprecated. whether to include the + * JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @return {!Object} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.toObject = function(opt_includeInstance) { + return proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.toObject(opt_includeInstance, this); +}; + + +/** + * Static version of the {@see toObject} method. + * @param {boolean|undefined} includeInstance Deprecated. Whether to include + * the JSPB instance for transitional soy proto support: + * http://goto/soy-param-migration + * @param {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} msg The msg instance to transform. + * @return {!Object} + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.toObject = function(includeInstance, msg) { + var f, obj = { + amount: jspb.Message.getFieldWithDefault(msg, 1, 0), + actionsDigest: msg.getActionsDigest_asB64(), + publicNote: jspb.Message.getFieldWithDefault(msg, 3, "") + }; + + if (includeInstance) { + obj.$jspbMessageInstance = msg; + } + return obj; +}; +} + + +/** + * Deserializes binary data (in protobuf wire format). + * @param {jspb.ByteSource} bytes The bytes to deserialize. + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.deserializeBinary = function(bytes) { + var reader = new jspb.BinaryReader(bytes); + var msg = new proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent; + return proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.deserializeBinaryFromReader(msg, reader); +}; + + +/** + * Deserializes binary data (in protobuf wire format) from the + * given reader into the given message object. + * @param {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} msg The message object to deserialize into. + * @param {!jspb.BinaryReader} reader The BinaryReader to use. + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.deserializeBinaryFromReader = function(msg, reader) { + while (reader.nextField()) { + if (reader.isEndGroup()) { + break; + } + var field = reader.getFieldNumber(); + switch (field) { + case 1: + var value = /** @type {number} */ (reader.readUint64()); + msg.setAmount(value); + break; + case 2: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setActionsDigest(value); + break; + case 3: + var value = /** @type {string} */ (reader.readString()); + msg.setPublicNote(value); + break; + default: + reader.skipField(); + break; + } + } + return msg; +}; + + +/** + * Serializes the message to binary data (in protobuf wire format). + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.serializeBinary = function() { + var writer = new jspb.BinaryWriter(); + proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.serializeBinaryToWriter(this, writer); + return writer.getResultBuffer(); +}; + + +/** + * Serializes the given message to binary data (in protobuf wire + * format), writing to the given BinaryWriter. + * @param {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} message + * @param {!jspb.BinaryWriter} writer + * @suppress {unusedLocalVariables} f is only used for nested messages + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.serializeBinaryToWriter = function(message, writer) { + var f = undefined; + f = message.getAmount(); + if (f !== 0) { + writer.writeUint64( + 1, + f + ); + } + f = message.getActionsDigest_asU8(); + if (f.length > 0) { + writer.writeBytes( + 2, + f + ); + } + f = /** @type {string} */ (jspb.Message.getField(message, 3)); + if (f != null) { + writer.writeString( + 3, + f + ); + } +}; + + +/** + * optional uint64 amount = 1; + * @return {number} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.getAmount = function() { + return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0)); +}; + + +/** + * @param {number} value + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} returns this + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.setAmount = function(value) { + return jspb.Message.setProto3IntField(this, 1, value); +}; + + +/** + * optional bytes actions_digest = 2; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.getActionsDigest = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * optional bytes actions_digest = 2; + * This is a type-conversion wrapper around `getActionsDigest()` + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.getActionsDigest_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getActionsDigest())); +}; + + +/** + * optional bytes actions_digest = 2; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getActionsDigest()` + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.getActionsDigest_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getActionsDigest())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} returns this + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.setActionsDigest = function(value) { + return jspb.Message.setProto3BytesField(this, 2, value); +}; + + +/** + * optional string public_note = 3; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.getPublicNote = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "")); +}; + + +/** + * @param {string} value + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} returns this + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.setPublicNote = function(value) { + return jspb.Message.setField(this, 3, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} returns this + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.clearPublicNote = function() { + return jspb.Message.setField(this, 3, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.prototype.hasPublicNote = function() { + return jspb.Message.getField(this, 3) != null; +}; + + + /** * Oneof group definitions for this message. Each group defines the field * numbers belonging to that group. When of these fields' value is set, all @@ -98733,7 +99241,7 @@ proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV * @private {!Array>} * @const */ -proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.oneofGroups_ = [[1,2,3,4,5,6,7,8]]; +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.oneofGroups_ = [[1,2,3,4,5,6,7,8,9,10]]; /** * @enum {number} @@ -98747,7 +99255,9 @@ proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV DESTROY_FROZEN_FUNDS: 5, EMERGENCY_ACTION: 6, TOKEN_CONFIG_UPDATE: 7, - UPDATE_PRICE: 8 + UPDATE_PRICE: 8, + MINT_TO_POOL: 9, + BURN_FROM_POOL: 10 }; /** @@ -98795,7 +99305,9 @@ proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV destroyFrozenFunds: (f = msg.getDestroyFrozenFunds()) && proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.DestroyFrozenFundsEvent.toObject(includeInstance, f), emergencyAction: (f = msg.getEmergencyAction()) && proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.EmergencyActionEvent.toObject(includeInstance, f), tokenConfigUpdate: (f = msg.getTokenConfigUpdate()) && proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenConfigUpdateEvent.toObject(includeInstance, f), - updatePrice: (f = msg.getUpdatePrice()) && proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.toObject(includeInstance, f) + updatePrice: (f = msg.getUpdatePrice()) && proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.toObject(includeInstance, f), + mintToPool: (f = msg.getMintToPool()) && proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.toObject(includeInstance, f), + burnFromPool: (f = msg.getBurnFromPool()) && proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.toObject(includeInstance, f) }; if (includeInstance) { @@ -98872,6 +99384,16 @@ proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV reader.readMessage(value,proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.deserializeBinaryFromReader); msg.setUpdatePrice(value); break; + case 9: + var value = new proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent; + reader.readMessage(value,proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.deserializeBinaryFromReader); + msg.setMintToPool(value); + break; + case 10: + var value = new proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent; + reader.readMessage(value,proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.deserializeBinaryFromReader); + msg.setBurnFromPool(value); + break; default: reader.skipField(); break; @@ -98965,6 +99487,22 @@ proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.serializeBinaryToWriter ); } + f = message.getMintToPool(); + if (f != null) { + writer.writeMessage( + 9, + f, + proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.serializeBinaryToWriter + ); + } + f = message.getBurnFromPool(); + if (f != null) { + writer.writeMessage( + 10, + f, + proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.serializeBinaryToWriter + ); + } }; @@ -99264,6 +99802,80 @@ proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV }; +/** + * optional MintToPoolEvent mint_to_pool = 9; + * @return {?proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.prototype.getMintToPool = function() { + return /** @type{?proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent} */ ( + jspb.Message.getWrapperField(this, proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent, 9)); +}; + + +/** + * @param {?proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent|undefined} value + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent} returns this +*/ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.prototype.setMintToPool = function(value) { + return jspb.Message.setOneofWrapperField(this, 9, proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.oneofGroups_[0], value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent} returns this + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.prototype.clearMintToPool = function() { + return this.setMintToPool(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.prototype.hasMintToPool = function() { + return jspb.Message.getField(this, 9) != null; +}; + + +/** + * optional BurnFromPoolEvent burn_from_pool = 10; + * @return {?proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.prototype.getBurnFromPool = function() { + return /** @type{?proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent} */ ( + jspb.Message.getWrapperField(this, proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent, 10)); +}; + + +/** + * @param {?proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent|undefined} value + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent} returns this +*/ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.prototype.setBurnFromPool = function(value) { + return jspb.Message.setOneofWrapperField(this, 10, proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.oneofGroups_[0], value); +}; + + +/** + * Clears the message field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent} returns this + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.prototype.clearBurnFromPool = function() { + return this.setBurnFromPool(undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.prototype.hasBurnFromPool = function() { + return jspb.Message.getField(this, 10) != null; +}; + + @@ -108004,7 +108616,8 @@ proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncr var f, obj = { startIndex: jspb.Message.getFieldWithDefault(msg, 1, 0), count: jspb.Message.getFieldWithDefault(msg, 2, 0), - prove: jspb.Message.getBooleanFieldWithDefault(msg, 3, false) + prove: jspb.Message.getBooleanFieldWithDefault(msg, 3, false), + tokenId: msg.getTokenId_asB64() }; if (includeInstance) { @@ -108053,6 +108666,10 @@ proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncr var value = /** @type {boolean} */ (reader.readBool()); msg.setProve(value); break; + case 4: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setTokenId(value); + break; default: reader.skipField(); break; @@ -108103,6 +108720,13 @@ proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncr f ); } + f = /** @type {!(string|Uint8Array)} */ (jspb.Message.getField(message, 4)); + if (f != null) { + writer.writeBytes( + 4, + f + ); + } }; @@ -108160,6 +108784,66 @@ proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncr }; +/** + * optional bytes token_id = 4; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0.prototype.getTokenId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 4, "")); +}; + + +/** + * optional bytes token_id = 4; + * This is a type-conversion wrapper around `getTokenId()` + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0.prototype.getTokenId_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getTokenId())); +}; + + +/** + * optional bytes token_id = 4; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getTokenId()` + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0.prototype.getTokenId_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getTokenId())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0.prototype.setTokenId = function(value) { + return jspb.Message.setField(this, 4, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0.prototype.clearTokenId = function() { + return jspb.Message.setField(this, 4, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0.prototype.hasTokenId = function() { + return jspb.Message.getField(this, 4) != null; +}; + + /** * optional GetShieldedEncryptedNotesRequestV0 v0 = 1; * @return {?proto.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0} @@ -109299,7 +109983,8 @@ proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequ */ proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0.toObject = function(includeInstance, msg) { var f, obj = { - prove: jspb.Message.getBooleanFieldWithDefault(msg, 1, false) + prove: jspb.Message.getBooleanFieldWithDefault(msg, 1, false), + tokenId: msg.getTokenId_asB64() }; if (includeInstance) { @@ -109340,6 +110025,10 @@ proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequ var value = /** @type {boolean} */ (reader.readBool()); msg.setProve(value); break; + case 2: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setTokenId(value); + break; default: reader.skipField(); break; @@ -109376,6 +110065,13 @@ proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequ f ); } + f = /** @type {!(string|Uint8Array)} */ (jspb.Message.getField(message, 2)); + if (f != null) { + writer.writeBytes( + 2, + f + ); + } }; @@ -109397,6 +110093,66 @@ proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequ }; +/** + * optional bytes token_id = 2; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0.prototype.getTokenId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * optional bytes token_id = 2; + * This is a type-conversion wrapper around `getTokenId()` + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0.prototype.getTokenId_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getTokenId())); +}; + + +/** + * optional bytes token_id = 2; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getTokenId()` + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0.prototype.getTokenId_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getTokenId())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0.prototype.setTokenId = function(value) { + return jspb.Message.setField(this, 2, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0.prototype.clearTokenId = function() { + return jspb.Message.setField(this, 2, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0.prototype.hasTokenId = function() { + return jspb.Message.getField(this, 2) != null; +}; + + /** * optional GetShieldedAnchorsRequestV0 v0 = 1; * @return {?proto.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0} @@ -110240,7 +110996,8 @@ proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecent */ proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0.toObject = function(includeInstance, msg) { var f, obj = { - prove: jspb.Message.getBooleanFieldWithDefault(msg, 1, false) + prove: jspb.Message.getBooleanFieldWithDefault(msg, 1, false), + tokenId: msg.getTokenId_asB64() }; if (includeInstance) { @@ -110281,6 +111038,10 @@ proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecent var value = /** @type {boolean} */ (reader.readBool()); msg.setProve(value); break; + case 2: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setTokenId(value); + break; default: reader.skipField(); break; @@ -110317,6 +111078,13 @@ proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecent f ); } + f = /** @type {!(string|Uint8Array)} */ (jspb.Message.getField(message, 2)); + if (f != null) { + writer.writeBytes( + 2, + f + ); + } }; @@ -110338,6 +111106,66 @@ proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecent }; +/** + * optional bytes token_id = 2; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0.prototype.getTokenId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * optional bytes token_id = 2; + * This is a type-conversion wrapper around `getTokenId()` + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0.prototype.getTokenId_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getTokenId())); +}; + + +/** + * optional bytes token_id = 2; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getTokenId()` + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0.prototype.getTokenId_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getTokenId())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0.prototype.setTokenId = function(value) { + return jspb.Message.setField(this, 2, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0.prototype.clearTokenId = function() { + return jspb.Message.setField(this, 2, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0.prototype.hasTokenId = function() { + return jspb.Message.getField(this, 2) != null; +}; + + /** * optional GetMostRecentShieldedAnchorRequestV0 v0 = 1; * @return {?proto.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0} @@ -111022,7 +111850,8 @@ proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolState */ proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0.toObject = function(includeInstance, msg) { var f, obj = { - prove: jspb.Message.getBooleanFieldWithDefault(msg, 1, false) + prove: jspb.Message.getBooleanFieldWithDefault(msg, 1, false), + tokenId: msg.getTokenId_asB64() }; if (includeInstance) { @@ -111063,6 +111892,10 @@ proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolState var value = /** @type {boolean} */ (reader.readBool()); msg.setProve(value); break; + case 2: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setTokenId(value); + break; default: reader.skipField(); break; @@ -111099,6 +111932,13 @@ proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolState f ); } + f = /** @type {!(string|Uint8Array)} */ (jspb.Message.getField(message, 2)); + if (f != null) { + writer.writeBytes( + 2, + f + ); + } }; @@ -111120,6 +111960,66 @@ proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolState }; +/** + * optional bytes token_id = 2; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0.prototype.getTokenId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * optional bytes token_id = 2; + * This is a type-conversion wrapper around `getTokenId()` + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0.prototype.getTokenId_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getTokenId())); +}; + + +/** + * optional bytes token_id = 2; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getTokenId()` + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0.prototype.getTokenId_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getTokenId())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0.prototype.setTokenId = function(value) { + return jspb.Message.setField(this, 2, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0.prototype.clearTokenId = function() { + return jspb.Message.setField(this, 2, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0.prototype.hasTokenId = function() { + return jspb.Message.getField(this, 2) != null; +}; + + /** * optional GetShieldedPoolStateRequestV0 v0 = 1; * @return {?proto.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0} @@ -111780,7 +112680,8 @@ proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCou */ proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0.toObject = function(includeInstance, msg) { var f, obj = { - prove: jspb.Message.getBooleanFieldWithDefault(msg, 1, false) + prove: jspb.Message.getBooleanFieldWithDefault(msg, 1, false), + tokenId: msg.getTokenId_asB64() }; if (includeInstance) { @@ -111821,6 +112722,10 @@ proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCou var value = /** @type {boolean} */ (reader.readBool()); msg.setProve(value); break; + case 2: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setTokenId(value); + break; default: reader.skipField(); break; @@ -111857,6 +112762,13 @@ proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCou f ); } + f = /** @type {!(string|Uint8Array)} */ (jspb.Message.getField(message, 2)); + if (f != null) { + writer.writeBytes( + 2, + f + ); + } }; @@ -111878,6 +112790,66 @@ proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCou }; +/** + * optional bytes token_id = 2; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0.prototype.getTokenId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 2, "")); +}; + + +/** + * optional bytes token_id = 2; + * This is a type-conversion wrapper around `getTokenId()` + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0.prototype.getTokenId_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getTokenId())); +}; + + +/** + * optional bytes token_id = 2; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getTokenId()` + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0.prototype.getTokenId_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getTokenId())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0.prototype.setTokenId = function(value) { + return jspb.Message.setField(this, 2, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0.prototype.clearTokenId = function() { + return jspb.Message.setField(this, 2, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0.prototype.hasTokenId = function() { + return jspb.Message.getField(this, 2) != null; +}; + + /** * optional GetShieldedNotesCountRequestV0 v0 = 1; * @return {?proto.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0} @@ -112546,7 +113518,8 @@ proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifie proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0.toObject = function(includeInstance, msg) { var f, obj = { nullifiersList: msg.getNullifiersList_asB64(), - prove: jspb.Message.getBooleanFieldWithDefault(msg, 2, false) + prove: jspb.Message.getBooleanFieldWithDefault(msg, 2, false), + tokenId: msg.getTokenId_asB64() }; if (includeInstance) { @@ -112591,6 +113564,10 @@ proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifie var value = /** @type {boolean} */ (reader.readBool()); msg.setProve(value); break; + case 3: + var value = /** @type {!Uint8Array} */ (reader.readBytes()); + msg.setTokenId(value); + break; default: reader.skipField(); break; @@ -112634,6 +113611,13 @@ proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifie f ); } + f = /** @type {!(string|Uint8Array)} */ (jspb.Message.getField(message, 3)); + if (f != null) { + writer.writeBytes( + 3, + f + ); + } }; @@ -112716,6 +113700,66 @@ proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifie }; +/** + * optional bytes token_id = 3; + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0.prototype.getTokenId = function() { + return /** @type {string} */ (jspb.Message.getFieldWithDefault(this, 3, "")); +}; + + +/** + * optional bytes token_id = 3; + * This is a type-conversion wrapper around `getTokenId()` + * @return {string} + */ +proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0.prototype.getTokenId_asB64 = function() { + return /** @type {string} */ (jspb.Message.bytesAsB64( + this.getTokenId())); +}; + + +/** + * optional bytes token_id = 3; + * Note that Uint8Array is not supported on all browsers. + * @see http://caniuse.com/Uint8Array + * This is a type-conversion wrapper around `getTokenId()` + * @return {!Uint8Array} + */ +proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0.prototype.getTokenId_asU8 = function() { + return /** @type {!Uint8Array} */ (jspb.Message.bytesAsU8( + this.getTokenId())); +}; + + +/** + * @param {!(string|Uint8Array)} value + * @return {!proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0.prototype.setTokenId = function(value) { + return jspb.Message.setField(this, 3, value); +}; + + +/** + * Clears the field making it undefined. + * @return {!proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0} returns this + */ +proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0.prototype.clearTokenId = function() { + return jspb.Message.setField(this, 3, undefined); +}; + + +/** + * Returns whether this field is set. + * @return {boolean} + */ +proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0.prototype.hasTokenId = function() { + return jspb.Message.getField(this, 3) != null; +}; + + /** * optional GetShieldedNullifiersRequestV0 v0 = 1; * @return {?proto.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0} diff --git a/packages/dapi-grpc/protos/platform/v0/platform.proto b/packages/dapi-grpc/protos/platform/v0/platform.proto index 0f21adbdc85..19b7a01bb8d 100644 --- a/packages/dapi-grpc/protos/platform/v0/platform.proto +++ b/packages/dapi-grpc/protos/platform/v0/platform.proto @@ -3847,7 +3847,7 @@ message GetRecentCompactedAddressBalanceChangesResponse { // --- Shielded Pool Queries --- // Every shielded pool query below targets the credit shielded pool unless `token_id` is set, -// in which case it targets that token's own shielded pool (protocol version 14+; the 32-byte +// in which case it targets that token's own shielded pool (protocol version 15+; the 32-byte // token id). Responses have the same shape for both pools. message GetShieldedEncryptedNotesRequest { message GetShieldedEncryptedNotesRequestV0 { diff --git a/packages/dapi-grpc/test/unit/shielded-requests.spec.js b/packages/dapi-grpc/test/unit/shielded-requests.spec.js new file mode 100644 index 00000000000..4268a74c344 --- /dev/null +++ b/packages/dapi-grpc/test/unit/shielded-requests.spec.js @@ -0,0 +1,51 @@ +const assert = require('node:assert/strict'); +const nodeMessages = require('../../clients/platform/v0/nodejs/platform_protoc'); +const webMessages = require('../../clients/platform/v0/web/platform_pb'); +const protobufMessages = require('../../clients/platform/v0/nodejs/platform_pbjs'); +const driveMessages = require('../../clients/drive/v0/nodejs/drive_pbjs'); + +const requestNames = [ + 'GetShieldedEncryptedNotesRequest', + 'GetShieldedAnchorsRequest', + 'GetMostRecentShieldedAnchorRequest', + 'GetShieldedPoolStateRequest', + 'GetShieldedNotesCountRequest', + 'GetShieldedNullifiersRequest', +]; + +describe('shielded query token selectors', () => { + requestNames.forEach((name) => { + describe(name, () => { + const NodeRequest = nodeMessages[name][`${name}V0`]; + const WebRequest = webMessages[name][`${name}V0`]; + const ProtobufRequest = protobufMessages.org.dash.platform.dapi.v0[name][`${name}V0`]; + const DriveRequest = driveMessages.org.dash.platform.dapi.v0[name][`${name}V0`]; + + [undefined, Buffer.alloc(32, 7)].forEach((tokenId) => { + it(`should preserve the ${tokenId ? 'token' : 'credit'} pool selector across clients`, () => { + const request = new NodeRequest(); + if (tokenId) { + request.setTokenId(tokenId); + } + const webRequest = WebRequest.deserializeBinary(request.serializeBinary()); + assert.equal(webRequest.hasTokenId(), Boolean(tokenId)); + if (tokenId) { + assert.deepEqual(Buffer.from(webRequest.getTokenId_asU8()), tokenId); + } + + const protobufRequest = ProtobufRequest.decode(webRequest.serializeBinary()); + assert.equal(Object.hasOwn(protobufRequest, 'tokenId'), Boolean(tokenId)); + const driveRequest = DriveRequest.decode(ProtobufRequest.encode(protobufRequest).finish()); + assert.equal(Object.hasOwn(driveRequest, 'tokenId'), Boolean(tokenId)); + const roundTrip = NodeRequest.deserializeBinary(DriveRequest.encode(driveRequest).finish()); + assert.equal(roundTrip.hasTokenId(), Boolean(tokenId)); + if (tokenId) { + assert.deepEqual(Buffer.from(roundTrip.getTokenId_asU8()), tokenId); + roundTrip.clearTokenId(); + assert.equal(roundTrip.hasTokenId(), false); + } + }); + }); + }); + }); +}); diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/mod.rs index a0a12019fbf..147905dbfde 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/mod.rs @@ -9,6 +9,7 @@ use platform_version::version::PlatformVersion; use std::collections::BTreeMap; mod v0; +mod v1; impl TokenConfiguration { pub fn validate_token_config_update( @@ -33,9 +34,16 @@ impl TokenConfiguration { action_taker, goal, )), + 1 => Ok(self.validate_token_config_update_v1( + new_config, + contract_owner_id, + groups, + action_taker, + goal, + )), version => Err(ProtocolError::UnknownVersionMismatch { method: "validate_token_config_update".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, }), } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/v0/mod.rs index 46bbd1503ce..97b3a1a9789 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/v0/mod.rs @@ -1,6 +1,5 @@ use crate::consensus::basic::data_contract::DataContractTokenConfigurationUpdateError; use crate::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; -use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters; use crate::data_contract::associated_token::token_configuration::TokenConfiguration; use crate::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; use crate::data_contract::associated_token::token_marketplace_rules::accessors::v0::TokenMarketplaceRulesV0Getters; @@ -449,20 +448,6 @@ impl TokenConfiguration { ); } - // The shielded pool opt-in is decided at creation: enabling it later would need the - // pool subtree created by the update, and a pool holding notes can never be removed. - if self.has_shielded_pool() != new_config.has_shielded_pool() { - return SimpleConsensusValidationResult::new_with_error( - DataContractTokenConfigurationUpdateError::new( - "update".to_string(), - "hasShieldedPool".to_string(), - self.clone(), - new_config.clone(), - ) - .into(), - ); - } - // If we reach here with no errors, return an empty result SimpleConsensusValidationResult::new() } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/v1/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/v1/mod.rs new file mode 100644 index 00000000000..529fccb1c8c --- /dev/null +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/v1/mod.rs @@ -0,0 +1,91 @@ +use crate::consensus::basic::data_contract::DataContractTokenConfigurationUpdateError; +use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters; +use crate::data_contract::associated_token::token_configuration::TokenConfiguration; +use crate::data_contract::group::Group; +use crate::data_contract::GroupContractPosition; +use crate::group::action_taker::{ActionGoal, ActionTaker}; +use crate::validation::SimpleConsensusValidationResult; +use platform_value::Identifier; +use std::collections::BTreeMap; + +impl TokenConfiguration { + pub(super) fn validate_token_config_update_v1( + &self, + new_config: &TokenConfiguration, + contract_owner_id: &Identifier, + groups: &BTreeMap, + action_taker: &ActionTaker, + goal: ActionGoal, + ) -> SimpleConsensusValidationResult { + let result = self.validate_token_config_update_v0( + new_config, + contract_owner_id, + groups, + action_taker, + goal, + ); + if !result.is_valid() { + return result; + } + // The shielded pool opt-in is decided at creation: enabling it later would need the + // pool subtree created by the update, and a pool holding notes can never be removed. + if self.has_shielded_pool() != new_config.has_shielded_pool() { + return SimpleConsensusValidationResult::new_with_error( + DataContractTokenConfigurationUpdateError::new( + "update".to_string(), + "hasShieldedPool".to_string(), + self.clone(), + new_config.clone(), + ) + .into(), + ); + } + + SimpleConsensusValidationResult::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters; + use crate::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; + use platform_version::version::PlatformVersion; + + #[test] + fn should_freeze_the_shielded_pool_opt_in_only_from_protocol_15() { + let owner = Identifier::from([1; 32]); + let groups = BTreeMap::new(); + let action_taker = ActionTaker::SingleIdentity(owner); + let plain = TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + let mut shielded = plain.clone(); + shielded.set_has_shielded_pool(true); + + for platform_version in [PlatformVersion::get(14).unwrap(), PlatformVersion::latest()] { + for (old, new) in [ + (&plain, &shielded), + (&shielded, &plain), + (&shielded, &shielded), + ] { + let result = old + .validate_token_config_update( + new, + &owner, + &groups, + &action_taker, + ActionGoal::ActionCompletion, + platform_version, + ) + .expect("validate token configuration update"); + assert_eq!( + result.is_valid(), + platform_version.protocol_version < 15 + || old.has_shielded_pool() == new.has_shielded_pool(), + "unexpected update validation on protocol {}: {:?}", + platform_version.protocol_version, + result.errors, + ); + } + } + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs index 5ab7dd1d669..7a32a9659f9 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs @@ -9,6 +9,8 @@ use dpp::state_transition::batch_transition::batched_transition::document_transi use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransition; use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRef; use dpp::state_transition::batch_transition::document_base_transition::v1::v1_methods::DocumentBaseTransitionV1Methods; +use dpp::state_transition::data_contract_create_transition::accessors::DataContractCreateTransitionAccessorsV0; +use dpp::state_transition::data_contract_update_transition::accessors::DataContractUpdateTransitionAccessorsV0; use dpp::state_transition::StateTransition; use dpp::tokens::token_payment_info::v1::v1_accessors::TokenPaymentInfoAccessorsV1; use dpp::version::feature_initial_protocol_versions::{ @@ -57,9 +59,19 @@ impl StateTransitionIsAllowedValidationV0 for StateTransition { | StateTransition::IdentityKeyLimitsUpdate(_) | StateTransition::ContractUserModeration(_) | StateTransition::ContractFeeClaim(_) => Ok(true), - StateTransition::DataContractCreate(_) - | StateTransition::DataContractUpdate(_) - | StateTransition::IdentityCreate(_) + // Newly decoded token formats need an unpaid activation check even while the + // older contract basic-structure generations remain frozen. + StateTransition::DataContractCreate(st) => Ok(st + .data_contract() + .tokens() + .values() + .any(|configuration| configuration.format_version() > 0)), + StateTransition::DataContractUpdate(st) => Ok(st + .data_contract() + .tokens() + .values() + .any(|configuration| configuration.format_version() > 0)), + StateTransition::IdentityCreate(_) | StateTransition::IdentityTopUp(_) | StateTransition::IdentityCreditWithdrawal(_) | StateTransition::IdentityUpdate(_) @@ -73,6 +85,20 @@ impl StateTransitionIsAllowedValidationV0 for StateTransition { platform: &PlatformRef, platform_version: &PlatformVersion, ) -> Result, Error> { + let contract = match self { + StateTransition::DataContractCreate(st) => Some(st.data_contract()), + StateTransition::DataContractUpdate(st) => Some(st.data_contract()), + _ => None, + }; + if let Some(contract) = contract { + for configuration in contract.tokens().values() { + let result = configuration.validate_format_version(platform_version); + if !result.is_valid() { + return Ok(result); + } + } + return Ok(ConsensusValidationResult::new()); + } match self { StateTransition::Batch(st) => { // Token shielded pools (the batch transitions that use them and a document token diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs index c002a3a77c1..f50a86b4fa7 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs @@ -995,6 +995,14 @@ fn validate_batch_token_shielded_proofs( ) } BatchedTransitionRef::Token(TokenTransition::BurnFromPool(t)) => { + if t.base() + .using_group_info() + .is_some_and(|info| !info.action_is_proposer) + { + // A confirmer reuses the proposer's bundle. Its burner identity comes + // from the stored group action, so state validation verifies this proof. + continue; + } let extra_sighash_data = dpp::shielded::token_burn_from_pool_extra_sighash_data( &t.base().token_id().to_buffer(), &owner_id, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 0fc4dc5c0b2..63db1666c32 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -13,7 +13,10 @@ mod token_shielded_pool_tests { use crate::platform_types::state_transitions_processing_result::StateTransitionsProcessingResult; use crate::rpc::core::MockCoreRPCLike; use crate::test::helpers::setup::TempPlatform; + use dpp::data_contract::accessors::v0::DataContractV0Setters; + use dpp::data_contract::accessors::v1::DataContractV1Setters; use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters; + use dpp::data_contract::config::v0::DataContractConfigSettersV0; use dpp::data_contract::DataContract; use dpp::identity::accessors::IdentityGettersV0; use dpp::identity::identity_nonce::IDENTITY_NONCE_VALUE_FILTER; @@ -24,7 +27,10 @@ mod token_shielded_pool_tests { }; use dpp::state_transition::data_contract_create_transition::methods::DataContractCreateTransitionMethodsV0; use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; + use dpp::state_transition::data_contract_update_transition::methods::DataContractUpdateTransitionMethodsV0; + use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; use dpp::state_transition::StateTransition; + use dpp::tests::fixtures::get_data_contract_fixture; use dpp::tests::json_document::json_document_to_contract_with_ids; use dpp::version::feature_initial_protocol_versions::TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION; use drive::drive::shielded::paths::token_shielded_pool_anchors_path_vec; @@ -1075,6 +1081,110 @@ mod token_shielded_pool_tests { ); } + #[tokio::test] + async fn should_gate_new_shielded_tokens_and_validate_their_rules_on_contract_update() { + for (platform_version, incompatible_rules) in [ + (PlatformVersion::get(14).unwrap(), false), + (PlatformVersion::latest(), true), + (PlatformVersion::latest(), false), + ] { + let mut platform = TestPlatformBuilder::new() + .with_initial_protocol_version(platform_version.protocol_version) + .build_with_mock_rpc() + .set_genesis_state(); + let (identity, signer, key) = + setup_identity(&mut platform, 9012, dash_to_credits!(1.0)); + let mut contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + contract.set_owner_id(identity.id()); + contract.config_mut().set_readonly(false); + platform + .drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("store original contract"); + + let token_contract = shielded_token_contract(identity.id(), platform_version); + let mut configuration = token_contract + .expected_token_configuration(0) + .expect("shielded token configuration") + .clone(); + if incompatible_rules { + configuration.set_freeze_rules(ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::ContractOwner, + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + })); + } + contract.add_token(0, configuration); + contract.increment_version(); + let token_id = contract.token_id(0).expect("token id"); + let update = DataContractUpdateTransition::new_from_data_contract( + contract, + &identity.into_partial_identity_info(), + key.id(), + 1, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("contract update transition"); + let state = platform.state.load(); + let transaction = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &[update.serialize_to_bytes().expect("serialize update")], + &state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("process contract update"); + + if platform_version.protocol_version < 15 { + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::BasicError(BasicError::UnsupportedVersionError(_)) + )] + ); + } else if incompatible_rules { + assert_matches!(result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::BasicError(BasicError::TokenShieldedPoolIncompatibleRulesError(error)) + )] if error.token_contract_position() == 0 && error.rule() == "freezeRules"); + } else { + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert!(platform + .drive + .has_token_shielded_pool( + token_id.to_buffer(), + Some(&transaction), + &mut vec![], + platform_version, + ) + .expect("new token pool must exist")); + } + } + } + /// `hasShieldedPool` is a format-version-1 field, so clients see it in the contract JSON /// and it survives the round trip. #[test] @@ -1102,6 +1212,9 @@ mod token_pool_mint_burn_claim_purchase_tests { OWNER_INITIAL_BALANCE, }; use super::*; + use crate::execution::check_tx::CheckTxLevel; + use crate::execution::validation::state_transition::processor::traits::shielded_proof::StateTransitionShieldedProofValidationV0; + use crate::platform_types::platform::PlatformRef; use crate::rpc::core::MockCoreRPCLike; use crate::test::helpers::fast_forward_to_block::fast_forward_to_block; use crate::test::helpers::setup::TempPlatform; @@ -1118,9 +1231,38 @@ mod token_pool_mint_burn_claim_purchase_tests { use dpp::state_transition::batch_transition::{ TokenBurnFromPoolTransition, TokenSetPriceForDirectPurchaseTransition, }; + use dpp::state_transition::StateTransition; use dpp::tokens::token_pricing_schedule::TokenPricingSchedule; use platform_version::version::PlatformVersion; + fn assert_check_tx_accepts( + platform: &TempPlatform, + transition: &StateTransition, + ) { + let state = platform.state.load(); + let platform_ref = PlatformRef { + drive: &platform.drive, + state: &state, + config: &platform.config, + core_rpc: &platform.core_rpc, + }; + let result = platform + .check_tx( + &transition + .serialize_to_bytes() + .expect("serialize transition"), + CheckTxLevel::FirstTimeCheck, + &platform_ref, + PlatformVersion::latest(), + ) + .expect("check tx"); + assert!( + result.is_valid(), + "unexpected CheckTx errors: {:?}", + result.errors + ); + } + fn total_supply(platform: &TempPlatform, token_id: Identifier) -> u64 { platform .drive @@ -1512,6 +1654,7 @@ mod token_pool_mint_burn_claim_purchase_tests { ) .await .expect("token burn from pool proposal"); + assert_check_tx_accepts(&platform, &proposal); let result = process(&platform, &proposal); assert_matches!( result.execution_results().as_slice(), @@ -1610,6 +1753,59 @@ mod token_pool_mint_burn_claim_purchase_tests { ) .await .expect("token burn from pool confirmation"); + assert_check_tx_accepts(&platform, &confirmation); + + // Stateless admission must defer a confirmer's proof, but block execution must + // still verify it after recovering the proposer. Corrupting only the binding + // signature preserves the group action's pinned actions digest. + let mut corrupted_bundle = burn_bundle.clone(); + corrupted_bundle.binding_signature[0] ^= 1; + let corrupted = BatchTransition::new_token_burn_from_pool_transition( + token_id, + confirmer.id(), + contract.id(), + 0, + burn_amount, + corrupted_bundle, + None, + as_other_signer(), + &confirmer_key, + 3, + 0, + &confirmer_signer, + platform_version, + None, + ) + .await + .expect("confirmation with an invalid binding signature"); + assert!(corrupted + .validate_shielded_proof(platform_version) + .expect("stateless proof check") + .is_valid()); + let state = platform.state.load(); + let transaction = platform.drive.grove.start_transaction(); + let rejected = platform + .platform + .process_raw_state_transitions( + &[corrupted + .serialize_to_bytes() + .expect("serialize corrupted confirmation")], + &state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("validate corrupted confirmation"); + assert_matches!( + rejected.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::InvalidShieldedProofError(_)), + .. + }] + ); + drop(transaction); let result = process(&platform, &confirmation); assert_matches!( result.execution_results().as_slice(), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/mod.rs index 4598692d1d6..1b391a4bcc9 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/mod.rs @@ -1,3 +1,4 @@ pub(crate) mod v0; pub(crate) mod v1; pub(crate) mod v2; +pub(crate) mod v3; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs index b5aebe758d7..8cee70813d9 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v0/mod.rs @@ -73,25 +73,6 @@ impl DataContractCreateStateTransitionBasicStructureValidationV0 for DataContrac )); } - // A newer configuration format than this protocol version admits is a consensus - - // error so that nodes already running newer software agree with the rest of the - - // network until the version that introduces the format activates. - - let validation_result = token_configuration.validate_format_version(platform_version); - - if !validation_result.is_valid() { - return Ok(validation_result); - } - - let validation_result = - token_configuration.validate_shielded_pool_rules(*token_contract_position); - - if !validation_result.is_valid() { - return Ok(validation_result); - } - let validation_result = token_configuration .conventions() .validate_localizations(platform_version)?; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v3/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v3/mod.rs new file mode 100644 index 00000000000..5ecc107f64a --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v3/mod.rs @@ -0,0 +1,41 @@ +use crate::error::Error; +use dpp::dashcore::Network; +use dpp::state_transition::data_contract_create_transition::accessors::DataContractCreateTransitionAccessorsV0; +use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; + +use super::v2::DataContractCreateStateTransitionBasicStructureValidationV2; + +pub(in crate::execution::validation::state_transition::state_transitions::data_contract_create) trait DataContractCreateStateTransitionBasicStructureValidationV3 { + fn validate_basic_structure_v3( + &self, + network_type: Network, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl DataContractCreateStateTransitionBasicStructureValidationV3 for DataContractCreateTransition { + /// Protocol 15 retains the previous rules and validates token shielded pool opt-ins. + fn validate_basic_structure_v3( + &self, + network_type: Network, + platform_version: &PlatformVersion, + ) -> Result { + let result = self.validate_basic_structure_v2(network_type, platform_version)?; + if !result.is_valid() { + return Ok(result); + } + for (position, configuration) in self.data_contract().tokens() { + let result = configuration.validate_format_version(platform_version); + if !result.is_valid() { + return Ok(result); + } + let result = configuration.validate_shielded_pool_rules(*position); + if !result.is_valid() { + return Ok(result); + } + } + Ok(SimpleConsensusValidationResult::new()) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs index 3721ab3896c..bfa4b8b5b94 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs @@ -9,6 +9,7 @@ use advanced_structure::v1::DataContractCreatedStateTransitionAdvancedStructureV use basic_structure::v0::DataContractCreateStateTransitionBasicStructureValidationV0; use basic_structure::v1::DataContractCreateStateTransitionBasicStructureValidationV1; use basic_structure::v2::DataContractCreateStateTransitionBasicStructureValidationV2; +use basic_structure::v3::DataContractCreateStateTransitionBasicStructureValidationV3; use dpp::address_funds::PlatformAddress; use dpp::block::block_info::BlockInfo; use dpp::dashcore::Network; @@ -103,14 +104,15 @@ impl StateTransitionBasicStructureValidationV0 for DataContractCreateTransition Some(0) => self.validate_basic_structure_v0(network_type, platform_version), Some(1) => self.validate_basic_structure_v1(network_type, platform_version), Some(2) => self.validate_basic_structure_v2(network_type, platform_version), + Some(3) => self.validate_basic_structure_v3(network_type, platform_version), Some(version) => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "data contract create transition: validate_basic_structure".to_string(), - known_versions: vec![0, 1, 2], + known_versions: vec![0, 1, 2, 3], received: version, })), None => Err(Error::Execution(ExecutionError::VersionNotActive { method: "data contract create transition: validate_basic_structure".to_string(), - known_versions: vec![0, 1, 2], + known_versions: vec![0, 1, 2, 3], })), } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/mod.rs index 4598692d1d6..1b391a4bcc9 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/mod.rs @@ -1,3 +1,4 @@ pub(crate) mod v0; pub(crate) mod v1; pub(crate) mod v2; +pub(crate) mod v3; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v0/mod.rs index e19d7bc469d..0f95b20d133 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v0/mod.rs @@ -58,25 +58,6 @@ impl DataContractUpdateStateTransitionBasicStructureValidationV0 for DataContrac )); } - // A newer configuration format than this protocol version admits is a consensus - - // error so that nodes already running newer software agree with the rest of the - - // network until the version that introduces the format activates. - - let validation_result = token_configuration.validate_format_version(platform_version); - - if !validation_result.is_valid() { - return Ok(validation_result); - } - - let validation_result = - token_configuration.validate_shielded_pool_rules(*token_contract_position); - - if !validation_result.is_valid() { - return Ok(validation_result); - } - let validation_result = token_configuration .conventions() .validate_localizations(platform_version)?; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v3/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v3/mod.rs new file mode 100644 index 00000000000..331feac889e --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v3/mod.rs @@ -0,0 +1,41 @@ +use crate::error::Error; +use dpp::dashcore::Network; +use dpp::state_transition::data_contract_update_transition::accessors::DataContractUpdateTransitionAccessorsV0; +use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; +use dpp::validation::SimpleConsensusValidationResult; +use dpp::version::PlatformVersion; + +use super::v2::DataContractUpdateStateTransitionBasicStructureValidationV2; + +pub(in crate::execution::validation::state_transition::state_transitions::data_contract_update) trait DataContractUpdateStateTransitionBasicStructureValidationV3 { + fn validate_basic_structure_v3( + &self, + network_type: Network, + platform_version: &PlatformVersion, + ) -> Result; +} + +impl DataContractUpdateStateTransitionBasicStructureValidationV3 for DataContractUpdateTransition { + /// Protocol 15 retains the previous rules and validates token shielded pool opt-ins. + fn validate_basic_structure_v3( + &self, + network_type: Network, + platform_version: &PlatformVersion, + ) -> Result { + let result = self.validate_basic_structure_v2(network_type, platform_version)?; + if !result.is_valid() { + return Ok(result); + } + for (position, configuration) in self.data_contract().tokens() { + let result = configuration.validate_format_version(platform_version); + if !result.is_valid() { + return Ok(result); + } + let result = configuration.validate_shielded_pool_rules(*position); + if !result.is_valid() { + return Ok(result); + } + } + Ok(SimpleConsensusValidationResult::new()) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs index 9e004d3034c..5259902e010 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs @@ -5,6 +5,7 @@ mod state; use basic_structure::v0::DataContractUpdateStateTransitionBasicStructureValidationV0; use basic_structure::v1::DataContractUpdateStateTransitionBasicStructureValidationV1; use basic_structure::v2::DataContractUpdateStateTransitionBasicStructureValidationV2; +use basic_structure::v3::DataContractUpdateStateTransitionBasicStructureValidationV3; use dpp::address_funds::PlatformAddress; use dpp::block::block_info::BlockInfo; use dpp::dashcore::Network; @@ -48,9 +49,10 @@ impl StateTransitionBasicStructureValidationV0 for DataContractUpdateTransition Some(0) => self.validate_basic_structure_v0(network_type, platform_version), Some(1) => self.validate_basic_structure_v1(network_type, platform_version), Some(2) => self.validate_basic_structure_v2(network_type, platform_version), + Some(3) => self.validate_basic_structure_v3(network_type, platform_version), Some(version) => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "data contract update transition: validate_basic_structure".to_string(), - known_versions: vec![0, 1, 2], + known_versions: vec![0, 1, 2, 3], received: version, })), None => Err(Error::Execution(ExecutionError::VersionNotActive { diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/mod.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/mod.rs index 5bc1d9b3133..55c7537efce 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/mod.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/mod.rs @@ -1,6 +1,7 @@ mod v0; mod v1; mod v2; +mod v3; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -59,9 +60,12 @@ impl Drive { 2 => { self.insert_contract_v2(contract, block_info, apply, transaction, platform_version) } + 3 => { + self.insert_contract_v3(contract, block_info, apply, transaction, platform_version) + } version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "insert_contract".to_string(), - known_versions: vec![0, 1, 2], + known_versions: vec![0, 1, 2, 3], received: version, })), } @@ -131,9 +135,18 @@ impl Drive { transaction, platform_version, ), + 3 => self.insert_contract_add_operations_v3( + contract_element, + contract, + block_info, + estimated_costs_only_with_layer_info, + drive_operations, + transaction, + platform_version, + ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "insert_contract_add_operations".to_string(), - known_versions: vec![0, 1, 2], + known_versions: vec![0, 1, 2, 3], received: version, })), } diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v1/mod.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v1/mod.rs index c729cccd9aa..afbc243b51f 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/v1/mod.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v1/mod.rs @@ -19,7 +19,6 @@ use crate::util::object_size_info::PathKeyElementInfo::PathKeyElement; use crate::util::object_size_info::{DriveKeyInfo, PathKeyElementInfo}; use dpp::data_contract::accessors::v1::DataContractV1Getters; use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; -use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters; use dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; use dpp::data_contract::associated_token::token_distribution_rules::accessors::v1::TokenDistributionRulesV1Getters; use dpp::serialization::{PlatformSerializable, PlatformSerializableWithPlatformVersion}; @@ -263,16 +262,6 @@ impl Drive { )?; } - if token_config.has_shielded_pool() { - batch_operations.extend(self.create_token_shielded_pool_trees_operations( - token_id_bytes, - false, - estimated_costs_only_with_layer_info, - transaction, - platform_version, - )?); - } - if token_config.start_as_paused() { // no status also means active. let starting_status = TokenStatus::new(true, platform_version)?; diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v3/mod.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v3/mod.rs new file mode 100644 index 00000000000..744bb9484db --- /dev/null +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v3/mod.rs @@ -0,0 +1,128 @@ +use crate::drive::Drive; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use dpp::block::block_info::BlockInfo; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::accessors::v1::DataContractV1Getters; +use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters; +use dpp::data_contract::DataContract; +use dpp::fee::fee_result::FeeResult; +use dpp::tokens::calculate_token_id; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::{Element, EstimatedLayerInformation, TransactionArg}; +use std::collections::HashMap; + +impl Drive { + /// Protocol 15 contract insertion, including shielded pools for opted-in tokens. + #[inline(always)] + pub(super) fn insert_contract_v3( + &self, + contract: &DataContract, + block_info: BlockInfo, + apply: bool, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result { + let mut drive_operations: Vec = vec![]; + + let contract_element = + Self::contract_element_for_insert_v1(contract, &block_info, platform_version)?; + + let mut estimated_costs_only_with_layer_info = if apply { + None::> + } else { + Some(HashMap::new()) + }; + let batch_operations = self.insert_contract_operations_v3( + contract_element, + contract, + &block_info, + &mut estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?; + self.apply_batch_low_level_drive_operations( + estimated_costs_only_with_layer_info, + transaction, + batch_operations, + &mut drive_operations, + &platform_version.drive, + )?; + + Drive::calculate_fee( + None, + Some(drive_operations), + &block_info.epoch, + self.config.epochs_per_era, + platform_version, + None, + ) + } + + /// The operations for adding a contract, appended to `drive_operations`. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + pub(super) fn insert_contract_add_operations_v3( + &self, + contract_element: Element, + contract: &DataContract, + block_info: &BlockInfo, + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + drive_operations: &mut Vec, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let batch_operations = self.insert_contract_operations_v3( + contract_element, + contract, + block_info, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?; + drive_operations.extend(batch_operations); + Ok(()) + } + + /// Protocol 14 operations with token pool trees for opted-in tokens. + fn insert_contract_operations_v3( + &self, + contract_element: Element, + contract: &DataContract, + block_info: &BlockInfo, + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let mut batch_operations: Vec = vec![]; + self.insert_contract_add_operations_v2( + contract_element, + contract, + block_info, + estimated_costs_only_with_layer_info, + &mut batch_operations, + transaction, + platform_version, + )?; + + for (position, configuration) in contract.tokens() { + if configuration.has_shielded_pool() { + let token_id = calculate_token_id(contract.id().as_bytes(), *position); + batch_operations.extend(self.create_token_shielded_pool_trees_operations( + token_id, + false, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?); + } + } + + Ok(batch_operations) + } +} diff --git a/packages/rs-drive/src/drive/contract/update/update_contract/mod.rs b/packages/rs-drive/src/drive/contract/update/update_contract/mod.rs index 5b838566042..d78150b7fd1 100644 --- a/packages/rs-drive/src/drive/contract/update/update_contract/mod.rs +++ b/packages/rs-drive/src/drive/contract/update/update_contract/mod.rs @@ -1,6 +1,7 @@ mod v0; mod v1; mod v2; +mod v3; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -82,9 +83,17 @@ impl Drive { platform_version, previous_fee_versions, ), + 3 => self.update_contract_v3( + contract, + block_info, + apply, + transaction, + previous_fee_versions, + platform_version, + ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "update_contract".to_string(), - known_versions: vec![0, 1, 2], + known_versions: vec![0, 1, 2, 3], received: version, })), } @@ -162,9 +171,18 @@ impl Drive { drive_operations, platform_version, ), + 3 => self.update_contract_element_v3( + contract_element, + contract, + original_contract, + block_info, + transaction, + drive_operations, + platform_version, + ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "update_contract_element".to_string(), - known_versions: vec![0, 1, 2], + known_versions: vec![0, 1, 2, 3], received: version, })), } @@ -250,9 +268,19 @@ impl Drive { drive_operations, platform_version, ), + 3 => self.update_contract_add_operations_v3( + contract_element, + contract, + original_contract, + block_info, + estimated_costs_only_with_layer_info, + transaction, + drive_operations, + platform_version, + ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "update_contract_add_operations".to_string(), - known_versions: vec![0, 1, 2], + known_versions: vec![0, 1, 2, 3], received: version, })), } diff --git a/packages/rs-drive/src/drive/contract/update/update_contract/v1/mod.rs b/packages/rs-drive/src/drive/contract/update/update_contract/v1/mod.rs index dc6a9187bb5..c19c1d5ce3f 100644 --- a/packages/rs-drive/src/drive/contract/update/update_contract/v1/mod.rs +++ b/packages/rs-drive/src/drive/contract/update/update_contract/v1/mod.rs @@ -14,7 +14,6 @@ use dpp::serialization::PlatformSerializableWithPlatformVersion; use crate::error::contract::DataContractError; use dpp::data_contract::accessors::v1::DataContractV1Getters; use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; -use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters; use dpp::fee::default_costs::CachedEpochIndexFeeVersions; use dpp::version::PlatformVersion; use grovedb::batch::KeyInfoPath; @@ -242,16 +241,6 @@ impl Drive { transaction, platform_version, )?); - - if configuration.has_shielded_pool() { - batch_operations.extend(self.create_token_shielded_pool_trees_operations( - token_id.to_buffer(), - true, - estimated_costs_only_with_layer_info, - transaction, - platform_version, - )?); - } } if !contract.groups().is_empty() { diff --git a/packages/rs-drive/src/drive/contract/update/update_contract/v3/mod.rs b/packages/rs-drive/src/drive/contract/update/update_contract/v3/mod.rs new file mode 100644 index 00000000000..103594d7add --- /dev/null +++ b/packages/rs-drive/src/drive/contract/update/update_contract/v3/mod.rs @@ -0,0 +1,324 @@ +use crate::drive::Drive; +use crate::error::drive::DriveError; +use crate::error::Error; +use crate::fees::op::LowLevelDriveOperation; +use crate::util::storage_flags::StorageFlags; +use dpp::block::block_info::BlockInfo; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::accessors::v1::DataContractV1Getters; +use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters; +use dpp::data_contract::config::v0::DataContractConfigGettersV0; +use dpp::data_contract::DataContract; +use dpp::fee::default_costs::CachedEpochIndexFeeVersions; +use dpp::fee::fee_result::FeeResult; +use dpp::serialization::PlatformSerializableWithPlatformVersion; +use dpp::tokens::calculate_token_id; +use dpp::version::PlatformVersion; +use grovedb::batch::KeyInfoPath; +use grovedb::{Element, EstimatedLayerInformation, TransactionArg}; +use std::collections::HashMap; + +impl Drive { + /// Protocol 15 contract updates, including pools for newly added shielded tokens. + #[inline(always)] + pub(super) fn update_contract_v3( + &self, + contract: &DataContract, + block_info: BlockInfo, + apply: bool, + transaction: TransactionArg, + previous_fee_versions: Option<&CachedEpochIndexFeeVersions>, + platform_version: &PlatformVersion, + ) -> Result { + if !apply { + return self.insert_contract( + contract, + block_info, + false, + transaction, + platform_version, + ); + } + + let mut drive_operations: Vec = vec![]; + + let contract_bytes = contract.serialize_to_bytes_with_platform_version(platform_version)?; + + // Since we can update the contract by definition it already has storage flags + let storage_flags = Some(StorageFlags::new_single_epoch( + block_info.epoch.index, + Some(contract.owner_id().to_buffer()), + )); + + let contract_element = Element::Item( + contract_bytes, + StorageFlags::map_to_some_element_flags(storage_flags.as_ref()), + ); + + let original_contract_fetch_info = self + .get_contract_with_fetch_info_and_add_to_operations( + contract.id().to_buffer(), + Some(&block_info.epoch), + true, + transaction, + &mut drive_operations, + platform_version, + )? + .ok_or(Error::Drive(DriveError::CorruptedCodeExecution( + "contract should exist", + )))?; + + if original_contract_fetch_info.contract.config().readonly() { + return Err(Error::Drive(DriveError::UpdatingReadOnlyImmutableContract( + "original contract is readonly", + ))); + } + + self.update_contract_element_v3( + contract_element, + contract, + &original_contract_fetch_info.contract, + &block_info, + transaction, + &mut drive_operations, + platform_version, + )?; + + // Update DataContracts cache with the new contract + let updated_contract_fetch_info = self + .fetch_contract_and_add_operations( + contract.id().to_buffer(), + Some(&block_info.epoch), + transaction, + &mut drive_operations, + platform_version, + )? + .ok_or(Error::Drive(DriveError::CorruptedCodeExecution( + "contract should exist", + )))?; + + self.cache + .data_contracts + .insert_rewritten(updated_contract_fetch_info, transaction.is_some()); + + Drive::calculate_fee( + None, + Some(drive_operations), + &block_info.epoch, + self.config.epochs_per_era, + platform_version, + previous_fee_versions, + ) + } + + /// Updates a contract. + #[inline(always)] + #[allow(clippy::too_many_arguments)] + pub(super) fn update_contract_element_v3( + &self, + contract_element: Element, + contract: &DataContract, + original_contract: &DataContract, + block_info: &BlockInfo, + transaction: TransactionArg, + drive_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let mut estimated_costs_only_with_layer_info = + None::>; + let batch_operations = self.update_contract_operations_v3( + contract_element, + contract, + original_contract, + block_info, + &mut estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?; + self.apply_batch_low_level_drive_operations( + estimated_costs_only_with_layer_info, + transaction, + batch_operations, + drive_operations, + &platform_version.drive, + ) + } + + /// Updates a contract. + #[allow(clippy::too_many_arguments)] + #[inline(always)] + pub(super) fn update_contract_add_operations_v3( + &self, + contract_element: Element, + contract: &DataContract, + original_contract: &DataContract, + block_info: &BlockInfo, + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + drive_operations: &mut Vec, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let batch_operations = self.update_contract_operations_v3( + contract_element, + contract, + original_contract, + block_info, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?; + drive_operations.extend(batch_operations); + Ok(()) + } + + /// Protocol 14 update operations plus shielded pools for newly added tokens. + #[allow(clippy::too_many_arguments)] + fn update_contract_operations_v3( + &self, + contract_element: Element, + contract: &DataContract, + original_contract: &DataContract, + block_info: &BlockInfo, + estimated_costs_only_with_layer_info: &mut Option< + HashMap, + >, + transaction: TransactionArg, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let mut batch_operations = vec![]; + self.update_contract_add_operations_v2( + contract_element, + contract, + original_contract, + block_info, + estimated_costs_only_with_layer_info, + transaction, + &mut batch_operations, + platform_version, + )?; + + for (position, configuration) in contract.tokens() { + if original_contract.tokens().contains_key(position) + || !configuration.has_shielded_pool() + { + continue; + } + let token_id = calculate_token_id(contract.id().as_bytes(), *position); + batch_operations.extend(self.create_token_shielded_pool_trees_operations( + token_id, + true, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?); + } + Ok(batch_operations) + } +} + +#[cfg(test)] +mod tests { + use crate::util::storage_flags::StorageFlags; + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use dpp::block::block_info::BlockInfo; + use dpp::data_contract::accessors::v0::{DataContractV0Getters, DataContractV0Setters}; + use dpp::data_contract::accessors::v1::{DataContractV1Getters, DataContractV1Setters}; + use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters; + use dpp::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; + use dpp::data_contract::associated_token::token_configuration::TokenConfiguration; + use dpp::data_contract::config::v0::DataContractConfigSettersV0; + use dpp::tests::fixtures::get_dashpay_contract_fixture; + use dpp::version::PlatformVersion; + use std::collections::BTreeMap; + + #[test] + fn should_create_token_pools_only_from_protocol_15_for_registration_and_updates() { + for platform_version in [PlatformVersion::get(14).unwrap(), PlatformVersion::latest()] { + for add_by_update in [false, true] { + let drive = setup_drive_with_initial_state_structure(None); + let mut contract = + get_dashpay_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + contract.config_mut().set_readonly(false); + + if add_by_update { + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("insert initial contract without tokens"); + contract.increment_version(); + } + + let mut configuration = TokenConfiguration::V0( + TokenConfigurationV0::default_most_restrictive().with_base_supply(100), + ); + configuration.set_has_shielded_pool(true); + contract.set_tokens(BTreeMap::from([(0, configuration)])); + if add_by_update { + drive + .update_contract( + &contract, + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("add token through contract update"); + } else { + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("register token contract"); + } + + let token_id = contract.token_id(0).expect("token id").to_buffer(); + assert_eq!( + drive + .has_token_shielded_pool(token_id, None, &mut vec![], platform_version) + .expect("check token pool"), + platform_version.protocol_version >= 15, + ); + assert_eq!( + drive + .fetch_token_total_supply(token_id, None, platform_version) + .expect("read total supply"), + Some(100) + ); + + // Updating an existing token must not recreate its pool or mint its supply again. + contract.increment_version(); + drive + .update_contract( + &contract, + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("update existing token without recreating storage"); + assert_eq!( + drive + .fetch_token_total_supply(token_id, None, platform_version) + .expect("read unchanged total supply"), + Some(100) + ); + } + } + } +} diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/mod.rs index 52c521e4309..c16174c22a6 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/mod.rs @@ -5,6 +5,7 @@ pub mod v2; pub mod v3; pub mod v4; pub mod v5; +pub mod v6; #[derive(Clone, Debug, Default)] pub struct DPPValidationVersions { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v6.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v6.rs new file mode 100644 index 00000000000..79460a1a00c --- /dev/null +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v6.rs @@ -0,0 +1,11 @@ +use super::v5::DPP_VALIDATION_VERSIONS_V5; +use super::{DPPValidationVersions, DataContractValidationVersions}; + +/// Protocol 15 makes the token shielded pool opt-in immutable after creation. +pub const DPP_VALIDATION_VERSIONS_V6: DPPValidationVersions = DPPValidationVersions { + data_contract: DataContractValidationVersions { + validate_token_config_update: 1, + ..DPP_VALIDATION_VERSIONS_V5.data_contract + }, + ..DPP_VALIDATION_VERSIONS_V5 +}; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs index 1679726214e..a13f97d443b 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs @@ -8,6 +8,18 @@ use super::{ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V11: DriveAbciValidationVersions = DriveAbciValidationVersions { state_transitions: DriveAbciStateTransitionValidationVersions { + contract_create_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(3), + ..DRIVE_ABCI_VALIDATION_VERSIONS_V10 + .state_transitions + .contract_create_state_transition + }, + contract_update_state_transition: DriveAbciStateTransitionValidationVersion { + basic_structure: Some(3), + ..DRIVE_ABCI_VALIDATION_VERSIONS_V10 + .state_transitions + .contract_update_state_transition + }, batch_state_transition: DriveAbciDocumentsStateTransitionValidationVersions { document_base_transition_state_validation: 1, ..DRIVE_ABCI_VALIDATION_VERSIONS_V10 diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/mod.rs index b7a25f8c676..ba6c2368c1a 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/mod.rs @@ -4,6 +4,7 @@ pub mod v1; pub mod v2; pub mod v3; pub mod v4; +pub mod v5; #[derive(Clone, Debug, Default)] pub struct DriveContractMethodVersions { diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v5.rs b/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v5.rs new file mode 100644 index 00000000000..781c2786261 --- /dev/null +++ b/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v5.rs @@ -0,0 +1,19 @@ +use super::v4::DRIVE_CONTRACT_METHOD_VERSIONS_V4; +use super::{ + DriveContractInsertMethodVersions, DriveContractMethodVersions, + DriveContractUpdateMethodVersions, +}; + +/// Protocol 15 creates shielded pool storage for opted-in tokens at creation and update. +pub const DRIVE_CONTRACT_METHOD_VERSIONS_V5: DriveContractMethodVersions = + DriveContractMethodVersions { + insert: DriveContractInsertMethodVersions { + insert_contract: 3, + ..DRIVE_CONTRACT_METHOD_VERSIONS_V4.insert + }, + update: DriveContractUpdateMethodVersions { + update_contract: 3, + ..DRIVE_CONTRACT_METHOD_VERSIONS_V4.update + }, + ..DRIVE_CONTRACT_METHOD_VERSIONS_V4 + }; diff --git a/packages/rs-platform-version/src/version/drive_versions/v10.rs b/packages/rs-platform-version/src/version/drive_versions/v10.rs index 47ee3ba61c3..cfef672d806 100644 --- a/packages/rs-platform-version/src/version/drive_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_versions/v10.rs @@ -1,3 +1,4 @@ +use super::drive_contract_method_versions::v5::DRIVE_CONTRACT_METHOD_VERSIONS_V5; use super::drive_token_method_versions::v3::DRIVE_TOKEN_METHOD_VERSIONS_V3; use super::v9::DRIVE_VERSION_V9; use super::{DriveInitializationMethodVersions, DriveMethodVersions, DriveVersion}; @@ -9,6 +10,7 @@ pub const DRIVE_VERSION_V10: DriveVersion = DriveVersion { create_initial_state_structure: 5, }, token: DRIVE_TOKEN_METHOD_VERSIONS_V3, + contract: DRIVE_CONTRACT_METHOD_VERSIONS_V5, ..DRIVE_VERSION_V9.methods }, ..DRIVE_VERSION_V9 diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs index 503558e78ef..a6025703d02 100644 --- a/packages/rs-platform-version/src/version/v15.rs +++ b/packages/rs-platform-version/src/version/v15.rs @@ -1,4 +1,5 @@ use super::dpp_versions::dpp_contract_versions::v7::CONTRACT_VERSIONS_V7; +use super::dpp_versions::dpp_validation_versions::v6::DPP_VALIDATION_VERSIONS_V6; use super::dpp_versions::DPPVersion; use super::drive_abci_versions::drive_abci_method_versions::v11::DRIVE_ABCI_METHOD_VERSIONS_V11; use super::drive_abci_versions::drive_abci_validation_versions::v11::DRIVE_ABCI_VALIDATION_VERSIONS_V11; @@ -46,6 +47,7 @@ pub const PLATFORM_V15: PlatformVersion = PlatformVersion { }, dpp: DPPVersion { contract_versions: CONTRACT_VERSIONS_V7, + validation: DPP_VALIDATION_VERSIONS_V6, ..PLATFORM_V14.dpp }, ..PLATFORM_V14 From a4ee83d87699bf9cbb5133f13df19e32abdea7dd Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 21 Sep 2026 21:39:03 +0700 Subject: [PATCH 12/94] fix(platform)!: pin mint-to-pool destinations, drop pool frozen checks and tidy the pool validators TokenMintToPool is refused where the configuration forbids the minter to choose the destination of minted tokens, since the notes' recipients are the minter's choice; a perpetual claim into the pool must name the moment it claims up to; the identity-less purchase treats a missing supply item as a corrupted state like its batch counterparts. The pool validators no longer read and bill a frozen-account check a pooled token cannot fail. CheckTx is asserted on every sighash-binding batch transition in the drive-abci tests, with the admission helper shared by the test modules. The identity-less anchor and nullifier checks reuse the batch helpers, the three credit-pool fee lowerings share one helper, inline module paths become imports, the document base transformer binds the token id once instead of unwrapping it, and the v15 doc names the burner rule. Co-Authored-By: Claude Fable 5.1 --- book/src/data-model/token-shielded-pools.md | 20 +- .../v0/v0_methods.rs | 4 +- .../v0/v0_methods.rs | 4 +- .../processor/traits/shielded_proof.rs | 35 ++-- .../state_v0/mod.rs | 16 ++ .../state_v0/mod.rs | 23 +- .../token/token_shielded_pool_common/mod.rs | 46 +--- .../state_v0/mod.rs | 28 +-- .../state_transitions/batch/mod.rs | 2 +- .../batch/tests/token/shielded/mod.rs | 198 +++++++++--------- .../token_pool_paid_common/mod.rs | 44 ++-- .../transform_into_action/v0/mod.rs | 18 +- .../transform_into_action/v0/mod.rs | 31 +-- .../shielded/mod.rs | 26 +++ ..._purchase_from_shielded_pool_transition.rs | 20 +- ...d_transfer_with_shielded_fee_transition.rs | 20 +- ...n_unshield_with_shielded_fee_transition.rs | 20 +- .../v0/transformer.rs | 19 +- .../v0/transformer.rs | 31 +++ .../rs-platform-version/src/version/v15.rs | 5 +- 20 files changed, 297 insertions(+), 313 deletions(-) diff --git a/book/src/data-model/token-shielded-pools.md b/book/src/data-model/token-shielded-pools.md index c22f4109e1c..350967179be 100644 --- a/book/src/data-model/token-shielded-pools.md +++ b/book/src/data-model/token-shielded-pools.md @@ -64,8 +64,8 @@ balances, a token with `hasShieldedPool` must disable them permanently: `freezeR have no admin action takers, so no later configuration update can switch them on. Contract create and update reject anything else with `TokenShieldedPoolIncompatibleRulesError` (10278). Pausing still works: shield, unshield and shielded transfer are all rejected while the token is -paused. The frozen-account checks in the shield and unshield validators remain as defence in -depth for state written before this rule. +paused. A pooled token can never freeze an account, so the pool validators read and bill no +frozen-account check. ## Batch transitions @@ -78,7 +78,7 @@ fees, so unlike the credit pool nothing is carved from the bundle's value balanc | `TokenShield` | outputs only | `-amount` | none | `amount` leaves the owner's balance and enters the pool as new notes. | | `TokenUnshield` | spends and outputs | `+amount` | `token_id, owner_id, recipient_id, amount` | Notes are spent; `amount` is credited to `recipient_id`; change comes back as new notes. | | `TokenShieldedTransfer` | spends and outputs | `0` | `token_id, owner_id` | Notes are spent and recreated; the pool balance is unchanged. | -| `TokenMintToPool` | outputs only | `-amount` | none | An authorized minter (manual minting rules, group actions supported) mints `amount` into new notes; the supply and the pool balance grow. | +| `TokenMintToPool` | outputs only | `-amount` | none | An authorized minter (manual minting rules, group actions supported) mints `amount` into new notes; the supply and the pool balance grow. Allowed only where `mintingAllowChoosingDestination` is set, since the notes' recipients are the minter's choice. | | `TokenBurnFromPool` | spends and outputs | `+amount` | `token_id, burner_id, amount` | An authorized burner (manual burning rules, group actions supported) spends notes and destroys `amount`; the supply and the pool balance shrink. `burner_id` is the batch owner, or the proposer of a group action. | | `TokenClaimToPool` | outputs only | `-amount` | none | A distribution claim released into new notes instead of the claimant's balance; a perpetual claim names the cycle-aligned moment it claims up to so the amount is predictable. | | `TokenDirectPurchaseToPool` | outputs only | `-token_count` | none | The buyer pays credits at the direct purchase price and the tokens are minted into new notes. | @@ -151,8 +151,8 @@ flat storage of what the transition writes outside the pools; a purchase adds th on top), both proofs are verified statelessly, and the transform validates the pools: the token owns a pool and is not paused, the token bundle's anchor is recorded and its nullifiers unspent in the token pool, the credit pool holds what leaves it and the fee bundle's anchor and -nullifiers check out there, plus the transition's own rules (recipient exists and is not frozen -for an unshield, the pricing schedule and the max supply for a purchase). Execution is a +nullifiers check out there, plus the transition's own rules (recipient exists for an +unshield, the pricing schedule and the max supply for a purchase). Execution is a `PaidFromShieldedPool` event: the fee goes to the fee pools, the token side is the matching token operation, and a purchase credits the contract owner. Uniqueness is by the spent nullifiers of both bundles; a replay is an unpaid rejection. CheckTx admits their proofs under @@ -171,11 +171,11 @@ State validation runs in this order, and the first failure is returned: 1. The token base transition (contract exists, position valid, nonce). 2. `hasShieldedPool` on the token's configuration, else `TokenShieldedPoolNotEnabledError`. -3. Shield: the owner holds `amount`, the owner's account is not frozen, the token is not - paused. Unshield: the token is not paused, the recipient identity exists, the recipient's - account is not frozen unless the token allows transfers to frozen balances. Shielded - transfer: the token is not paused. Mint to pool: the minting rules authorize the identity - (or group) and the max supply is not exceeded. Burn from pool: the burning rules authorize +3. Shield: the owner holds `amount` and the token is not paused. Unshield: the token is + not paused and the recipient identity exists. Shielded transfer: the token is not paused. + Mint to pool: the configuration lets the minter choose the destination + (`mintingAllowChoosingDestination`), the minting rules authorize the identity (or group) + and the max supply is not exceeded. Burn from pool: the burning rules authorize the identity and the token is not paused. Claim to pool: the claim resolves exactly as a claim into a balance does (the shared `resolve_token_claim`). Purchase to pool: the pricing schedule and the max supply. diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/v0/v0_methods.rs index a96d7d703fb..adb38185b92 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/v0/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_burn_from_pool_transition/v0/v0_methods.rs @@ -1,5 +1,5 @@ use crate::balances::credits::TokenAmount; -use crate::shielded::SerializedAction; +use crate::shielded::{serialized_actions_digest, SerializedAction}; use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; use crate::state_transition::batch_transition::token_burn_from_pool_transition::TokenBurnFromPoolTransition; @@ -87,7 +87,7 @@ impl AllowedAsMultiPartyAction for TokenBurnFromPoolTransitionV0 { owner_id.as_bytes(), self.base.identity_contract_nonce(), self.amount, - &crate::shielded::serialized_actions_digest(&self.actions), + &serialized_actions_digest(&self.actions), ), ) } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/v0/v0_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/v0/v0_methods.rs index 31c110815ac..7b166becc85 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/v0/v0_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_mint_to_pool_transition/v0/v0_methods.rs @@ -1,5 +1,5 @@ use crate::balances::credits::TokenAmount; -use crate::shielded::SerializedAction; +use crate::shielded::{serialized_actions_digest, SerializedAction}; use crate::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; use crate::state_transition::batch_transition::token_mint_to_pool_transition::TokenMintToPoolTransition; @@ -86,7 +86,7 @@ impl AllowedAsMultiPartyAction for TokenMintToPoolTransitionV0 { owner_id.as_bytes(), self.base.identity_contract_nonce(), self.amount, - &crate::shielded::serialized_actions_digest(&self.actions), + &serialized_actions_digest(&self.actions), )) } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs index f50a86b4fa7..f370fa63e79 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs @@ -28,6 +28,9 @@ use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransition use dpp::state_transition::batch_transition::batched_transition::token_transition::{ TokenTransition, TokenTransitionV0Methods, }; +use dpp::shielded::{ + TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE, TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE, compute_token_purchase_from_shielded_pool_fee, compute_token_shielded_transfer_with_shielded_fee_fee, compute_token_unshield_with_shielded_fee_fee, document_token_payment_extra_sighash_data, token_burn_from_pool_extra_sighash_data, token_pool_fee_bundle_extra_sighash_data, token_purchase_from_shielded_pool_extra_sighash_data, token_shielded_transfer_extra_sighash_data, token_shielded_transfer_with_shielded_fee_extra_sighash_data, token_unshield_extra_sighash_data, token_unshield_with_shielded_fee_extra_sighash_data, +}; use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRef; use dpp::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; use dpp::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; @@ -589,21 +592,21 @@ impl StateTransitionShieldedMinimumFeeValidationV0 for StateTransition { )? } ShieldedMinFeeKind::TokenShieldedTransfer { token_actions } => { - dpp::shielded::compute_token_shielded_transfer_with_shielded_fee_fee( + compute_token_shielded_transfer_with_shielded_fee_fee( token_actions, num_actions, platform_version, )? } ShieldedMinFeeKind::TokenUnshield { token_actions } => { - dpp::shielded::compute_token_unshield_with_shielded_fee_fee( + compute_token_unshield_with_shielded_fee_fee( token_actions, num_actions, platform_version, )? } ShieldedMinFeeKind::TokenPurchase { token_actions } => { - dpp::shielded::compute_token_purchase_from_shielded_pool_fee( + compute_token_purchase_from_shielded_pool_fee( token_actions, num_actions, platform_version, @@ -950,7 +953,7 @@ fn validate_batch_token_shielded_proofs( ) } BatchedTransitionRef::Token(TokenTransition::Unshield(t)) => { - let extra_sighash_data = dpp::shielded::token_unshield_extra_sighash_data( + let extra_sighash_data = token_unshield_extra_sighash_data( &t.base().token_id().to_buffer(), &owner_id, &t.recipient_id().to_buffer(), @@ -968,7 +971,7 @@ fn validate_batch_token_shielded_proofs( ) } BatchedTransitionRef::Token(TokenTransition::ShieldedTransfer(t)) => { - let extra_sighash_data = dpp::shielded::token_shielded_transfer_extra_sighash_data( + let extra_sighash_data = token_shielded_transfer_extra_sighash_data( &t.base().token_id().to_buffer(), &owner_id, platform_version, @@ -1003,7 +1006,7 @@ fn validate_batch_token_shielded_proofs( // from the stored group action, so state validation verifies this proof. continue; } - let extra_sighash_data = dpp::shielded::token_burn_from_pool_extra_sighash_data( + let extra_sighash_data = token_burn_from_pool_extra_sighash_data( &t.base().token_id().to_buffer(), &owner_id, t.amount(), @@ -1044,7 +1047,7 @@ fn validate_batch_token_shielded_proofs( let Some(payment) = token_payment_info.shielded_payment() else { continue; }; - let extra_sighash_data = dpp::shielded::document_token_payment_extra_sighash_data( + let extra_sighash_data = document_token_payment_extra_sighash_data( &token_payment_info .token_id(base.data_contract_id()) .to_buffer(), @@ -1183,13 +1186,13 @@ fn validate_shielded_proof_v2( }, StateTransition::TokenShieldedTransferWithShieldedFee(st) => match st { TokenShieldedTransferWithShieldedFeeTransition::V0(v0) => { - let token_extra_sighash_data = dpp::shielded::token_shielded_transfer_with_shielded_fee_extra_sighash_data( + let token_extra_sighash_data = token_shielded_transfer_with_shielded_fee_extra_sighash_data( &v0.token_id.to_buffer(), platform_version, )?; let fee_extra_sighash_data = - dpp::shielded::token_pool_fee_bundle_extra_sighash_data( - dpp::shielded::TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE, + token_pool_fee_bundle_extra_sighash_data( + TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE, &v0.token_id.to_buffer(), &v0.token_actions, platform_version, @@ -1218,15 +1221,15 @@ fn validate_shielded_proof_v2( }, StateTransition::TokenUnshieldWithShieldedFee(st) => match st { TokenUnshieldWithShieldedFeeTransition::V0(v0) => { - let token_extra_sighash_data = dpp::shielded::token_unshield_with_shielded_fee_extra_sighash_data( + let token_extra_sighash_data = token_unshield_with_shielded_fee_extra_sighash_data( &v0.token_id.to_buffer(), &v0.recipient_id.to_buffer(), v0.amount, platform_version, )?; let fee_extra_sighash_data = - dpp::shielded::token_pool_fee_bundle_extra_sighash_data( - dpp::shielded::TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE, + token_pool_fee_bundle_extra_sighash_data( + TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE, &v0.token_id.to_buffer(), &v0.token_actions, platform_version, @@ -1255,15 +1258,15 @@ fn validate_shielded_proof_v2( }, StateTransition::TokenPurchaseFromShieldedPool(st) => match st { TokenPurchaseFromShieldedPoolTransition::V0(v0) => { - let token_extra_sighash_data = dpp::shielded::token_purchase_from_shielded_pool_extra_sighash_data( + let token_extra_sighash_data = token_purchase_from_shielded_pool_extra_sighash_data( &v0.token_id.to_buffer(), v0.token_count, v0.total_agreed_price, platform_version, )?; let fee_extra_sighash_data = - dpp::shielded::token_pool_fee_bundle_extra_sighash_data( - dpp::shielded::TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, + token_pool_fee_bundle_extra_sighash_data( + TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, &v0.token_id.to_buffer(), &v0.token_actions, platform_version, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs index 9bf2a508ec2..0b8f500e2f6 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs @@ -14,6 +14,8 @@ use crate::execution::validation::state_transition::batch::action_validation::to validate_token_shielded_pool_enabled, verify_token_pool_bundle, }; use crate::execution::validation::state_transition::state_transitions::shielded_common::FLAGS_OUTPUTS_ONLY; +use dpp::consensus::basic::token::ChoosingTokenMintRecipientNotAllowedError; +use dpp::consensus::basic::BasicError; use dpp::consensus::state::group::ModificationOfGroupActionMainParametersNotPermittedError; use dpp::consensus::state::state_error::StateError; use dpp::consensus::state::token::TokenMintPastMaxSupplyError; @@ -21,6 +23,7 @@ use dpp::consensus::ConsensusError; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::accessors::v1::DataContractV1Getters; use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; use dpp::group::action_event::GroupActionEvent; use dpp::group::group_action::GroupActionAccessors; use dpp::shielded::serialized_actions_digest; @@ -80,6 +83,19 @@ impl TokenMintToPoolTransitionActionStateValidationV0 for TokenMintToPoolTransit let contract = &self.data_contract_fetch_info_ref().contract; let token_configuration = contract.expected_token_configuration(self.token_position())?; + // The notes' recipients are the minter's choice, so a mint into the pool is only + // allowed where the minter may choose the destination of a transparent mint. + if !token_configuration + .distribution_rules() + .minting_allow_choosing_destination() + { + return Ok(SimpleConsensusValidationResult::new_with_error( + BasicError::ChoosingTokenMintRecipientNotAllowedError( + ChoosingTokenMintRecipientNotAllowedError::new(token_id), + ) + .into(), + )); + } let rules = token_configuration.manual_minting_rules(); let main_control_group = token_configuration.main_control_group(); let validation_result = self.base().validate_group_action( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs index 424c9ace9a4..bf0e9d4c57c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs @@ -5,8 +5,7 @@ use crate::execution::types::state_transition_execution_context::{ }; use crate::execution::validation::state_transition::batch::action_validation::token::token_base_transition_action::TokenBaseTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_pool_common::{ - validate_identity_token_account_not_frozen, validate_token_not_paused, - validate_token_shielded_pool_enabled, verify_token_pool_bundle, + validate_token_not_paused, validate_token_shielded_pool_enabled, verify_token_pool_bundle, }; use crate::execution::validation::state_transition::state_transitions::shielded_common::FLAGS_OUTPUTS_ONLY; use crate::execution::validation::state_transition::ValidationMode; @@ -39,7 +38,11 @@ pub(in crate::execution::validation::state_transition::state_transitions::batch: impl TokenShieldTransitionActionStateValidationV0 for TokenShieldTransitionAction { /// Shielding is a transfer from the owner's balance into the pool, so it runs the sender - /// side of the transfer checks (balance, frozen account, paused token) on top of the pool + /// side of the transfer checks (balance, paused token) on top of the pool opt-in; a pooled + /// token can never freeze an account, so no frozen check is read or billed. It then + /// verifies the outputs-only bundle. + /// + /// Historical note: the pool /// opt-in, then verifies the outputs-only bundle. The bundle's anchor is not checked /// against the pool: with spends disabled the anchor is not consumed. fn validate_state_v0( @@ -97,20 +100,6 @@ impl TokenShieldTransitionActionStateValidationV0 for TokenShieldTransitionActio )); } - let validation_result = validate_identity_token_account_not_frozen( - platform, - token_id, - owner_id, - "shield", - block_info, - execution_context, - transaction, - platform_version, - )?; - if !validation_result.is_valid() { - return Ok(validation_result); - } - let validation_result = validate_token_not_paused( platform, token_id, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs index bb04ed192c7..7ffbfe7c39e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs @@ -22,14 +22,11 @@ use dpp::consensus::state::shielded::insufficient_pool_notes_error::Insufficient use dpp::consensus::state::shielded::invalid_anchor_error::InvalidAnchorError; use dpp::consensus::state::shielded::nullifier_already_spent_error::NullifierAlreadySpentError; use dpp::consensus::state::state_error::StateError; -use dpp::consensus::state::token::{ - IdentityTokenAccountFrozenError, TokenIsPausedError, TokenShieldedPoolNotEnabledError, -}; +use dpp::consensus::state::token::{TokenIsPausedError, TokenShieldedPoolNotEnabledError}; use dpp::consensus::ConsensusError; use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters; use dpp::prelude::Identifier; use dpp::shielded::{SerializedAction}; -use dpp::tokens::info::v0::IdentityTokenInfoV0Accessors; use dpp::tokens::status::v0::TokenStatusV0Accessors; use dpp::validation::SimpleConsensusValidationResult; use dpp::version::PlatformVersion; @@ -86,47 +83,6 @@ pub(crate) fn validate_token_not_paused( Ok(SimpleConsensusValidationResult::new()) } -/// A frozen identity token account can neither shield out of nor (unless the token allows -/// transfers to frozen balances) receive an unshield into its balance. -#[allow(clippy::too_many_arguments)] -pub(crate) fn validate_identity_token_account_not_frozen( - platform: &PlatformStateRef, - token_id: Identifier, - identity_id: Identifier, - action_name: &str, - block_info: &BlockInfo, - execution_context: &mut StateTransitionExecutionContext, - transaction: TransactionArg, - platform_version: &PlatformVersion, -) -> Result { - let (info, fee_result) = platform.drive.fetch_identity_token_info_with_costs( - token_id.to_buffer(), - identity_id.to_buffer(), - block_info, - true, - transaction, - platform_version, - )?; - - execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee_result)); - - if let Some(info) = info { - if info.frozen() { - return Ok(SimpleConsensusValidationResult::new_with_error( - ConsensusError::StateError(StateError::IdentityTokenAccountFrozenError( - IdentityTokenAccountFrozenError::new( - token_id, - identity_id, - action_name.to_string(), - ), - )), - )); - } - } - - Ok(SimpleConsensusValidationResult::new()) -} - /// Meters the GroveDB reads collected in `drive_operations` into the execution context so /// the batch owner pays for them, as every other identity-paid read does. pub(crate) fn charge_drive_operations( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/state_v0/mod.rs index 0fd25f5e117..0baf159f971 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/state_v0/mod.rs @@ -2,8 +2,7 @@ use crate::error::Error; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; use crate::execution::validation::state_transition::batch::action_validation::token::token_base_transition_action::TokenBaseTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_pool_common::{ - charge_drive_operations, validate_identity_token_account_not_frozen, - validate_minimum_token_pool_notes, validate_token_not_paused, validate_token_pool_anchor_exists, + charge_drive_operations, validate_minimum_token_pool_notes, validate_token_not_paused, validate_token_pool_anchor_exists, validate_token_pool_nullifiers, validate_token_shielded_pool_enabled, verify_token_pool_bundle, }; @@ -15,13 +14,11 @@ use dpp::block::block_info::BlockInfo; use dpp::consensus::state::shielded::invalid_shielded_proof_error::InvalidShieldedProofError; use dpp::consensus::state::state_error::StateError; use dpp::consensus::state::token::TokenTransferRecipientIdentityNotExistError; -use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; use dpp::prelude::Identifier; use dpp::shielded::token_unshield_extra_sighash_data; use dpp::validation::SimpleConsensusValidationResult; use dpp::version::PlatformVersion; use drive::query::TransactionArg; -use drive::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionActionAccessorsV0; use drive::state_transition_action::batch::batched_transition::token_transition::token_unshield_transition_action::{ TokenUnshieldTransitionAction, TokenUnshieldTransitionActionAccessorsV0, }; @@ -42,7 +39,8 @@ pub(in crate::execution::validation::state_transition::state_transitions::batch: impl TokenUnshieldTransitionActionStateValidationV0 for TokenUnshieldTransitionAction { /// Unshielding is a transfer from the pool into the recipient's balance: the recipient - /// side of the transfer checks (exists, frozen account unless allowed, paused token) plus + /// side of the transfer checks (exists, paused token; a pooled token can never freeze an + /// account, so no frozen check is read or billed) plus /// the pool-side checks the credit pool's `Unshield` runs (notes floor, anchor, unspent /// nullifiers, pool balance), then the spend bundle is verified with the token, batch /// owner, recipient and amount bound into the sighash. @@ -101,26 +99,6 @@ impl TokenUnshieldTransitionActionStateValidationV0 for TokenUnshieldTransitionA )); } - if !self - .base() - .token_configuration()? - .is_allowed_transfer_to_frozen_balance() - { - let validation_result = validate_identity_token_account_not_frozen( - platform, - token_id, - recipient_id, - "unshield", - block_info, - execution_context, - transaction, - platform_version, - )?; - if !validation_result.is_valid() { - return Ok(validation_result); - } - } - let token_id_bytes = token_id.to_buffer(); let mut drive_operations = vec![]; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs index dd82a18c728..73c6ff23b4a 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/mod.rs @@ -1,5 +1,5 @@ use advanced_structure::v1::DocumentsBatchStateTransitionStructureValidationV1; -mod action_validation; +pub(in crate::execution::validation::state_transition::state_transitions) mod action_validation; mod advanced_structure; mod data_triggers; mod identity_contract_nonce; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 63db1666c32..383e7393f1d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -7,9 +7,11 @@ use super::*; /// nullifier checks, and token conservation with the pool as a balance term. mod token_shielded_pool_tests { use super::*; + use crate::execution::check_tx::CheckTxLevel; use crate::execution::validation::state_transition::state_transitions::test_helpers::{ create_dummy_serialized_action, get_proving_key, serialize_authorized_bundle_i64, }; + use crate::platform_types::platform::PlatformRef; use crate::platform_types::state_transitions_processing_result::StateTransitionsProcessingResult; use crate::rpc::core::MockCoreRPCLike; use crate::test::helpers::setup::TempPlatform; @@ -304,6 +306,37 @@ mod token_shielded_pool_tests { result } + /// CheckTx, the mempool admission path: it verifies every token shielded bundle a batch + /// carries statelessly, under the identity contract nonce limiter, so the sighash it binds + /// must agree with the one block validation binds. + pub(super) fn assert_check_tx_accepts( + platform: &TempPlatform, + transition: &StateTransition, + ) { + let state = platform.state.load(); + let platform_ref = PlatformRef { + drive: &platform.drive, + state: &state, + config: &platform.config, + core_rpc: &platform.core_rpc, + }; + let result = platform + .check_tx( + &transition + .serialize_to_bytes() + .expect("serialize transition"), + CheckTxLevel::FirstTimeCheck, + &platform_ref, + PlatformVersion::latest(), + ) + .expect("check tx"); + assert!( + result.is_valid(), + "unexpected CheckTx errors: {:?}", + result.errors + ); + } + pub(super) fn platform_with_latest_version() -> TempPlatform { TestPlatformBuilder::new() .with_latest_protocol_version() @@ -435,6 +468,7 @@ mod token_shielded_pool_tests { .await .expect("token unshield transition"); + assert_check_tx_accepts(&platform, &unshield); let result = process(&platform, &unshield); assert_matches!( result.execution_results().as_slice(), @@ -513,6 +547,7 @@ mod token_shielded_pool_tests { .await .expect("token shielded transfer transition"); + assert_check_tx_accepts(&platform, &transfer); let result = process(&platform, &transfer); assert_matches!( result.execution_results().as_slice(), @@ -682,64 +717,6 @@ mod token_shielded_pool_tests { assert_eq!(pool_balance(&platform, token_id), 0); } - #[tokio::test] - async fn test_token_shield_rejected_when_identity_frozen() { - let platform_version = PlatformVersion::latest(); - let mut platform = platform_with_latest_version(); - let mut rng = StdRng::seed_from_u64(9005); - - let (identity, signer, key) = - setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); - let (contract, token_id) = create_token_contract_with_owner_identity( - &mut platform, - identity.id(), - Some(enable_shielded_pool), - None, - None, - None, - platform_version, - ); - - platform - .drive - .token_freeze( - token_id, - identity.id(), - &BlockInfo::default(), - true, - None, - platform_version, - ) - .expect("freeze identity token account"); - - let shield = BatchTransition::new_token_shield_transition( - token_id, - identity.id(), - contract.id(), - 0, - SHIELD_AMOUNT, - dummy_bundle(), - &key, - 2, - 0, - &signer, - platform_version, - None, - ) - .await - .expect("token shield transition"); - - let result = process(&platform, &shield); - assert_matches!( - result.execution_results().as_slice(), - [StateTransitionExecutionResult::PaidConsensusError { - error: ConsensusError::StateError(StateError::IdentityTokenAccountFrozenError(_)), - .. - }] - ); - assert_eq!(pool_balance(&platform, token_id), 0); - } - #[tokio::test] async fn test_token_shield_with_invalid_proof_is_a_paid_failure() { let platform_version = PlatformVersion::latest(); @@ -1206,21 +1183,22 @@ mod token_shielded_pool_tests { /// Mint, burn, claim and purchase straight into or out of the token shielded pool. mod token_pool_mint_burn_claim_purchase_tests { use super::token_shielded_pool_tests::{ - assert_tokens_conserved, build_shield_bundle, build_spend_bundle, dummy_bundle, - enable_shielded_pool, identity_token_balance, insert_token_pool_anchor, nullifier_is_spent, - platform_with_latest_version, pool_balance, pool_notes_count, process, spendable_note, - OWNER_INITIAL_BALANCE, + assert_check_tx_accepts, assert_tokens_conserved, build_shield_bundle, build_spend_bundle, + dummy_bundle, enable_shielded_pool, identity_token_balance, insert_token_pool_anchor, + nullifier_is_spent, platform_with_latest_version, pool_balance, pool_notes_count, process, + spendable_note, OWNER_INITIAL_BALANCE, }; use super::*; - use crate::execution::check_tx::CheckTxLevel; use crate::execution::validation::state_transition::processor::traits::shielded_proof::StateTransitionShieldedProofValidationV0; - use crate::platform_types::platform::PlatformRef; use crate::rpc::core::MockCoreRPCLike; use crate::test::helpers::fast_forward_to_block::fast_forward_to_block; use crate::test::helpers::setup::TempPlatform; use dpp::block::epoch::Epoch; + use dpp::consensus::basic::BasicError; + use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters; use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; + use dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Setters; use dpp::data_contract::associated_token::token_distribution_rules::accessors::v1::TokenDistributionRulesV1Setters; use dpp::data_contract::associated_token::token_once_per_identity_distribution::v0::TokenOncePerIdentityDistributionV0; use dpp::data_contract::associated_token::token_once_per_identity_distribution::TokenOncePerIdentityDistribution; @@ -1235,34 +1213,6 @@ mod token_pool_mint_burn_claim_purchase_tests { use dpp::tokens::token_pricing_schedule::TokenPricingSchedule; use platform_version::version::PlatformVersion; - fn assert_check_tx_accepts( - platform: &TempPlatform, - transition: &StateTransition, - ) { - let state = platform.state.load(); - let platform_ref = PlatformRef { - drive: &platform.drive, - state: &state, - config: &platform.config, - core_rpc: &platform.core_rpc, - }; - let result = platform - .check_tx( - &transition - .serialize_to_bytes() - .expect("serialize transition"), - CheckTxLevel::FirstTimeCheck, - &platform_ref, - PlatformVersion::latest(), - ) - .expect("check tx"); - assert!( - result.is_valid(), - "unexpected CheckTx errors: {:?}", - result.errors - ); - } - fn total_supply(platform: &TempPlatform, token_id: Identifier) -> u64 { platform .drive @@ -1333,6 +1283,64 @@ mod token_pool_mint_burn_claim_purchase_tests { assert_tokens_conserved(&platform); } + /// A mint into the pool sends the tokens wherever the minter's notes say, so it is refused + /// where the configuration pins the destination of minted tokens. + #[tokio::test] + async fn test_token_mint_to_pool_rejected_when_destination_is_fixed() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9106); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|token_configuration: &mut TokenConfiguration| { + enable_shielded_pool(token_configuration); + token_configuration + .distribution_rules_mut() + .set_minting_allow_choosing_destination(false); + }), + None, + None, + None, + platform_version, + ); + + let mint = BatchTransition::new_token_mint_to_pool_transition( + token_id, + identity.id(), + contract.id(), + 0, + 1_337, + build_shield_bundle(1_337, 27), + None, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token mint to pool transition"); + + let result = process(&platform, &mint); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::BasicError( + BasicError::ChoosingTokenMintRecipientNotAllowedError(_) + ), + .. + }] + ); + assert_eq!(pool_balance(&platform, token_id), 0); + assert_eq!(total_supply(&platform, token_id), OWNER_INITIAL_BALANCE); + } + #[tokio::test] async fn test_token_mint_to_pool_rejected_past_max_supply() { let platform_version = PlatformVersion::latest(); @@ -1509,6 +1517,7 @@ mod token_pool_mint_burn_claim_purchase_tests { .await .expect("token burn from pool transition"); + assert_check_tx_accepts(&platform, &burn); let result = process(&platform, &burn); assert_matches!( result.execution_results().as_slice(), @@ -2186,8 +2195,8 @@ mod token_pool_mint_burn_claim_purchase_tests { /// cost with a spend bundle bound to the token, the buyer, the contract and the document id. mod document_shielded_token_payment_tests { use super::token_shielded_pool_tests::{ - assert_tokens_conserved, build_shield_bundle, build_spend_bundle, dummy_bundle, - identity_token_balance, insert_token_pool_anchor, nullifier_is_spent, + assert_check_tx_accepts, assert_tokens_conserved, build_shield_bundle, build_spend_bundle, + dummy_bundle, identity_token_balance, insert_token_pool_anchor, nullifier_is_spent, platform_with_latest_version, pool_balance, process, spendable_note, }; use super::*; @@ -2417,6 +2426,7 @@ mod document_shielded_token_payment_tests { .await .expect("document create transition"); + assert_check_tx_accepts(&platform, &transition); let result = process(&platform, &transition); assert_matches!( result.execution_results().as_slice(), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_pool_paid_common/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_pool_paid_common/mod.rs index 34a20c5b5cd..3728545c48e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_pool_paid_common/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_pool_paid_common/mod.rs @@ -7,13 +7,14 @@ //! pool-paid transitions. use crate::error::Error; +use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_pool_common::{ + validate_token_pool_anchor_exists, validate_token_pool_nullifiers, +}; use crate::execution::validation::state_transition::state_transitions::shielded_common::{ read_pool_total_balance, validate_anchor_exists, validate_nullifiers, }; use dpp::consensus::state::data_contract::data_contract_not_found_error::DataContractNotFoundError; -use dpp::consensus::state::shielded::invalid_anchor_error::InvalidAnchorError; use dpp::consensus::state::shielded::invalid_shielded_proof_error::InvalidShieldedProofError; -use dpp::consensus::state::shielded::nullifier_already_spent_error::NullifierAlreadySpentError; use dpp::consensus::state::state_error::StateError; use dpp::consensus::state::token::{TokenIsPausedError, TokenShieldedPoolNotEnabledError}; use dpp::consensus::ConsensusError; @@ -29,7 +30,6 @@ use drive::drive::contract::DataContractFetchInfo; use drive::drive::Drive; use drive::grovedb::TransactionArg; use drive::state_transition_action::StateTransitionAction; -use std::collections::HashSet; use std::sync::Arc; /// A resolved pooled token: its contract and its configuration. @@ -106,33 +106,31 @@ pub(crate) fn validate_token_pool_spend( ) -> Result>, Error> { let token_id_bytes = token_id.to_buffer(); let mut drive_operations = vec![]; - if !drive.has_token_pool_anchor( + let result = validate_token_pool_anchor_exists( + drive, &token_id_bytes, anchor, transaction, &mut drive_operations, platform_version, - )? { - return Ok(Some(ConsensusValidationResult::new_with_error( - StateError::InvalidAnchorError(InvalidAnchorError::new(*anchor)).into(), + )?; + if !result.is_valid() { + return Ok(Some(ConsensusValidationResult::new_with_errors( + result.errors, ))); } - let mut seen = HashSet::new(); - for nullifier in nullifiers { - if !seen.insert(nullifier) - || drive.has_token_pool_nullifier( - &token_id_bytes, - nullifier, - transaction, - &mut drive_operations, - platform_version, - )? - { - return Ok(Some(ConsensusValidationResult::new_with_error( - StateError::NullifierAlreadySpentError(NullifierAlreadySpentError::new(*nullifier)) - .into(), - ))); - } + let result = validate_token_pool_nullifiers( + drive, + &token_id_bytes, + nullifiers, + transaction, + &mut drive_operations, + platform_version, + )?; + if !result.is_valid() { + return Ok(Some(ConsensusValidationResult::new_with_errors( + result.errors, + ))); } Ok(None) } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs index 94b9f874099..684dc12db86 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs @@ -10,6 +10,7 @@ use dpp::prelude::ConsensusValidationResult; use dpp::state_transition::token_purchase_from_shielded_pool_transition::TokenPurchaseFromShieldedPoolTransition; use dpp::version::PlatformVersion; use drive::drive::Drive; +use drive::error::drive::DriveError; use drive::grovedb::TransactionArg; use drive::state_transition_action::shielded::token_purchase_from_shielded_pool::TokenPurchaseFromShieldedPoolTransitionAction; use drive::state_transition_action::StateTransitionAction; @@ -69,9 +70,20 @@ impl TokenPurchaseFromShieldedPoolStateTransitionTransformIntoActionValidationV0 return Ok(ConsensusValidationResult::new_with_error(consensus_error)); } // Minting into the pool must respect the max supply. - let total_supply = drive - .fetch_token_total_supply(v0.token_id.to_buffer(), transaction, platform_version)? - .unwrap_or_default(); + let Some(total_supply) = drive.fetch_token_total_supply( + v0.token_id.to_buffer(), + transaction, + platform_version, + )? + else { + return Err(Error::Drive( + DriveError::CorruptedDriveState(format!( + "token {} total supply not found", + v0.token_id + )) + .into(), + )); + }; if let Some(max_supply) = configuration.max_supply() { match total_supply.checked_add(v0.token_count) { Some(after) if after <= max_supply => {} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_unshield_with_shielded_fee/transform_into_action/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_unshield_with_shielded_fee/transform_into_action/v0/mod.rs index 24617145ad9..756543d25e7 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_unshield_with_shielded_fee/transform_into_action/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_unshield_with_shielded_fee/transform_into_action/v0/mod.rs @@ -3,13 +3,9 @@ use crate::execution::validation::state_transition::state_transitions::token_poo resolve_pooled_token, validate_credit_pool_fee_spend, validate_token_pool_holds, validate_token_pool_spend, }; -use dpp::consensus::state::token::{ - IdentityTokenAccountFrozenError, TokenTransferRecipientIdentityNotExistError, -}; -use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dpp::consensus::state::token::TokenTransferRecipientIdentityNotExistError; use dpp::prelude::ConsensusValidationResult; use dpp::state_transition::token_unshield_with_shielded_fee_transition::TokenUnshieldWithShieldedFeeTransition; -use dpp::tokens::info::v0::IdentityTokenInfoV0Accessors; use dpp::version::PlatformVersion; use drive::drive::Drive; use drive::grovedb::TransactionArg; @@ -40,7 +36,7 @@ impl TokenUnshieldWithShieldedFeeStateTransitionTransformIntoActionValidationV0 platform_version: &PlatformVersion, ) -> Result, Error> { let TokenUnshieldWithShieldedFeeTransition::V0(v0) = self; - let (_contract, configuration) = match resolve_pooled_token( + let (_contract, _configuration) = match resolve_pooled_token( drive, v0.data_contract_id, v0.token_contract_position, @@ -51,8 +47,8 @@ impl TokenUnshieldWithShieldedFeeStateTransitionTransformIntoActionValidationV0 Ok(resolved) => resolved, Err(rejection) => return Ok(rejection), }; - // The recipient must exist, and unless the token allows transfers into frozen - // balances its account must not be frozen. + // The recipient must exist. A pooled token can never freeze an account, so no frozen + // check is read. if drive .fetch_identity_balance(v0.recipient_id.to_buffer(), transaction, platform_version)? .is_none() @@ -61,25 +57,6 @@ impl TokenUnshieldWithShieldedFeeStateTransitionTransformIntoActionValidationV0 TokenTransferRecipientIdentityNotExistError::new(v0.recipient_id).into(), )); } - if !configuration.is_allowed_transfer_to_frozen_balance() { - if let Some(info) = drive.fetch_identity_token_info( - v0.token_id.to_buffer(), - v0.recipient_id.to_buffer(), - transaction, - platform_version, - )? { - if info.frozen() { - return Ok(ConsensusValidationResult::new_with_error( - IdentityTokenAccountFrozenError::new( - v0.token_id, - v0.recipient_id, - "token unshield with shielded fee".to_string(), - ) - .into(), - )); - } - } - } let token_nullifiers: Vec<[u8; 32]> = v0.token_actions.iter().map(|a| a.nullifier).collect(); if let Some(rejection) = validate_token_pool_spend( diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/mod.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/mod.rs index a06d8c21ce7..a8d237ac4c3 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/mod.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/mod.rs @@ -10,9 +10,12 @@ mod token_shielded_transfer_with_shielded_fee_transition; mod token_unshield_with_shielded_fee_transition; mod unshield_transition; +use crate::error::drive::DriveError; +use crate::error::Error; use crate::state_transition_action::shielded::ShieldedActionNote; use crate::util::batch::drive_op_batch::ShieldedPoolOperationType; use crate::util::batch::DriveOperation; +use dpp::fee::Credits; /// Insert nullifiers into the permanent tree (double-spend prevention) and /// per-block sync storage (catch-up RPCs). @@ -54,6 +57,29 @@ pub(super) fn update_balance<'a>(ops: &mut Vec>, new_total_ba )); } +/// The credit pool side of an identity-less token pool transition: the fee bundle's spent +/// nullifiers are recorded, its change notes appended and the pool's total balance lowered +/// by `credits_leaving` (the fee, plus whatever else left the pool with it). +pub(super) fn pay_from_credit_pool<'a>( + ops: &mut Vec>, + fee_notes: &[ShieldedActionNote], + current_credit_pool_balance: Credits, + credits_leaving: Credits, +) -> Result<(), Error> { + insert_nullifiers(ops, fee_notes); + insert_notes(ops, fee_notes); + let new_total_balance = current_credit_pool_balance + .checked_sub(credits_leaving) + .ok_or_else(|| { + Error::Drive(DriveError::CorruptedDriveState( + "shielded pool total balance underflow when paying a token pool transition" + .to_string(), + )) + })?; + update_balance(ops, new_total_balance); + Ok(()) +} + /// Measurement support for the pool-paid shielded fee-floor tests. /// /// The pool-paid shielded transitions (ShieldedTransfer, Unshield, diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_purchase_from_shielded_pool_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_purchase_from_shielded_pool_transition.rs index ca85499b6ef..bf32169e85e 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_purchase_from_shielded_pool_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_purchase_from_shielded_pool_transition.rs @@ -1,4 +1,4 @@ -use super::{insert_notes, insert_nullifiers, update_balance}; +use super::pay_from_credit_pool; use crate::error::drive::DriveError; use crate::error::Error; use crate::state_transition_action::action_convert_to_operations::DriveHighLevelOperationConverter; @@ -54,18 +54,12 @@ impl DriveHighLevelOperationConverter for TokenPurchaseFromShieldedPoolTransitio )) })?; - insert_nullifiers(&mut ops, &v0.fee_notes); - insert_notes(&mut ops, &v0.fee_notes); - let new_total_balance = v0 - .current_credit_pool_balance - .checked_sub(credits_leaving) - .ok_or_else(|| { - Error::Drive(DriveError::CorruptedDriveState( - "shielded pool total balance underflow when paying a token pool transition" - .to_string(), - )) - })?; - update_balance(&mut ops, new_total_balance); + pay_from_credit_pool( + &mut ops, + &v0.fee_notes, + v0.current_credit_pool_balance, + credits_leaving, + )?; Ok(ops) } }, diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_shielded_transfer_with_shielded_fee_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_shielded_transfer_with_shielded_fee_transition.rs index 6572e2c4c79..b736909e6f4 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_shielded_transfer_with_shielded_fee_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_shielded_transfer_with_shielded_fee_transition.rs @@ -1,4 +1,4 @@ -use super::{insert_notes, insert_nullifiers, update_balance}; +use super::pay_from_credit_pool; use crate::error::drive::DriveError; use crate::error::Error; use crate::state_transition_action::action_convert_to_operations::DriveHighLevelOperationConverter; @@ -36,18 +36,12 @@ impl DriveHighLevelOperationConverter for TokenShieldedTransferWithShieldedFeeTr })); let credits_leaving = v0.fee_amount; - insert_nullifiers(&mut ops, &v0.fee_notes); - insert_notes(&mut ops, &v0.fee_notes); - let new_total_balance = v0 - .current_credit_pool_balance - .checked_sub(credits_leaving) - .ok_or_else(|| { - Error::Drive(DriveError::CorruptedDriveState( - "shielded pool total balance underflow when paying a token pool transition" - .to_string(), - )) - })?; - update_balance(&mut ops, new_total_balance); + pay_from_credit_pool( + &mut ops, + &v0.fee_notes, + v0.current_credit_pool_balance, + credits_leaving, + )?; Ok(ops) } }, diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_unshield_with_shielded_fee_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_unshield_with_shielded_fee_transition.rs index 091bc7bf651..c9473184477 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_unshield_with_shielded_fee_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/token_unshield_with_shielded_fee_transition.rs @@ -1,4 +1,4 @@ -use super::{insert_notes, insert_nullifiers, update_balance}; +use super::pay_from_credit_pool; use crate::error::drive::DriveError; use crate::error::Error; use crate::state_transition_action::action_convert_to_operations::DriveHighLevelOperationConverter; @@ -38,18 +38,12 @@ impl DriveHighLevelOperationConverter for TokenUnshieldWithShieldedFeeTransition })); let credits_leaving = v0.fee_amount; - insert_nullifiers(&mut ops, &v0.fee_notes); - insert_notes(&mut ops, &v0.fee_notes); - let new_total_balance = v0 - .current_credit_pool_balance - .checked_sub(credits_leaving) - .ok_or_else(|| { - Error::Drive(DriveError::CorruptedDriveState( - "shielded pool total balance underflow when paying a token pool transition" - .to_string(), - )) - })?; - update_balance(&mut ops, new_total_balance); + pay_from_credit_pool( + &mut ops, + &v0.fee_notes, + v0.current_credit_pool_balance, + credits_leaving, + )?; Ok(ops) } }, diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/transformer.rs index 449c703dbe8..e5813dcb552 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/transformer.rs @@ -63,13 +63,18 @@ impl DocumentBaseTransitionActionV0 { ); let mut shielded_token_payment = None; if let Some(document_action_token_cost) = document_action_token_cost { + let token_id: Identifier = calculate_token_id( + document_action_token_cost + .contract_id + .unwrap_or(data_contract_id) + .as_bytes(), + document_action_token_cost.token_contract_position, + ) + .into(); let Some(token_payment_info) = value.token_payment_info_ref() else { return Ok(ConsensusValidationResult::new_with_error( ConsensusError::StateError(StateError::RequiredTokenPaymentInfoNotSetError( - RequiredTokenPaymentInfoNotSetError::new( - token_cost.expect("expected token cost").0, - action.to_string(), - ), + RequiredTokenPaymentInfoNotSetError::new(token_id, action.to_string()), )), )); }; @@ -83,7 +88,7 @@ impl DocumentBaseTransitionActionV0 { IdentityTryingToPayWithWrongTokenError::new( document_action_token_cost.contract_id, document_action_token_cost.token_contract_position, - token_cost.expect("expected token cost").0, + token_id, token_payment_info.payment_token_contract_id(), token_payment_info.token_contract_position(), token_payment_info.token_id(data_contract_id), @@ -99,7 +104,7 @@ impl DocumentBaseTransitionActionV0 { ConsensusError::StateError( StateError::IdentityHasNotAgreedToPayRequiredTokenAmountError( IdentityHasNotAgreedToPayRequiredTokenAmountError::new( - token_cost.expect("expected token cost").0, + token_id, document_action_token_cost.token_amount, token_payment_info.minimum_token_cost(), token_payment_info.maximum_token_cost(), @@ -117,7 +122,7 @@ impl DocumentBaseTransitionActionV0 { ConsensusError::StateError( StateError::TokenShieldedPaymentAmountMismatchError( TokenShieldedPaymentAmountMismatchError::new( - token_cost.expect("expected token cost").0, + token_id, document_action_token_cost.token_amount, payment.amount, action.to_string(), diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/v0/transformer.rs index 9e49c291745..14aee4c61bf 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/v0/transformer.rs @@ -8,6 +8,11 @@ use crate::state_transition_action::batch::batched_transition::token_transition: use crate::state_transition_action::batch::BatchedTransitionAction; use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; use dpp::block::block_info::BlockInfo; +use dpp::consensus::state::state_error::StateError; +use dpp::consensus::state::token::InvalidTokenClaimPropertyMismatch; +use dpp::consensus::ConsensusError; +use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; +use dpp::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; use dpp::fee::fee_result::FeeResult; use dpp::shielded::compute_shielded_verification_fee; use dpp::identifier::Identifier; @@ -106,6 +111,32 @@ impl TokenClaimToPoolTransitionActionV0 { } }; + // A perpetual claim into the pool must name the moment it claims up to: the bundle + // proves an exact amount, which only a pinned moment makes predictable. + if matches!(distribution_type, TokenDistributionType::Perpetual) && claim_up_to.is_none() { + let bump_action = + BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( + base, + owner_id, + user_fee_increase, + ); + let batched_action = + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); + return Ok(( + ConsensusValidationResult::new_with_data_and_errors( + batched_action, + vec![ConsensusError::StateError( + StateError::InvalidTokenClaimPropertyMismatch( + InvalidTokenClaimPropertyMismatch::new( + "claim up to moment", + base.token_id(), + ), + ), + )], + ), + fee_result, + )); + } let (amount, distribution_info) = match resolve_token_claim( drive, owner_id, diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs index a6025703d02..0946a5a91d2 100644 --- a/packages/rs-platform-version/src/version/v15.rs +++ b/packages/rs-platform-version/src/version/v15.rs @@ -26,8 +26,9 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// `TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION`) move tokens between an /// identity balance, the supply and the pool or inside it; the identity /// signs and pays the fee in credits, and every spend bundle binds the -/// token id and owner id (plus the recipient and amount where tokens leave -/// the pool) into the Orchard sighash. The pool balances are a term of the +/// token id and the batch owner (a burn binds the burner: the batch owner, +/// or the proposer of a group action), plus the recipient and amount where +/// tokens leave the pool, into the Orchard sighash. The pool balances are a term of the /// token conservation check (`calculate_total_tokens_balance` v1 in /// `DRIVE_TOKEN_METHOD_VERSIONS_V3`, reached through `DRIVE_VERSION_V10`). /// `record_token_shielded_pool_anchors` (`DRIVE_ABCI_METHOD_VERSIONS_V11`) From c1ba6392c3a529d6cd364a7b2616b955af403fa8 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Mon, 21 Sep 2026 22:12:31 +0700 Subject: [PATCH 13/94] fix(drive): stop pricing an existing token pool in contract update estimates A contract update is estimated through the insert path, whose stateless existence read reports no pool, so every pooled token the contract already had was priced as a freshly created pool on each update. The insert generation now skips a pool that exists when estimating; a real insert never finds one. The drive test estimates both the registration and the update of a pooled contract and pins that the update estimate is the smaller. The three copies of the token configuration checks (format admitted, pool rules compatible) in the contract create and update basic structure generations and the pre-activation gate become one dpp helper. A perpetual claim into the pool is now covered end to end: refused without the moment it claims up to, and paying the accrued rewards into the pool with it. Co-Authored-By: Claude Fable 5.1 --- .../token_configuration/mod.rs | 22 +++ .../processor/traits/is_allowed.rs | 14 +- .../batch/tests/token/shielded/mod.rs | 155 ++++++++++++++++++ .../basic_structure/v3/mod.rs | 16 +- .../basic_structure/v3/mod.rs | 16 +- .../contract/insert/insert_contract/v3/mod.rs | 27 ++- .../contract/update/update_contract/v3/mod.rs | 48 +++++- 7 files changed, 261 insertions(+), 37 deletions(-) diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs index ff2e681a38f..caaaa1f065d 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs @@ -17,6 +17,7 @@ use derive_more::From; use platform_version::version::PlatformVersion; use serde::{Deserialize, Serialize}; use std::borrow::Cow; +use std::collections::BTreeMap; use std::fmt; pub mod accessors; @@ -121,6 +122,27 @@ impl TokenConfiguration { } } +/// Validates every token configuration of a contract for `platform_version`: the format +/// version must be admitted and a pooled token's rules must be compatible with a pool. Returns +/// the first error. Shared by the contract create and update basic structure generations and +/// by the pre-activation gate, so the three cannot drift. +pub fn validate_token_configurations( + tokens: &BTreeMap, + platform_version: &PlatformVersion, +) -> SimpleConsensusValidationResult { + for (position, configuration) in tokens { + let result = configuration.validate_format_version(platform_version); + if !result.is_valid() { + return result; + } + let result = configuration.validate_shielded_pool_rules(*position); + if !result.is_valid() { + return result; + } + } + SimpleConsensusValidationResult::new() +} + impl fmt::Display for TokenConfiguration { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs index 7a32a9659f9..173c75a2eac 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs @@ -3,6 +3,7 @@ use crate::error::Error; use crate::platform_types::platform::PlatformRef; use crate::rpc::core::CoreRPCLike; use dpp::consensus::basic::state_transition::StateTransitionNotActiveError; +use dpp::data_contract::associated_token::token_configuration::validate_token_configurations; use dpp::prelude::ConsensusValidationResult; use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; @@ -91,13 +92,12 @@ impl StateTransitionIsAllowedValidationV0 for StateTransition { _ => None, }; if let Some(contract) = contract { - for configuration in contract.tokens().values() { - let result = configuration.validate_format_version(platform_version); - if !result.is_valid() { - return Ok(result); - } - } - return Ok(ConsensusValidationResult::new()); + // The pre-activation gate: a token configuration format the protocol version does + // not admit is refused unpaid, whatever the frozen basic structure generations do. + return Ok(validate_token_configurations( + contract.tokens(), + platform_version, + )); } match self { StateTransition::Batch(st) => { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 383e7393f1d..ea76535dfee 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -1202,6 +1202,11 @@ mod token_pool_mint_burn_claim_purchase_tests { use dpp::data_contract::associated_token::token_distribution_rules::accessors::v1::TokenDistributionRulesV1Setters; use dpp::data_contract::associated_token::token_once_per_identity_distribution::v0::TokenOncePerIdentityDistributionV0; use dpp::data_contract::associated_token::token_once_per_identity_distribution::TokenOncePerIdentityDistribution; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::TokenDistributionRecipient; + use dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_type::RewardDistributionType; + use dpp::data_contract::associated_token::token_perpetual_distribution::v0::TokenPerpetualDistributionV0; + use dpp::data_contract::associated_token::token_perpetual_distribution::TokenPerpetualDistribution; use dpp::data_contract::associated_token::token_pre_programmed_distribution::v0::TokenPreProgrammedDistributionV0; use dpp::data_contract::associated_token::token_pre_programmed_distribution::TokenPreProgrammedDistribution; use dpp::identity::accessors::IdentityGettersV0; @@ -1838,6 +1843,156 @@ mod token_pool_mint_burn_claim_purchase_tests { assert_token_claim_to_pool(TokenDistributionType::PreProgrammed).await; } + /// A perpetual claim into the pool must name the cycle-aligned moment it claims up to, so + /// the amount its bundle proves is fixed before the block lands: without it the claim is a + /// paid failure, with it the rewards accrued up to that moment enter the pool. + #[tokio::test] + async fn test_token_claim_to_pool_perpetual_requires_claim_up_to() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9107); + + let (owner, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + owner.id(), + Some(|configuration: &mut TokenConfiguration| { + configuration.set_has_shielded_pool(true); + configuration + .distribution_rules_mut() + .set_perpetual_distribution(Some(TokenPerpetualDistribution::V0( + TokenPerpetualDistributionV0 { + distribution_type: RewardDistributionType::BlockBasedDistribution { + interval: 10, + function: DistributionFunction::FixedAmount { amount: 50 }, + }, + distribution_recipient: TokenDistributionRecipient::ContractOwner, + }, + ))); + }), + None, + None, + None, + platform_version, + ); + + fast_forward_to_block(&platform, 100, 40, 42, 1, false); + let platform_state = platform.state.load(); + let block_info = BlockInfo { + time_ms: 200, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }; + // Four cycles (heights 10, 20, 30 and 40) of 50 have accrued by height 41. + let accrued = 200; + + // Without the moment the claim is refused before its proof is verified. + let unpinned = BatchTransition::new_token_claim_to_pool_transition( + token_id, + owner.id(), + contract.id(), + 0, + TokenDistributionType::Perpetual, + None, + build_shield_bundle(accrued, 27), + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token claim to pool transition"); + let transaction = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &[unpinned.serialize_to_bytes().expect("serialize")], + &platform_state, + &block_info, + &transaction, + platform_version, + false, + None, + ) + .expect("process state transition"); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::InvalidTokenClaimPropertyMismatch(_)), + .. + }] + ); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("commit"); + assert_eq!(pool_balance(&platform, token_id), 0); + + // Pinned to the current cycle, the bundle proves exactly the accrued rewards. + let claim_bundle = build_shield_bundle(accrued, 28); + let pinned = BatchTransition::new_token_claim_to_pool_transition( + token_id, + owner.id(), + contract.id(), + 0, + TokenDistributionType::Perpetual, + Some(40), + claim_bundle.clone(), + None, + &key, + 3, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token claim to pool transition"); + let transaction = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &[pinned.serialize_to_bytes().expect("serialize")], + &platform_state, + &block_info, + &transaction, + platform_version, + false, + None, + ) + .expect("process state transition"); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("commit"); + assert_eq!(pool_balance(&platform, token_id), accrued); + assert_eq!( + identity_token_balance(&platform, token_id, owner.id()), + Some(OWNER_INITIAL_BALANCE) + ); + assert_eq!( + total_supply(&platform, token_id), + OWNER_INITIAL_BALANCE + accrued + ); + assert_eq!( + pool_notes_count(&platform, token_id), + claim_bundle.actions.len() as u64 + ); + assert_tokens_conserved(&platform); + } + #[tokio::test] async fn should_claim_once_per_identity_into_the_pool_only_once() { assert_token_claim_to_pool(TokenDistributionType::OncePerIdentity).await; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v3/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v3/mod.rs index 5ecc107f64a..93a891fe990 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v3/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v3/mod.rs @@ -1,5 +1,6 @@ use crate::error::Error; use dpp::dashcore::Network; +use dpp::data_contract::associated_token::token_configuration::validate_token_configurations; use dpp::state_transition::data_contract_create_transition::accessors::DataContractCreateTransitionAccessorsV0; use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; use dpp::validation::SimpleConsensusValidationResult; @@ -26,16 +27,9 @@ impl DataContractCreateStateTransitionBasicStructureValidationV3 for DataContrac if !result.is_valid() { return Ok(result); } - for (position, configuration) in self.data_contract().tokens() { - let result = configuration.validate_format_version(platform_version); - if !result.is_valid() { - return Ok(result); - } - let result = configuration.validate_shielded_pool_rules(*position); - if !result.is_valid() { - return Ok(result); - } - } - Ok(SimpleConsensusValidationResult::new()) + Ok(validate_token_configurations( + self.data_contract().tokens(), + platform_version, + )) } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v3/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v3/mod.rs index 331feac889e..2a411012d82 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v3/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v3/mod.rs @@ -1,5 +1,6 @@ use crate::error::Error; use dpp::dashcore::Network; +use dpp::data_contract::associated_token::token_configuration::validate_token_configurations; use dpp::state_transition::data_contract_update_transition::accessors::DataContractUpdateTransitionAccessorsV0; use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; use dpp::validation::SimpleConsensusValidationResult; @@ -26,16 +27,9 @@ impl DataContractUpdateStateTransitionBasicStructureValidationV3 for DataContrac if !result.is_valid() { return Ok(result); } - for (position, configuration) in self.data_contract().tokens() { - let result = configuration.validate_format_version(platform_version); - if !result.is_valid() { - return Ok(result); - } - let result = configuration.validate_shielded_pool_rules(*position); - if !result.is_valid() { - return Ok(result); - } - } - Ok(SimpleConsensusValidationResult::new()) + Ok(validate_token_configurations( + self.data_contract().tokens(), + platform_version, + )) } } diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v3/mod.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v3/mod.rs index 744bb9484db..893448b7bd5 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/v3/mod.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v3/mod.rs @@ -111,18 +111,31 @@ impl Drive { )?; for (position, configuration) in contract.tokens() { - if configuration.has_shielded_pool() { - let token_id = calculate_token_id(contract.id().as_bytes(), *position); - batch_operations.extend(self.create_token_shielded_pool_trees_operations( + if !configuration.has_shielded_pool() { + continue; + } + let token_id = calculate_token_id(contract.id().as_bytes(), *position); + // An update is estimated through the insert path (the stateless existence read + // reports nothing), so a pool the token already owns must not be priced as created + // again; a real insert never finds one. + if estimated_costs_only_with_layer_info.is_some() + && self.has_token_shielded_pool( token_id, - false, - estimated_costs_only_with_layer_info, transaction, + &mut vec![], platform_version, - )?); + )? + { + continue; } + batch_operations.extend(self.create_token_shielded_pool_trees_operations( + token_id, + false, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?); } - Ok(batch_operations) } } diff --git a/packages/rs-drive/src/drive/contract/update/update_contract/v3/mod.rs b/packages/rs-drive/src/drive/contract/update/update_contract/v3/mod.rs index 103594d7add..885acc4b083 100644 --- a/packages/rs-drive/src/drive/contract/update/update_contract/v3/mod.rs +++ b/packages/rs-drive/src/drive/contract/update/update_contract/v3/mod.rs @@ -262,6 +262,32 @@ mod tests { ); configuration.set_has_shielded_pool(true); contract.set_tokens(BTreeMap::from([(0, configuration)])); + // The fee estimate CheckTx runs (apply = false) goes through the stateless + // insert path and must price the pool once, while it is still to be created. + let estimate_before_pool = if add_by_update { + drive + .update_contract( + &contract, + BlockInfo::default(), + false, + None, + platform_version, + None, + ) + .expect("estimate adding a pooled token through contract update") + } else { + drive + .apply_contract( + &contract, + BlockInfo::default(), + false, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("estimate registering a pooled token contract") + }; + assert!(estimate_before_pool.processing_fee > 0); if add_by_update { drive .update_contract( @@ -300,8 +326,28 @@ mod tests { Some(100) ); - // Updating an existing token must not recreate its pool or mint its supply again. + // Updating an existing token must not recreate its pool or mint its supply again, + // and its estimate must not price the pool the token already owns. contract.increment_version(); + let estimate_with_pool = drive + .update_contract( + &contract, + BlockInfo::default(), + false, + None, + platform_version, + None, + ) + .expect("estimate updating a contract whose token owns a pool"); + assert!(estimate_with_pool.processing_fee > 0); + if platform_version.protocol_version >= 15 { + assert!( + estimate_with_pool.storage_fee < estimate_before_pool.storage_fee, + "an existing pool must not be priced again: {} vs {}", + estimate_with_pool.storage_fee, + estimate_before_pool.storage_fee + ); + } drive .update_contract( &contract, From 039ac9fb1268682d49ab576a3905eadbccf24e25 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Tue, 22 Sep 2026 00:01:44 +0700 Subject: [PATCH 14/94] fix(platform): count a pool purchase in the batch's required balance, pause every pool inflow Review round 4 on the token shielded pools: - `all_purchases_amount` now counts `DirectPurchaseToPoolAction`, so a buyer whose credits cover the fee but not the price is refused with `IdentityInsufficientBalanceError` before execution instead of failing the balance removal on the execution path (a Drive error that would have stalled block processing); pinned by a test. - Mint, claim and purchase into the pool check the token status, so pause covers every pool operation as the PR describes; the book says so. - The identity-less purchase transform splits the fee with `checked_sub` and refuses a price over the credits leaving the pool as a consensus error. - The seven pool transformers share `bump_with_errors` for their paid failures. - The pool total balance's read-modify-write documents its dependence on the one-transition batch cap; the missing minimum-notes floor for token pools is documented in the book. Co-Authored-By: Claude Fable 5.1 --- book/src/data-model/token-shielded-pools.md | 16 +++-- .../state_v0/mod.rs | 14 +++- .../state_v0/mod.rs | 14 +++- .../state_v0/mod.rs | 14 +++- .../batch/tests/token/shielded/mod.rs | 32 +++++++++ .../transform_into_action/v0/mod.rs | 13 +++- .../src/drive/tokens/shielded_pool/mod.rs | 6 ++ .../token_transition/mod.rs | 29 ++++++++ .../v0/transformer.rs | 22 ++---- .../v0/transformer.rs | 70 ++++++------------- .../v0/transformer.rs | 40 +++-------- .../v0/transformer.rs | 22 ++---- .../v0/transformer.rs | 22 ++---- .../v0/transformer.rs | 22 ++---- .../v0/transformer.rs | 22 ++---- .../state_transition_action/batch/v0/mod.rs | 4 ++ 16 files changed, 197 insertions(+), 165 deletions(-) diff --git a/book/src/data-model/token-shielded-pools.md b/book/src/data-model/token-shielded-pools.md index 350967179be..490e85bdd11 100644 --- a/book/src/data-model/token-shielded-pools.md +++ b/book/src/data-model/token-shielded-pools.md @@ -63,8 +63,9 @@ balances, a token with `hasShieldedPool` must disable them permanently: `freezeR `unfreezeRules` and `destroyFrozenFundsRules` must each authorize no one to take the action and have no admin action takers, so no later configuration update can switch them on. Contract create and update reject anything else with `TokenShieldedPoolIncompatibleRulesError` (10278). -Pausing still works: shield, unshield and shielded transfer are all rejected while the token is -paused. A pooled token can never freeze an account, so the pool validators read and bill no +Pausing still works: every pool operation, inflows included (shield, mint, claim and purchase +into the pool) and outflows (unshield, shielded transfer, burn from the pool), is rejected while +the token is paused. A pooled token can never freeze an account, so the pool validators read and bill no frozen-account check. ## Batch transitions @@ -167,12 +168,19 @@ Structure validation checks the amount bounds, the action count against `SystemLimits::max_shielded_transition_actions`, the encrypted note sizes, a non-empty proof and a non-zero anchor. +Unlike the credit pool, which refuses an outgoing spend until it holds +`minimum_pool_notes_for_outgoing` (250) notes, a token pool has no such floor: +`minimum_token_pool_notes_for_outgoing` is 0, so a pool with a single note lets that note be +spent at once. The floor was left out so that a small token's pool is usable from its first +note; the price is that a spend from a nearly empty pool is linkable to the shield that filled +it. The constant lives in the event constants and can be raised by a later protocol version. + State validation runs in this order, and the first failure is returned: 1. The token base transition (contract exists, position valid, nonce). 2. `hasShieldedPool` on the token's configuration, else `TokenShieldedPoolNotEnabledError`. -3. Shield: the owner holds `amount` and the token is not paused. Unshield: the token is - not paused and the recipient identity exists. Shielded transfer: the token is not paused. +3. The token is not paused, for every operation. Shield: the owner holds `amount`. Unshield: + the recipient identity exists. Mint to pool: the configuration lets the minter choose the destination (`mintingAllowChoosingDestination`), the minting rules authorize the identity (or group) and the max supply is not exceeded. Burn from pool: the burning rules authorize diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs index 05e0020adad..dacab95e8d3 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs @@ -11,7 +11,7 @@ use drive::query::TransactionArg; use crate::execution::types::execution_operation::ValidationOperation; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContextMethodsV0; use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_pool_common::{ - validate_token_shielded_pool_enabled, verify_token_pool_bundle, + validate_token_not_paused, validate_token_shielded_pool_enabled, verify_token_pool_bundle, }; use crate::execution::validation::state_transition::state_transitions::shielded_common::FLAGS_OUTPUTS_ONLY; use dpp::consensus::state::state_error::StateError; @@ -71,6 +71,18 @@ impl TokenClaimToPoolTransitionActionStateValidationV0 for TokenClaimToPoolTrans let token_id = self.token_id(); + let validation_result = validate_token_not_paused( + platform, + token_id, + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + let contract = &self.data_contract_fetch_info_ref().contract; let token_configuration = contract.expected_token_configuration(self.token_position())?; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs index 3f1609cb676..b77c569d85f 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs @@ -11,7 +11,7 @@ use drive::query::TransactionArg; use crate::execution::types::execution_operation::ValidationOperation; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContextMethodsV0; use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_pool_common::{ - validate_token_shielded_pool_enabled, verify_token_pool_bundle, + validate_token_not_paused, validate_token_shielded_pool_enabled, verify_token_pool_bundle, }; use crate::execution::validation::state_transition::state_transitions::shielded_common::FLAGS_OUTPUTS_ONLY; use dpp::consensus::state::state_error::StateError; @@ -72,6 +72,18 @@ impl TokenDirectPurchaseToPoolTransitionActionStateValidationV0 let token_id = self.token_id(); + let validation_result = validate_token_not_paused( + platform, + token_id, + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + let contract = &self.data_contract_fetch_info_ref().contract; let token_configuration = contract.expected_token_configuration(self.token_position())?; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs index 0b8f500e2f6..97c105c7d14 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs @@ -11,7 +11,7 @@ use drive::query::TransactionArg; use crate::execution::types::execution_operation::ValidationOperation; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContextMethodsV0; use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_pool_common::{ - validate_token_shielded_pool_enabled, verify_token_pool_bundle, + validate_token_not_paused, validate_token_shielded_pool_enabled, verify_token_pool_bundle, }; use crate::execution::validation::state_transition::state_transitions::shielded_common::FLAGS_OUTPUTS_ONLY; use dpp::consensus::basic::token::ChoosingTokenMintRecipientNotAllowedError; @@ -81,6 +81,18 @@ impl TokenMintToPoolTransitionActionStateValidationV0 for TokenMintToPoolTransit let token_id = self.token_id(); + let validation_result = validate_token_not_paused( + platform, + token_id, + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + let contract = &self.data_contract_fetch_info_ref().contract; let token_configuration = contract.expected_token_configuration(self.token_position())?; // The notes' recipients are the minter's choice, so a mint into the pool is only diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index ea76535dfee..2380e436dd2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -2340,6 +2340,38 @@ mod token_pool_mint_burn_claim_purchase_tests { }] ); assert_eq!(pool_balance(&platform, token_id), 3); + + // A buyer whose credits cover the fee but not the price is refused before execution: + // the agreed price is part of the batch's required balance, so the shortfall is a + // consensus rejection and never reaches the balance removal. + let (poor_buyer, poor_signer, poor_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.02)); + let unaffordable = BatchTransition::new_token_direct_purchase_to_pool_transition( + token_id, + poor_buyer.id(), + contract.id(), + 0, + 3, + dash_to_credits!(0.03), + build_shield_bundle(3, 29), + &poor_key, + 1, + 0, + &poor_signer, + platform_version, + None, + ) + .await + .expect("token direct purchase to pool transition"); + let result = process(&platform, &unaffordable); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::StateError(StateError::IdentityInsufficientBalanceError(_)) + )] + ); + assert_eq!(pool_balance(&platform, token_id), 3); + assert_tokens_conserved(&platform); } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs index 684dc12db86..35443ec10aa 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs @@ -2,6 +2,8 @@ use crate::error::Error; use crate::execution::validation::state_transition::state_transitions::token_pool_paid_common::{ resolve_pooled_token, validate_credit_pool_fee_spend, }; +use dpp::consensus::basic::state_transition::ShieldedInvalidValueBalanceError; +use dpp::consensus::basic::BasicError; use dpp::consensus::state::token::{TokenMintPastMaxSupplyError, TokenNotForDirectSale}; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; @@ -102,7 +104,16 @@ impl TokenPurchaseFromShieldedPoolStateTransitionTransformIntoActionValidationV0 } // The credit bundle carries the price plus the fee; the fee was pinned by the // minimum-fee validation and the price by the schedule, so the split is exact. - let fee_amount = v0.credit_amount - v0.total_agreed_price; + let Some(fee_amount) = v0.credit_amount.checked_sub(v0.total_agreed_price) else { + return Ok(ConsensusValidationResult::new_with_error( + BasicError::ShieldedInvalidValueBalanceError( + ShieldedInvalidValueBalanceError::new( + "the credits leaving the pool must cover the agreed price".to_string(), + ), + ) + .into(), + )); + }; let fee_nullifiers: Vec<[u8; 32]> = v0.fee_actions.iter().map(|a| a.nullifier).collect(); let current_credit_pool_balance = match validate_credit_pool_fee_spend( drive, diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs index 76909af14e7..01067c351aa 100644 --- a/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs @@ -113,6 +113,12 @@ impl Drive { let apply = estimated_costs_only_with_layer_info.is_none(); + // The total is read from state and written back as an absolute value, so two pool + // operations on the same token in one batch would each start from the pre-batch + // total and the second write would discard the first. Every state transition applies + // in its own batch, and `SystemLimits::max_transitions_in_documents_batch` is 1, so a + // batch holds at most one pool operation (a document paid from the pool counts as + // one). If that cap is ever raised, this must become a delta on the sum item. let new_total_balance = match balance_change { TokenPoolBalanceChange::Unchanged => None, TokenPoolBalanceChange::Add(amount) => { diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/mod.rs index a2dcf1f2e81..3211f052539 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/mod.rs @@ -271,3 +271,32 @@ impl TokenTransitionAction { } } } + +/// The batched action a token transition becomes when it fails its transform: a bump of the +/// owner's contract nonce carrying `errors`, returned with the fee accrued so far so the +/// failure is paid. +pub(crate) fn bump_with_errors( + base: &dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition, + owner_id: dpp::identifier::Identifier, + user_fee_increase: dpp::prelude::UserFeeIncrease, + errors: Vec, + fee_result: dpp::fee::fee_result::FeeResult, +) -> ( + dpp::prelude::ConsensusValidationResult< + crate::state_transition_action::batch::BatchedTransitionAction, + >, + dpp::fee::fee_result::FeeResult, +) { + let bump_action = crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( + base, + owner_id, + user_fee_increase, + ); + ( + dpp::prelude::ConsensusValidationResult::new_with_data_and_errors( + crate::state_transition_action::batch::BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action), + errors, + ), + fee_result, + ) +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_from_pool_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_from_pool_transition_action/v0/transformer.rs index c59a11c04a7..cb818e4d8df 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_from_pool_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_burn_from_pool_transition_action/v0/transformer.rs @@ -3,9 +3,8 @@ use crate::drive::Drive; use crate::error::Error; use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; use crate::state_transition_action::batch::batched_transition::token_transition::token_burn_from_pool_transition_action::{TokenBurnFromPoolTransitionAction, TokenBurnFromPoolTransitionActionV0}; -use crate::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; +use crate::state_transition_action::batch::batched_transition::token_transition::{bump_with_errors, TokenTransitionAction}; use crate::state_transition_action::batch::BatchedTransitionAction; -use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; use dpp::block::block_info::BlockInfo; use dpp::fee::fee_result::FeeResult; use dpp::shielded::compute_shielded_verification_fee; @@ -82,20 +81,11 @@ impl TokenBurnFromPoolTransitionActionV0 { let (base_action, _change_note) = match base_action_validation_result.is_valid() { true => base_action_validation_result.into_data()?, false => { - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( - base, - owner_id, - user_fee_increase, - ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); - - return Ok(( - ConsensusValidationResult::new_with_data_and_errors( - batched_action, - base_action_validation_result.errors, - ), + return Ok(bump_with_errors( + base, + owner_id, + user_fee_increase, + base_action_validation_result.errors, fee_result, )); } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/v0/transformer.rs index 14aee4c61bf..38ec71157a2 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_to_pool_transition_action/v0/transformer.rs @@ -4,9 +4,8 @@ use crate::error::Error; use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; use crate::state_transition_action::batch::batched_transition::token_transition::token_claim_to_pool_transition_action::{TokenClaimToPoolTransitionAction, TokenClaimToPoolTransitionActionV0}; use crate::state_transition_action::batch::batched_transition::token_transition::token_claim_transition_action::resolve_token_claim; -use crate::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; +use crate::state_transition_action::batch::batched_transition::token_transition::{bump_with_errors, TokenTransitionAction}; use crate::state_transition_action::batch::BatchedTransitionAction; -use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; use dpp::block::block_info::BlockInfo; use dpp::consensus::state::state_error::StateError; use dpp::consensus::state::token::InvalidTokenClaimPropertyMismatch; @@ -92,20 +91,11 @@ impl TokenClaimToPoolTransitionActionV0 { let (base_action, _change_note) = match base_action_validation_result.is_valid() { true => base_action_validation_result.into_data()?, false => { - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( - base, - owner_id, - user_fee_increase, - ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); - - return Ok(( - ConsensusValidationResult::new_with_data_and_errors( - batched_action, - base_action_validation_result.errors, - ), + return Ok(bump_with_errors( + base, + owner_id, + user_fee_increase, + base_action_validation_result.errors, fee_result, )); } @@ -114,26 +104,18 @@ impl TokenClaimToPoolTransitionActionV0 { // A perpetual claim into the pool must name the moment it claims up to: the bundle // proves an exact amount, which only a pinned moment makes predictable. if matches!(distribution_type, TokenDistributionType::Perpetual) && claim_up_to.is_none() { - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( - base, - owner_id, - user_fee_increase, - ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); - return Ok(( - ConsensusValidationResult::new_with_data_and_errors( - batched_action, - vec![ConsensusError::StateError( - StateError::InvalidTokenClaimPropertyMismatch( - InvalidTokenClaimPropertyMismatch::new( - "claim up to moment", - base.token_id(), - ), + return Ok(bump_with_errors( + base, + owner_id, + user_fee_increase, + vec![ConsensusError::StateError( + StateError::InvalidTokenClaimPropertyMismatch( + InvalidTokenClaimPropertyMismatch::new( + "claim up to moment", + base.token_id(), ), - )], - ), + ), + )], fee_result, )); } @@ -151,19 +133,11 @@ impl TokenClaimToPoolTransitionActionV0 { )? { Ok(resolved) => resolved, Err(consensus_error) => { - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( - base, - owner_id, - user_fee_increase, - ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); - return Ok(( - ConsensusValidationResult::new_with_data_and_errors( - batched_action, - vec![consensus_error], - ), + return Ok(bump_with_errors( + base, + owner_id, + user_fee_increase, + vec![consensus_error], fee_result, )); } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_to_pool_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_to_pool_transition_action/v0/transformer.rs index d997a2dc748..1511aa3e142 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_to_pool_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_to_pool_transition_action/v0/transformer.rs @@ -4,9 +4,8 @@ use crate::error::Error; use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; use crate::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_to_pool_transition_action::{TokenDirectPurchaseToPoolTransitionAction, TokenDirectPurchaseToPoolTransitionActionV0}; use crate::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_transition_action::resolve_direct_purchase_price; -use crate::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; +use crate::state_transition_action::batch::batched_transition::token_transition::{bump_with_errors, TokenTransitionAction}; use crate::state_transition_action::batch::BatchedTransitionAction; -use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; use dpp::block::block_info::BlockInfo; use dpp::fee::fee_result::FeeResult; use dpp::shielded::compute_shielded_verification_fee; @@ -83,20 +82,11 @@ impl TokenDirectPurchaseToPoolTransitionActionV0 { let (base_action, _change_note) = match base_action_validation_result.is_valid() { true => base_action_validation_result.into_data()?, false => { - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( - base, - owner_id, - user_fee_increase, - ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); - - return Ok(( - ConsensusValidationResult::new_with_data_and_errors( - batched_action, - base_action_validation_result.errors, - ), + return Ok(bump_with_errors( + base, + owner_id, + user_fee_increase, + base_action_validation_result.errors, fee_result, )); } @@ -114,19 +104,11 @@ impl TokenDirectPurchaseToPoolTransitionActionV0 { )? { Ok(required_price) => required_price, Err(error) => { - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( - base, - owner_id, - user_fee_increase, - ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); - return Ok(( - ConsensusValidationResult::new_with_data_and_errors( - batched_action, - vec![error], - ), + return Ok(bump_with_errors( + base, + owner_id, + user_fee_increase, + vec![error], fee_result, )); } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_to_pool_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_to_pool_transition_action/v0/transformer.rs index 739a21609d8..e0af04f8d4f 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_to_pool_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_mint_to_pool_transition_action/v0/transformer.rs @@ -3,9 +3,8 @@ use crate::drive::Drive; use crate::error::Error; use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; use crate::state_transition_action::batch::batched_transition::token_transition::token_mint_to_pool_transition_action::{TokenMintToPoolTransitionAction, TokenMintToPoolTransitionActionV0}; -use crate::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; +use crate::state_transition_action::batch::batched_transition::token_transition::{bump_with_errors, TokenTransitionAction}; use crate::state_transition_action::batch::BatchedTransitionAction; -use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; use dpp::block::block_info::BlockInfo; use dpp::fee::fee_result::FeeResult; use dpp::shielded::compute_shielded_verification_fee; @@ -82,20 +81,11 @@ impl TokenMintToPoolTransitionActionV0 { let (base_action, _change_note) = match base_action_validation_result.is_valid() { true => base_action_validation_result.into_data()?, false => { - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( - base, - owner_id, - user_fee_increase, - ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); - - return Ok(( - ConsensusValidationResult::new_with_data_and_errors( - batched_action, - base_action_validation_result.errors, - ), + return Ok(bump_with_errors( + base, + owner_id, + user_fee_increase, + base_action_validation_result.errors, fee_result, )); } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/v0/transformer.rs index 27575c8892c..23b2e58f096 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shield_transition_action/v0/transformer.rs @@ -3,9 +3,8 @@ use crate::drive::Drive; use crate::error::Error; use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; use crate::state_transition_action::batch::batched_transition::token_transition::token_shield_transition_action::{TokenShieldTransitionAction, TokenShieldTransitionActionV0}; -use crate::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; +use crate::state_transition_action::batch::batched_transition::token_transition::{bump_with_errors, TokenTransitionAction}; use crate::state_transition_action::batch::BatchedTransitionAction; -use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; use dpp::block::block_info::BlockInfo; use dpp::fee::fee_result::FeeResult; use dpp::shielded::compute_shielded_verification_fee; @@ -83,20 +82,11 @@ impl TokenShieldTransitionActionV0 { let (base_action, _change_note) = match base_action_validation_result.is_valid() { true => base_action_validation_result.into_data()?, false => { - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( - base, - owner_id, - user_fee_increase, - ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); - - return Ok(( - ConsensusValidationResult::new_with_data_and_errors( - batched_action, - base_action_validation_result.errors, - ), + return Ok(bump_with_errors( + base, + owner_id, + user_fee_increase, + base_action_validation_result.errors, fee_result, )); } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/v0/transformer.rs index 9280f1f4637..0fa2cf911ec 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_shielded_transfer_transition_action/v0/transformer.rs @@ -3,9 +3,8 @@ use crate::drive::Drive; use crate::error::Error; use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; use crate::state_transition_action::batch::batched_transition::token_transition::token_shielded_transfer_transition_action::{TokenShieldedTransferTransitionAction, TokenShieldedTransferTransitionActionV0}; -use crate::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; +use crate::state_transition_action::batch::batched_transition::token_transition::{bump_with_errors, TokenTransitionAction}; use crate::state_transition_action::batch::BatchedTransitionAction; -use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; use dpp::block::block_info::BlockInfo; use dpp::fee::fee_result::FeeResult; use dpp::shielded::compute_shielded_verification_fee; @@ -82,20 +81,11 @@ impl TokenShieldedTransferTransitionActionV0 { let (base_action, _change_note) = match base_action_validation_result.is_valid() { true => base_action_validation_result.into_data()?, false => { - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( - base, - owner_id, - user_fee_increase, - ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); - - return Ok(( - ConsensusValidationResult::new_with_data_and_errors( - batched_action, - base_action_validation_result.errors, - ), + return Ok(bump_with_errors( + base, + owner_id, + user_fee_increase, + base_action_validation_result.errors, fee_result, )); } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/v0/transformer.rs index 1082b87a2e9..78c47fa04a1 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_unshield_transition_action/v0/transformer.rs @@ -3,9 +3,8 @@ use crate::drive::Drive; use crate::error::Error; use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionAction; use crate::state_transition_action::batch::batched_transition::token_transition::token_unshield_transition_action::{TokenUnshieldTransitionAction, TokenUnshieldTransitionActionV0}; -use crate::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; +use crate::state_transition_action::batch::batched_transition::token_transition::{bump_with_errors, TokenTransitionAction}; use crate::state_transition_action::batch::BatchedTransitionAction; -use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; use dpp::block::block_info::BlockInfo; use dpp::fee::fee_result::FeeResult; use dpp::shielded::compute_shielded_verification_fee; @@ -84,20 +83,11 @@ impl TokenUnshieldTransitionActionV0 { let (base_action, _change_note) = match base_action_validation_result.is_valid() { true => base_action_validation_result.into_data()?, false => { - let bump_action = - BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( - base, - owner_id, - user_fee_increase, - ); - let batched_action = - BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action); - - return Ok(( - ConsensusValidationResult::new_with_data_and_errors( - batched_action, - base_action_validation_result.errors, - ), + return Ok(bump_with_errors( + base, + owner_id, + user_fee_increase, + base_action_validation_result.errors, fee_result, )); } diff --git a/packages/rs-drive/src/state_transition_action/batch/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/v0/mod.rs index 0005f748125..1429c8b7bff 100644 --- a/packages/rs-drive/src/state_transition_action/batch/v0/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/v0/mod.rs @@ -20,6 +20,7 @@ use crate::state_transition_action::batch::batched_transition::BatchedTransition use crate::state_transition_action::batch::batched_transition::document_transition::document_create_transition_action::DocumentCreateTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::document_transition::document_purchase_transition_action::DocumentPurchaseTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_transition_action::TokenDirectPurchaseTransitionActionAccessorsV0; +use crate::state_transition_action::batch::batched_transition::token_transition::token_direct_purchase_to_pool_transition_action::TokenDirectPurchaseToPoolTransitionActionAccessorsV0; use crate::state_transition_action::batch::batched_transition::token_transition::TokenTransitionAction; /// action v0 @@ -193,6 +194,9 @@ impl BatchTransitionActionV0 { BatchedTransitionAction::TokenAction( TokenTransitionAction::DirectPurchaseAction(token_purchase), ) => Some(token_purchase.total_agreed_price()), + BatchedTransitionAction::TokenAction( + TokenTransitionAction::DirectPurchaseToPoolAction(token_purchase), + ) => Some(token_purchase.total_agreed_price()), _ => None, }) .fold((None, false), |(acc, _), price| match acc { From f539bfa71fbbf6efaff6a588e04913c1bf5ab27e Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 22 Sep 2026 12:27:53 +0700 Subject: [PATCH 15/94] fix(platform)!: land token shielded pools in protocol version 14 The feature was written against v4.3-dev, where it opened protocol version 15. On v4.2-dev protocol version 14 is the unreleased version, so the change belongs in v14's own snapshot and there is no v15. Version tables follow released boundaries, not PRs: a second feature landing in the same unreleased protocol version amends that version's table constants rather than adding new ones, which would leave a constant referenced by no PLATFORM_V*. So CONTRACT_VERSIONS_V7, DPP_VALIDATION_VERSIONS_V6, DRIVE_ABCI_METHOD_VERSIONS_V11, DRIVE_ABCI_VALIDATION_VERSIONS_V11, DRIVE_CONTRACT_METHOD_VERSIONS_V5, DRIVE_TOKEN_METHOD_VERSIONS_V3, DRIVE_VERSION_V10 and PLATFORM_V15 are gone, folded into the V6/V5/V10/V4/V2/V9 constants v14 already selects. v14.rs gains the feature as a numbered changelog item and a `// changed:` note on each slot it moves. The same rule applies to an implementation generation that is still unreleased: the repository amends it in place rather than stacking another one on top (eight v14 features have amended data_contract_create's basic_structure v2, and the same holds for initialization v4, insert_contract v2 and update_contract v2). create_initial_state_structure v5, insert_contract v3, update_contract v3, both basic_structure v3 modules and validate_shielded_proof v2 are therefore folded into the v14 generation they extended, and transition_to_version_15 into transition_to_version_14. The three generations that supersede shipped (v13 and earlier) code stay as new modules: calculate_total_tokens_balance v1, document_base_transition_state_validation v1 and validate_token_config_update v1. TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION becomes 14, the three shielded-fee token transitions join the existing 14..=LATEST_VERSION bounds arm rather than an empty 15..=LATEST_VERSION range, the GroveDB structure description records the pools since 14, and the tests that pinned the pre-feature version move from 14 to 13. Every table amended here is reachable only from PLATFORM_V14; released versions keep the neutral backfills (None, 0, max_version 0), so no shipped behaviour moves. Co-Authored-By: Claude Opus 5 (1M context) --- book/src/data-model/token-shielded-pools.md | 8 +- book/src/fees/shielded-fees.md | 2 +- .../platform/v0/objective-c/Platform.pbobjc.h | 2 +- .../protos/platform/v0/platform.proto | 2 +- .../src/balances/total_tokens_balance/mod.rs | 2 +- .../v1/mod.rs | 4 +- .../token_configuration/mod.rs | 2 +- packages/rs-dpp/src/state_transition/mod.rs | 7 +- .../src/tokens/token_payment_info/mod.rs | 2 +- .../v0/mod.rs | 49 +-- .../processor/traits/shielded_proof.rs | 30 +- .../state_v1/mod.rs | 2 +- .../batch/tests/token/shielded/mod.rs | 4 +- .../basic_structure/mod.rs | 1 - .../basic_structure/v2/mod.rs | 9 +- .../basic_structure/v3/mod.rs | 35 -- .../data_contract_create/mod.rs | 6 +- .../basic_structure/mod.rs | 1 - .../basic_structure/v2/mod.rs | 9 +- .../basic_structure/v3/mod.rs | 35 -- .../data_contract_update/mod.rs | 4 +- .../rs-drive-abci/src/query/shielded/mod.rs | 2 +- packages/rs-drive-proof-verifier/src/types.rs | 2 +- packages/rs-drive/grovedb-structure.json | 92 ++--- .../contract/insert/insert_contract/mod.rs | 17 +- .../contract/insert/insert_contract/v2/mod.rs | 34 +- .../contract/insert/insert_contract/v3/mod.rs | 141 ------- .../contract/update/update_contract/mod.rs | 34 +- .../contract/update/update_contract/v2/mod.rs | 158 ++++++++ .../contract/update/update_contract/v3/mod.rs | 370 ------------------ .../rs-drive/src/drive/initialization/mod.rs | 4 +- .../src/drive/initialization/v4/mod.rs | 7 + .../src/drive/initialization/v5/mod.rs | 19 - .../calculate_total_tokens_balance/v0/mod.rs | 2 +- .../calculate_total_tokens_balance/v1/mod.rs | 2 +- packages/rs-drive/src/drive/tokens/paths.rs | 2 +- .../tokens/shielded_pool/insert_root_tree.rs | 6 +- .../rs-drive/src/drive/tokens/structure.rs | 4 +- .../dpp_versions/dpp_contract_versions/mod.rs | 1 - .../dpp_versions/dpp_contract_versions/v6.rs | 2 +- .../dpp_versions/dpp_contract_versions/v7.rs | 16 - .../dpp_validation_versions/mod.rs | 1 - .../dpp_validation_versions/v5.rs | 1 + .../dpp_validation_versions/v6.rs | 11 - .../drive_abci_method_versions/mod.rs | 1 - .../drive_abci_method_versions/v10.rs | 2 +- .../drive_abci_method_versions/v11.rs | 11 - .../drive_abci_validation_versions/mod.rs | 1 - .../drive_abci_validation_versions/v10.rs | 8 +- .../drive_abci_validation_versions/v11.rs | 54 --- .../drive_contract_method_versions/mod.rs | 1 - .../drive_contract_method_versions/v5.rs | 19 - .../drive_token_method_versions/mod.rs | 1 - .../drive_token_method_versions/v2.rs | 1 + .../drive_token_method_versions/v3.rs | 8 - .../src/version/drive_versions/mod.rs | 1 - .../src/version/drive_versions/v10.rs | 17 - .../feature_initial_protocol_versions.rs | 4 +- .../rs-platform-version/src/version/mod.rs | 5 +- .../src/version/protocol_version.rs | 4 +- .../rs-platform-version/src/version/v14.rs | 38 +- .../rs-platform-version/src/version/v15.rs | 55 --- packages/rs-sdk/src/platform/query.rs | 2 +- .../batch/token_payment_info.rs | 2 +- .../configuration/token_configuration.rs | 2 +- 65 files changed, 375 insertions(+), 1006 deletions(-) delete mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v3/mod.rs delete mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v3/mod.rs delete mode 100644 packages/rs-drive/src/drive/contract/insert/insert_contract/v3/mod.rs delete mode 100644 packages/rs-drive/src/drive/contract/update/update_contract/v3/mod.rs delete mode 100644 packages/rs-drive/src/drive/initialization/v5/mod.rs delete mode 100644 packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v7.rs delete mode 100644 packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v6.rs delete mode 100644 packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs delete mode 100644 packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs delete mode 100644 packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v5.rs delete mode 100644 packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v3.rs delete mode 100644 packages/rs-platform-version/src/version/drive_versions/v10.rs delete mode 100644 packages/rs-platform-version/src/version/v15.rs diff --git a/book/src/data-model/token-shielded-pools.md b/book/src/data-model/token-shielded-pools.md index 490e85bdd11..a8a0155183d 100644 --- a/book/src/data-model/token-shielded-pools.md +++ b/book/src/data-model/token-shielded-pools.md @@ -1,6 +1,6 @@ # Token Shielded Pools -From protocol version 15 a token can own a shielded pool: an Orchard pool that holds that token +From protocol version 14 a token can own a shielded pool: an Orchard pool that holds that token instead of credits. Holders move tokens between their identity balance and the pool with token transitions inside a batch, issuers mint, burn, release and sell straight into or out of it, document costs can be paid out of it, and three identity-less transitions move tokens with the @@ -37,7 +37,7 @@ a pool-agnostic form taking the pool path, and a token twin that supplies the to root of all token pools is a BigSumTree so the amount of every token that is shielded is one sum, which the token conservation check reads. -The root tree is created by the version 15 upgrade transition (`transition_to_version_15`) on +The root tree is created by the version 14 upgrade transition (`transition_to_version_14`) on an existing chain and by `create_initial_state_structure` version 4 on a new one. A pool's five trees are created when a contract with the flag is inserted or updated. @@ -47,7 +47,7 @@ trees are created when a contract with the flag is inserted or updated. `hasShieldedPool: bool`. A version 0 configuration behaves as `hasShieldedPool: false`. The format version is admitted by `dpp.contract_versions.token_versions.token_configuration_format`: protocol versions 13 and -below allow only version 0, protocol version 15 allows versions 0 and 1. Contract create and +below allow only version 0, protocol version 14 allows versions 0 and 1. Contract create and update reject a token configuration outside the bounds with `UnsupportedVersionError`, so a pre-14 network never stores the flag. @@ -238,7 +238,7 @@ The six shielded pool queries (`getShieldedPoolState`, `getShieldedNotesCount`, `getShieldedAnchors`, `getMostRecentShieldedAnchor`, `getShieldedEncryptedNotes`, `getShieldedNullifiers`) take an optional `token_id`. Without it they target the credit pool; with a 32-byte token id they target that token's pool and answer with the same response shape. -A token id is rejected with `InvalidArgument` before protocol version 15 or when it is not 32 +A token id is rejected with `InvalidArgument` before protocol version 14 or when it is not 32 bytes. The proof verifier routes on the same field to the token twins of the verify functions (`verify_token_shielded_pool_state` and the rest), and the Rust SDK exposes `TokenShieldedPoolQuery`, `TokenShieldedEncryptedNotesQuery` and `TokenShieldedNullifiersQuery`. diff --git a/book/src/fees/shielded-fees.md b/book/src/fees/shielded-fees.md index 6b6646b1448..73c4d97c857 100644 --- a/book/src/fees/shielded-fees.md +++ b/book/src/fees/shielded-fees.md @@ -48,7 +48,7 @@ The fee is derived differently depending on the shielded transition type: ### Token shielded pool fees -Token pools (protocol version 15, see [Token Shielded Pools](../data-model/token-shielded-pools.md)) +Token pools (protocol version 14, see [Token Shielded Pools](../data-model/token-shielded-pools.md)) hold tokens, and tokens cannot pay fees, so none of the three token pool transitions carves a fee from the bundle. They are `TokenTransition` variants inside a `Batch`, and the batch's signing identity pays in credits through the standard identity-paid path. diff --git a/packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.h b/packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.h index 0f400eb8083..551450e5473 100644 --- a/packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.h +++ b/packages/dapi-grpc/clients/platform/v0/objective-c/Platform.pbobjc.h @@ -12759,7 +12759,7 @@ typedef GPB_ENUM(GetShieldedEncryptedNotesRequest_Version_OneOfCase) { /** * Every shielded pool query below targets the credit shielded pool unless `token_id` is set, - * in which case it targets that token's own shielded pool (protocol version 15+; the 32-byte + * in which case it targets that token's own shielded pool (protocol version 14+; the 32-byte * token id). Responses have the same shape for both pools. **/ GPB_FINAL @interface GetShieldedEncryptedNotesRequest : GPBMessage diff --git a/packages/dapi-grpc/protos/platform/v0/platform.proto b/packages/dapi-grpc/protos/platform/v0/platform.proto index 19b7a01bb8d..0f21adbdc85 100644 --- a/packages/dapi-grpc/protos/platform/v0/platform.proto +++ b/packages/dapi-grpc/protos/platform/v0/platform.proto @@ -3847,7 +3847,7 @@ message GetRecentCompactedAddressBalanceChangesResponse { // --- Shielded Pool Queries --- // Every shielded pool query below targets the credit shielded pool unless `token_id` is set, -// in which case it targets that token's own shielded pool (protocol version 15+; the 32-byte +// in which case it targets that token's own shielded pool (protocol version 14+; the 32-byte // token id). Responses have the same shape for both pools. message GetShieldedEncryptedNotesRequest { message GetShieldedEncryptedNotesRequestV0 { diff --git a/packages/rs-dpp/src/balances/total_tokens_balance/mod.rs b/packages/rs-dpp/src/balances/total_tokens_balance/mod.rs index 02cafebabd8..7f920242e54 100644 --- a/packages/rs-dpp/src/balances/total_tokens_balance/mod.rs +++ b/packages/rs-dpp/src/balances/total_tokens_balance/mod.rs @@ -9,7 +9,7 @@ pub struct TotalTokensBalance { pub total_tokens_in_platform: SumTokenAmount, /// all the tokens in identity token balances pub total_identity_token_balances: SumTokenAmount, - /// all the tokens held in token shielded pools (0 before protocol version 15) + /// all the tokens held in token shielded pools (0 before protocol version 14) pub total_token_shielded_pool_balances: SumTokenAmount, } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/v1/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/v1/mod.rs index 529fccb1c8c..351026aabe9 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/v1/mod.rs @@ -61,7 +61,7 @@ mod tests { let mut shielded = plain.clone(); shielded.set_has_shielded_pool(true); - for platform_version in [PlatformVersion::get(14).unwrap(), PlatformVersion::latest()] { + for platform_version in [PlatformVersion::get(13).unwrap(), PlatformVersion::latest()] { for (old, new) in [ (&plain, &shielded), (&shielded, &plain), @@ -79,7 +79,7 @@ mod tests { .expect("validate token configuration update"); assert_eq!( result.is_valid(), - platform_version.protocol_version < 15 + platform_version.protocol_version < 14 || old.has_shielded_pool() == new.has_shielded_pool(), "unexpected update validation on protocol {}: {:?}", platform_version.protocol_version, diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs index caaaa1f065d..269d290605f 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs @@ -34,7 +34,7 @@ pub mod v1; pub enum TokenConfiguration { #[serde(rename = "0")] V0(TokenConfigurationV0), - /// V0 plus the per-token shielded pool opt-in. Admitted from protocol version 15 + /// V0 plus the per-token shielded pool opt-in. Admitted from protocol version 14 /// (`token_versions.token_configuration_format`). #[serde(rename = "1")] V1(TokenConfigurationV1), diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index 01d05e58996..d909f6001a0 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -1135,11 +1135,10 @@ impl StateTransition { | StateTransition::IdentityTopUpFromShieldedPool(_) | StateTransition::IdentityKeyLimitsUpdate(_) | StateTransition::ContractUserModeration(_) - | StateTransition::ContractFeeClaim(_) => 14..=LATEST_VERSION, - - StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::ContractFeeClaim(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) | StateTransition::TokenUnshieldWithShieldedFee(_) - | StateTransition::TokenPurchaseFromShieldedPool(_) => 15..=LATEST_VERSION, + | StateTransition::TokenPurchaseFromShieldedPool(_) => 14..=LATEST_VERSION, } } diff --git a/packages/rs-dpp/src/tokens/token_payment_info/mod.rs b/packages/rs-dpp/src/tokens/token_payment_info/mod.rs index 69e86642a61..38812d6e01d 100644 --- a/packages/rs-dpp/src/tokens/token_payment_info/mod.rs +++ b/packages/rs-dpp/src/tokens/token_payment_info/mod.rs @@ -8,7 +8,7 @@ //! The enum is versioned to allow future evolution without breaking callers. //! [`v0::TokenPaymentInfoV0`] pays from the identity's token balance; [`v1::TokenPaymentInfoV1`] //! adds a [`v1::TokenShieldedPayment`], a spend bundle that pays the cost out of the token's -//! shielded pool instead (protocol version 15 and up). Accessors are provided via +//! shielded pool instead (protocol version 14 and up). Accessors are provided via //! [`v0::v0_accessors::TokenPaymentInfoAccessorsV0`], and convenience methods (such //! as `token_id()` and `is_valid_for_required_cost()`) are available through //! [`methods::v0::TokenPaymentInfoMethodsV0`]. diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index aad476dd146..f1f724bef0e 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -119,10 +119,6 @@ impl Platform { self.transition_to_version_14(block_info, transaction, platform_version)?; } - if previous_protocol_version < 15 && platform_version.protocol_version >= 15 { - self.transition_to_version_15(transaction, platform_version)?; - } - Ok(()) } @@ -815,21 +811,14 @@ impl Platform { // exists, so both node populations build the same prefunded balances Merk. self.drive .insert_contract_fee_pot_trees(Some(transaction), platform_version)?; - Ok(()) - } - /// When transitioning to version 15 we insert the token shielded pools root: the BigSumTree - /// under the Tokens tree that holds one Orchard pool per token opting in - /// (`TokenConfigurationV1::has_shielded_pool`). CONSENSUS-CRITICAL: the genesis-v15 path - /// (`Drive::create_initial_state_structure_v5`) calls the same helper, so a chain born at v15 - /// and one upgraded to it build a byte-identical `[Tokens]` subtree. - fn transition_to_version_15( - &self, - transaction: &Transaction, - platform_version: &PlatformVersion, - ) -> Result<(), Error> { + + // Token shielded pools root: the BigSumTree under the Tokens tree that holds one Orchard + // pool per token opting in (`TokenConfigurationV1::has_shielded_pool`). + // CONSENSUS-CRITICAL: the genesis path (`Drive::create_initial_state_structure_v4`) calls + // the same helper, so a chain born at this version and one upgraded to it build a + // byte-identical `[Tokens]` subtree. self.drive .insert_token_shielded_pools_root_tree(Some(transaction), platform_version)?; - Ok(()) } } @@ -3112,23 +3101,23 @@ mod tests { /// because both paths now call the shared /// `Drive::insert_shielded_pool_structure`). /// The token shielded pools root is built by two paths that must coincide byte for byte: - /// `Drive::create_initial_state_structure_v5` at a v15 genesis and - /// `transition_to_version_15` on a chain upgraded from v14. + /// `Drive::create_initial_state_structure_v4` at a v14 genesis and + /// `transition_to_version_14` on a chain upgraded from v13. #[test] - fn test_genesis_v15_and_upgrade_to_v15_build_identical_token_shielded_pools_root() { - let platform_version_15 = PlatformVersion::get(15).expect("expected v15"); + fn test_genesis_v14_and_upgrade_to_v14_build_identical_token_shielded_pools_root() { + let platform_version_14 = PlatformVersion::get(14).expect("expected v14"); let platform_a = TestPlatformBuilder::new() - .with_initial_protocol_version(15) + .with_initial_protocol_version(14) .build_with_mock_rpc() .set_genesis_state(); let platform_b = TestPlatformBuilder::new() - .with_initial_protocol_version(14) + .with_initial_protocol_version(13) .build_with_mock_rpc() .set_genesis_state(); - // A genuine v14 genesis must not contain the pools root yet. + // A genuine v13 genesis must not contain the pools root yet. { let txn = platform_b.drive.grove.start_transaction(); let tokens_path: [&[u8]; 1] = [&[RootTree::Tokens as u8]]; @@ -3136,19 +3125,19 @@ mod tests { SubtreePath::from(&tokens_path[..]), &[TOKEN_SHIELDED_POOLS_KEY], Some(&txn), - &platform_version_15.drive.grove_version, + &platform_version_14.drive.grove_version, ); assert!( pools_root_pre.value.is_err(), - "v14 genesis must not contain the token shielded pools root before the upgrade; got {:?}", + "v13 genesis must not contain the token shielded pools root before the upgrade; got {:?}", pools_root_pre.value ); } let txn_b = platform_b.drive.grove.start_transaction(); platform_b - .transition_to_version_15(&txn_b, platform_version_15) - .expect("upgrade: transition_to_version_15 should succeed"); + .transition_to_version_14(&BlockInfo::default(), &txn_b, platform_version_14) + .expect("upgrade: transition_to_version_14 should succeed"); let diffs = collect_subtree_diffs( &platform_a, @@ -3158,8 +3147,8 @@ mod tests { ); assert!( diffs.is_empty(), - "CONSENSUS FORK: the [Tokens] subtree differs between a fresh genesis-v15 node and an \ - in-place-upgraded v15 node.\n{}", + "CONSENSUS FORK: the [Tokens] subtree differs between a fresh genesis-v14 node and an \ + in-place-upgraded v14 node.\n{}", diffs.join("\n"), ); } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs index f370fa63e79..7e007a5cf02 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs @@ -708,24 +708,10 @@ impl StateTransitionShieldedProofValidationV0 for StateTransition { .validate_shielded_proof { 0 => validate_shielded_proof_v0(self, platform_version), - 1 => { - // v1 refuses, in `IdentityCreateFromShieldedPool`, the keys the v0 Orchard sighash - // preimage cannot bind, before that preimage is built: a key bound to a contract - // group (the layout predates group bounds, and an error out of the preimage - // builder would be an internal error rather than a rejection) and a key that - // carries a budget or an expiry (they are not in the layout, so nothing would - // stop a relay from altering them when no key has a proof of possession). A - // version 1 key without limits is fine: everything it holds is in the layout. - // Everything else is v0. - if let Some(error) = key_not_allowed_in_shielded_creation(self) { - return Ok(SimpleConsensusValidationResult::new_with_error(error)); - } - validate_shielded_proof_v0(self, platform_version) - } - 2 => validate_shielded_proof_v2(self, platform_version), + 1 => validate_shielded_proof_v1(self, platform_version), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "StateTransition::validate_shielded_proof".to_string(), - known_versions: vec![0, 1, 2], + known_versions: vec![0, 1], received: version, })), } @@ -1078,8 +1064,16 @@ fn validate_batch_token_shielded_proofs( Ok(SimpleConsensusValidationResult::new()) } -/// Protocol 15 proof validation adds token pools while retaining the identity key restrictions. -fn validate_shielded_proof_v2( +/// Generation 1 (protocol version 14): the credit pool rules of v0, the token shielded pools, +/// and the identity key restrictions v0 cannot express. +/// +/// In `IdentityCreateFromShieldedPool` it refuses the keys the v0 Orchard sighash preimage cannot +/// bind, before that preimage is built: a key bound to a contract group (the layout predates group +/// bounds, and an error out of the preimage builder would be an internal error rather than a +/// rejection) and a key that carries a budget or an expiry (they are not in the layout, so nothing +/// would stop a relay from altering them when no key has a proof of possession). A version 1 key +/// without limits is fine: everything it holds is in the layout. +fn validate_shielded_proof_v1( state_transition: &StateTransition, platform_version: &PlatformVersion, ) -> Result { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_base_transaction_action/state_v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_base_transaction_action/state_v1/mod.rs index 4d58f4d7438..d07c520e15f 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_base_transaction_action/state_v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_base_transaction_action/state_v1/mod.rs @@ -24,7 +24,7 @@ pub(in crate::execution::validation::state_transition::state_transitions::batch: } impl DocumentBaseTransitionActionStateValidationV1 for DocumentBaseTransitionAction { - /// Version 1 (protocol version 15): a token cost paid out of the token's shielded pool + /// Version 1 (protocol version 14): a token cost paid out of the token's shielded pool /// (`TokenPaymentInfo::V1`) does not touch the owner's token balance, so the balance and /// frozen-account checks of v0 do not apply; the pool side (pool exists, token not paused, /// anchor, unspent nullifiers, pool balance and the proof) is validated by diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 2380e436dd2..79b0a7a3323 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -1061,7 +1061,7 @@ mod token_shielded_pool_tests { #[tokio::test] async fn should_gate_new_shielded_tokens_and_validate_their_rules_on_contract_update() { for (platform_version, incompatible_rules) in [ - (PlatformVersion::get(14).unwrap(), false), + (PlatformVersion::get(13).unwrap(), false), (PlatformVersion::latest(), true), (PlatformVersion::latest(), false), ] { @@ -1132,7 +1132,7 @@ mod token_shielded_pool_tests { ) .expect("process contract update"); - if platform_version.protocol_version < 15 { + if platform_version.protocol_version < 14 { assert_matches!( result.execution_results().as_slice(), [StateTransitionExecutionResult::UnpaidConsensusError( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/mod.rs index 1b391a4bcc9..4598692d1d6 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/mod.rs @@ -1,4 +1,3 @@ pub(crate) mod v0; pub(crate) mod v1; pub(crate) mod v2; -pub(crate) mod v3; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v2/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v2/mod.rs index e4f819a011c..05548adabf4 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v2/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v2/mod.rs @@ -11,6 +11,7 @@ use dpp::consensus::ConsensusError; use dpp::contract_group::ContractGroupMember; use dpp::dashcore::Network; use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dpp::data_contract::associated_token::token_configuration::validate_token_configurations; use dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; use dpp::data_contract::config::v2::DataContractConfigGettersV2; use dpp::data_contract::document_type::action_fees::DocumentActionFees; @@ -154,7 +155,13 @@ impl DataContractCreateStateTransitionBasicStructureValidationV2 for DataContrac } } - Ok(SimpleConsensusValidationResult::new()) + // The token configurations the contract declares: a token that opts into a shielded + // pool must leave its freeze, unfreeze and destroy-frozen-funds rules unassigned, + // since notes in a pool have no owner to freeze. + Ok(validate_token_configurations( + self.data_contract().tokens(), + platform_version, + )) } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v3/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v3/mod.rs deleted file mode 100644 index 93a891fe990..00000000000 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/basic_structure/v3/mod.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::error::Error; -use dpp::dashcore::Network; -use dpp::data_contract::associated_token::token_configuration::validate_token_configurations; -use dpp::state_transition::data_contract_create_transition::accessors::DataContractCreateTransitionAccessorsV0; -use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; -use dpp::validation::SimpleConsensusValidationResult; -use dpp::version::PlatformVersion; - -use super::v2::DataContractCreateStateTransitionBasicStructureValidationV2; - -pub(in crate::execution::validation::state_transition::state_transitions::data_contract_create) trait DataContractCreateStateTransitionBasicStructureValidationV3 { - fn validate_basic_structure_v3( - &self, - network_type: Network, - platform_version: &PlatformVersion, - ) -> Result; -} - -impl DataContractCreateStateTransitionBasicStructureValidationV3 for DataContractCreateTransition { - /// Protocol 15 retains the previous rules and validates token shielded pool opt-ins. - fn validate_basic_structure_v3( - &self, - network_type: Network, - platform_version: &PlatformVersion, - ) -> Result { - let result = self.validate_basic_structure_v2(network_type, platform_version)?; - if !result.is_valid() { - return Ok(result); - } - Ok(validate_token_configurations( - self.data_contract().tokens(), - platform_version, - )) - } -} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs index bfa4b8b5b94..3721ab3896c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_create/mod.rs @@ -9,7 +9,6 @@ use advanced_structure::v1::DataContractCreatedStateTransitionAdvancedStructureV use basic_structure::v0::DataContractCreateStateTransitionBasicStructureValidationV0; use basic_structure::v1::DataContractCreateStateTransitionBasicStructureValidationV1; use basic_structure::v2::DataContractCreateStateTransitionBasicStructureValidationV2; -use basic_structure::v3::DataContractCreateStateTransitionBasicStructureValidationV3; use dpp::address_funds::PlatformAddress; use dpp::block::block_info::BlockInfo; use dpp::dashcore::Network; @@ -104,15 +103,14 @@ impl StateTransitionBasicStructureValidationV0 for DataContractCreateTransition Some(0) => self.validate_basic_structure_v0(network_type, platform_version), Some(1) => self.validate_basic_structure_v1(network_type, platform_version), Some(2) => self.validate_basic_structure_v2(network_type, platform_version), - Some(3) => self.validate_basic_structure_v3(network_type, platform_version), Some(version) => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "data contract create transition: validate_basic_structure".to_string(), - known_versions: vec![0, 1, 2, 3], + known_versions: vec![0, 1, 2], received: version, })), None => Err(Error::Execution(ExecutionError::VersionNotActive { method: "data contract create transition: validate_basic_structure".to_string(), - known_versions: vec![0, 1, 2, 3], + known_versions: vec![0, 1, 2], })), } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/mod.rs index 1b391a4bcc9..4598692d1d6 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/mod.rs @@ -1,4 +1,3 @@ pub(crate) mod v0; pub(crate) mod v1; pub(crate) mod v2; -pub(crate) mod v3; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v2/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v2/mod.rs index a7bcd1bae61..86d80bf7bc8 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v2/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v2/mod.rs @@ -1,6 +1,7 @@ use crate::error::Error; use dpp::consensus::basic::contract_moderation::DocumentActionFeesWithoutModerationError; use dpp::dashcore::Network; +use dpp::data_contract::associated_token::token_configuration::validate_token_configurations; use dpp::data_contract::config::v2::DataContractConfigGettersV2; use dpp::data_contract::document_type::action_fees::DocumentActionFees; use dpp::state_transition::data_contract_update_transition::accessors::DataContractUpdateTransitionAccessorsV0; @@ -58,6 +59,12 @@ impl DataContractUpdateStateTransitionBasicStructureValidationV2 for DataContrac } } - Ok(SimpleConsensusValidationResult::new()) + // The token configurations the contract declares: a token that opts into a shielded + // pool must leave its freeze, unfreeze and destroy-frozen-funds rules unassigned, + // since notes in a pool have no owner to freeze. + Ok(validate_token_configurations( + self.data_contract().tokens(), + platform_version, + )) } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v3/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v3/mod.rs deleted file mode 100644 index 2a411012d82..00000000000 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/basic_structure/v3/mod.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::error::Error; -use dpp::dashcore::Network; -use dpp::data_contract::associated_token::token_configuration::validate_token_configurations; -use dpp::state_transition::data_contract_update_transition::accessors::DataContractUpdateTransitionAccessorsV0; -use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; -use dpp::validation::SimpleConsensusValidationResult; -use dpp::version::PlatformVersion; - -use super::v2::DataContractUpdateStateTransitionBasicStructureValidationV2; - -pub(in crate::execution::validation::state_transition::state_transitions::data_contract_update) trait DataContractUpdateStateTransitionBasicStructureValidationV3 { - fn validate_basic_structure_v3( - &self, - network_type: Network, - platform_version: &PlatformVersion, - ) -> Result; -} - -impl DataContractUpdateStateTransitionBasicStructureValidationV3 for DataContractUpdateTransition { - /// Protocol 15 retains the previous rules and validates token shielded pool opt-ins. - fn validate_basic_structure_v3( - &self, - network_type: Network, - platform_version: &PlatformVersion, - ) -> Result { - let result = self.validate_basic_structure_v2(network_type, platform_version)?; - if !result.is_valid() { - return Ok(result); - } - Ok(validate_token_configurations( - self.data_contract().tokens(), - platform_version, - )) - } -} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs index 5259902e010..9e004d3034c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_update/mod.rs @@ -5,7 +5,6 @@ mod state; use basic_structure::v0::DataContractUpdateStateTransitionBasicStructureValidationV0; use basic_structure::v1::DataContractUpdateStateTransitionBasicStructureValidationV1; use basic_structure::v2::DataContractUpdateStateTransitionBasicStructureValidationV2; -use basic_structure::v3::DataContractUpdateStateTransitionBasicStructureValidationV3; use dpp::address_funds::PlatformAddress; use dpp::block::block_info::BlockInfo; use dpp::dashcore::Network; @@ -49,10 +48,9 @@ impl StateTransitionBasicStructureValidationV0 for DataContractUpdateTransition Some(0) => self.validate_basic_structure_v0(network_type, platform_version), Some(1) => self.validate_basic_structure_v1(network_type, platform_version), Some(2) => self.validate_basic_structure_v2(network_type, platform_version), - Some(3) => self.validate_basic_structure_v3(network_type, platform_version), Some(version) => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "data contract update transition: validate_basic_structure".to_string(), - known_versions: vec![0, 1, 2, 3], + known_versions: vec![0, 1, 2], received: version, })), None => Err(Error::Execution(ExecutionError::VersionNotActive { diff --git a/packages/rs-drive-abci/src/query/shielded/mod.rs b/packages/rs-drive-abci/src/query/shielded/mod.rs index 5b22cbc3429..ab01fb9ff4b 100644 --- a/packages/rs-drive-abci/src/query/shielded/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/mod.rs @@ -29,7 +29,7 @@ pub(super) enum ShieldedPoolSelector { impl ShieldedPoolSelector { /// Resolves the optional `token_id` a request carries. A token pool is only addressable - /// once token shielded pools exist (protocol version 15), and the id must be 32 bytes. + /// once token shielded pools exist (protocol version 14), and the id must be 32 bytes. pub(super) fn from_request( token_id: Option>, platform_version: &PlatformVersion, diff --git a/packages/rs-drive-proof-verifier/src/types.rs b/packages/rs-drive-proof-verifier/src/types.rs index 8d0f29ad702..165accd54f7 100644 --- a/packages/rs-drive-proof-verifier/src/types.rs +++ b/packages/rs-drive-proof-verifier/src/types.rs @@ -939,7 +939,7 @@ pub struct ShieldedEncryptedNotesQuery { )] pub struct ShieldedNullifiersQuery(pub Vec<[u8; 32]>); -/// Targets one token's shielded pool (protocol version 15+) for the parameterless shielded +/// Targets one token's shielded pool (protocol version 14+) for the parameterless shielded /// queries: pool state, notes count, anchors and most recent anchor. #[derive(Debug, Clone)] #[cfg_attr( diff --git a/packages/rs-drive/grovedb-structure.json b/packages/rs-drive/grovedb-structure.json index e93b7c915f3..690a869416c 100644 --- a/packages/rs-drive/grovedb-structure.json +++ b/packages/rs-drive/grovedb-structure.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "latest_protocol_version": 15, + "latest_protocol_version": 14, "element_kinds": [ { "name": "Item", @@ -956,7 +956,7 @@ "kinds": [ "BigSumTree" ], - "since": 15, + "since": 14, "presence": "always", "source": "packages/rs-drive/src/drive/tokens/paths.rs", "book": "data-model/token-shielded-pools.md", @@ -977,7 +977,7 @@ "kinds": [ "SumTree" ], - "since": 15, + "since": 14, "presence": "always", "source": "packages/rs-drive/src/drive/shielded/paths.rs", "description": "The Orchard pool for one token.", @@ -994,7 +994,7 @@ "SumItem" ], "value": "token amount", - "since": 15, + "since": 14, "presence": "always", "source": "packages/rs-drive/src/drive/shielded/paths.rs", "description": "Every token in the pool.", @@ -1011,7 +1011,7 @@ "kinds": [ "ProvableCountTree" ], - "since": 15, + "since": 14, "presence": "always", "source": "packages/rs-drive/src/drive/shielded/paths.rs", "description": "The nullifier of every spent note, so none is spent twice.", @@ -1032,7 +1032,7 @@ "Item" ], "value": "empty", - "since": 15, + "since": 14, "presence": "always", "source": "packages/rs-drive/src/drive/shielded/paths.rs", "description": "One spent note.", @@ -1051,7 +1051,7 @@ "kinds": [ "Tree" ], - "since": 15, + "since": 14, "presence": "always", "source": "packages/rs-drive/src/drive/shielded/paths.rs", "description": "The commitment tree anchor after each block that added notes, for pruning old anchors.", @@ -1072,7 +1072,7 @@ "Item" ], "value": "the anchor, 32 bytes", - "since": 15, + "since": 14, "presence": "always", "source": "packages/rs-drive/src/drive/shielded/paths.rs", "description": "The anchor at that height.", @@ -1091,7 +1091,7 @@ "kinds": [ "CommitmentTree" ], - "since": 15, + "since": 14, "presence": "always", "source": "packages/rs-drive/src/drive/shielded/paths.rs", "description": "Every note ever added to the pool. Read by every wallet sync.", @@ -1109,7 +1109,7 @@ "kinds": [ "Tree" ], - "since": 15, + "since": 14, "presence": "always", "source": "packages/rs-drive/src/drive/shielded/paths.rs", "description": "The anchors a spend may still prove against.", @@ -1130,7 +1130,7 @@ "Item" ], "value": "the block height, u64 big endian", - "since": 15, + "since": 14, "presence": "always", "source": "packages/rs-drive/src/drive/shielded/paths.rs", "description": "One valid anchor.", @@ -5247,7 +5247,7 @@ }, "layer_shapes": { "contract_groups": { - "origin": "genesis@15", + "origin": "genesis@14", "tree": { "hex": "00", "right": { @@ -5256,7 +5256,7 @@ } }, "contract_groups.groups.group": { - "origin": "fixture contract_groups_and_bound_keys@15", + "origin": "fixture contract_groups_and_bound_keys@14", "tree": { "hex": "02", "left": { @@ -5271,7 +5271,7 @@ } }, "contract_groups.members.contract": { - "origin": "fixture contract_groups_and_bound_keys@15", + "origin": "fixture contract_groups_and_bound_keys@14", "tree": { "hex": "01", "left": { @@ -5283,7 +5283,7 @@ } }, "contracts.contract": { - "origin": "fixture contracts_with_documents@15", + "origin": "fixture contracts_with_documents@14", "tree": { "hex": "01", "left": { @@ -5295,7 +5295,7 @@ } }, "contracts.contract.other": { - "origin": "fixture moderated_contract@15", + "origin": "fixture moderated_contract@14", "tree": { "hex": "80", "left": { @@ -5313,7 +5313,7 @@ } }, "group_actions.contract.group": { - "origin": "fixture tokens_and_group_actions@15", + "origin": "fixture tokens_and_group_actions@14", "tree": { "hex": "4d", "left": { @@ -5325,7 +5325,7 @@ } }, "group_actions.contract.group.active.action": { - "origin": "fixture tokens_and_group_actions@15", + "origin": "fixture tokens_and_group_actions@14", "tree": { "hex": "53", "left": { @@ -5334,7 +5334,7 @@ } }, "group_actions.contract.group.closed.action": { - "origin": "fixture tokens_and_group_actions@15", + "origin": "fixture tokens_and_group_actions@14", "tree": { "hex": "53", "left": { @@ -5343,7 +5343,7 @@ } }, "identities.identity": { - "origin": "fixture contract_groups_and_bound_keys@15", + "origin": "fixture contract_groups_and_bound_keys@14", "tree": { "hex": "80", "left": { @@ -5368,7 +5368,7 @@ "states": [ { "state": "created", - "origin": "fixture identities@15", + "origin": "fixture identities@14", "tree": { "hex": "80", "left": { @@ -5387,7 +5387,7 @@ }, { "state": "used_with_a_contract", - "origin": "fixture identities@15", + "origin": "fixture identities@14", "tree": { "hex": "80", "left": { @@ -5409,7 +5409,7 @@ }, { "state": "budgeted_key_and_contract", - "origin": "fixture contract_groups_and_bound_keys@15", + "origin": "fixture contract_groups_and_bound_keys@14", "tree": { "hex": "80", "left": { @@ -5435,7 +5435,7 @@ ] }, "identities.identity.contract_info.bound": { - "origin": "fixture contract_groups_and_bound_keys@15", + "origin": "fixture contract_groups_and_bound_keys@14", "tree": { "hex": "01", "left": { @@ -5444,7 +5444,7 @@ } }, "identities.identity.key_references": { - "origin": "fixture identities@15", + "origin": "fixture identities@14", "tree": { "hex": "03", "left": { @@ -5456,7 +5456,7 @@ } }, "identities.identity.key_references.authentication": { - "origin": "fixture identities@15", + "origin": "fixture identities@14", "tree": { "hex": "02", "left": { @@ -5471,7 +5471,7 @@ } }, "misc": { - "origin": "genesis@15", + "origin": "genesis@14", "tree": { "hex": "54", "left": { @@ -5480,7 +5480,7 @@ } }, "pools.epoch": { - "origin": "fixture current_epoch@15", + "origin": "fixture current_epoch@14", "tree": { "hex": "6d", "left": { @@ -5508,14 +5508,14 @@ "states": [ { "state": "future", - "origin": "fixture current_epoch@15", + "origin": "fixture current_epoch@14", "tree": { "hex": "73" } }, { "state": "running", - "origin": "fixture current_epoch@15", + "origin": "fixture current_epoch@14", "tree": { "hex": "6d", "left": { @@ -5543,7 +5543,7 @@ }, { "state": "paid", - "origin": "fixture paid_epoch@15", + "origin": "fixture paid_epoch@14", "tree": { "hex": "74", "left": { @@ -5566,7 +5566,7 @@ ] }, "prefunded_balances": { - "origin": "genesis@15", + "origin": "genesis@14", "tree": { "hex": "80", "left": { @@ -5578,7 +5578,7 @@ } }, "root": { - "origin": "genesis@15", + "origin": "genesis@14", "tree": { "hex": "40", "left": { @@ -5635,7 +5635,7 @@ } }, "saved_block_transactions": { - "origin": "genesis@15", + "origin": "genesis@14", "tree": { "hex": "65", "left": { @@ -5647,7 +5647,7 @@ } }, "shielded_balances.main_pool": { - "origin": "genesis@15", + "origin": "genesis@14", "tree": { "hex": "80", "left": { @@ -5665,7 +5665,7 @@ } }, "tokens": { - "origin": "genesis@15", + "origin": "genesis@14", "tree": { "hex": "80", "left": { @@ -5689,7 +5689,7 @@ } }, "tokens.distributions": { - "origin": "genesis@15", + "origin": "genesis@14", "tree": { "hex": "80", "left": { @@ -5704,7 +5704,7 @@ } }, "tokens.distributions.perpetual.token": { - "origin": "fixture token_distributions_unclaimed@15", + "origin": "fixture token_distributions_unclaimed@14", "tree": { "hex": "c0", "left": { @@ -5713,7 +5713,7 @@ } }, "tokens.distributions.timed": { - "origin": "genesis@15", + "origin": "genesis@14", "tree": { "hex": "80", "left": { @@ -5725,7 +5725,7 @@ } }, "tokens.shielded_pools.token": { - "origin": "fixture token_shielded_pool@15", + "origin": "fixture token_shielded_pool@14", "tree": { "hex": "60", "left": { @@ -5743,7 +5743,7 @@ } }, "versions": { - "origin": "genesis@15", + "origin": "genesis@14", "tree": { "hex": "01", "left": { @@ -5752,7 +5752,7 @@ } }, "votes": { - "origin": "genesis@15", + "origin": "genesis@14", "tree": { "hex": "64", "left": { @@ -5764,7 +5764,7 @@ } }, "votes.contested_resource": { - "origin": "genesis@15", + "origin": "genesis@14", "tree": { "hex": "70", "left": { @@ -5773,7 +5773,7 @@ } }, "votes.contested_resource.active_polls.contract.document_type": { - "origin": "fixture contested_documents@15", + "origin": "fixture contested_documents@14", "tree": { "hex": "01", "left": { @@ -5782,7 +5782,7 @@ } }, "votes.contested_resource.active_polls.contract.document_type.indexes.value.contender": { - "origin": "fixture contested_documents@15", + "origin": "fixture contested_documents@14", "tree": { "hex": "01", "left": { @@ -5791,7 +5791,7 @@ } }, "withdrawals": { - "origin": "genesis@15", + "origin": "genesis@14", "tree": { "hex": "03", "left": { diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/mod.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/mod.rs index 55c7537efce..5bc1d9b3133 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/mod.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/mod.rs @@ -1,7 +1,6 @@ mod v0; mod v1; mod v2; -mod v3; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -60,12 +59,9 @@ impl Drive { 2 => { self.insert_contract_v2(contract, block_info, apply, transaction, platform_version) } - 3 => { - self.insert_contract_v3(contract, block_info, apply, transaction, platform_version) - } version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "insert_contract".to_string(), - known_versions: vec![0, 1, 2, 3], + known_versions: vec![0, 1, 2], received: version, })), } @@ -135,18 +131,9 @@ impl Drive { transaction, platform_version, ), - 3 => self.insert_contract_add_operations_v3( - contract_element, - contract, - block_info, - estimated_costs_only_with_layer_info, - drive_operations, - transaction, - platform_version, - ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "insert_contract_add_operations".to_string(), - known_versions: vec![0, 1, 2, 3], + known_versions: vec![0, 1, 2], received: version, })), } diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v2/mod.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v2/mod.rs index a1af8d5018e..b743805bb9a 100644 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/v2/mod.rs +++ b/packages/rs-drive/src/drive/contract/insert/insert_contract/v2/mod.rs @@ -4,10 +4,13 @@ use crate::fees::op::LowLevelDriveOperation; use crate::util::storage_flags::StorageFlags; use dpp::block::block_info::BlockInfo; use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::accessors::v1::DataContractV1Getters; +use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters; use dpp::data_contract::config::v2::DataContractConfigGettersV2; use dpp::data_contract::document_type::accessors::{DocumentTypeV0Getters, DocumentTypeV2Getters}; use dpp::data_contract::DataContract; use dpp::fee::fee_result::FeeResult; +use dpp::tokens::calculate_token_id; use dpp::version::PlatformVersion; use grovedb::batch::KeyInfoPath; use grovedb::{Element, EstimatedLayerInformation, TransactionArg}; @@ -16,6 +19,7 @@ use std::collections::HashMap; impl Drive { /// Generation 2 (protocol version 14): generation 1, and a contract whose config declares /// moderation also gets its banlist and suspension list trees under keys `128` and `192` of the contract's other tree (`[64, id, 2]`). + /// Each of the contract's tokens that opts into a shielded pool also gets its pool trees. #[inline(always)] pub(super) fn insert_contract_v2( &self, @@ -88,7 +92,8 @@ impl Drive { Ok(()) } - /// The generation 1 operations, then the moderation list trees the config declares. + /// The generation 1 operations, then the moderation list trees the config declares and the + /// shielded pool trees of the tokens that opt in. fn insert_contract_operations_v2( &self, contract_element: Element, @@ -149,6 +154,33 @@ impl Drive { } } + for (position, configuration) in contract.tokens() { + if !configuration.has_shielded_pool() { + continue; + } + let token_id = calculate_token_id(contract.id().as_bytes(), *position); + // An update is estimated through the insert path (the stateless existence read + // reports nothing), so a pool the token already owns must not be priced as created + // again; a real insert never finds one. + if estimated_costs_only_with_layer_info.is_some() + && self.has_token_shielded_pool( + token_id, + transaction, + &mut vec![], + platform_version, + )? + { + continue; + } + batch_operations.extend(self.create_token_shielded_pool_trees_operations( + token_id, + false, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?); + } + Ok(batch_operations) } } diff --git a/packages/rs-drive/src/drive/contract/insert/insert_contract/v3/mod.rs b/packages/rs-drive/src/drive/contract/insert/insert_contract/v3/mod.rs deleted file mode 100644 index 893448b7bd5..00000000000 --- a/packages/rs-drive/src/drive/contract/insert/insert_contract/v3/mod.rs +++ /dev/null @@ -1,141 +0,0 @@ -use crate::drive::Drive; -use crate::error::Error; -use crate::fees::op::LowLevelDriveOperation; -use dpp::block::block_info::BlockInfo; -use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::data_contract::accessors::v1::DataContractV1Getters; -use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters; -use dpp::data_contract::DataContract; -use dpp::fee::fee_result::FeeResult; -use dpp::tokens::calculate_token_id; -use dpp::version::PlatformVersion; -use grovedb::batch::KeyInfoPath; -use grovedb::{Element, EstimatedLayerInformation, TransactionArg}; -use std::collections::HashMap; - -impl Drive { - /// Protocol 15 contract insertion, including shielded pools for opted-in tokens. - #[inline(always)] - pub(super) fn insert_contract_v3( - &self, - contract: &DataContract, - block_info: BlockInfo, - apply: bool, - transaction: TransactionArg, - platform_version: &PlatformVersion, - ) -> Result { - let mut drive_operations: Vec = vec![]; - - let contract_element = - Self::contract_element_for_insert_v1(contract, &block_info, platform_version)?; - - let mut estimated_costs_only_with_layer_info = if apply { - None::> - } else { - Some(HashMap::new()) - }; - let batch_operations = self.insert_contract_operations_v3( - contract_element, - contract, - &block_info, - &mut estimated_costs_only_with_layer_info, - transaction, - platform_version, - )?; - self.apply_batch_low_level_drive_operations( - estimated_costs_only_with_layer_info, - transaction, - batch_operations, - &mut drive_operations, - &platform_version.drive, - )?; - - Drive::calculate_fee( - None, - Some(drive_operations), - &block_info.epoch, - self.config.epochs_per_era, - platform_version, - None, - ) - } - - /// The operations for adding a contract, appended to `drive_operations`. - #[inline(always)] - #[allow(clippy::too_many_arguments)] - pub(super) fn insert_contract_add_operations_v3( - &self, - contract_element: Element, - contract: &DataContract, - block_info: &BlockInfo, - estimated_costs_only_with_layer_info: &mut Option< - HashMap, - >, - drive_operations: &mut Vec, - transaction: TransactionArg, - platform_version: &PlatformVersion, - ) -> Result<(), Error> { - let batch_operations = self.insert_contract_operations_v3( - contract_element, - contract, - block_info, - estimated_costs_only_with_layer_info, - transaction, - platform_version, - )?; - drive_operations.extend(batch_operations); - Ok(()) - } - - /// Protocol 14 operations with token pool trees for opted-in tokens. - fn insert_contract_operations_v3( - &self, - contract_element: Element, - contract: &DataContract, - block_info: &BlockInfo, - estimated_costs_only_with_layer_info: &mut Option< - HashMap, - >, - transaction: TransactionArg, - platform_version: &PlatformVersion, - ) -> Result, Error> { - let mut batch_operations: Vec = vec![]; - self.insert_contract_add_operations_v2( - contract_element, - contract, - block_info, - estimated_costs_only_with_layer_info, - &mut batch_operations, - transaction, - platform_version, - )?; - - for (position, configuration) in contract.tokens() { - if !configuration.has_shielded_pool() { - continue; - } - let token_id = calculate_token_id(contract.id().as_bytes(), *position); - // An update is estimated through the insert path (the stateless existence read - // reports nothing), so a pool the token already owns must not be priced as created - // again; a real insert never finds one. - if estimated_costs_only_with_layer_info.is_some() - && self.has_token_shielded_pool( - token_id, - transaction, - &mut vec![], - platform_version, - )? - { - continue; - } - batch_operations.extend(self.create_token_shielded_pool_trees_operations( - token_id, - false, - estimated_costs_only_with_layer_info, - transaction, - platform_version, - )?); - } - Ok(batch_operations) - } -} diff --git a/packages/rs-drive/src/drive/contract/update/update_contract/mod.rs b/packages/rs-drive/src/drive/contract/update/update_contract/mod.rs index d78150b7fd1..5b838566042 100644 --- a/packages/rs-drive/src/drive/contract/update/update_contract/mod.rs +++ b/packages/rs-drive/src/drive/contract/update/update_contract/mod.rs @@ -1,7 +1,6 @@ mod v0; mod v1; mod v2; -mod v3; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -83,17 +82,9 @@ impl Drive { platform_version, previous_fee_versions, ), - 3 => self.update_contract_v3( - contract, - block_info, - apply, - transaction, - previous_fee_versions, - platform_version, - ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "update_contract".to_string(), - known_versions: vec![0, 1, 2, 3], + known_versions: vec![0, 1, 2], received: version, })), } @@ -171,18 +162,9 @@ impl Drive { drive_operations, platform_version, ), - 3 => self.update_contract_element_v3( - contract_element, - contract, - original_contract, - block_info, - transaction, - drive_operations, - platform_version, - ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "update_contract_element".to_string(), - known_versions: vec![0, 1, 2, 3], + known_versions: vec![0, 1, 2], received: version, })), } @@ -268,19 +250,9 @@ impl Drive { drive_operations, platform_version, ), - 3 => self.update_contract_add_operations_v3( - contract_element, - contract, - original_contract, - block_info, - estimated_costs_only_with_layer_info, - transaction, - drive_operations, - platform_version, - ), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "update_contract_add_operations".to_string(), - known_versions: vec![0, 1, 2, 3], + known_versions: vec![0, 1, 2], received: version, })), } diff --git a/packages/rs-drive/src/drive/contract/update/update_contract/v2/mod.rs b/packages/rs-drive/src/drive/contract/update/update_contract/v2/mod.rs index 6cd928e574e..1fce6a9047f 100644 --- a/packages/rs-drive/src/drive/contract/update/update_contract/v2/mod.rs +++ b/packages/rs-drive/src/drive/contract/update/update_contract/v2/mod.rs @@ -18,6 +18,7 @@ use crate::error::contract::DataContractError; use dpp::balances::credits::TokenAmount; use dpp::data_contract::accessors::v1::DataContractV1Getters; use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters; use dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; use dpp::data_contract::associated_token::token_distribution_rules::accessors::v1::TokenDistributionRulesV1Getters; use dpp::fee::default_costs::CachedEpochIndexFeeVersions; @@ -327,6 +328,19 @@ impl Drive { platform_version, )?; } + + // The token's own Orchard pool, as `insert_contract` creates it for the tokens of + // a new contract. A token is new to the contract exactly once, so a token that + // already owns a pool never reaches here and its pool is never recreated. + if configuration.has_shielded_pool() { + batch_operations.extend(self.create_token_shielded_pool_trees_operations( + token_id.to_buffer(), + true, + estimated_costs_only_with_layer_info, + transaction, + platform_version, + )?); + } } // The removal records tree of every document type the update adds that moderators may @@ -444,6 +458,7 @@ mod tests { use dpp::data_contract::accessors::v0::{DataContractV0Getters, DataContractV0Setters}; use dpp::data_contract::accessors::v1::{DataContractV1Getters, DataContractV1Setters}; use dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; + use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters; use dpp::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; use dpp::data_contract::associated_token::token_configuration::TokenConfiguration; use dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Setters; @@ -1172,4 +1187,147 @@ mod tests { ); } } + + #[test] + fn should_create_token_pools_only_from_protocol_14_for_registration_and_updates() { + for platform_version in [PlatformVersion::get(13).unwrap(), PlatformVersion::latest()] { + for add_by_update in [false, true] { + let drive = setup_drive_with_initial_state_structure(None); + let mut contract = + get_dashpay_contract_fixture(None, 0, platform_version.protocol_version) + .data_contract_owned(); + contract.config_mut().set_readonly(false); + + if add_by_update { + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("insert initial contract without tokens"); + contract.increment_version(); + } + + let mut configuration = TokenConfiguration::V0( + TokenConfigurationV0::default_most_restrictive().with_base_supply(100), + ); + configuration.set_has_shielded_pool(true); + contract.set_tokens(BTreeMap::from([(0, configuration)])); + // The fee estimate CheckTx runs (apply = false) goes through the stateless + // insert path and must price the pool once, while it is still to be created. + let estimate_before_pool = if add_by_update { + drive + .update_contract( + &contract, + BlockInfo::default(), + false, + None, + platform_version, + None, + ) + .expect("estimate adding a pooled token through contract update") + } else { + drive + .apply_contract( + &contract, + BlockInfo::default(), + false, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("estimate registering a pooled token contract") + }; + assert!(estimate_before_pool.processing_fee > 0); + if add_by_update { + drive + .update_contract( + &contract, + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("add token through contract update"); + } else { + drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("register token contract"); + } + + let token_id = contract.token_id(0).expect("token id").to_buffer(); + assert_eq!( + drive + .has_token_shielded_pool(token_id, None, &mut vec![], platform_version) + .expect("check token pool"), + platform_version.protocol_version >= 14, + ); + // Minting the base supply of a token an update adds is itself generation 2: + // the shipped update path leaves it at zero, while registration has always + // minted it. + let expected_supply = if add_by_update && platform_version.protocol_version < 14 { + Some(0) + } else { + Some(100) + }; + assert_eq!( + drive + .fetch_token_total_supply(token_id, None, platform_version) + .expect("read total supply"), + expected_supply + ); + + // Updating an existing token must not recreate its pool or mint its supply again, + // and its estimate must not price the pool the token already owns. + contract.increment_version(); + let estimate_with_pool = drive + .update_contract( + &contract, + BlockInfo::default(), + false, + None, + platform_version, + None, + ) + .expect("estimate updating a contract whose token owns a pool"); + assert!(estimate_with_pool.processing_fee > 0); + if platform_version.protocol_version >= 14 { + assert!( + estimate_with_pool.storage_fee < estimate_before_pool.storage_fee, + "an existing pool must not be priced again: {} vs {}", + estimate_with_pool.storage_fee, + estimate_before_pool.storage_fee + ); + } + drive + .update_contract( + &contract, + BlockInfo::default(), + true, + None, + platform_version, + None, + ) + .expect("update existing token without recreating storage"); + assert_eq!( + drive + .fetch_token_total_supply(token_id, None, platform_version) + .expect("read unchanged total supply"), + expected_supply + ); + } + } + } } diff --git a/packages/rs-drive/src/drive/contract/update/update_contract/v3/mod.rs b/packages/rs-drive/src/drive/contract/update/update_contract/v3/mod.rs deleted file mode 100644 index 885acc4b083..00000000000 --- a/packages/rs-drive/src/drive/contract/update/update_contract/v3/mod.rs +++ /dev/null @@ -1,370 +0,0 @@ -use crate::drive::Drive; -use crate::error::drive::DriveError; -use crate::error::Error; -use crate::fees::op::LowLevelDriveOperation; -use crate::util::storage_flags::StorageFlags; -use dpp::block::block_info::BlockInfo; -use dpp::data_contract::accessors::v0::DataContractV0Getters; -use dpp::data_contract::accessors::v1::DataContractV1Getters; -use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters; -use dpp::data_contract::config::v0::DataContractConfigGettersV0; -use dpp::data_contract::DataContract; -use dpp::fee::default_costs::CachedEpochIndexFeeVersions; -use dpp::fee::fee_result::FeeResult; -use dpp::serialization::PlatformSerializableWithPlatformVersion; -use dpp::tokens::calculate_token_id; -use dpp::version::PlatformVersion; -use grovedb::batch::KeyInfoPath; -use grovedb::{Element, EstimatedLayerInformation, TransactionArg}; -use std::collections::HashMap; - -impl Drive { - /// Protocol 15 contract updates, including pools for newly added shielded tokens. - #[inline(always)] - pub(super) fn update_contract_v3( - &self, - contract: &DataContract, - block_info: BlockInfo, - apply: bool, - transaction: TransactionArg, - previous_fee_versions: Option<&CachedEpochIndexFeeVersions>, - platform_version: &PlatformVersion, - ) -> Result { - if !apply { - return self.insert_contract( - contract, - block_info, - false, - transaction, - platform_version, - ); - } - - let mut drive_operations: Vec = vec![]; - - let contract_bytes = contract.serialize_to_bytes_with_platform_version(platform_version)?; - - // Since we can update the contract by definition it already has storage flags - let storage_flags = Some(StorageFlags::new_single_epoch( - block_info.epoch.index, - Some(contract.owner_id().to_buffer()), - )); - - let contract_element = Element::Item( - contract_bytes, - StorageFlags::map_to_some_element_flags(storage_flags.as_ref()), - ); - - let original_contract_fetch_info = self - .get_contract_with_fetch_info_and_add_to_operations( - contract.id().to_buffer(), - Some(&block_info.epoch), - true, - transaction, - &mut drive_operations, - platform_version, - )? - .ok_or(Error::Drive(DriveError::CorruptedCodeExecution( - "contract should exist", - )))?; - - if original_contract_fetch_info.contract.config().readonly() { - return Err(Error::Drive(DriveError::UpdatingReadOnlyImmutableContract( - "original contract is readonly", - ))); - } - - self.update_contract_element_v3( - contract_element, - contract, - &original_contract_fetch_info.contract, - &block_info, - transaction, - &mut drive_operations, - platform_version, - )?; - - // Update DataContracts cache with the new contract - let updated_contract_fetch_info = self - .fetch_contract_and_add_operations( - contract.id().to_buffer(), - Some(&block_info.epoch), - transaction, - &mut drive_operations, - platform_version, - )? - .ok_or(Error::Drive(DriveError::CorruptedCodeExecution( - "contract should exist", - )))?; - - self.cache - .data_contracts - .insert_rewritten(updated_contract_fetch_info, transaction.is_some()); - - Drive::calculate_fee( - None, - Some(drive_operations), - &block_info.epoch, - self.config.epochs_per_era, - platform_version, - previous_fee_versions, - ) - } - - /// Updates a contract. - #[inline(always)] - #[allow(clippy::too_many_arguments)] - pub(super) fn update_contract_element_v3( - &self, - contract_element: Element, - contract: &DataContract, - original_contract: &DataContract, - block_info: &BlockInfo, - transaction: TransactionArg, - drive_operations: &mut Vec, - platform_version: &PlatformVersion, - ) -> Result<(), Error> { - let mut estimated_costs_only_with_layer_info = - None::>; - let batch_operations = self.update_contract_operations_v3( - contract_element, - contract, - original_contract, - block_info, - &mut estimated_costs_only_with_layer_info, - transaction, - platform_version, - )?; - self.apply_batch_low_level_drive_operations( - estimated_costs_only_with_layer_info, - transaction, - batch_operations, - drive_operations, - &platform_version.drive, - ) - } - - /// Updates a contract. - #[allow(clippy::too_many_arguments)] - #[inline(always)] - pub(super) fn update_contract_add_operations_v3( - &self, - contract_element: Element, - contract: &DataContract, - original_contract: &DataContract, - block_info: &BlockInfo, - estimated_costs_only_with_layer_info: &mut Option< - HashMap, - >, - transaction: TransactionArg, - drive_operations: &mut Vec, - platform_version: &PlatformVersion, - ) -> Result<(), Error> { - let batch_operations = self.update_contract_operations_v3( - contract_element, - contract, - original_contract, - block_info, - estimated_costs_only_with_layer_info, - transaction, - platform_version, - )?; - drive_operations.extend(batch_operations); - Ok(()) - } - - /// Protocol 14 update operations plus shielded pools for newly added tokens. - #[allow(clippy::too_many_arguments)] - fn update_contract_operations_v3( - &self, - contract_element: Element, - contract: &DataContract, - original_contract: &DataContract, - block_info: &BlockInfo, - estimated_costs_only_with_layer_info: &mut Option< - HashMap, - >, - transaction: TransactionArg, - platform_version: &PlatformVersion, - ) -> Result, Error> { - let mut batch_operations = vec![]; - self.update_contract_add_operations_v2( - contract_element, - contract, - original_contract, - block_info, - estimated_costs_only_with_layer_info, - transaction, - &mut batch_operations, - platform_version, - )?; - - for (position, configuration) in contract.tokens() { - if original_contract.tokens().contains_key(position) - || !configuration.has_shielded_pool() - { - continue; - } - let token_id = calculate_token_id(contract.id().as_bytes(), *position); - batch_operations.extend(self.create_token_shielded_pool_trees_operations( - token_id, - true, - estimated_costs_only_with_layer_info, - transaction, - platform_version, - )?); - } - Ok(batch_operations) - } -} - -#[cfg(test)] -mod tests { - use crate::util::storage_flags::StorageFlags; - use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; - use dpp::block::block_info::BlockInfo; - use dpp::data_contract::accessors::v0::{DataContractV0Getters, DataContractV0Setters}; - use dpp::data_contract::accessors::v1::{DataContractV1Getters, DataContractV1Setters}; - use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters; - use dpp::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; - use dpp::data_contract::associated_token::token_configuration::TokenConfiguration; - use dpp::data_contract::config::v0::DataContractConfigSettersV0; - use dpp::tests::fixtures::get_dashpay_contract_fixture; - use dpp::version::PlatformVersion; - use std::collections::BTreeMap; - - #[test] - fn should_create_token_pools_only_from_protocol_15_for_registration_and_updates() { - for platform_version in [PlatformVersion::get(14).unwrap(), PlatformVersion::latest()] { - for add_by_update in [false, true] { - let drive = setup_drive_with_initial_state_structure(None); - let mut contract = - get_dashpay_contract_fixture(None, 0, platform_version.protocol_version) - .data_contract_owned(); - contract.config_mut().set_readonly(false); - - if add_by_update { - drive - .apply_contract( - &contract, - BlockInfo::default(), - true, - StorageFlags::optional_default_as_cow(), - None, - platform_version, - ) - .expect("insert initial contract without tokens"); - contract.increment_version(); - } - - let mut configuration = TokenConfiguration::V0( - TokenConfigurationV0::default_most_restrictive().with_base_supply(100), - ); - configuration.set_has_shielded_pool(true); - contract.set_tokens(BTreeMap::from([(0, configuration)])); - // The fee estimate CheckTx runs (apply = false) goes through the stateless - // insert path and must price the pool once, while it is still to be created. - let estimate_before_pool = if add_by_update { - drive - .update_contract( - &contract, - BlockInfo::default(), - false, - None, - platform_version, - None, - ) - .expect("estimate adding a pooled token through contract update") - } else { - drive - .apply_contract( - &contract, - BlockInfo::default(), - false, - StorageFlags::optional_default_as_cow(), - None, - platform_version, - ) - .expect("estimate registering a pooled token contract") - }; - assert!(estimate_before_pool.processing_fee > 0); - if add_by_update { - drive - .update_contract( - &contract, - BlockInfo::default(), - true, - None, - platform_version, - None, - ) - .expect("add token through contract update"); - } else { - drive - .apply_contract( - &contract, - BlockInfo::default(), - true, - StorageFlags::optional_default_as_cow(), - None, - platform_version, - ) - .expect("register token contract"); - } - - let token_id = contract.token_id(0).expect("token id").to_buffer(); - assert_eq!( - drive - .has_token_shielded_pool(token_id, None, &mut vec![], platform_version) - .expect("check token pool"), - platform_version.protocol_version >= 15, - ); - assert_eq!( - drive - .fetch_token_total_supply(token_id, None, platform_version) - .expect("read total supply"), - Some(100) - ); - - // Updating an existing token must not recreate its pool or mint its supply again, - // and its estimate must not price the pool the token already owns. - contract.increment_version(); - let estimate_with_pool = drive - .update_contract( - &contract, - BlockInfo::default(), - false, - None, - platform_version, - None, - ) - .expect("estimate updating a contract whose token owns a pool"); - assert!(estimate_with_pool.processing_fee > 0); - if platform_version.protocol_version >= 15 { - assert!( - estimate_with_pool.storage_fee < estimate_before_pool.storage_fee, - "an existing pool must not be priced again: {} vs {}", - estimate_with_pool.storage_fee, - estimate_before_pool.storage_fee - ); - } - drive - .update_contract( - &contract, - BlockInfo::default(), - true, - None, - platform_version, - None, - ) - .expect("update existing token without recreating storage"); - assert_eq!( - drive - .fetch_token_total_supply(token_id, None, platform_version) - .expect("read unchanged total supply"), - Some(100) - ); - } - } - } -} diff --git a/packages/rs-drive/src/drive/initialization/mod.rs b/packages/rs-drive/src/drive/initialization/mod.rs index 5db1db9fdb7..ce157e315c0 100644 --- a/packages/rs-drive/src/drive/initialization/mod.rs +++ b/packages/rs-drive/src/drive/initialization/mod.rs @@ -6,7 +6,6 @@ mod v1; mod v2; mod v3; mod v4; -mod v5; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -33,10 +32,9 @@ impl Drive { 2 => self.create_initial_state_structure_v2(transaction, platform_version), 3 => self.create_initial_state_structure_v3(transaction, platform_version), 4 => self.create_initial_state_structure_v4(transaction, platform_version), - 5 => self.create_initial_state_structure_v5(transaction, platform_version), version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "create_initial_state_structure".to_string(), - known_versions: vec![0, 1, 2, 3, 4, 5], + known_versions: vec![0, 1, 2, 3, 4], received: version, })), } diff --git a/packages/rs-drive/src/drive/initialization/v4/mod.rs b/packages/rs-drive/src/drive/initialization/v4/mod.rs index c89df4de467..2657fc96482 100644 --- a/packages/rs-drive/src/drive/initialization/v4/mod.rs +++ b/packages/rs-drive/src/drive/initialization/v4/mod.rs @@ -104,6 +104,13 @@ impl Drive { // sequence of inserts on both node populations. self.insert_contract_fee_pot_trees(transaction, platform_version)?; + // Token shielded pools root tree (protocol version 14): the BigSumTree under the Tokens + // tree that holds one Orchard pool per token opting in + // (`TokenConfigurationV1::has_shielded_pool`). The upgrade path + // (`Platform::transition_to_version_14`) calls the same helper, so a chain born at + // version 14 and one upgraded to it build a byte-identical `[Tokens]` subtree. + self.insert_token_shielded_pools_root_tree(transaction, platform_version)?; + Ok(()) } } diff --git a/packages/rs-drive/src/drive/initialization/v5/mod.rs b/packages/rs-drive/src/drive/initialization/v5/mod.rs deleted file mode 100644 index b1de2e8d2a1..00000000000 --- a/packages/rs-drive/src/drive/initialization/v5/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Initialization of token shielded pools in protocol version 15. - -use crate::drive::Drive; -use crate::error::Error; -use dpp::version::PlatformVersion; -use grovedb::TransactionArg; - -impl Drive { - /// Creates protocol 14 storage followed by the token shielded pools root. - pub(super) fn create_initial_state_structure_v5( - &self, - transaction: TransactionArg, - platform_version: &PlatformVersion, - ) -> Result<(), Error> { - self.create_initial_state_structure_v4(transaction, platform_version)?; - self.insert_token_shielded_pools_root_tree(transaction, platform_version)?; - Ok(()) - } -} diff --git a/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v0/mod.rs b/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v0/mod.rs index 60b0d7c9b3a..6e723e81c4e 100644 --- a/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v0/mod.rs @@ -42,7 +42,7 @@ impl Drive { Ok(TotalTokensBalance { total_tokens_in_platform, total_identity_token_balances, - // Token shielded pools do not exist before protocol version 15. + // Token shielded pools do not exist before protocol version 14. total_token_shielded_pool_balances: 0, }) } diff --git a/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v1/mod.rs b/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v1/mod.rs index c75a60edb1e..ddd56681587 100644 --- a/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v1/mod.rs +++ b/packages/rs-drive/src/drive/tokens/calculate_total_tokens_balance/v1/mod.rs @@ -9,7 +9,7 @@ use grovedb::TransactionArg; use platform_version::version::PlatformVersion; impl Drive { - /// Version 1 (protocol version 15): the token shielded pools BigSumTree joins the identity + /// Version 1 (protocol version 14): the token shielded pools BigSumTree joins the identity /// balances on the balance side, so `identity balances + shielded pool balances` must equal /// the total supplies. Shielding moves tokens between the two terms without touching supply. #[inline(always)] diff --git a/packages/rs-drive/src/drive/tokens/paths.rs b/packages/rs-drive/src/drive/tokens/paths.rs index 7e8a3bf8d3f..69fc5945300 100644 --- a/packages/rs-drive/src/drive/tokens/paths.rs +++ b/packages/rs-drive/src/drive/tokens/paths.rs @@ -24,7 +24,7 @@ pub const TOKEN_DIRECT_SELL_PRICE_KEY: u8 = 92; pub const TOKEN_DISTRIBUTIONS_KEY: u8 = 32; /// Key for the token shielded pools tree (a BigSumTree of per-token pool SumTrees). /// -/// Introduced in protocol version 15. Each token whose configuration has `has_shielded_pool` +/// Introduced in protocol version 14. Each token whose configuration has `has_shielded_pool` /// owns one child SumTree keyed by its token id, laid out exactly like the credit shielded pool /// (`crate::drive::shielded::paths`): notes, nullifiers, anchors, anchors-by-height and a total /// balance SumItem. The BigSumTree total is the amount of every token currently shielded and diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/insert_root_tree.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/insert_root_tree.rs index d0f4475a302..0e50083c219 100644 --- a/packages/rs-drive/src/drive/tokens/shielded_pool/insert_root_tree.rs +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/insert_root_tree.rs @@ -9,9 +9,9 @@ impl Drive { /// Inserts the token shielded pools root tree `[Tokens, TOKEN_SHIELDED_POOLS_KEY]` (an empty /// BigSumTree) if it does not exist yet. /// - /// CONSENSUS-CRITICAL: both the genesis path (`Drive::create_initial_state_structure_v5`) - /// and the in-place upgrade path (`Platform::transition_to_version_15`) call this one helper - /// so a chain born at protocol version 15 and a chain upgraded to it build a byte-identical + /// CONSENSUS-CRITICAL: both the genesis path (`Drive::create_initial_state_structure_v4`) + /// and the in-place upgrade path (`Platform::transition_to_version_14`) call this one helper + /// so a chain born at protocol version 14 and a chain upgraded to it build a byte-identical /// `[Tokens]` subtree. The tree stays empty until a token with a shielded pool is registered. pub fn insert_token_shielded_pools_root_tree( &self, diff --git a/packages/rs-drive/src/drive/tokens/structure.rs b/packages/rs-drive/src/drive/tokens/structure.rs index 7dfc54888d5..6dec7165950 100644 --- a/packages/rs-drive/src/drive/tokens/structure.rs +++ b/packages/rs-drive/src/drive/tokens/structure.rs @@ -372,7 +372,7 @@ fn distributions() -> StructureNode { ]) } -/// One Orchard pool per token that enables shielded balances in protocol 15. +/// One Orchard pool per token that enables shielded balances in protocol 14. fn shielded_pools() -> StructureNode { StructureNode::fixed( "shielded_pools", @@ -381,7 +381,7 @@ fn shielded_pools() -> StructureNode { "TOKEN_SHIELDED_POOLS_KEY", ) .kind(ElementKind::BigSumTree) - .since(15) + .since(14) .source(SOURCE) .book("data-model/token-shielded-pools.md") .describe("The shielded balances of tokens that enable an Orchard pool.") diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs index 202aad14309..14fc0e7e10e 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/mod.rs @@ -5,7 +5,6 @@ pub mod v3; pub mod v4; pub mod v5; pub mod v6; -pub mod v7; #[derive(Clone, Debug, Default)] pub struct DPPContractVersions { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v6.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v6.rs index 56aaed612d4..eee4069730d 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v6.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v6.rs @@ -118,7 +118,7 @@ pub const CONTRACT_VERSIONS_V6: DPPContractVersions = DPPContractVersions { validate_pre_programmed_distribution_amounts: 0, token_configuration_format: FeatureVersionBounds { min_version: 0, - max_version: 0, + max_version: 1, default_current_version: 0, }, }, diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v7.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v7.rs deleted file mode 100644 index ff942a1786b..00000000000 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_contract_versions/v7.rs +++ /dev/null @@ -1,16 +0,0 @@ -use super::v6::CONTRACT_VERSIONS_V6; -use super::{DPPContractVersions, TokenVersions}; -use versioned_feature_core::FeatureVersionBounds; - -/// Protocol 15 admits token configuration format 1, retaining all protocol 14 contract rules. -pub const CONTRACT_VERSIONS_V7: DPPContractVersions = DPPContractVersions { - token_versions: TokenVersions { - token_configuration_format: FeatureVersionBounds { - min_version: 0, - max_version: 1, - default_current_version: 0, - }, - ..CONTRACT_VERSIONS_V6.token_versions - }, - ..CONTRACT_VERSIONS_V6 -}; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/mod.rs index c16174c22a6..52c521e4309 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/mod.rs @@ -5,7 +5,6 @@ pub mod v2; pub mod v3; pub mod v4; pub mod v5; -pub mod v6; #[derive(Clone, Debug, Default)] pub struct DPPValidationVersions { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v5.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v5.rs index 90ca0d7155b..694d96714c6 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v5.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v5.rs @@ -23,6 +23,7 @@ pub const DPP_VALIDATION_VERSIONS_V5: DPPValidationVersions = DPPValidationVersi data_contract: DataContractValidationVersions { validate_config_update: 2, validate_once_per_identity_distribution: Some(0), + validate_token_config_update: 1, ..DPP_VALIDATION_VERSIONS_V4.data_contract }, document_type: DocumentTypeValidationVersions { diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v6.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v6.rs deleted file mode 100644 index 79460a1a00c..00000000000 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v6.rs +++ /dev/null @@ -1,11 +0,0 @@ -use super::v5::DPP_VALIDATION_VERSIONS_V5; -use super::{DPPValidationVersions, DataContractValidationVersions}; - -/// Protocol 15 makes the token shielded pool opt-in immutable after creation. -pub const DPP_VALIDATION_VERSIONS_V6: DPPValidationVersions = DPPValidationVersions { - data_contract: DataContractValidationVersions { - validate_token_config_update: 1, - ..DPP_VALIDATION_VERSIONS_V5.data_contract - }, - ..DPP_VALIDATION_VERSIONS_V5 -}; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/mod.rs index 10edd8041c5..c69dd339fe1 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/mod.rs @@ -2,7 +2,6 @@ use versioned_feature_core::{FeatureVersion, OptionalFeatureVersion}; pub mod v1; pub mod v10; -pub mod v11; pub mod v2; pub mod v3; pub mod v4; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs index 73f85871a6d..8abdbbb6851 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v10.rs @@ -139,7 +139,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V10: DriveAbciMethodVersions = DriveAbciMet update_checkpoints: Some(0), record_shielded_pool_anchor: Some(0), prune_shielded_pool_anchors: Some(0), - record_token_shielded_pool_anchors: None, + record_token_shielded_pool_anchors: Some(0), }, platform_state_storage: DriveAbciPlatformStateStorageMethodVersions { fetch_platform_state: 1, // changed: reads structure 1 records together with their masternode and validator set entries diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs deleted file mode 100644 index 97e6d190bab..00000000000 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_method_versions/v11.rs +++ /dev/null @@ -1,11 +0,0 @@ -use super::v10::DRIVE_ABCI_METHOD_VERSIONS_V10; -use super::{DriveAbciBlockEndMethodVersions, DriveAbciMethodVersions}; - -/// Protocol 15 records and prunes the anchors of every token shielded pool touched by a block. -pub const DRIVE_ABCI_METHOD_VERSIONS_V11: DriveAbciMethodVersions = DriveAbciMethodVersions { - block_end: DriveAbciBlockEndMethodVersions { - record_token_shielded_pool_anchors: Some(0), - ..DRIVE_ABCI_METHOD_VERSIONS_V10.block_end - }, - ..DRIVE_ABCI_METHOD_VERSIONS_V10 -}; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs index a0175eae0cd..174eef77715 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs @@ -1,6 +1,5 @@ pub mod v1; pub mod v10; -pub mod v11; pub mod v2; pub mod v3; pub mod v4; diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs index b533013b833..cf58cce75c0 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs @@ -228,7 +228,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = document_transfer_transition_structure_validation: 0, document_purchase_transition_structure_validation: 0, document_update_price_transition_structure_validation: 0, - document_base_transition_state_validation: 0, + document_base_transition_state_validation: 1, // changed: a document token cost may be paid from the token shielded pool document_create_transition_state_validation: 2, document_delete_transition_state_validation: 0, document_index_only_delete_transition_state_validation: 0, @@ -387,7 +387,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = }, token_shielded_transfer_with_shielded_fee_state_transition: DriveAbciStateTransitionValidationVersion { - basic_structure: None, + basic_structure: Some(0), advanced_structure: None, identity_signatures: None, nonce: None, @@ -396,7 +396,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = }, token_unshield_with_shielded_fee_state_transition: DriveAbciStateTransitionValidationVersion { - basic_structure: None, + basic_structure: Some(0), advanced_structure: None, identity_signatures: None, nonce: None, @@ -405,7 +405,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = }, token_purchase_from_shielded_pool_state_transition: DriveAbciStateTransitionValidationVersion { - basic_structure: None, + basic_structure: Some(0), advanced_structure: None, identity_signatures: None, nonce: None, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs deleted file mode 100644 index a13f97d443b..00000000000 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v11.rs +++ /dev/null @@ -1,54 +0,0 @@ -use super::v10::DRIVE_ABCI_VALIDATION_VERSIONS_V10; -use super::{ - DriveAbciDocumentsStateTransitionValidationVersions, DriveAbciStateTransitionValidationVersion, - DriveAbciStateTransitionValidationVersions, DriveAbciValidationVersions, -}; - -/// Protocol 15 enables token pool proofs and payments, retaining protocol 14 validation. -pub const DRIVE_ABCI_VALIDATION_VERSIONS_V11: DriveAbciValidationVersions = - DriveAbciValidationVersions { - state_transitions: DriveAbciStateTransitionValidationVersions { - contract_create_state_transition: DriveAbciStateTransitionValidationVersion { - basic_structure: Some(3), - ..DRIVE_ABCI_VALIDATION_VERSIONS_V10 - .state_transitions - .contract_create_state_transition - }, - contract_update_state_transition: DriveAbciStateTransitionValidationVersion { - basic_structure: Some(3), - ..DRIVE_ABCI_VALIDATION_VERSIONS_V10 - .state_transitions - .contract_update_state_transition - }, - batch_state_transition: DriveAbciDocumentsStateTransitionValidationVersions { - document_base_transition_state_validation: 1, - ..DRIVE_ABCI_VALIDATION_VERSIONS_V10 - .state_transitions - .batch_state_transition - }, - token_shielded_transfer_with_shielded_fee_state_transition: - DriveAbciStateTransitionValidationVersion { - basic_structure: Some(0), - ..DRIVE_ABCI_VALIDATION_VERSIONS_V10 - .state_transitions - .token_shielded_transfer_with_shielded_fee_state_transition - }, - token_unshield_with_shielded_fee_state_transition: - DriveAbciStateTransitionValidationVersion { - basic_structure: Some(0), - ..DRIVE_ABCI_VALIDATION_VERSIONS_V10 - .state_transitions - .token_unshield_with_shielded_fee_state_transition - }, - token_purchase_from_shielded_pool_state_transition: - DriveAbciStateTransitionValidationVersion { - basic_structure: Some(0), - ..DRIVE_ABCI_VALIDATION_VERSIONS_V10 - .state_transitions - .token_purchase_from_shielded_pool_state_transition - }, - ..DRIVE_ABCI_VALIDATION_VERSIONS_V10.state_transitions - }, - validate_shielded_proof: 2, - ..DRIVE_ABCI_VALIDATION_VERSIONS_V10 - }; diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/mod.rs index ba6c2368c1a..b7a25f8c676 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/mod.rs @@ -4,7 +4,6 @@ pub mod v1; pub mod v2; pub mod v3; pub mod v4; -pub mod v5; #[derive(Clone, Debug, Default)] pub struct DriveContractMethodVersions { diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v5.rs b/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v5.rs deleted file mode 100644 index 781c2786261..00000000000 --- a/packages/rs-platform-version/src/version/drive_versions/drive_contract_method_versions/v5.rs +++ /dev/null @@ -1,19 +0,0 @@ -use super::v4::DRIVE_CONTRACT_METHOD_VERSIONS_V4; -use super::{ - DriveContractInsertMethodVersions, DriveContractMethodVersions, - DriveContractUpdateMethodVersions, -}; - -/// Protocol 15 creates shielded pool storage for opted-in tokens at creation and update. -pub const DRIVE_CONTRACT_METHOD_VERSIONS_V5: DriveContractMethodVersions = - DriveContractMethodVersions { - insert: DriveContractInsertMethodVersions { - insert_contract: 3, - ..DRIVE_CONTRACT_METHOD_VERSIONS_V4.insert - }, - update: DriveContractUpdateMethodVersions { - update_contract: 3, - ..DRIVE_CONTRACT_METHOD_VERSIONS_V4.update - }, - ..DRIVE_CONTRACT_METHOD_VERSIONS_V4 - }; diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/mod.rs index 901166381de..a5c0e65d8c6 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/mod.rs @@ -2,7 +2,6 @@ use versioned_feature_core::FeatureVersion; pub mod v1; pub mod v2; -pub mod v3; #[derive(Clone, Debug, Default)] pub struct DriveTokenMethodVersions { diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v2.rs b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v2.rs index 89a564d343d..22156b66455 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v2.rs @@ -19,6 +19,7 @@ use crate::version::drive_versions::drive_token_method_versions::{ /// default node; the processing fee drops by the existence read the later /// tokens no longer make. pub const DRIVE_TOKEN_METHOD_VERSIONS_V2: DriveTokenMethodVersions = DriveTokenMethodVersions { + calculate_total_tokens_balance: 1, distribution: DriveTokenDistributionMethodVersions { add_pre_programmed_distributions: 1, ..DRIVE_TOKEN_METHOD_VERSIONS_V1.distribution diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v3.rs b/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v3.rs deleted file mode 100644 index a1d40088d02..00000000000 --- a/packages/rs-platform-version/src/version/drive_versions/drive_token_method_versions/v3.rs +++ /dev/null @@ -1,8 +0,0 @@ -use super::v2::DRIVE_TOKEN_METHOD_VERSIONS_V2; -use super::DriveTokenMethodVersions; - -/// Protocol 15 includes shielded pool balances in token conservation. -pub const DRIVE_TOKEN_METHOD_VERSIONS_V3: DriveTokenMethodVersions = DriveTokenMethodVersions { - calculate_total_tokens_balance: 1, - ..DRIVE_TOKEN_METHOD_VERSIONS_V2 -}; diff --git a/packages/rs-platform-version/src/version/drive_versions/mod.rs b/packages/rs-platform-version/src/version/drive_versions/mod.rs index 889baec9f92..1d7c32caed6 100644 --- a/packages/rs-platform-version/src/version/drive_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_versions/mod.rs @@ -30,7 +30,6 @@ pub mod drive_token_method_versions; pub mod drive_verify_method_versions; pub mod drive_vote_method_versions; pub mod v1; -pub mod v10; pub mod v2; pub mod v3; pub mod v4; diff --git a/packages/rs-platform-version/src/version/drive_versions/v10.rs b/packages/rs-platform-version/src/version/drive_versions/v10.rs deleted file mode 100644 index cfef672d806..00000000000 --- a/packages/rs-platform-version/src/version/drive_versions/v10.rs +++ /dev/null @@ -1,17 +0,0 @@ -use super::drive_contract_method_versions::v5::DRIVE_CONTRACT_METHOD_VERSIONS_V5; -use super::drive_token_method_versions::v3::DRIVE_TOKEN_METHOD_VERSIONS_V3; -use super::v9::DRIVE_VERSION_V9; -use super::{DriveInitializationMethodVersions, DriveMethodVersions, DriveVersion}; - -/// Protocol 15 extends protocol 14 storage with token shielded pools and their conservation check. -pub const DRIVE_VERSION_V10: DriveVersion = DriveVersion { - methods: DriveMethodVersions { - initialization: DriveInitializationMethodVersions { - create_initial_state_structure: 5, - }, - token: DRIVE_TOKEN_METHOD_VERSIONS_V3, - contract: DRIVE_CONTRACT_METHOD_VERSIONS_V5, - ..DRIVE_VERSION_V9.methods - }, - ..DRIVE_VERSION_V9 -}; diff --git a/packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs b/packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs index 8d8dfea39a8..522ebbe750c 100644 --- a/packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs +++ b/packages/rs-platform-version/src/version/feature_initial_protocol_versions.rs @@ -24,5 +24,5 @@ pub const CONTRACT_FEE_CLAIM_INITIAL_PROTOCOL_VERSION: ProtocolVersion = 14; pub const APP_CONNECT_CONTRACT_INITIAL_PROTOCOL_VERSION: ProtocolVersion = 14; /// Token shielded pools (per-token Orchard pools behind `TokenConfigurationV1::has_shielded_pool`, /// with the batch token transitions that shield, unshield, transfer, mint, burn, claim and -/// purchase into or out of them) activate with protocol version 15. -pub const TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION: ProtocolVersion = 15; +/// purchase into or out of them) activate with protocol version 14. +pub const TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION: ProtocolVersion = 14; diff --git a/packages/rs-platform-version/src/version/mod.rs b/packages/rs-platform-version/src/version/mod.rs index ae5fd0887e1..1b1635efb42 100644 --- a/packages/rs-platform-version/src/version/mod.rs +++ b/packages/rs-platform-version/src/version/mod.rs @@ -1,6 +1,6 @@ mod protocol_version; -use crate::version::v15::PROTOCOL_VERSION_15; +use crate::version::v14::PROTOCOL_VERSION_14; pub use protocol_version::*; use std::ops::RangeInclusive; @@ -20,7 +20,6 @@ pub mod v11; pub mod v12; pub mod v13; pub mod v14; -pub mod v15; pub mod v2; pub mod v3; pub mod v4; @@ -34,5 +33,5 @@ pub type ProtocolVersion = u32; pub const ALL_VERSIONS: RangeInclusive = 1..=LATEST_VERSION; -pub const LATEST_VERSION: ProtocolVersion = PROTOCOL_VERSION_15; +pub const LATEST_VERSION: ProtocolVersion = PROTOCOL_VERSION_14; pub const INITIAL_PROTOCOL_VERSION: ProtocolVersion = 1; diff --git a/packages/rs-platform-version/src/version/protocol_version.rs b/packages/rs-platform-version/src/version/protocol_version.rs index d6273f0e766..00cc470bbc7 100644 --- a/packages/rs-platform-version/src/version/protocol_version.rs +++ b/packages/rs-platform-version/src/version/protocol_version.rs @@ -22,7 +22,6 @@ use crate::version::v11::PLATFORM_V11; use crate::version::v12::PLATFORM_V12; use crate::version::v13::PLATFORM_V13; use crate::version::v14::PLATFORM_V14; -use crate::version::v15::PLATFORM_V15; use crate::version::v2::PLATFORM_V2; use crate::version::v3::PLATFORM_V3; use crate::version::v4::PLATFORM_V4; @@ -62,7 +61,6 @@ pub const PLATFORM_VERSIONS: &[PlatformVersion] = &[ PLATFORM_V12, PLATFORM_V13, PLATFORM_V14, - PLATFORM_V15, ]; #[cfg(feature = "mock-versions")] @@ -71,7 +69,7 @@ pub static PLATFORM_TEST_VERSIONS: OnceLock> = OnceLock::ne #[cfg(feature = "mock-versions")] const DEFAULT_PLATFORM_TEST_VERSIONS: &[PlatformVersion] = &[TEST_PLATFORM_V2, TEST_PLATFORM_V3]; -pub const LATEST_PLATFORM_VERSION: &PlatformVersion = &PLATFORM_V15; +pub const LATEST_PLATFORM_VERSION: &PlatformVersion = &PLATFORM_V14; pub const DESIRED_PLATFORM_VERSION: &PlatformVersion = LATEST_PLATFORM_VERSION; diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index 2dcdc60b4e3..b3e57fa5496 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -30,7 +30,7 @@ use crate::version::ProtocolVersion; pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; -/// v14 hosts six consensus changes: +/// v14 hosts twenty-five consensus changes: /// /// 1. **Contract-level ranked aggregates**: an index can /// declare that its groups are rankable by an aggregate, so a query like @@ -525,7 +525,6 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// **earliest** contender (creation time, block height, core height, /// document id) for every resolution, where the shipped rule awarded the /// latest; DPNS contests ending from this version on follow the new rule. -/// /// 24. **Contract references may require elected moderation**: a `contract` /// `refersTo` declaration may carry `contractRequirements`, what the referenced /// contract must declare beyond existing, with `moderation: "elected"` as @@ -537,6 +536,31 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// `ReferencedContractRequirementNotMetError` (40135). A changed /// `contractRequirements` is an incompatible schema change on update. /// +/// 25. **Token shielded pools**: a token configuration in format version 1 +/// (`TokenConfiguration::V1`, admitted by `CONTRACT_VERSIONS_V6`'s +/// `token_configuration_format` bounds) can set `hasShieldedPool`, which +/// gives the token its own Orchard pool under +/// `[Tokens, TOKEN_SHIELDED_POOLS_KEY, token_id]` laid out like the credit +/// pool. A pooled token must leave its freeze, unfreeze and destroy-frozen- +/// funds rules unassigned, since notes have no owner to freeze. Seven batch +/// token transitions (`TokenShield`, `TokenUnshield`, +/// `TokenShieldedTransfer`, `TokenMintToPool`, `TokenBurnFromPool`, +/// `TokenClaimToPool` and `TokenDirectPurchaseToPool`, validated through +/// `DRIVE_ABCI_VALIDATION_VERSIONS_V10` and gated by +/// `TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION`) move tokens between an +/// identity balance, the supply and the pool or inside it; the identity +/// signs and pays the fee in credits, and every spend bundle binds the +/// token id and the batch owner (a burn binds the burner: the batch owner, +/// or the proposer of a group action), plus the recipient and amount where +/// tokens leave the pool, into the Orchard sighash. The pool balances are a +/// term of the token conservation check (`calculate_total_tokens_balance` v1 +/// in `DRIVE_TOKEN_METHOD_VERSIONS_V2`). `record_token_shielded_pool_anchors` +/// (`DRIVE_ABCI_METHOD_VERSIONS_V10`) records and prunes the anchors of the +/// pools a block touched. The pools root tree is inserted by +/// `transition_to_version_14` and by `create_initial_state_structure` v4; +/// the six shielded queries accept an optional `token_id` to target a token +/// pool. +/// /// The app-connect system contract (`SystemDataContract::AppConnect`, schema v1) /// carries only the wallet's `loginKeyResponse`: a flat indexOnly entry keyed by /// the app's ephemeral key hash and the responding identity, with the wallet's @@ -600,23 +624,23 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// its gates on; Drive identity methods v2 rewrite the key and raise the remaining budget). pub const PLATFORM_V14: PlatformVersion = PlatformVersion { protocol_version: PROTOCOL_VERSION_14, - drive: DRIVE_VERSION_V9, // changed: drive document method versions v4 — v2 index walkers (shared-prefix aggregate indexes become insertable) + the detect_ranked_mode slot; contract method versions v4: the moderation list trees, the document removal record trees and the moderation method table; apply_drive_operations 1 (a moderator's document deletion refunds nobody); index uniqueness gains validate_restored_document_uniqueness (a moderator's document restore) + drive: DRIVE_VERSION_V9, // changed: token method versions v2 also carries calculate_total_tokens_balance 1 (token shielded pool balances join token conservation); drive document method versions v4 — v2 index walkers (shared-prefix aggregate indexes become insertable) + the detect_ranked_mode slot; contract method versions v4: the moderation list trees, the document removal record trees and the moderation method table; apply_drive_operations 1 (a moderator's document deletion refunds nobody); index uniqueness gains validate_restored_document_uniqueness (a moderator's document restore) drive_abci: DriveAbciVersion { structs: DRIVE_ABCI_STRUCTURE_VERSIONS_V2, // changed: saved platform state structure 1 keeps masternodes and validator sets as one aux entry each - methods: DRIVE_ABCI_METHOD_VERSIONS_V10, // changed: records the per-block total credits history for the daily withdrawal limit - validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V10, // changed: contested-index cross-check + refersTo document reference validation; the ContractUserModeration gates and the batch transformer's contract_moderation_gate + methods: DRIVE_ABCI_METHOD_VERSIONS_V10, // changed: records the per-block total credits history for the daily withdrawal limit; record_token_shielded_pool_anchors records and prunes the anchors of the token pools a block touched + validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V10, // changed: contested-index cross-check + refersTo document reference validation; the ContractUserModeration gates and the batch transformer's contract_moderation_gate; the three shielded-fee token pool transitions gain basic structure validation and document_base_transition_state_validation 1 admits a document token cost paid from a token pool withdrawal_constants: DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V3, // changed: prune bound for the total credits history query: DRIVE_ABCI_QUERY_VERSIONS_V3, // changed: ranked + boolean-HAVING routing gate; the v1 handler also resolves IN_TIME_RANGE from committed block time checkpoints: DRIVE_ABCI_CHECKPOINT_PARAMETERS_V1, }, dpp: DPPVersion { costs: DPP_COSTS_VERSIONS_V1, - validation: DPP_VALIDATION_VERSIONS_V5, // changed: validate_config_update 2 admits the contract moderation declaration of config V2 + validation: DPP_VALIDATION_VERSIONS_V5, // changed: validate_config_update 2 admits the contract moderation declaration of config V2; validate_token_config_update 1 keeps the shielded pool opt-in immutable after creation state_transition_serialization_versions: STATE_TRANSITION_SERIALIZATION_VERSIONS_V3, // changed: the indexOnly delete-by-values kind (documentIndexOnlyDelete) joins the wire; the ContractUserModeration transition state_transition_conversion_versions: STATE_TRANSITION_CONVERSION_VERSIONS_V2, state_transition_method_versions: STATE_TRANSITION_METHOD_VERSIONS_V2, // changed: public keys in creation may carry a budget or an expiry state_transitions: STATE_TRANSITION_VERSIONS_V4, - contract_versions: CONTRACT_VERSIONS_V6, // changed: v3 document meta-schema hosts the ranked, refersTo, requiredSince and timeRange keywords; validate_structure_interval v1 rejects a zero epoch interval; config max_version 2 (the contract moderation declaration) and validate_moderation_config + contract_versions: CONTRACT_VERSIONS_V6, // changed: token_configuration_format max_version 1 admits the shielded pool opt-in; v3 document meta-schema hosts the ranked, refersTo, requiredSince and timeRange keywords; validate_structure_interval v1 rejects a zero epoch interval; config max_version 2 (the contract moderation declaration) and validate_moderation_config document_versions: DOCUMENT_VERSIONS_V4, // changed: document serialization format 3 — the contract version stamp that enables `requiredSince` properties identity_versions: IDENTITY_VERSIONS_V1, voting_versions: VOTING_VERSION_V2, diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs deleted file mode 100644 index 0946a5a91d2..00000000000 --- a/packages/rs-platform-version/src/version/v15.rs +++ /dev/null @@ -1,55 +0,0 @@ -use super::dpp_versions::dpp_contract_versions::v7::CONTRACT_VERSIONS_V7; -use super::dpp_versions::dpp_validation_versions::v6::DPP_VALIDATION_VERSIONS_V6; -use super::dpp_versions::DPPVersion; -use super::drive_abci_versions::drive_abci_method_versions::v11::DRIVE_ABCI_METHOD_VERSIONS_V11; -use super::drive_abci_versions::drive_abci_validation_versions::v11::DRIVE_ABCI_VALIDATION_VERSIONS_V11; -use super::drive_abci_versions::DriveAbciVersion; -use super::drive_versions::v10::DRIVE_VERSION_V10; -use super::v14::PLATFORM_V14; -use super::{PlatformVersion, ProtocolVersion}; - -pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; - -/// v15 hosts one consensus change: -/// -/// 1. **Token shielded pools**: a token configuration in format version 1 -/// (`TokenConfiguration::V1`, admitted by `CONTRACT_VERSIONS_V7`'s -/// `token_configuration_format` bounds) can set `hasShieldedPool`, which -/// gives the token its own Orchard pool under -/// `[Tokens, TOKEN_SHIELDED_POOLS_KEY, token_id]` laid out like the credit -/// pool. A pooled token must leave its freeze, unfreeze and destroy-frozen- -/// funds rules unassigned, since notes have no owner to freeze. Seven batch -/// token transitions (`TokenShield`, `TokenUnshield`, -/// `TokenShieldedTransfer`, `TokenMintToPool`, `TokenBurnFromPool`, -/// `TokenClaimToPool` and `TokenDirectPurchaseToPool`, validated through -/// `DRIVE_ABCI_VALIDATION_VERSIONS_V11` and gated by -/// `TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION`) move tokens between an -/// identity balance, the supply and the pool or inside it; the identity -/// signs and pays the fee in credits, and every spend bundle binds the -/// token id and the batch owner (a burn binds the burner: the batch owner, -/// or the proposer of a group action), plus the recipient and amount where -/// tokens leave the pool, into the Orchard sighash. The pool balances are a term of the -/// token conservation check (`calculate_total_tokens_balance` v1 in -/// `DRIVE_TOKEN_METHOD_VERSIONS_V3`, reached through `DRIVE_VERSION_V10`). -/// `record_token_shielded_pool_anchors` (`DRIVE_ABCI_METHOD_VERSIONS_V11`) -/// records and prunes the anchors of the pools a block touched. The pools -/// root tree is inserted by the version 15 upgrade transition and by -/// `create_initial_state_structure` v5; the six shielded queries accept an -/// optional `token_id` to target a token pool. -/// -/// Everything else matches v14. -pub const PLATFORM_V15: PlatformVersion = PlatformVersion { - protocol_version: PROTOCOL_VERSION_15, - drive: DRIVE_VERSION_V10, - drive_abci: DriveAbciVersion { - methods: DRIVE_ABCI_METHOD_VERSIONS_V11, - validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V11, - ..PLATFORM_V14.drive_abci - }, - dpp: DPPVersion { - contract_versions: CONTRACT_VERSIONS_V7, - validation: DPP_VALIDATION_VERSIONS_V6, - ..PLATFORM_V14.dpp - }, - ..PLATFORM_V14 -}; diff --git a/packages/rs-sdk/src/platform/query.rs b/packages/rs-sdk/src/platform/query.rs index 0e1cf0bb4dc..f64b8dfa488 100644 --- a/packages/rs-sdk/src/platform/query.rs +++ b/packages/rs-sdk/src/platform/query.rs @@ -1438,7 +1438,7 @@ impl Query for ShieldedNullifiersQuery { } } -// --- Token Shielded Pool Queries (protocol version 15+) --- +// --- Token Shielded Pool Queries (protocol version 14+) --- impl Query for TokenShieldedPoolQuery { fn query( diff --git a/packages/wasm-dpp2/src/state_transitions/batch/token_payment_info.rs b/packages/wasm-dpp2/src/state_transitions/batch/token_payment_info.rs index e014659e61a..43afea286bc 100644 --- a/packages/wasm-dpp2/src/state_transitions/batch/token_payment_info.rs +++ b/packages/wasm-dpp2/src/state_transitions/batch/token_payment_info.rs @@ -37,7 +37,7 @@ export interface TokenPaymentInfoOptions { gasFeesPaidBy?: GasFeesPaidByLike; /** * Pay the token cost out of the token's shielded pool with this spend bundle instead of - * the document owner's token balance (format version 1, protocol version 15+). + * the document owner's token balance (format version 1, protocol version 14+). */ shieldedPayment?: TokenShieldedPayment; } diff --git a/packages/wasm-dpp2/src/tokens/configuration/token_configuration.rs b/packages/wasm-dpp2/src/tokens/configuration/token_configuration.rs index 5d97918f694..211a3fbbba7 100644 --- a/packages/wasm-dpp2/src/tokens/configuration/token_configuration.rs +++ b/packages/wasm-dpp2/src/tokens/configuration/token_configuration.rs @@ -65,7 +65,7 @@ export interface TokenConfigurationOptions { mainControlGroupCanBeModified: AuthorizedActionTakers; description?: string; /** - * Give the token its own shielded pool (protocol version 15+). Produces a + * Give the token its own shielded pool (protocol version 14+). Produces a * format-version-1 token configuration; the flag cannot change after creation, and * freezeRules, unfreezeRules and destroyFrozenFundsRules must then authorize no one * (no action takers and no admins): shielded notes cannot be frozen or destroyed. From cad86345cad7ade2974673518e2c2d9dd9177fe1 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 22 Sep 2026 13:10:59 +0700 Subject: [PATCH 16/94] test(drive-abci): pin the token pool admission and anchor bookkeeping rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two review findings were fixed without a test that would have caught them, and the version retarget left four test names naming a protocol version that no longer exists. A paid rejection writes to no token pool, so its token id must not reach the block-end anchor recorder: recording an anchor for a pool that was never created fails the read, and that error propagates out of run_block_proposal and aborts the whole proposal. The success path already asserted the pool is registered; the rejection path now asserts the set stays empty. CheckTx must also price the Orchard bundle before it agrees to verify it. The compute fee is charged where the action is built, which CheckTx does, so the balance CheckTx demands already contains it. The new test funds an identity past the preliminary batch minimum — which has no Orchard component, and otherwise rejects before the real estimate is ever reached — and exactly one compute fee short of the batch, then asserts the reported required balance covers the fee. Both tests were run against the unfixed code: dropping the SuccessfulExecution guard makes the first report the pool it must not have registered, and dropping the compute fee from the shield action transformer makes CheckTx admit a transition the identity cannot pay for. ✖ before each fix, ✔ after. Co-Authored-By: Claude Opus 5 (1M context) --- .../batch/tests/token/shielded/mod.rs | 113 +++++++++++++++++- 1 file changed, 109 insertions(+), 4 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 79b0a7a3323..5485004dd49 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -337,6 +337,39 @@ mod token_shielded_pool_tests { ); } + /// The counterpart of [`assert_check_tx_accepts`] for an identity that cannot pay. + /// + /// The shielded compute fee is charged when the action is built, which happens inside + /// CheckTx, so an identity that cannot cover it is refused at admission rather than after + /// the node has already run the Halo 2 verification the batch asks for. + pub(super) fn assert_check_tx_rejects( + platform: &TempPlatform, + transition: &StateTransition, + ) -> Vec { + let state = platform.state.load(); + let platform_ref = PlatformRef { + drive: &platform.drive, + state: &state, + config: &platform.config, + core_rpc: &platform.core_rpc, + }; + let result = platform + .check_tx( + &transition + .serialize_to_bytes() + .expect("serialize transition"), + CheckTxLevel::FirstTimeCheck, + &platform_ref, + PlatformVersion::latest(), + ) + .expect("check tx"); + assert!( + !result.is_valid(), + "CheckTx admitted a transition the identity cannot pay for" + ); + result.errors + } + pub(super) fn platform_with_latest_version() -> TempPlatform { TestPlatformBuilder::new() .with_latest_protocol_version() @@ -618,6 +651,15 @@ mod token_shielded_pool_tests { identity_token_balance(&platform, token_id, identity.id()), Some(OWNER_INITIAL_BALANCE) ); + // The token owns no pool, so its id must not reach the block-end anchor recorder: + // recording an anchor for a pool that was never created fails the read, and that error + // propagates out of `run_block_proposal` and aborts the whole proposal. A paid rejection + // writes to no pool, so only a successful execution may register one. + assert!( + result.token_shielded_pools_touched().is_empty(), + "a paid rejection must register no pool for anchor recording, got {:?}", + result.token_shielded_pools_touched() + ); } #[tokio::test] @@ -668,6 +710,69 @@ mod token_shielded_pool_tests { assert_eq!(pool_balance(&platform, token_id), 0); } + /// CheckTx must price the Orchard bundle before it agrees to verify it. + /// + /// The flat shielded compute fee is charged where the action is built, and CheckTx builds + /// the action, so an identity whose credits cannot cover that fee is refused at admission. + /// If the fee were only added during batch state validation — which CheckTx skips — the + /// mempool would accept the batch, run the Halo 2 verification it asks for, and only the + /// block would discover the identity could never pay. + #[tokio::test] + async fn test_token_shield_rejected_by_check_tx_when_credits_cannot_cover_the_compute_fee() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9004); + + let bundle = build_shield_bundle(SHIELD_AMOUNT, 13); + let compute_fee = dpp::shielded::compute_shielded_verification_fee( + bundle.actions.len(), + platform_version, + ) + .expect("shielded compute fee"); + + // Funded well past the preliminary batch minimum — which has no Orchard component — so + // the rejection has to come from the full fee estimate, and exactly one compute fee + // short of affording the batch. + let (identity, signer, key) = setup_identity(&mut platform, rng.gen(), compute_fee); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + + let shield = BatchTransition::new_token_shield_transition( + token_id, + identity.id(), + contract.id(), + 0, + SHIELD_AMOUNT, + bundle, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token shield transition"); + + // The balance CheckTx demands must already contain the compute fee. If the fee were + // only added later, during batch state validation, the figure here would cover the + // metered storage alone and an identity funded for it would pass admission. + let errors = assert_check_tx_rejects(&platform, &shield); + assert_matches!( + errors.as_slice(), + [ConsensusError::StateError(StateError::IdentityInsufficientBalanceError(error))] + if error.required_balance() >= compute_fee + ); + assert_eq!(pool_balance(&platform, token_id), 0); + } + #[tokio::test] async fn test_token_shield_rejected_when_token_paused() { let platform_version = PlatformVersion::latest(); @@ -847,7 +952,7 @@ mod token_shielded_pool_tests { } #[tokio::test] - async fn test_token_shield_rejected_before_protocol_version_15() { + async fn test_token_shield_rejected_before_protocol_version_14() { let platform_version = PlatformVersion::get(TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION - 1) .expect("previous protocol version"); @@ -965,7 +1070,7 @@ mod token_shielded_pool_tests { } #[tokio::test] - async fn test_contract_create_with_shielded_pool_token_rejected_before_protocol_version_15() { + async fn test_contract_create_with_shielded_pool_token_rejected_before_protocol_version_14() { let platform_version = PlatformVersion::get(TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION - 1) .expect("previous protocol version"); @@ -2836,7 +2941,7 @@ mod document_shielded_token_payment_tests { } #[tokio::test] - async fn test_document_shielded_payment_rejected_before_protocol_version_15() { + async fn test_document_shielded_payment_rejected_before_protocol_version_14() { let platform_version = PlatformVersion::get(TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION - 1) .expect("previous protocol version"); @@ -3343,7 +3448,7 @@ mod token_pool_paid_transitions_tests { } #[tokio::test] - async fn test_token_pool_paid_transitions_rejected_before_protocol_version_15() { + async fn test_token_pool_paid_transitions_rejected_before_protocol_version_14() { let platform_version = PlatformVersion::get(TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION - 1) .expect("previous protocol version"); From f963665c6eeffa7da89b63f610ddf3495b799d79 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 22 Sep 2026 19:30:56 +0700 Subject: [PATCH 17/94] chore(platform): tighten the token pool freeze tests, comments and cost constants An audit of the token pool work turned up seven places where the code says something other than what it does. None changes behaviour. The frozen-discriminant tests are the load-bearing ones. `BasicError` is bincode-encoded positionally, and its test's own doc comment requires each new tail variant to get a line there; `TokenShieldedPoolIncompatibleRulesError` was appended to the enum without one, leaving 194 unpinned, so a later insertion above it would shift its wire discriminant unnoticed. `StateError` pins its three new variants but still carried the `the tail of the enum` marker on the variant that is no longer the tail. Both now say what is true. The structure fixture test had its expectation replaced by the same expression the fixture stamps the value with, so it could no longer catch an origin recorded under the wrong version; it goes back to the literal. The CheckTx affordability test is pinned with `>` rather than `>=`: the identity is funded with exactly one compute fee, so the required balance is strictly greater. `test_genesis_v14_...` had been inserted between the v11-to-v12 equivalence guard's doc comment and the test it documents, so a thirty-six line account of the `[ShieldedBalances]` subtree sat on the token pool test while the v12 test had none. The block is back where it belongs, and its development narration is now a statement of the mechanism instead. A token pool stores the same five items as the credit pool and was sizing them from its own copy of four constants. They now have one declaration: a divergence there would price one pool's writes wrong while the other stayed right. The enum-ordering rule on `StateError` was written as a `///` doc comment on one variant, where rustdoc renders it as that variant's description rather than a rule about the enum. It is a plain comment now, matching `BasicError`. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/errors/consensus/basic/basic_error.rs | 11 ++- .../src/errors/consensus/state/state_error.rs | 8 ++- .../v0/mod.rs | 69 +++++++++---------- .../batch/tests/token/shielded/mod.rs | 2 +- .../src/drive/shielded/estimated_costs.rs | 8 +-- .../tokens/shielded_pool/estimated_costs.rs | 18 ++--- packages/rs-drive/src/structure/tests.rs | 8 +-- 7 files changed, 57 insertions(+), 67 deletions(-) diff --git a/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs b/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs index 218f69798d0..c6bb30bdb81 100644 --- a/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs +++ b/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs @@ -884,13 +884,20 @@ mod tests { )), 192 ); - // Documents cited by a contract moderation reason (protocol version 14): the tail of - // the enum. + // Documents cited by a contract moderation reason (protocol version 14). assert_eq!( discriminant_of(BasicError::InvalidContractModerationReasonDocumentsError( InvalidContractModerationReasonDocumentsError::new("x".to_string()) )), 193 ); + // A token opting into a shielded pool keeps no freeze rules (protocol version 14): the + // tail of the enum. + assert_eq!( + discriminant_of(BasicError::TokenShieldedPoolIncompatibleRulesError( + TokenShieldedPoolIncompatibleRulesError::new(0, "freezeRules".to_string()) + )), + 194 + ); } } diff --git a/packages/rs-dpp/src/errors/consensus/state/state_error.rs b/packages/rs-dpp/src/errors/consensus/state/state_error.rs index 85cedb06697..0f98b5506d6 100644 --- a/packages/rs-dpp/src/errors/consensus/state/state_error.rs +++ b/packages/rs-dpp/src/errors/consensus/state/state_error.rs @@ -588,8 +588,9 @@ pub enum StateError { #[error(transparent)] ReferencedContractRequirementNotMetError(ReferencedContractRequirementNotMetError), - /// Variants are encoded by position: new ones go at the end (see the frozen - /// discriminant test below). + // NOTE: `StateError` is bincode-encoded positionally, so a new variant MUST be appended at + // the tail: inserting mid-enum shifts the wire discriminant of every variant after it and + // mis-decodes errors already encoded. The error code in `codes.rs` is independent of order. #[error(transparent)] TokenShieldedPoolNotEnabledError(TokenShieldedPoolNotEnabledError), @@ -1097,7 +1098,7 @@ mod tests { )), 142 ); - // Requirements on a referenced contract (protocol version 14): the tail of the enum. + // Requirements on a referenced contract (protocol version 14). assert_eq!( discriminant_of(StateError::ReferencedContractRequirementNotMetError( ReferencedContractRequirementNotMetError::new( @@ -1109,6 +1110,7 @@ mod tests { )), 143 ); + // Token shielded pools (protocol version 14): the tail of the enum. assert_eq!( discriminant_of(StateError::TokenShieldedPoolNotEnabledError( TokenShieldedPoolNotEnabledError::new(Identifier::from([1; 32])) diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index f1f724bef0e..81dedf336ff 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -3063,43 +3063,6 @@ mod tests { ); } - /// CONSENSUS-CRITICAL equivalence guard for the v11→v12 boundary. - /// - /// The `[ShieldedBalances]` subtree is built two ways that MUST be - /// byte-identical: - /// - /// * GENESIS path — a node that state-syncs a fresh v12 chain runs the real - /// `Drive::create_initial_state_structure_v3` (protocol-v12 genesis). - /// * UPGRADE path — a node already on v11 runs the real - /// `Platform::transition_to_version_12` at the activation block. - /// - /// Before the fix these diverged: genesis built the pool via a sorted - /// `GroveDbOpBatch`, which roots the parent Merk at the batch's median key - /// `[160]`; the upgrade built it with sequential breadth-first inserts, which - /// root it at `[128]` (the intended NOTES-at-root layout). Two different - /// subtree shapes ⇒ a state-synced v12 node and an in-place-upgraded v12 node - /// would compute different app hashes at the boundary block and fork. - /// - /// This test drives the REAL production functions (not a hand-rebuilt batch) - /// — Platform A is a genuine genesis-v12, Platform B is a genuine genesis-v11 - /// then the real `transition_to_version_12`. It asserts that the ENTIRE - /// `[ShieldedBalances]` subtree (the main pool element with its carried - /// `root_key`, all eight children, and every node below them) is byte- - /// identical via `collect_subtree_diffs`. - /// - /// We compare the named subtree rather than the whole-DB root hash on - /// purpose: the whole DB also carries the genesis epoch's recorded protocol- - /// version field (`[Pools, epoch_0, "v"]` = 12 for Platform A, 11 for - /// Platform B), which legitimately differs between a chain *born* at v12 and - /// a chain born at v11 then upgraded. That field has nothing to do with how - /// the shielded pool is constructed and would pollute a whole-DB comparison. - /// The diagnostic that localized the original bug confirmed the shielded - /// subtree was the ONLY construction-driven divergence. - /// - /// RED before the fix (genesis pool root `[160]` ≠ upgrade pool root `[128]`, - /// plus a cascade of differing child hashes), GREEN after (both `[128]`, - /// because both paths now call the shared - /// `Drive::insert_shielded_pool_structure`). /// The token shielded pools root is built by two paths that must coincide byte for byte: /// `Drive::create_initial_state_structure_v4` at a v14 genesis and /// `transition_to_version_14` on a chain upgraded from v13. @@ -3153,6 +3116,38 @@ mod tests { ); } + /// CONSENSUS-CRITICAL equivalence guard for the v11→v12 boundary. + /// + /// The `[ShieldedBalances]` subtree is built two ways that MUST be + /// byte-identical: + /// + /// * GENESIS path — a node that state-syncs a fresh v12 chain runs the real + /// `Drive::create_initial_state_structure_v3` (protocol-v12 genesis). + /// * UPGRADE path — a node already on v11 runs the real + /// `Platform::transition_to_version_12` at the activation block. + /// + /// Genesis builds the pool through a sorted `GroveDbOpBatch`, which roots the parent + /// Merk at the batch's median key `[160]`; sequential breadth-first inserts root it at + /// `[128]`, the intended NOTES-at-root layout. Two different subtree shapes mean a + /// state-synced node and an in-place-upgraded node compute different app hashes at the + /// boundary block and fork, so both paths go through the shared + /// `Drive::insert_shielded_pool_structure`. + /// + /// This test drives the REAL production functions (not a hand-rebuilt batch) + /// — Platform A is a genuine genesis-v12, Platform B is a genuine genesis-v11 + /// then the real `transition_to_version_12`. It asserts that the ENTIRE + /// `[ShieldedBalances]` subtree (the main pool element with its carried + /// `root_key`, all eight children, and every node below them) is byte- + /// identical via `collect_subtree_diffs`. + /// + /// We compare the named subtree rather than the whole-DB root hash on + /// purpose: the whole DB also carries the genesis epoch's recorded protocol- + /// version field (`[Pools, epoch_0, "v"]` = 12 for Platform A, 11 for + /// Platform B), which legitimately differs between a chain *born* at v12 and + /// a chain born at v11 then upgraded. That field has nothing to do with how + /// the shielded pool is constructed and would pollute a whole-DB comparison. + /// The diagnostic that localized the original bug confirmed the shielded + /// subtree was the ONLY construction-driven divergence. #[test] fn test_genesis_v12_and_upgrade_to_v12_build_identical_shielded_pool() { let platform_version_12 = PlatformVersion::get(12).expect("expected v12"); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 5485004dd49..93a514c9548 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -768,7 +768,7 @@ mod token_shielded_pool_tests { assert_matches!( errors.as_slice(), [ConsensusError::StateError(StateError::IdentityInsufficientBalanceError(error))] - if error.required_balance() >= compute_fee + if error.required_balance() > compute_fee ); assert_eq!(pool_balance(&platform, token_id), 0); } diff --git a/packages/rs-drive/src/drive/shielded/estimated_costs.rs b/packages/rs-drive/src/drive/shielded/estimated_costs.rs index 859286eb24b..7a1a5d61ed2 100644 --- a/packages/rs-drive/src/drive/shielded/estimated_costs.rs +++ b/packages/rs-drive/src/drive/shielded/estimated_costs.rs @@ -14,16 +14,16 @@ use std::collections::HashMap; /// Average size of a note value: 32 cmx + 32 rho + 32 cv_net + 216 encrypted note = 312 bytes /// (encrypted note = 32 epk + 104 enc_ciphertext + 80 out_ciphertext, using DashMemo 36-byte memos) /// The cmx, rho, and cv_net are prepended by GroveDB's commitment_tree_insert_op for client retrieval. -const AVERAGE_NOTE_VALUE_SIZE: u32 = 312; +pub(in crate::drive) const AVERAGE_NOTE_VALUE_SIZE: u32 = 312; /// Size of a nullifier key (32 bytes) -const NULLIFIER_KEY_SIZE: u8 = 32; +pub(in crate::drive) const NULLIFIER_KEY_SIZE: u8 = 32; /// Size of an anchor key (32 bytes) -const ANCHOR_KEY_SIZE: u8 = 32; +pub(in crate::drive) const ANCHOR_KEY_SIZE: u8 = 32; /// Size of an anchor value (u64 big-endian block height = 8 bytes) -const ANCHOR_VALUE_SIZE: u32 = 8; +pub(in crate::drive) const ANCHOR_VALUE_SIZE: u32 = 8; impl Drive { /// Adds estimation costs for shielded pool operations. diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/estimated_costs.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/estimated_costs.rs index 273fa31a1f6..71ed7fbfa0b 100644 --- a/packages/rs-drive/src/drive/tokens/shielded_pool/estimated_costs.rs +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/estimated_costs.rs @@ -1,3 +1,8 @@ +// A token pool stores the same five items as the credit pool, so it is sized from the same +// constants: a divergence here would price one pool's writes wrong while the other stays right. +use crate::drive::shielded::estimated_costs::{ + ANCHOR_KEY_SIZE, ANCHOR_VALUE_SIZE, AVERAGE_NOTE_VALUE_SIZE, NULLIFIER_KEY_SIZE, +}; use crate::drive::shielded::paths::{ token_shielded_pool_anchors_by_height_path_vec, token_shielded_pool_anchors_path_vec, token_shielded_pool_notes_path_vec, token_shielded_pool_nullifiers_path_vec, @@ -13,19 +18,6 @@ use grovedb::EstimatedSumTrees::{AllBigSumTrees, AllSumTrees, NoSumTrees, SomeSu use grovedb::{EstimatedLayerInformation, TreeType}; use std::collections::HashMap; -/// Average size of a note value: 32 cmx + 32 rho + 32 cv_net + 216 encrypted note = 312 bytes -/// (the same item the credit pool stores; see `crate::drive::shielded::estimated_costs`). -const AVERAGE_NOTE_VALUE_SIZE: u32 = 312; - -/// Size of a nullifier key (32 bytes) -const NULLIFIER_KEY_SIZE: u8 = 32; - -/// Size of an anchor key (32 bytes) -const ANCHOR_KEY_SIZE: u8 = 32; - -/// Size of an anchor value (u64 big-endian block height = 8 bytes) -const ANCHOR_VALUE_SIZE: u32 = 8; - impl Drive { /// Registers the layer information for every tree a token shielded pool write can touch, /// from the root down to the pool's five children, so a batch touching the pool can be diff --git a/packages/rs-drive/src/structure/tests.rs b/packages/rs-drive/src/structure/tests.rs index 205c594157b..886dc8b8dc0 100644 --- a/packages/rs-drive/src/structure/tests.rs +++ b/packages/rs-drive/src/structure/tests.rs @@ -119,13 +119,7 @@ fn should_record_a_contract_layer_with_its_documents_on_top() { // fixture. Documents are read most and sit at the root of the layer; the // contract itself and everything else hang below. let contract = &json["layer_shapes"]["contracts.contract"]; - assert_eq!( - contract["origin"], - format!( - "fixture contracts_with_documents@{}", - PlatformVersion::latest().protocol_version - ) - ); + assert_eq!(contract["origin"], "fixture contracts_with_documents@14"); assert_eq!(contract["tree"]["hex"], "01"); assert_eq!(contract["tree"]["left"]["hex"], "00"); assert_eq!(contract["tree"]["right"]["hex"], "02"); From 7e142c755e3b51decb929056e2fa650bc1bd3781 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 22 Sep 2026 19:39:16 +0700 Subject: [PATCH 18/94] fix(drive-abci)!: make the unreleased shielded proof dispatchers exhaustive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A catch-all arm in a consensus dispatcher does not report an error when a kind is missing — it reports success. `_ => continue` skips the proof of a batched transition that carries one, and `_ => return Ok(new())` declares a shielded transition valid without ever verifying it. Adding a transition kind later would compile cleanly and open the hole silently. Both dispatchers in the unreleased generation now name every kind they decline. A future kind that carries an Orchard bundle stops the build until someone gives it an arm, which turns a production hole into a compile error. The codebase already takes this position where a missed variant would matter, in `process_raw_state_transitions`: "Deliberately exhaustive: a new execution result variant must make an explicit savepoint decision here." Behaviour is unchanged. The enumerations were verified by the compiler, which accepts neither a missing nor an unreachable pattern, so the set of kinds that fall through is exactly what it was. The two matching arms in the shipped generations are deliberately left alone: `validate_shielded_proof` v0 and `validate_minimum_shielded_fee` generation 0 are selected by protocol versions 13 and below, where a shipped file must stay byte-identical to what consensus already ran. Neither is a live hole there — the kinds they would fall through on are refused by `is_allowed` before any proof path is reached. Co-Authored-By: Claude Opus 5 (1M context) --- .../processor/traits/shielded_proof.rs | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs index 7e007a5cf02..400df831a56 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs @@ -1053,7 +1053,22 @@ fn validate_batch_token_shielded_proofs( &extra_sighash_data, ) } - _ => continue, + // Deliberately exhaustive: a batched transition that carries an Orchard bundle must + // have an arm above. Naming the bundle-less kinds means a new kind that does carry + // one stops the build here rather than silently skipping proof verification. + BatchedTransitionRef::Token( + TokenTransition::Burn(_) + | TokenTransition::Mint(_) + | TokenTransition::Transfer(_) + | TokenTransition::Freeze(_) + | TokenTransition::Unfreeze(_) + | TokenTransition::DestroyFrozenFunds(_) + | TokenTransition::Claim(_) + | TokenTransition::EmergencyAction(_) + | TokenTransition::ConfigUpdate(_) + | TokenTransition::DirectPurchase(_) + | TokenTransition::SetPriceForDirectPurchase(_), + ) => continue, }; if let Err(e) = result { return Ok(SimpleConsensusValidationResult::new_with_error( @@ -1360,8 +1375,34 @@ fn validate_shielded_proof_v1( } }, // ShieldFromAssetLock retains proof verification in transform_into_action; - // its paid-failure action comes from the asset lock. - _ => return Ok(SimpleConsensusValidationResult::new()), + // its paid-failure action comes from the asset lock, so it is not reached + // through this path. + // + // Deliberately exhaustive: the remaining kinds carry no Orchard bundle, and + // `has_shielded_proof_validation` keeps them out of here. Naming them means a + // new shielded kind stops the build until it is given an arm above, rather + // than falling through and being reported valid without a proof ever running. + StateTransition::ShieldFromAssetLock(_) + | StateTransition::DataContractCreate(_) + | StateTransition::DataContractUpdate(_) + | StateTransition::Batch(_) + | StateTransition::IdentityCreate(_) + | StateTransition::IdentityTopUp(_) + | StateTransition::IdentityCreditWithdrawal(_) + | StateTransition::IdentityUpdate(_) + | StateTransition::IdentityCreditTransfer(_) + | StateTransition::MasternodeVote(_) + | StateTransition::IdentityCreditTransferToAddresses(_) + | StateTransition::IdentityCreateFromAddresses(_) + | StateTransition::IdentityTopUpFromAddresses(_) + | StateTransition::AddressFundsTransfer(_) + | StateTransition::AddressFundingFromAssetLock(_) + | StateTransition::AddressCreditWithdrawal(_) + | StateTransition::IdentityKeyLimitsUpdate(_) + | StateTransition::ContractUserModeration(_) + | StateTransition::ContractFeeClaim(_) => { + return Ok(SimpleConsensusValidationResult::new()) + } }; match result { From 52facecba314d1eb1014a53c5a97b3b427c58acf Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 22 Sep 2026 19:50:23 +0700 Subject: [PATCH 19/94] test(drive-abci): pin that a token pool's anchors and nullifiers are its own MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each token pool reads and writes through a path built from its token id, so a twin that supplied the wrong path would let one token's anchor authorize another token's spend, or one pool's spend make a note unspendable in every other pool. Nothing in the suite pinned that: no test used two tokens, and the unknown-anchor test spends against an anchor recorded in no pool at all, so it passes whether the lookup reads this token's tree, another token's, or the credit pool's. Two tests cross the boundary. The first records an anchor in a second token's pool and requires the spend against it to still be refused. The second spends a note in one pool and requires the nullifier to read as unspent in the other. Both were run against a deliberately broken path builder that ignores the token id, and both fail there; the unknown-anchor test passes under the same defect, which is the gap they close. The second contract is owned by a second identity because a contract id derives from its owner — with one owner the helper hands back the same contract, both tokens share a pool, and the tests prove nothing. An assertion pins that too, and it is what caught this while writing them. Co-Authored-By: Claude Opus 5 (1M context) --- .../batch/tests/token/shielded/mod.rs | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 93a514c9548..476df826d4d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -951,6 +951,195 @@ mod token_shielded_pool_tests { ); } + /// A token pool's anchors are namespaced by token id, so an anchor the chain recorded for + /// one token proves nothing about another token's note tree. + /// + /// The existing unknown-anchor test spends against an anchor recorded in no pool at all, so + /// it would still pass if the lookup read the credit pool's tree or the wrong token's. This + /// one records the anchor in a real, neighbouring pool and requires the spend to be refused + /// anyway. + #[tokio::test] + async fn test_token_unshield_rejects_an_anchor_recorded_in_another_tokens_pool() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9101); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (recipient, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.1)); + + let (contract_a, token_a) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + // A second owner, because a contract id derives from its owner: the same owner would + // give back the same contract and the same pool, and the test would prove nothing. + let (other_owner, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.1)); + let (_contract_b, token_b) = create_token_contract_with_owner_identity( + &mut platform, + other_owner.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + assert_ne!(token_a, token_b, "the two tokens must own separate pools"); + + // Recorded in token B's pool, and nowhere else. + let (note, anchor, merkle_path) = spendable_note(6_000, 21); + insert_token_pool_anchor(&platform, token_b, &anchor); + + let unshield_amount = 1_000; + let extra = token_unshield_extra_sighash_data_v0( + &token_a.to_buffer(), + &identity.id().to_buffer(), + &recipient.id().to_buffer(), + unshield_amount, + ); + let (unshield_bundle, _) = + build_spend_bundle(note, merkle_path, anchor, unshield_amount, &extra, 22); + + let unshield = BatchTransition::new_token_unshield_transition( + token_a, + identity.id(), + contract_a.id(), + 0, + unshield_amount, + recipient.id(), + unshield_bundle, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token unshield transition"); + + let result = process(&platform, &unshield); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::InvalidAnchorError(_)), + .. + }] + ); + assert_eq!( + identity_token_balance(&platform, token_a, recipient.id()), + None + ); + } + + /// Spent nullifiers are namespaced by token id too: spending a note in one token's pool must + /// not mark that nullifier spent in another's, or the first token to use a nullifier would + /// make every other pool's note with the same nullifier unspendable. + #[tokio::test] + async fn test_a_nullifier_spent_in_one_token_pool_is_unspent_in_another() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9102); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (recipient, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.1)); + + let (contract_a, token_a) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + // A second owner, because a contract id derives from its owner: the same owner would + // give back the same contract and the same pool, and the test would prove nothing. + let (other_owner, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.1)); + let (_contract_b, token_b) = create_token_contract_with_owner_identity( + &mut platform, + other_owner.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + assert_ne!(token_a, token_b, "the two tokens must own separate pools"); + + let shield = BatchTransition::new_token_shield_transition( + token_a, + identity.id(), + contract_a.id(), + 0, + SHIELD_AMOUNT, + build_shield_bundle(SHIELD_AMOUNT, 23), + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token shield transition"); + assert_matches!( + process(&platform, &shield).execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + let (note, anchor, merkle_path) = spendable_note(6_000, 24); + insert_token_pool_anchor(&platform, token_a, &anchor); + let unshield_amount = 4_000; + let extra = token_unshield_extra_sighash_data_v0( + &token_a.to_buffer(), + &identity.id().to_buffer(), + &recipient.id().to_buffer(), + unshield_amount, + ); + let (unshield_bundle, _) = + build_spend_bundle(note, merkle_path, anchor, unshield_amount, &extra, 25); + + let unshield = BatchTransition::new_token_unshield_transition( + token_a, + identity.id(), + contract_a.id(), + 0, + unshield_amount, + recipient.id(), + unshield_bundle.clone(), + &key, + 3, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token unshield transition"); + assert_matches!( + process(&platform, &unshield).execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + for action in unshield_bundle.actions.iter() { + assert!( + nullifier_is_spent(&platform, token_a, &action.nullifier), + "the spend must be recorded in its own pool" + ); + assert!( + !nullifier_is_spent(&platform, token_b, &action.nullifier), + "a spend in one pool must not mark the nullifier spent in another" + ); + } + } + #[tokio::test] async fn test_token_shield_rejected_before_protocol_version_14() { let platform_version = From adde7431b98d341bc34fcd3a715f752e1e652916 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 22 Sep 2026 21:38:51 +0700 Subject: [PATCH 20/94] docs(platform): correct two claims the token pool chapter contradicts itself on The chapter said a shielded note cannot be frozen, destroyed "or even seen", and that shielded activity is not recorded publicly. Both overstate what the pool hides, and the same chapter documents the queries that disprove them: `getShieldedEncryptedNotes` returns the note ciphertexts, and the pool's spent nullifiers, note count and anchors are all in state. What a pool actually hides is the owner and the amount, not the existence of a note or of the operation. An issuer reading this needs the distinction to hold: it is the reason freeze rules must be disabled, and it is also the reason a pool is not by itself privacy. Co-Authored-By: Claude Opus 5 (1M context) --- book/src/data-model/token-shielded-pools.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/book/src/data-model/token-shielded-pools.md b/book/src/data-model/token-shielded-pools.md index a8a0155183d..9279fda5866 100644 --- a/book/src/data-model/token-shielded-pools.md +++ b/book/src/data-model/token-shielded-pools.md @@ -57,8 +57,10 @@ enabled can hold notes that would become unspendable, and a pool that is enabled need a tree created under an existing token. A pool also makes freezing and confiscation unenforceable: shielded notes belong to no identity -account, so a holder who expects a freeze shields first and nothing can freeze, destroy or even -see those notes. Rather than let an issuer advertise controls that cover only transparent +account, so a holder who expects a freeze shields first and nothing can freeze or destroy those +notes, because no account holds them for an issuer to act on. The notes themselves are not +hidden — their commitments and ciphertexts are stored and queryable — but who owns one and how +much it carries are. Rather than let an issuer advertise controls that cover only transparent balances, a token with `hasShieldedPool` must disable them permanently: `freezeRules`, `unfreezeRules` and `destroyFrozenFundsRules` must each authorize no one to take the action and have no admin action takers, so no later configuration update can switch them on. Contract @@ -103,7 +105,8 @@ bundle is therefore proven once, by the proposer: the digest covers the spend au signatures, so the sighash cannot depend on which signer's batch carries the bundle. It binds the group action's proposer as `burner_id` (the batch owner for a direct burn) and every other signer submits the proposer's bundle unchanged. No token history document is written for pool -operations: shielded activity is not recorded publicly. +operations, so the ledger records no actor for them. The operations still leave public traces: +spent nullifiers, the pool's note count and its anchors are all in state. ## Documents paid from the pool From bda2cf0af9b026aaba84e366e7505d3d3880baf9 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 22 Sep 2026 23:05:05 +0700 Subject: [PATCH 21/94] fix(drive): describe the GroveDB root the same way in both shielded pools A token pool and the credit pool own separate storage and describe their own subtrees differently, which is correct. They also both describe the GroveDB root `[]`, because both traverse it to reach that storage, and there they disagreed: the credit pool reports three levels with sum trees among the subtrees, the token pool reported two levels with none. The second is untrue. `Balances`, `Tokens` and `PreFundedSpecializedBalances` are all sum trees directly under the root, and `EstimatedLevel` describes the tree at that path rather than the caller, so one description of one tree cannot be right in two different shapes. The token pool now registers the credit pool's values verbatim, with a comment saying the two must not drift. The three identity-less token pool transitions register both into one estimation map under this one key, so matching values also make that entry independent of the order the operations convert in. Scope, for whoever reads this later: registering `[]` twice with different values is not new and not confined to shielded pools. `insert_contract` v1 calls five `add_estimation_costs_for_token_*` helpers in one pass, four of which claim two levels and no sum trees while the fifth claims three levels with them, and that has shipped since protocol version 13. Across `rs-drive` the root is described seven different ways in twenty-six helpers. This commit does not attempt that cleanup; it fixes the one untrue description this work introduced and leaves the pattern documented. Co-Authored-By: Claude Opus 5 (1M context) --- .../tokens/shielded_pool/estimated_costs.rs | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/estimated_costs.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/estimated_costs.rs index 71ed7fbfa0b..4f2caecd8fe 100644 --- a/packages/rs-drive/src/drive/tokens/shielded_pool/estimated_costs.rs +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/estimated_costs.rs @@ -14,7 +14,7 @@ use crate::util::type_constants::DEFAULT_HASH_SIZE_U8; use grovedb::batch::KeyInfoPath; use grovedb::EstimatedLayerCount::EstimatedLevel; use grovedb::EstimatedLayerSizes::{AllItems, AllSubtrees, Mix}; -use grovedb::EstimatedSumTrees::{AllBigSumTrees, AllSumTrees, NoSumTrees, SomeSumTrees}; +use grovedb::EstimatedSumTrees::{AllBigSumTrees, AllSumTrees, SomeSumTrees}; use grovedb::{EstimatedLayerInformation, TreeType}; use std::collections::HashMap; @@ -26,13 +26,36 @@ impl Drive { token_id: [u8; 32], estimated_costs_only_with_layer_info: &mut HashMap, ) { - // Root level: [] — the Tokens tree sits on layer 2 like the balances estimation assumes. + // Root level: []. This MUST stay identical to the credit pool's root registration in + // `crate::drive::shielded::estimated_costs`, because the three identity-less token pool + // transitions write both into one estimation map under this one key, and the later write + // wins. A divergence would make the surviving estimate depend on the order the operations + // happen to be converted in. + // + // `NoSumTrees` was also untrue of the root, whoever is asking: `Balances`, `Tokens` and + // `PreFundedSpecializedBalances` are sum trees directly under it. Of the two descriptions + // this one is the larger, which is the safe direction — `validate_fees_of_event` prices a + // batch with this estimated model during block execution and requires `estimated >= actual`. estimated_costs_only_with_layer_info.insert( KeyInfoPath::from_known_path([]), EstimatedLayerInformation { tree_type: TreeType::NormalTree, - estimated_layer_count: EstimatedLevel(2, false), - estimated_layer_sizes: AllSubtrees(1, NoSumTrees, None), + estimated_layer_count: EstimatedLevel(3, false), + estimated_layer_sizes: AllSubtrees( + 1, + SomeSumTrees { + sum_trees_weight: 2, + big_sum_trees_weight: 0, + count_trees_weight: 0, + count_sum_trees_weight: 0, + non_sum_trees_weight: 2, + provable_sum_trees_weight: 0, + provable_count_trees_weight: 0, + provable_count_sum_trees_weight: 0, + provable_count_provable_sum_trees_weight: 0, + }, + None, + ), }, ); From 4f827526b70b21fc4781daa42db2d1402c80ab68 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 23 Sep 2026 19:26:59 +0700 Subject: [PATCH 22/94] fix(platform): bind the pool and kind into outputs-only token bundle sighashes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four outputs-only token pool transitions — TokenShield, TokenMintToPool, TokenClaimToPool and TokenDirectPurchaseToPool — bound nothing into their Orchard sighash. Such a bundle also carries no anchor of its own: every token pool starts from the same empty-tree anchor, so its proof and binding signature verify against all of them. That left the authorized bundle bytes a free-standing, self-verifying object. Anyone could lift one out of the mempool into a transition of their own, paid for with their own tokens, against a different pool or a different transition kind, and land a second note with the same commitment and the same rho — hence the same nullifier, of which only one can ever be spent. There is no theft and no inflation in that: the copier pays, and the note is encrypted to the original recipient. The reason to close it is Faerie Gold. A wallet that merges notes by nullifier is safe, but any wallet or indexer that counts by commitment sees two payments where only one is real. Each of the four now binds `bundle tag (1) || token_id (32)`, the layout the identity-less token transitions already use. The tag is needed as well as the token id: a shield and a mint into the same pool agree on everything the proof covers — same flags, same empty-tree anchor, same value_balance — so the token id alone would let either be resubmitted as the other. The kind is named by the existing TokenTransitionActionType rather than by a second enumeration of the same four, and the sighash builder takes that type instead of a loose u8, so no call site can reach for another kind's tag. The byte each kind gets is still written out explicitly: that enum is append-only for the benefit of clients and promises consensus nothing about its ordering. The bytes sit above the state transition type range, which shares the slot and keeps growing, so the two tag spaces cannot collide as new types are added. The three shipped credit shields keep an empty preimage and their bytes are unchanged. The change lives in validate_shielded_proof_v1 and in the new feature's own v0; no shipped generation is touched. Not covered: a copy into the same pool and kind. Catching that needs the bundle's dummy nullifiers to be recorded and checked, which no outputs-only path does yet, here or in the credit pool. That is a separate change, because the credit paths have shipped and would need a new generation. Tests would have caught this in CI: test_token_shield_rejects_a_bundle_proved_for_another_tokens_pool test_token_mint_to_pool_rejects_a_bundle_proved_as_a_shield_of_the_same_token ✖ before the fix — both replays executed successfully — ✔ after. Co-Authored-By: Claude Opus 5 (1M context) --- packages/rs-dpp/src/shielded/builder/mod.rs | 27 +- .../rs-dpp/src/shielded/builder/shield.rs | 3 +- .../builder/shield_from_asset_lock.rs | 11 +- .../shielded/builder/shield_from_identity.rs | 3 +- .../shielded/builder/token_claim_to_pool.rs | 18 +- .../builder/token_direct_purchase_to_pool.rs | 18 +- .../shielded/builder/token_mint_to_pool.rs | 18 +- .../src/shielded/builder/token_shield.rs | 26 +- packages/rs-dpp/src/shielded/mod.rs | 9 +- packages/rs-dpp/src/shielded/sighash.rs | 192 +++++++++++++ .../processor/traits/shielded_proof.rs | 24 +- .../state_v0/mod.rs | 13 +- .../state_v0/mod.rs | 13 +- .../state_v0/mod.rs | 13 +- .../state_v0/mod.rs | 17 +- .../batch/tests/token/shielded/mod.rs | 260 ++++++++++++++++-- .../rs-platform-version/src/version/v14.rs | 17 +- packages/wasm-dpp2/src/shielded/mod.rs | 5 + 18 files changed, 620 insertions(+), 67 deletions(-) diff --git a/packages/rs-dpp/src/shielded/builder/mod.rs b/packages/rs-dpp/src/shielded/builder/mod.rs index dce3bc432fe..3651ecbf12b 100644 --- a/packages/rs-dpp/src/shielded/builder/mod.rs +++ b/packages/rs-dpp/src/shielded/builder/mod.rs @@ -219,6 +219,7 @@ pub(crate) fn build_output_only_bundle( memo: [u8; 36], sender_ovk: Option, dummy_outputs: usize, + extra_sighash_data: &[u8], prover: &P, ) -> Result, ProtocolError> { let payment_address = PaymentAddress::from(recipient); @@ -251,7 +252,7 @@ pub(crate) fn build_output_only_bundle( })?; } - prove_and_sign_bundle(builder, prover, &[], &[]) + prove_and_sign_bundle(builder, prover, &[], extra_sighash_data) } /// Builds a spend+output Orchard bundle. @@ -533,8 +534,9 @@ mod mod_tests { #[test] fn output_only_bundle_flags_and_value_balance() { let recipient = test_orchard_address(); - let bundle = build_output_only_bundle(&recipient, 10_000, [0u8; 36], None, 0, &TestProver) - .expect("bundle should build"); + let bundle = + build_output_only_bundle(&recipient, 10_000, [0u8; 36], None, 0, &[], &TestProver) + .expect("bundle should build"); // Spends are disabled for Shield / ShieldFromAssetLock bundles. assert!(!bundle.flags().spends_enabled()); @@ -567,9 +569,16 @@ mod mod_tests { // (dummy_outputs, expected on-wire action count). for (dummies, expected_actions) in [(0usize, 2usize), (1, 2), (5, 6)] { - let bundle = - build_output_only_bundle(&recipient, amount, [0u8; 36], None, dummies, &TestProver) - .expect("bundle should build"); + let bundle = build_output_only_bundle( + &recipient, + amount, + [0u8; 36], + None, + dummies, + &[], + &TestProver, + ) + .expect("bundle should build"); assert_eq!( bundle.actions().len(), expected_actions, @@ -592,8 +601,9 @@ mod mod_tests { #[test] fn serialize_authorized_bundle_preserves_fields() { let recipient = test_orchard_address(); - let bundle = build_output_only_bundle(&recipient, 7_777, [3u8; 36], None, 0, &TestProver) - .expect("bundle should build"); + let bundle = + build_output_only_bundle(&recipient, 7_777, [3u8; 36], None, 0, &[], &TestProver) + .expect("bundle should build"); let sb = serialize_authorized_bundle(&bundle); assert_eq!(sb.value_balance, *bundle.value_balance()); @@ -641,6 +651,7 @@ mod mod_tests { memo, Some(sender_ovk.clone()), 0, + &[], &TestProver, ) .expect("bundle should build"); diff --git a/packages/rs-dpp/src/shielded/builder/shield.rs b/packages/rs-dpp/src/shielded/builder/shield.rs index 7f8ffbbf8b0..e8d9a5cc558 100644 --- a/packages/rs-dpp/src/shielded/builder/shield.rs +++ b/packages/rs-dpp/src/shielded/builder/shield.rs @@ -54,7 +54,8 @@ pub async fn build_shield_transition, P: OrchardProve // Shield (Type 15) never pads with anonymity-set fillers — only the // Type 18 ShieldFromAssetLock pool-seeding path does (`dummy_outputs`). - let bundle = build_output_only_bundle(recipient, shield_amount, memo, sender_ovk, 0, prover)?; + let bundle = + build_output_only_bundle(recipient, shield_amount, memo, sender_ovk, 0, &[], prover)?; let sb = serialize_authorized_bundle(&bundle); ShieldTransition::try_from_bundle_with_signer( diff --git a/packages/rs-dpp/src/shielded/builder/shield_from_asset_lock.rs b/packages/rs-dpp/src/shielded/builder/shield_from_asset_lock.rs index cb9a3d70503..2b46bead121 100644 --- a/packages/rs-dpp/src/shielded/builder/shield_from_asset_lock.rs +++ b/packages/rs-dpp/src/shielded/builder/shield_from_asset_lock.rs @@ -52,6 +52,7 @@ pub fn build_shield_from_asset_lock_transition( memo, sender_ovk, dummy_outputs, + &[], prover, )?; let sb = serialize_authorized_bundle(&bundle); @@ -133,6 +134,7 @@ where memo, sender_ovk, dummy_outputs, + &[], prover, )?; let sb = serialize_authorized_bundle(&bundle); @@ -177,8 +179,9 @@ mod tests { let recipient = test_orchard_address(); let amount = 50_000u64; - let bundle = build_output_only_bundle(&recipient, amount, [0u8; 36], None, 0, &TestProver) - .expect("bundle should build successfully"); + let bundle = + build_output_only_bundle(&recipient, amount, [0u8; 36], None, 0, &[], &TestProver) + .expect("bundle should build successfully"); let sb = serialize_authorized_bundle(&bundle); // Output-only bundles have negative value_balance (value entering the pool) @@ -206,7 +209,7 @@ mod tests { fn test_output_only_bundle_serializes_to_min_actions() { let recipient = test_orchard_address(); let bundle = - build_output_only_bundle(&recipient, 50_000u64, [0u8; 36], None, 0, &TestProver) + build_output_only_bundle(&recipient, 50_000u64, [0u8; 36], None, 0, &[], &TestProver) .expect("bundle should build"); let sb = serialize_authorized_bundle(&bundle); assert_eq!( @@ -249,7 +252,7 @@ mod tests { for amount in [1u64, 100, 1_000_000, u32::MAX as u64] { let recipient = test_orchard_address(); let bundle = - build_output_only_bundle(&recipient, amount, [0u8; 36], None, 0, &TestProver) + build_output_only_bundle(&recipient, amount, [0u8; 36], None, 0, &[], &TestProver) .expect("bundle should build"); let sb = serialize_authorized_bundle(&bundle); assert_eq!( diff --git a/packages/rs-dpp/src/shielded/builder/shield_from_identity.rs b/packages/rs-dpp/src/shielded/builder/shield_from_identity.rs index 36d6e510b42..dc1cef250b4 100644 --- a/packages/rs-dpp/src/shielded/builder/shield_from_identity.rs +++ b/packages/rs-dpp/src/shielded/builder/shield_from_identity.rs @@ -38,7 +38,8 @@ pub async fn build_shield_from_identity_transition< )); } - let bundle = build_output_only_bundle(recipient, shield_amount, memo, sender_ovk, 0, prover)?; + let bundle = + build_output_only_bundle(recipient, shield_amount, memo, sender_ovk, 0, &[], prover)?; let sb = serialize_authorized_bundle(&bundle); ShieldFromIdentityTransition::try_from_bundle_with_identity_signer( diff --git a/packages/rs-dpp/src/shielded/builder/token_claim_to_pool.rs b/packages/rs-dpp/src/shielded/builder/token_claim_to_pool.rs index 414e17eec64..19e1ba10b63 100644 --- a/packages/rs-dpp/src/shielded/builder/token_claim_to_pool.rs +++ b/packages/rs-dpp/src/shielded/builder/token_claim_to_pool.rs @@ -6,7 +6,8 @@ use crate::data_contract::associated_token::token_distribution_key::TokenDistrib use crate::identity::signer::Signer; use crate::identity::IdentityPublicKey; use crate::prelude::{Identifier, IdentityNonce, UserFeeIncrease}; -use crate::shielded::OrchardBundleParams; +use crate::shielded::{token_pool_output_only_extra_sighash_data, OrchardBundleParams}; +use crate::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType; use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1; use crate::state_transition::batch_transition::BatchTransition; use crate::state_transition::StateTransition; @@ -64,7 +65,20 @@ pub async fn build_token_claim_to_pool_transition< )); } - let bundle = build_output_only_bundle(recipient, amount, memo, sender_ovk, 0, prover)?; + let extra_sighash_data = token_pool_output_only_extra_sighash_data( + TokenTransitionActionType::ClaimToPool, + token_id.as_bytes(), + platform_version, + )?; + let bundle = build_output_only_bundle( + recipient, + amount, + memo, + sender_ovk, + 0, + &extra_sighash_data, + prover, + )?; let sb = serialize_authorized_bundle(&bundle); if sb.value_balance != -(amount as i64) { diff --git a/packages/rs-dpp/src/shielded/builder/token_direct_purchase_to_pool.rs b/packages/rs-dpp/src/shielded/builder/token_direct_purchase_to_pool.rs index 011b2cba269..3d0f599f7fe 100644 --- a/packages/rs-dpp/src/shielded/builder/token_direct_purchase_to_pool.rs +++ b/packages/rs-dpp/src/shielded/builder/token_direct_purchase_to_pool.rs @@ -6,7 +6,8 @@ use crate::fee::Credits; use crate::identity::signer::Signer; use crate::identity::IdentityPublicKey; use crate::prelude::{Identifier, IdentityNonce, UserFeeIncrease}; -use crate::shielded::OrchardBundleParams; +use crate::shielded::{token_pool_output_only_extra_sighash_data, OrchardBundleParams}; +use crate::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType; use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1; use crate::state_transition::batch_transition::BatchTransition; use crate::state_transition::StateTransition; @@ -53,7 +54,20 @@ pub async fn build_token_direct_purchase_to_pool_transition< ))); } - let bundle = build_output_only_bundle(recipient, token_count, memo, sender_ovk, 0, prover)?; + let extra_sighash_data = token_pool_output_only_extra_sighash_data( + TokenTransitionActionType::DirectPurchaseToPool, + token_id.as_bytes(), + platform_version, + )?; + let bundle = build_output_only_bundle( + recipient, + token_count, + memo, + sender_ovk, + 0, + &extra_sighash_data, + prover, + )?; let sb = serialize_authorized_bundle(&bundle); if sb.value_balance != -(token_count as i64) { diff --git a/packages/rs-dpp/src/shielded/builder/token_mint_to_pool.rs b/packages/rs-dpp/src/shielded/builder/token_mint_to_pool.rs index 5c0b6acef30..d5668e21be0 100644 --- a/packages/rs-dpp/src/shielded/builder/token_mint_to_pool.rs +++ b/packages/rs-dpp/src/shielded/builder/token_mint_to_pool.rs @@ -6,7 +6,8 @@ use crate::group::GroupStateTransitionInfoStatus; use crate::identity::signer::Signer; use crate::identity::IdentityPublicKey; use crate::prelude::{Identifier, IdentityNonce, UserFeeIncrease}; -use crate::shielded::OrchardBundleParams; +use crate::shielded::{token_pool_output_only_extra_sighash_data, OrchardBundleParams}; +use crate::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType; use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1; use crate::state_transition::batch_transition::BatchTransition; use crate::state_transition::StateTransition; @@ -51,7 +52,20 @@ pub async fn build_token_mint_to_pool_transition, P ))); } - let bundle = build_output_only_bundle(recipient, amount, memo, sender_ovk, 0, prover)?; + let extra_sighash_data = token_pool_output_only_extra_sighash_data( + TokenTransitionActionType::MintToPool, + token_id.as_bytes(), + platform_version, + )?; + let bundle = build_output_only_bundle( + recipient, + amount, + memo, + sender_ovk, + 0, + &extra_sighash_data, + prover, + )?; let sb = serialize_authorized_bundle(&bundle); if sb.value_balance != -(amount as i64) { diff --git a/packages/rs-dpp/src/shielded/builder/token_shield.rs b/packages/rs-dpp/src/shielded/builder/token_shield.rs index ef8b73d8e0a..a5b1c686ea6 100644 --- a/packages/rs-dpp/src/shielded/builder/token_shield.rs +++ b/packages/rs-dpp/src/shielded/builder/token_shield.rs @@ -5,7 +5,8 @@ use crate::balances::credits::TokenAmount; use crate::identity::signer::Signer; use crate::identity::IdentityPublicKey; use crate::prelude::{Identifier, IdentityNonce, UserFeeIncrease}; -use crate::shielded::OrchardBundleParams; +use crate::shielded::{token_pool_output_only_extra_sighash_data, OrchardBundleParams}; +use crate::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType; use crate::state_transition::batch_transition::methods::v1::DocumentsBatchTransitionMethodsV1; use crate::state_transition::batch_transition::BatchTransition; use crate::state_transition::StateTransition; @@ -19,9 +20,11 @@ use super::{build_output_only_bundle, serialize_authorized_bundle, OrchardProver /// a batch transition signed by `owner_id`, the identity whose token balance funds the shield. /// /// The identity pays the fee in credits; `amount` tokens leave its balance for the pool at -/// execution. The bundle has no spends, so it carries no anchor of the pool and no extra -/// sighash data: the identity signature over the whole batch binds it. `sender_ovk` lets the -/// sending wallet recover the note it created (recipient, value, memo) from chain data. +/// execution. The bundle has no spends, so it carries no anchor pinning it to a pool, and the +/// identity signature over the batch only binds it inside *this* batch — the authorized bundle +/// bytes on their own would verify in any pool. The extra sighash data pins them to this token +/// and to the shield kind. `sender_ovk` lets the sending wallet recover the note it created +/// (recipient, value, memo) from chain data. #[allow(clippy::too_many_arguments)] pub async fn build_token_shield_transition, P: OrchardProver>( token_id: Identifier, @@ -52,7 +55,20 @@ pub async fn build_token_shield_transition, P: Orch ))); } - let bundle = build_output_only_bundle(recipient, amount, memo, sender_ovk, 0, prover)?; + let extra_sighash_data = token_pool_output_only_extra_sighash_data( + TokenTransitionActionType::Shield, + token_id.as_bytes(), + platform_version, + )?; + let bundle = build_output_only_bundle( + recipient, + amount, + memo, + sender_ovk, + 0, + &extra_sighash_data, + prover, + )?; let sb = serialize_authorized_bundle(&bundle); if sb.value_balance != -(amount as i64) { diff --git a/packages/rs-dpp/src/shielded/mod.rs b/packages/rs-dpp/src/shielded/mod.rs index b820130fc8f..e33c30106a2 100644 --- a/packages/rs-dpp/src/shielded/mod.rs +++ b/packages/rs-dpp/src/shielded/mod.rs @@ -51,7 +51,8 @@ pub use sighash::{ identity_top_up_from_shielded_extra_sighash_data_v0, shielded_withdrawal_extra_sighash_data, shielded_withdrawal_extra_sighash_data_v0, token_burn_from_pool_extra_sighash_data, token_burn_from_pool_extra_sighash_data_v0, token_pool_fee_bundle_extra_sighash_data, - token_pool_fee_bundle_extra_sighash_data_v0, + token_pool_fee_bundle_extra_sighash_data_v0, token_pool_output_only_extra_sighash_data, + token_pool_output_only_extra_sighash_data_v0, token_purchase_from_shielded_pool_extra_sighash_data, token_purchase_from_shielded_pool_extra_sighash_data_v0, token_shielded_transfer_extra_sighash_data, token_shielded_transfer_extra_sighash_data_v0, @@ -60,8 +61,10 @@ pub use sighash::{ token_unshield_extra_sighash_data, token_unshield_extra_sighash_data_v0, token_unshield_with_shielded_fee_extra_sighash_data, token_unshield_with_shielded_fee_extra_sighash_data_v0, unshield_extra_sighash_data, - unshield_extra_sighash_data_v0, TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, - TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE, TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE, + unshield_extra_sighash_data_v0, TOKEN_CLAIM_TO_POOL_BUNDLE_TAG, + TOKEN_DIRECT_PURCHASE_TO_POOL_BUNDLE_TAG, TOKEN_MINT_TO_POOL_BUNDLE_TAG, + TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE, + TOKEN_SHIELD_BUNDLE_TAG, TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE, }; /// Calibrated effective storage-byte cost of the Core withdrawal document a diff --git a/packages/rs-dpp/src/shielded/sighash.rs b/packages/rs-dpp/src/shielded/sighash.rs index 22f6799f9a4..df506b3555d 100644 --- a/packages/rs-dpp/src/shielded/sighash.rs +++ b/packages/rs-dpp/src/shielded/sighash.rs @@ -9,6 +9,7 @@ use crate::address_funds::PlatformAddress; use crate::identity::identity_public_key::contract_bounds::ContractBounds; use crate::shielded::{serialized_actions_digest, SerializedAction}; +use crate::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType; use crate::state_transition::public_key_in_creation::accessors::IdentityPublicKeyInCreationV0Getters; use crate::state_transition::public_key_in_creation::IdentityPublicKeyInCreation; use crate::withdrawal::Pooling; @@ -27,6 +28,18 @@ pub const TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE: u8 = 27; /// The state transition type byte the token bundle of a `TokenPurchaseFromShieldedPool` commits to. pub const TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE: u8 = 28; +/// Domain tag an outputs-only token pool bundle commits to. Unlike the three constants above +/// these are not `StateTransitionType` bytes — every one of these bundles rides inside a batch +/// transition — so they are drawn from a high range the state transition type space cannot +/// reach, keeping the two tag spaces from ever colliding as new transition types are added. +pub const TOKEN_SHIELD_BUNDLE_TAG: u8 = 0x80; +/// Domain tag of a `TokenMintToPool` bundle. See [`TOKEN_SHIELD_BUNDLE_TAG`]. +pub const TOKEN_MINT_TO_POOL_BUNDLE_TAG: u8 = 0x81; +/// Domain tag of a `TokenClaimToPool` bundle. See [`TOKEN_SHIELD_BUNDLE_TAG`]. +pub const TOKEN_CLAIM_TO_POOL_BUNDLE_TAG: u8 = 0x82; +/// Domain tag of a `TokenDirectPurchaseToPool` bundle. See [`TOKEN_SHIELD_BUNDLE_TAG`]. +pub const TOKEN_DIRECT_PURCHASE_TO_POOL_BUNDLE_TAG: u8 = 0x83; + /// Computes the platform sighash from an Orchard bundle commitment and optional /// transparent field data. /// @@ -615,6 +628,73 @@ pub fn token_shielded_transfer_extra_sighash_data_v0( data } +/// Extra sighash data of an outputs-only token pool bundle — `TokenShield`, `TokenMintToPool`, +/// `TokenClaimToPool` and `TokenDirectPurchaseToPool`: the bundle's domain tag and the token id. +/// +/// These bundles have no spends, so they carry no anchor pinning them to a pool: every token +/// pool starts from the same empty-tree anchor, and the proof and binding signature verify +/// against any of them. Without this data the authorized bundle bytes are a free-standing, +/// self-verifying object that anybody can lift out of the mempool into a transition of their +/// own, funded by their own tokens, against a different pool or a different transition kind. +/// The result is a second note with the same commitment and the same `rho`, hence the same +/// nullifier: only one of the two can ever be spent. That is the Faerie Gold problem — the +/// recipient's wallet is safe if it merges notes by nullifier, but any wallet or indexer that +/// counts by commitment sees two payments where one is real. +/// +/// Binding the tag and the token id makes a copied bundle fail the sighash in any pool or kind +/// but its own. A copy into the *same* pool and kind is not covered: catching that needs the +/// bundle's dummy nullifiers to be recorded and checked, which no outputs-only path does yet, +/// here or in the credit pool. +pub fn token_pool_output_only_extra_sighash_data( + action_type: TokenTransitionActionType, + token_id: &[u8; 32], + platform_version: &PlatformVersion, +) -> Result, ProtocolError> { + match platform_version.dpp.methods.shielded_extra_sighash_data { + 0 => Ok(token_pool_output_only_extra_sighash_data_v0( + outputs_only_bundle_tag_v0(action_type)?, + token_id, + )), + version => Err(ProtocolError::UnknownVersionMismatch { + method: "token_pool_output_only_extra_sighash_data".to_string(), + known_versions: vec![0], + received: version, + }), + } +} + +/// The v0 domain tag of an outputs-only token pool bundle. Frozen: the mapping is part of the +/// sighash preimage, so a kind keeps its byte forever and a new kind takes a new one. +/// +/// The bytes are written out here rather than derived from the variant's position, because +/// `TokenTransitionActionType` is append-only for the sake of clients and nothing about it +/// promises a stable ordering to consensus. +fn outputs_only_bundle_tag_v0(action_type: TokenTransitionActionType) -> Result { + match action_type { + TokenTransitionActionType::Shield => Ok(TOKEN_SHIELD_BUNDLE_TAG), + TokenTransitionActionType::MintToPool => Ok(TOKEN_MINT_TO_POOL_BUNDLE_TAG), + TokenTransitionActionType::ClaimToPool => Ok(TOKEN_CLAIM_TO_POOL_BUNDLE_TAG), + TokenTransitionActionType::DirectPurchaseToPool => { + Ok(TOKEN_DIRECT_PURCHASE_TO_POOL_BUNDLE_TAG) + } + other => Err(ProtocolError::InvalidStateTransitionType(format!( + "{other} is not an outputs-only token pool transition and has no bundle tag" + ))), + } +} + +/// Version 0 layout: `bundle tag (1) || token_id (32)`. Frozen: never mutate; a layout change +/// requires a new `_v1` + version bump. +pub fn token_pool_output_only_extra_sighash_data_v0( + bundle_tag: u8, + token_id: &[u8; 32], +) -> Vec { + let mut data = Vec::with_capacity(1 + 32); + data.push(bundle_tag); + data.extend_from_slice(token_id); + data +} + #[cfg(test)] mod tests { use super::*; @@ -972,4 +1052,116 @@ mod tests { assert_eq!(&data[32..64], &[2u8; 32]); assert_eq!(&data[64..], &300u64.to_le_bytes()); } + + #[test] + fn outputs_only_token_pool_sighash_data_pins_its_v0_layout() { + let token_id = [7u8; 32]; + let data = token_pool_output_only_extra_sighash_data_v0(TOKEN_SHIELD_BUNDLE_TAG, &token_id); + + let mut expected = Vec::with_capacity(33); + expected.push(0x80); + expected.extend_from_slice(&token_id); + assert_eq!( + data, expected, + "v0 layout is frozen: tag (1) || token_id (32)" + ); + } + + #[test] + fn outputs_only_token_pool_sighash_data_separates_pools() { + // The whole point of the data: an outputs-only bundle carries no anchor, so without it + // the same proved bytes verify against every token pool. + let version = PlatformVersion::latest(); + let a = token_pool_output_only_extra_sighash_data( + TokenTransitionActionType::Shield, + &[1u8; 32], + version, + ) + .expect("shield tag"); + let b = token_pool_output_only_extra_sighash_data( + TokenTransitionActionType::Shield, + &[2u8; 32], + version, + ) + .expect("shield tag"); + assert_ne!( + a, b, + "a bundle must not verify against another token's pool" + ); + } + + #[test] + fn outputs_only_token_pool_sighash_data_separates_transition_kinds() { + // All four kinds bind the same token id, so only the tag keeps a shield bundle from + // being resubmitted as a mint, a claim or a purchase into the very same pool. + let version = PlatformVersion::latest(); + let token_id = [9u8; 32]; + let built: Vec> = [ + TokenTransitionActionType::Shield, + TokenTransitionActionType::MintToPool, + TokenTransitionActionType::ClaimToPool, + TokenTransitionActionType::DirectPurchaseToPool, + ] + .into_iter() + .map(|action_type| { + token_pool_output_only_extra_sighash_data(action_type, &token_id, version) + .expect("outputs-only kind") + }) + .collect(); + + for (i, a) in built.iter().enumerate() { + for b in built.iter().skip(i + 1) { + assert_ne!(a, b, "each transition kind must get its own preimage"); + } + } + } + + #[test] + fn outputs_only_token_pool_sighash_data_refuses_every_other_kind() { + // The kinds come from an enum shared with clients that carries fourteen other + // variants. Handing one of those in is a caller bug, and it must be loud: silently + // falling back to some default byte would hand two kinds the same preimage. + let version = PlatformVersion::latest(); + for action_type in [ + TokenTransitionActionType::Mint, + TokenTransitionActionType::Burn, + TokenTransitionActionType::Transfer, + TokenTransitionActionType::Claim, + TokenTransitionActionType::DirectPurchase, + TokenTransitionActionType::Unshield, + TokenTransitionActionType::ShieldedTransfer, + TokenTransitionActionType::BurnFromPool, + ] { + let error = token_pool_output_only_extra_sighash_data(action_type, &[1u8; 32], version) + .expect_err("only outputs-only pool bundles have a tag"); + assert!( + matches!(error, ProtocolError::InvalidStateTransitionType(_)), + "{action_type} must be refused, got {error:?}" + ); + } + } + + #[test] + fn outputs_only_token_pool_tags_cannot_collide_with_state_transition_types() { + // The tags share a preimage position with the `StateTransitionType` bytes used by the + // identity-less bundles, and that enum keeps growing. Keep the ranges disjoint. + for tag in [ + TOKEN_SHIELD_BUNDLE_TAG, + TOKEN_MINT_TO_POOL_BUNDLE_TAG, + TOKEN_CLAIM_TO_POOL_BUNDLE_TAG, + TOKEN_DIRECT_PURCHASE_TO_POOL_BUNDLE_TAG, + ] { + assert!( + tag >= 0x80, + "bundle tags live above the state transition type range, got {tag}" + ); + } + for state_transition_type in [ + TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE, + TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE, + TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, + ] { + assert!(state_transition_type < 0x80); + } + } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs index 400df831a56..f7eed742dc2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs @@ -29,9 +29,10 @@ use dpp::state_transition::batch_transition::batched_transition::token_transitio TokenTransition, TokenTransitionV0Methods, }; use dpp::shielded::{ - TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE, TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE, compute_token_purchase_from_shielded_pool_fee, compute_token_shielded_transfer_with_shielded_fee_fee, compute_token_unshield_with_shielded_fee_fee, document_token_payment_extra_sighash_data, token_burn_from_pool_extra_sighash_data, token_pool_fee_bundle_extra_sighash_data, token_purchase_from_shielded_pool_extra_sighash_data, token_shielded_transfer_extra_sighash_data, token_shielded_transfer_with_shielded_fee_extra_sighash_data, token_unshield_extra_sighash_data, token_unshield_with_shielded_fee_extra_sighash_data, + TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE, TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE, compute_token_purchase_from_shielded_pool_fee, compute_token_shielded_transfer_with_shielded_fee_fee, compute_token_unshield_with_shielded_fee_fee, document_token_payment_extra_sighash_data, token_burn_from_pool_extra_sighash_data, token_pool_fee_bundle_extra_sighash_data, token_pool_output_only_extra_sighash_data, token_purchase_from_shielded_pool_extra_sighash_data, token_shielded_transfer_extra_sighash_data, token_shielded_transfer_with_shielded_fee_extra_sighash_data, token_unshield_extra_sighash_data, token_unshield_with_shielded_fee_extra_sighash_data, }; use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRef; +use dpp::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType; use dpp::state_transition::batch_transition::token_base_transition::token_base_transition_accessors::TokenBaseTransitionAccessors; use dpp::state_transition::batch_transition::token_base_transition::v0::v0_methods::TokenBaseTransitionV0Methods; use dpp::state_transition::batch_transition::token_shield_transition::v0::v0_methods::TokenShieldTransitionV0Methods; @@ -928,6 +929,11 @@ fn validate_batch_token_shielded_proofs( for transition in batch.transitions_iter() { let result = match transition { BatchedTransitionRef::Token(TokenTransition::Shield(t)) => { + let extra_sighash_data = token_pool_output_only_extra_sighash_data( + TokenTransitionActionType::Shield, + &t.base().token_id().to_buffer(), + platform_version, + )?; reconstruct_and_verify_bundle( t.actions(), FLAGS_OUTPUTS_ONLY, @@ -935,7 +941,7 @@ fn validate_batch_token_shielded_proofs( t.anchor(), t.proof(), t.binding_signature(), - &[], + &extra_sighash_data, ) } BatchedTransitionRef::Token(TokenTransition::Unshield(t)) => { @@ -973,6 +979,11 @@ fn validate_batch_token_shielded_proofs( ) } BatchedTransitionRef::Token(TokenTransition::MintToPool(t)) => { + let extra_sighash_data = token_pool_output_only_extra_sighash_data( + TokenTransitionActionType::MintToPool, + &t.base().token_id().to_buffer(), + platform_version, + )?; reconstruct_and_verify_bundle( t.actions(), FLAGS_OUTPUTS_ONLY, @@ -980,7 +991,7 @@ fn validate_batch_token_shielded_proofs( t.anchor(), t.proof(), t.binding_signature(), - &[], + &extra_sighash_data, ) } BatchedTransitionRef::Token(TokenTransition::BurnFromPool(t)) => { @@ -1012,6 +1023,11 @@ fn validate_batch_token_shielded_proofs( // verified in block validation only; its proof is still admitted per nonce. BatchedTransitionRef::Token(TokenTransition::ClaimToPool(_)) => continue, BatchedTransitionRef::Token(TokenTransition::DirectPurchaseToPool(t)) => { + let extra_sighash_data = token_pool_output_only_extra_sighash_data( + TokenTransitionActionType::DirectPurchaseToPool, + &t.base().token_id().to_buffer(), + platform_version, + )?; reconstruct_and_verify_bundle( t.actions(), FLAGS_OUTPUTS_ONLY, @@ -1019,7 +1035,7 @@ fn validate_batch_token_shielded_proofs( t.anchor(), t.proof(), t.binding_signature(), - &[], + &extra_sighash_data, ) } // A document whose token cost is paid from the token's shielded pool: the payment diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs index dacab95e8d3..aacc318fde2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs @@ -5,6 +5,8 @@ use crate::execution::validation::state_transition::ValidationMode; use crate::platform_types::platform::PlatformStateRef; use dpp::block::block_info::BlockInfo; use dpp::prelude::Identifier; +use dpp::shielded::token_pool_output_only_extra_sighash_data; +use dpp::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType; use dpp::validation::SimpleConsensusValidationResult; use dpp::version::PlatformVersion; use drive::query::TransactionArg; @@ -119,6 +121,15 @@ impl TokenClaimToPoolTransitionActionStateValidationV0 for TokenClaimToPoolTrans } } + // An outputs-only bundle carries no anchor pinning it to a pool — every token pool + // starts from the same empty-tree anchor — and the identity signature over the batch + // only binds it inside this batch. The extra sighash data is what stops the authorized + // bundle bytes from being lifted into another pool or another transition kind. + let extra_sighash_data = token_pool_output_only_extra_sighash_data( + TokenTransitionActionType::ClaimToPool, + token_id.as_bytes(), + platform_version, + )?; verify_token_pool_bundle( validation_mode, self.actions(), @@ -127,7 +138,7 @@ impl TokenClaimToPoolTransitionActionStateValidationV0 for TokenClaimToPoolTrans self.anchor(), self.proof(), self.binding_signature(), - &[], + &extra_sighash_data, ) } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs index b77c569d85f..575f7fbcb71 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs @@ -5,6 +5,8 @@ use crate::execution::validation::state_transition::ValidationMode; use crate::platform_types::platform::PlatformStateRef; use dpp::block::block_info::BlockInfo; use dpp::prelude::Identifier; +use dpp::shielded::token_pool_output_only_extra_sighash_data; +use dpp::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType; use dpp::validation::SimpleConsensusValidationResult; use dpp::version::PlatformVersion; use drive::query::TransactionArg; @@ -120,6 +122,15 @@ impl TokenDirectPurchaseToPoolTransitionActionStateValidationV0 } } + // An outputs-only bundle carries no anchor pinning it to a pool — every token pool + // starts from the same empty-tree anchor — and the identity signature over the batch + // only binds it inside this batch. The extra sighash data is what stops the authorized + // bundle bytes from being lifted into another pool or another transition kind. + let extra_sighash_data = token_pool_output_only_extra_sighash_data( + TokenTransitionActionType::DirectPurchaseToPool, + token_id.as_bytes(), + platform_version, + )?; verify_token_pool_bundle( validation_mode, self.actions(), @@ -128,7 +139,7 @@ impl TokenDirectPurchaseToPoolTransitionActionStateValidationV0 self.anchor(), self.proof(), self.binding_signature(), - &[], + &extra_sighash_data, ) } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs index 97c105c7d14..5889c76fc09 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs @@ -5,6 +5,8 @@ use crate::execution::validation::state_transition::ValidationMode; use crate::platform_types::platform::PlatformStateRef; use dpp::block::block_info::BlockInfo; use dpp::prelude::Identifier; +use dpp::shielded::token_pool_output_only_extra_sighash_data; +use dpp::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType; use dpp::validation::SimpleConsensusValidationResult; use dpp::version::PlatformVersion; use drive::query::TransactionArg; @@ -195,6 +197,15 @@ impl TokenMintToPoolTransitionActionStateValidationV0 for TokenMintToPoolTransit } } + // An outputs-only bundle carries no anchor pinning it to a pool — every token pool + // starts from the same empty-tree anchor — and the identity signature over the batch + // only binds it inside this batch. The extra sighash data is what stops the authorized + // bundle bytes from being lifted into another pool or another transition kind. + let extra_sighash_data = token_pool_output_only_extra_sighash_data( + TokenTransitionActionType::MintToPool, + token_id.as_bytes(), + platform_version, + )?; verify_token_pool_bundle( validation_mode, self.actions(), @@ -203,7 +214,7 @@ impl TokenMintToPoolTransitionActionStateValidationV0 for TokenMintToPoolTransit self.anchor(), self.proof(), self.binding_signature(), - &[], + &extra_sighash_data, ) } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs index bf0e9d4c57c..f665048aa4f 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs @@ -15,6 +15,8 @@ use dpp::consensus::state::state_error::StateError; use dpp::consensus::state::token::IdentityDoesNotHaveEnoughTokenBalanceError; use dpp::consensus::ConsensusError; use dpp::prelude::Identifier; +use dpp::shielded::token_pool_output_only_extra_sighash_data; +use dpp::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType; use dpp::validation::SimpleConsensusValidationResult; use dpp::version::PlatformVersion; use drive::query::TransactionArg; @@ -112,9 +114,16 @@ impl TokenShieldTransitionActionStateValidationV0 for TokenShieldTransitionActio return Ok(validation_result); } - // Outputs-only bundle entering the pool: value balance is `-amount`. The identity - // signature over the batch already binds the bundle to this token, owner and amount, so - // no extra sighash data is bound (exactly like `ShieldFromIdentity`). + // Outputs-only bundle entering the pool: value balance is `-amount`. + // An outputs-only bundle carries no anchor pinning it to a pool — every token pool + // starts from the same empty-tree anchor — and the identity signature over the batch + // only binds it inside this batch. The extra sighash data is what stops the authorized + // bundle bytes from being lifted into another pool or another transition kind. + let extra_sighash_data = token_pool_output_only_extra_sighash_data( + TokenTransitionActionType::Shield, + token_id.as_bytes(), + platform_version, + )?; verify_token_pool_bundle( validation_mode, self.actions(), @@ -123,7 +132,7 @@ impl TokenShieldTransitionActionStateValidationV0 for TokenShieldTransitionActio self.anchor(), self.proof(), self.binding_signature(), - &[], + &extra_sighash_data, ) } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 476df826d4d..974a7222e64 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -24,9 +24,11 @@ mod token_shielded_pool_tests { use dpp::identity::identity_nonce::IDENTITY_NONCE_VALUE_FILTER; use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; use dpp::shielded::{ - compute_platform_sighash, token_shielded_transfer_extra_sighash_data_v0, - token_unshield_extra_sighash_data_v0, OrchardBundleParams, + compute_platform_sighash, token_pool_output_only_extra_sighash_data, + token_shielded_transfer_extra_sighash_data_v0, token_unshield_extra_sighash_data_v0, + OrchardBundleParams, }; + use dpp::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType; use dpp::state_transition::data_contract_create_transition::methods::DataContractCreateTransitionMethodsV0; use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; use dpp::state_transition::data_contract_update_transition::methods::DataContractUpdateTransitionMethodsV0; @@ -78,7 +80,15 @@ mod token_shielded_pool_tests { /// An outputs-only bundle paying `amount` to the test wallet: what a token shield carries. /// The bundle has no spends, so no extra sighash data is bound. - pub(super) fn build_shield_bundle(amount: u64, seed: u64) -> OrchardBundleParams { + /// Builds the outputs-only bundle of a token pool transition that only creates notes. + /// `action_type` and `token_id` are what the bundle's sighash commits to, so a bundle built + /// for one pool or one transition kind will not verify as another. + pub(super) fn build_shield_bundle( + amount: u64, + seed: u64, + action_type: TokenTransitionActionType, + token_id: Identifier, + ) -> OrchardBundleParams { let mut rng = StdRng::seed_from_u64(seed); let (fvk, _, address) = spend_keys(); let mut builder = Builder::::new( @@ -98,7 +108,13 @@ mod token_shielded_pool_tests { .unwrap(); let (unauthorized, _) = builder.build::(&mut rng).unwrap().unwrap(); let commitment: [u8; 32] = unauthorized.commitment().into(); - let sighash = compute_platform_sighash(&commitment, &[]); + let extra_sighash_data = token_pool_output_only_extra_sighash_data( + action_type, + &token_id.to_buffer(), + PlatformVersion::latest(), + ) + .expect("outputs-only token pool sighash data"); + let sighash = compute_platform_sighash(&commitment, &extra_sighash_data); let proven = unauthorized .create_proof(get_proving_key(), &mut rng) .unwrap(); @@ -404,7 +420,12 @@ mod token_shielded_pool_tests { assert_tokens_conserved(&platform); // Shield: identity balance to pool. - let shield_bundle = build_shield_bundle(SHIELD_AMOUNT, 11); + let shield_bundle = build_shield_bundle( + SHIELD_AMOUNT, + 11, + TokenTransitionActionType::Shield, + token_id, + ); let shield = BatchTransition::new_token_shield_transition( token_id, identity.id(), @@ -723,9 +744,17 @@ mod token_shielded_pool_tests { let mut platform = platform_with_latest_version(); let mut rng = StdRng::seed_from_u64(9004); - let bundle = build_shield_bundle(SHIELD_AMOUNT, 13); + // The identity has to be funded before the contract exists, but the fee depends only on + // the action count, which does not depend on the token. Probe it with a throwaway pool + // id, then assert below that the real bundle has the same count. + let fee_probe = build_shield_bundle( + SHIELD_AMOUNT, + 13, + TokenTransitionActionType::Shield, + Identifier::from([0u8; 32]), + ); let compute_fee = dpp::shielded::compute_shielded_verification_fee( - bundle.actions.len(), + fee_probe.actions.len(), platform_version, ) .expect("shielded compute fee"); @@ -744,6 +773,18 @@ mod token_shielded_pool_tests { platform_version, ); + let bundle = build_shield_bundle( + SHIELD_AMOUNT, + 13, + TokenTransitionActionType::Shield, + token_id, + ); + assert_eq!( + bundle.actions.len(), + fee_probe.actions.len(), + "the funded figure must be this bundle's compute fee" + ); + let shield = BatchTransition::new_token_shield_transition( token_id, identity.id(), @@ -1037,6 +1078,81 @@ mod token_shielded_pool_tests { ); } + /// An outputs-only bundle carries no anchor, so nothing in the proved bytes themselves says + /// which pool they were built for: the proof and the binding signature verify against every + /// token pool, all of which start from the same empty-tree anchor. Only the sighash tells + /// them apart. Without it anyone could lift a shield bundle out of the mempool into a shield + /// of their own, funded by their own tokens, and mint a second note with the same commitment + /// and the same nullifier as the original — of which only one can ever be spent. + #[tokio::test] + async fn test_token_shield_rejects_a_bundle_proved_for_another_tokens_pool() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9103); + + let (victim, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (_contract_a, token_a) = create_token_contract_with_owner_identity( + &mut platform, + victim.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + // A second owner, because a contract id derives from its owner: the same owner would + // give back the same contract and the same pool, and the test would prove nothing. + let (attacker, attacker_signer, attacker_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract_b, token_b) = create_token_contract_with_owner_identity( + &mut platform, + attacker.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + assert_ne!(token_a, token_b, "the two tokens must own separate pools"); + + // Proved for the victim's pool, then replayed into the attacker's own, paid for with + // the attacker's own tokens. + let stolen_bundle = build_shield_bundle( + SHIELD_AMOUNT, + 31, + TokenTransitionActionType::Shield, + token_a, + ); + let replay = BatchTransition::new_token_shield_transition( + token_b, + attacker.id(), + contract_b.id(), + 0, + SHIELD_AMOUNT, + stolen_bundle, + &attacker_key, + 2, + 0, + &attacker_signer, + platform_version, + None, + ) + .await + .expect("token shield transition"); + + let result = process(&platform, &replay); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::InvalidShieldedProofError(_)), + .. + }] + ); + assert_eq!(pool_balance(&platform, token_b), 0); + assert_eq!(pool_notes_count(&platform, token_b), 0); + assert_tokens_conserved(&platform); + } + /// Spent nullifiers are namespaced by token id too: spending a note in one token's pool must /// not mark that nullifier spent in another's, or the first token to use a nullifier would /// make every other pool's note with the same nullifier unspendable. @@ -1079,7 +1195,12 @@ mod token_shielded_pool_tests { contract_a.id(), 0, SHIELD_AMOUNT, - build_shield_bundle(SHIELD_AMOUNT, 23), + build_shield_bundle( + SHIELD_AMOUNT, + 23, + TokenTransitionActionType::Shield, + token_a, + ), &key, 2, 0, @@ -1505,6 +1626,7 @@ mod token_pool_mint_burn_claim_purchase_tests { use dpp::data_contract::associated_token::token_pre_programmed_distribution::TokenPreProgrammedDistribution; use dpp::identity::accessors::IdentityGettersV0; use dpp::shielded::{serialized_actions_digest, token_burn_from_pool_extra_sighash_data_v0}; + use dpp::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType; use dpp::state_transition::batch_transition::{ TokenBurnFromPoolTransition, TokenSetPriceForDirectPurchaseTransition, }; @@ -1520,6 +1642,70 @@ mod token_pool_mint_burn_claim_purchase_tests { .expect("supply exists") } + /// Binding the token id alone would not be enough. A shield and a mint into the same pool + /// produce outputs-only bundles that agree on everything the proof covers — same flags, same + /// empty-tree anchor, same `value_balance` — so without a per-kind tag in the sighash the + /// very same proved bytes would satisfy both. A shield bundle would then be resubmittable as + /// a mint of the same token, which prints supply against a proof its author never made for + /// that purpose. + #[tokio::test] + async fn test_token_mint_to_pool_rejects_a_bundle_proved_as_a_shield_of_the_same_token() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9113); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + let supply_before = total_supply(&platform, token_id); + + // Same pool, same amount, same flags — only the transition kind differs. + let shield_bundle = + build_shield_bundle(1_337, 21, TokenTransitionActionType::Shield, token_id); + let mint = BatchTransition::new_token_mint_to_pool_transition( + token_id, + identity.id(), + contract.id(), + 0, + 1_337, + shield_bundle, + None, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token mint to pool transition"); + + let result = process(&platform, &mint); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::InvalidShieldedProofError(_)), + .. + }] + ); + assert_eq!(pool_balance(&platform, token_id), 0); + assert_eq!( + total_supply(&platform, token_id), + supply_before, + "a rejected mint must not print supply" + ); + assert_tokens_conserved(&platform); + } + #[tokio::test] async fn test_token_mint_to_pool_by_owner() { let platform_version = PlatformVersion::latest(); @@ -1538,7 +1724,8 @@ mod token_pool_mint_burn_claim_purchase_tests { platform_version, ); - let bundle = build_shield_bundle(1_337, 21); + let bundle = + build_shield_bundle(1_337, 21, TokenTransitionActionType::MintToPool, token_id); let mint = BatchTransition::new_token_mint_to_pool_transition( token_id, identity.id(), @@ -1613,7 +1800,7 @@ mod token_pool_mint_burn_claim_purchase_tests { contract.id(), 0, 1_337, - build_shield_bundle(1_337, 27), + build_shield_bundle(1_337, 27, TokenTransitionActionType::MintToPool, token_id), None, None, &key, @@ -1761,7 +1948,8 @@ mod token_pool_mint_burn_claim_purchase_tests { let token = token_id.to_buffer(); // Fund the pool first. - let shield_bundle = build_shield_bundle(10_000, 22); + let shield_bundle = + build_shield_bundle(10_000, 22, TokenTransitionActionType::Shield, token_id); let shield = BatchTransition::new_token_shield_transition( token_id, identity.id(), @@ -1914,7 +2102,7 @@ mod token_pool_mint_burn_claim_purchase_tests { contract.id(), 0, 10_000, - build_shield_bundle(10_000, 24), + build_shield_bundle(10_000, 24, TokenTransitionActionType::Shield, token_id), &proposer_key, 2, 0, @@ -2189,7 +2377,12 @@ mod token_pool_mint_burn_claim_purchase_tests { 0, TokenDistributionType::Perpetual, None, - build_shield_bundle(accrued, 27), + build_shield_bundle( + accrued, + 27, + TokenTransitionActionType::ClaimToPool, + token_id, + ), None, &key, 2, @@ -2229,7 +2422,12 @@ mod token_pool_mint_burn_claim_purchase_tests { assert_eq!(pool_balance(&platform, token_id), 0); // Pinned to the current cycle, the bundle proves exactly the accrued rewards. - let claim_bundle = build_shield_bundle(accrued, 28); + let claim_bundle = build_shield_bundle( + accrued, + 28, + TokenTransitionActionType::ClaimToPool, + token_id, + ); let pinned = BatchTransition::new_token_claim_to_pool_transition( token_id, owner.id(), @@ -2347,7 +2545,7 @@ mod token_pool_mint_burn_claim_purchase_tests { 0, distribution_type, None, - build_shield_bundle(444, 24), + build_shield_bundle(444, 24, TokenTransitionActionType::ClaimToPool, token_id), None, &key, 2, @@ -2385,7 +2583,8 @@ mod token_pool_mint_burn_claim_purchase_tests { .unwrap() .expect("commit"); - let claim_bundle = build_shield_bundle(445, 25); + let claim_bundle = + build_shield_bundle(445, 25, TokenTransitionActionType::ClaimToPool, token_id); let claim = BatchTransition::new_token_claim_to_pool_transition( token_id, claimant_id, @@ -2451,7 +2650,7 @@ mod token_pool_mint_burn_claim_purchase_tests { 0, distribution_type, None, - build_shield_bundle(445, 26), + build_shield_bundle(445, 26, TokenTransitionActionType::ClaimToPool, token_id), None, &key, 4, @@ -2560,7 +2759,12 @@ mod token_pool_mint_burn_claim_purchase_tests { .expect("balance") .expect("seller exists"); - let purchase_bundle = build_shield_bundle(3, 27); + let purchase_bundle = build_shield_bundle( + 3, + 27, + TokenTransitionActionType::DirectPurchaseToPool, + token_id, + ); let purchase = BatchTransition::new_token_direct_purchase_to_pool_transition( token_id, buyer.id(), @@ -2613,7 +2817,12 @@ mod token_pool_mint_burn_claim_purchase_tests { 0, 3, dash_to_credits!(0.02), - build_shield_bundle(3, 28), + build_shield_bundle( + 3, + 28, + TokenTransitionActionType::DirectPurchaseToPool, + token_id, + ), &key, 3, 0, @@ -2647,7 +2856,12 @@ mod token_pool_mint_burn_claim_purchase_tests { 0, 3, dash_to_credits!(0.03), - build_shield_bundle(3, 29), + build_shield_bundle( + 3, + 29, + TokenTransitionActionType::DirectPurchaseToPool, + token_id, + ), &poor_key, 1, 0, @@ -2694,6 +2908,7 @@ mod document_shielded_token_payment_tests { use dpp::identity::{Identity, IdentityPublicKey}; use dpp::prelude::IdentityNonce; use dpp::shielded::{document_token_payment_extra_sighash_data_v0, OrchardBundleParams}; + use dpp::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType; use dpp::tokens::calculate_token_id; use dpp::tokens::gas_fees_paid_by::GasFeesPaidBy; use dpp::tokens::token_payment_info::v1::{TokenPaymentInfoV1, TokenShieldedPayment}; @@ -2826,7 +3041,7 @@ mod document_shielded_token_payment_tests { contract.id(), 0, SHIELDED, - build_shield_bundle(SHIELDED, seed), + build_shield_bundle(SHIELDED, seed, TokenTransitionActionType::Shield, token_id), key, 1, 0, @@ -3233,6 +3448,7 @@ mod token_pool_paid_transitions_tests { build_token_unshield_with_shielded_fee_transition, OrchardProver, ShieldedFeePayer, SpendableNote, TokenPoolSpender, }; + use dpp::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType; use dpp::state_transition::token_purchase_from_shielded_pool_transition::v0::TokenPurchaseFromShieldedPoolTransitionV0; use dpp::state_transition::token_purchase_from_shielded_pool_transition::TokenPurchaseFromShieldedPoolTransition; use dpp::state_transition::StateTransition; @@ -3306,7 +3522,7 @@ mod token_pool_paid_transitions_tests { contract.id(), 0, SHIELDED, - build_shield_bundle(SHIELDED, seed), + build_shield_bundle(SHIELDED, seed, TokenTransitionActionType::Shield, token_id), key, 2, 0, diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index 90a9b74f349..8ec5ba8706e 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -734,12 +734,17 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// `DRIVE_ABCI_VALIDATION_VERSIONS_V10` and gated by /// `TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION`) move tokens between an /// identity balance, the supply and the pool or inside it; the identity -/// signs and pays the fee in credits, and every spend bundle binds the -/// token id and the batch owner (a burn binds the burner: the batch owner, -/// or the proposer of a group action), plus the recipient and amount where -/// tokens leave the pool, into the Orchard sighash. The pool balances are a -/// term of the token conservation check (`calculate_total_tokens_balance` v1 -/// in `DRIVE_TOKEN_METHOD_VERSIONS_V2`). `record_token_shielded_pool_anchors` +/// signs and pays the fee in credits, and every bundle binds its pool into +/// the Orchard sighash, since all pools share the empty-tree anchor an +/// unbound bundle would verify against: a spend bundle binds the token id +/// and the batch owner (a burn binds the burner: the batch owner, or the +/// proposer of a group action), plus the recipient and amount where tokens +/// leave the pool; an outputs-only bundle (`TokenShield`, +/// `TokenMintToPool`, `TokenClaimToPool`, `TokenDirectPurchaseToPool`), +/// which has no anchor of its own, binds a per-kind tag and the token id. +/// The pool balances are a term of the token conservation check +/// (`calculate_total_tokens_balance` v1 in `DRIVE_TOKEN_METHOD_VERSIONS_V2`). +/// `record_token_shielded_pool_anchors` /// (`DRIVE_ABCI_METHOD_VERSIONS_V10`) records and prunes the anchors of the /// pools a block touched. The pools root tree is inserted by /// `transition_to_version_14` and by `create_initial_state_structure` v4; diff --git a/packages/wasm-dpp2/src/shielded/mod.rs b/packages/wasm-dpp2/src/shielded/mod.rs index 919ead179ac..944a88fb72b 100644 --- a/packages/wasm-dpp2/src/shielded/mod.rs +++ b/packages/wasm-dpp2/src/shielded/mod.rs @@ -37,6 +37,11 @@ use wasm_bindgen::prelude::wasm_bindgen; /// - For shield and shielded_transfer transitions, `extraData` should be empty. /// - For unshield transitions, `extraData` = serialized `outputAddress` bytes. /// - For shielded withdrawal transitions, `extraData` = `outputScript` bytes. +/// - For token pool transitions, `extraData` is the layout that transition binds. An +/// outputs-only one — token shield, and mint, claim or direct purchase into the pool — +/// binds its one-byte kind tag followed by the 32-byte token id; leaving it empty is +/// rejected, since all token pools share the empty-tree anchor such a bundle proves +/// against and nothing else would say which pool it was built for. #[wasm_bindgen(js_name = computePlatformSighash)] pub fn compute_platform_sighash_wasm( bundle_commitment: Vec, From 4cb0576a80696807033e9e4e47ef4e67ab9bcbeb Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 23 Sep 2026 21:19:01 +0700 Subject: [PATCH 23/94] fix(platform): correct the outputs-only bundle tag rationale, docs and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the previous commit, which bound a kind tag and the token id into the four outputs-only token pool bundles. The binding itself stands; its justification, one of its arms and the tests around it did not. Three statements in that commit message were wrong, and are corrected here. It called ShieldFromIdentity one of "three shipped credit shields" left on an empty preimage. It is not shipped: SHIELD_FROM_IDENTITY_INITIAL_PROTOCOL_VERSION is 14 and 14 is LATEST_VERSION. Only the credit pool itself shipped, at 12. Binding it is still free and is left to the follow-up that binds owners. It also led with Faerie Gold as the reason, which overstates what the change reaches. Nullifiers are namespaced per pool, so a bundle copied into another pool lands in a different nullifier tree: both notes stay spendable and nothing is Faerie Gold there. The harm of that copy is that one `rho` now yields notes in two pools, linking the recipient's spends across them. Faerie Gold is the same-pool case, and of it the tag reaches only copies into another kind. The larger vector — same pool, same kind, submitted by somebody else — stays open, because the preimage binds no owner. The rustdoc, the four validators and the book now say this instead. It said the typed helper left no call site able to reach for another kind's tag. The type stops a caller passing something that is not a kind; it does nothing about a builder passing a different outputs-only kind, which compiles. Nothing checked that none of them did, so a test now builds a transition with each of the four public builders and verifies its bundle against the preimage that kind's verifier rebuilds, through the same verification consensus runs. It proves and verifies each bundle without executing a block, so covering all four costs one Halo 2 proof apiece. `outputs_only_bundle_tag_v0` had an `other =>` arm. A catch-all on a consensus path turns a kind somebody forgot into a runtime error discovered when a node rejects a block; every kind is now named, so the same omission stops the build instead. Client-facing docs went further out of date than that commit noticed: the WASM sighash helper described unshield as binding only the output address and a withdrawal as binding only the output script. Both have bound the amount since they shipped, and a withdrawal also binds the core fee per byte and the pooling byte. A client following that text produced an invalid sighash. The four token pool layouts are documented there too. On the tests, the previous commit's two replay tests had no positive control: they only ever asserted a rejection, so they passed whenever the bundle was invalid for any reason at all — including a builder and a verifier that simply disagreed. Reverting only the verifier left them green. The new test builds a shield through the public `build_token_shield_transition`, shows CheckTx admitting it and a block executing it, and only then replays that proven bundle into a second pool. Reverting only the verifier now fails it at the positive half. Three CheckTx arms changed in the previous commit and none of them had a test that put a valid bundle through CheckTx. A kind reaching for the wrong tag there would have dropped every honest transition of that kind at admission with the suite still green; the shield, mint and purchase happy paths now assert admission. The tag guard compared 0x80-0x83 against the three transition types that exist today, which cannot fail when a fourth is added inside the range. It now asks the enum. The layout test pinned only 0x80; it pins all four, and the test that rejects every other kind names all fourteen rather than a sample of eight: exhaustiveness catches a kind being added and forgotten, not an existing arm edited into the accepting half. Two claims this commit itself first made were also too broad and are narrowed here. The rustdoc on the tags said the state transition type space could not reach them; `StateTransitionType` is `repr(u8)` and could be given any of those bytes, so what holds the reservation is the guard, not the type system. The book said a bundle proven for one token, owner, recipient or amount could not be replayed with another, which is true of the layouts that spend and not of these four, which bind neither owner nor amount. Co-Authored-By: Claude Opus 5 (1M context) --- book/src/data-model/token-shielded-pools.md | 17 +- packages/rs-dpp/src/shielded/sighash.rs | 127 +++++-- .../token_shield_transition/mod.rs | 5 +- .../batch_transition/methods/v1/mod.rs | 6 +- .../state_v0/mod.rs | 6 +- .../state_v0/mod.rs | 6 +- .../state_v0/mod.rs | 6 +- .../state_v0/mod.rs | 6 +- .../batch/tests/token/shielded/mod.rs | 327 ++++++++++++++++-- packages/wasm-dpp2/src/shielded/mod.rs | 23 +- 10 files changed, 435 insertions(+), 94 deletions(-) diff --git a/book/src/data-model/token-shielded-pools.md b/book/src/data-model/token-shielded-pools.md index 9279fda5866..5d5d5aa03d3 100644 --- a/book/src/data-model/token-shielded-pools.md +++ b/book/src/data-model/token-shielded-pools.md @@ -78,21 +78,24 @@ fees, so unlike the credit pool nothing is carved from the bundle's value balanc | Transition | Flags | Value balance | Extra sighash data | Effect | |---|---|---|---|---| -| `TokenShield` | outputs only | `-amount` | none | `amount` leaves the owner's balance and enters the pool as new notes. | +| `TokenShield` | outputs only | `-amount` | `tag(0x80), token_id` | `amount` leaves the owner's balance and enters the pool as new notes. | | `TokenUnshield` | spends and outputs | `+amount` | `token_id, owner_id, recipient_id, amount` | Notes are spent; `amount` is credited to `recipient_id`; change comes back as new notes. | | `TokenShieldedTransfer` | spends and outputs | `0` | `token_id, owner_id` | Notes are spent and recreated; the pool balance is unchanged. | -| `TokenMintToPool` | outputs only | `-amount` | none | An authorized minter (manual minting rules, group actions supported) mints `amount` into new notes; the supply and the pool balance grow. Allowed only where `mintingAllowChoosingDestination` is set, since the notes' recipients are the minter's choice. | +| `TokenMintToPool` | outputs only | `-amount` | `tag(0x81), token_id` | An authorized minter (manual minting rules, group actions supported) mints `amount` into new notes; the supply and the pool balance grow. Allowed only where `mintingAllowChoosingDestination` is set, since the notes' recipients are the minter's choice. | | `TokenBurnFromPool` | spends and outputs | `+amount` | `token_id, burner_id, amount` | An authorized burner (manual burning rules, group actions supported) spends notes and destroys `amount`; the supply and the pool balance shrink. `burner_id` is the batch owner, or the proposer of a group action. | -| `TokenClaimToPool` | outputs only | `-amount` | none | A distribution claim released into new notes instead of the claimant's balance; a perpetual claim names the cycle-aligned moment it claims up to so the amount is predictable. | -| `TokenDirectPurchaseToPool` | outputs only | `-token_count` | none | The buyer pays credits at the direct purchase price and the tokens are minted into new notes. | +| `TokenClaimToPool` | outputs only | `-amount` | `tag(0x82), token_id` | A distribution claim released into new notes instead of the claimant's balance; a perpetual claim names the cycle-aligned moment it claims up to so the amount is predictable. | +| `TokenDirectPurchaseToPool` | outputs only | `-token_count` | `tag(0x83), token_id` | The buyer pays credits at the direct purchase price and the tokens are minted into new notes. | Each transition carries the Orchard bundle (`actions`, `anchor`, `proof`, `binding_signature`) next to the token base transition (`token_id`, contract id, contract position, identity contract nonce). The extra sighash data is bound into the Orchard sighash by the client and -recomputed by consensus from the transition's own fields, so a bundle proven for one token, -owner, recipient or amount cannot be replayed with another. The layouts are in +recomputed by consensus from the transition's own fields, so a bundle cannot be replayed +against whatever its layout names. The layouts differ: the ones that spend bind the token, the +owner and, where tokens leave the pool, the recipient and amount; the four that only create +notes bind their kind and the token, and nothing about who submits them. The layouts are in `dpp::shielded::sighash` (`token_unshield_extra_sighash_data`, -`token_shielded_transfer_extra_sighash_data`). +`token_shielded_transfer_extra_sighash_data`, `token_burn_from_pool_extra_sighash_data` and +`token_pool_output_only_extra_sighash_data`). Outputs-only bundles (shield, mint, claim, purchase) have no spends, so their anchor is not checked against the pool; the client builds them against the empty tree. Spending bundles must diff --git a/packages/rs-dpp/src/shielded/sighash.rs b/packages/rs-dpp/src/shielded/sighash.rs index df506b3555d..7c660b011bd 100644 --- a/packages/rs-dpp/src/shielded/sighash.rs +++ b/packages/rs-dpp/src/shielded/sighash.rs @@ -30,8 +30,11 @@ pub const TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE: u8 = 28; /// Domain tag an outputs-only token pool bundle commits to. Unlike the three constants above /// these are not `StateTransitionType` bytes — every one of these bundles rides inside a batch -/// transition — so they are drawn from a high range the state transition type space cannot -/// reach, keeping the two tag spaces from ever colliding as new transition types are added. +/// transition — yet they share a preimage slot with them at the same length. They are drawn +/// from a high range that space has not reached, which nothing in the type system enforces: +/// `StateTransitionType` is `repr(u8)` and could be given one of these bytes. What holds the +/// reservation is `outputs_only_token_pool_tags_cannot_collide_with_state_transition_types`, +/// which asks the enum and fails the build's tests the day one is assigned here. pub const TOKEN_SHIELD_BUNDLE_TAG: u8 = 0x80; /// Domain tag of a `TokenMintToPool` bundle. See [`TOKEN_SHIELD_BUNDLE_TAG`]. pub const TOKEN_MINT_TO_POOL_BUNDLE_TAG: u8 = 0x81; @@ -51,9 +54,13 @@ pub const TOKEN_DIRECT_PURCHASE_TO_POOL_BUNDLE_TAG: u8 = 0x83; /// replay attacks where an attacker substitutes transparent fields while reusing a /// valid Orchard bundle. /// -/// The same computation must be used on both the signing (client) and verification -/// (platform) sides. For transitions without transparent fields (shield and -/// shielded_transfer), `extra_data` is empty. +/// It also binds a bundle that has no transparent fields to the one context it was proved +/// for, which an outputs-only bundle cannot do on its own: carrying no anchor, it verifies +/// against every pool that shares the empty-tree anchor. +/// +/// The same computation must be used on both the signing (client) and verification (platform) +/// sides. `extra_data` is empty only for the credit pool's own `Shield` and `ShieldedTransfer`; +/// each other transition has a builder in this module that spells out its layout. pub fn compute_platform_sighash(bundle_commitment: &[u8; 32], extra_data: &[u8]) -> [u8; 32] { let mut hasher = Sha256::new(); hasher.update(SIGHASH_DOMAIN); @@ -636,15 +643,25 @@ pub fn token_shielded_transfer_extra_sighash_data_v0( /// against any of them. Without this data the authorized bundle bytes are a free-standing, /// self-verifying object that anybody can lift out of the mempool into a transition of their /// own, funded by their own tokens, against a different pool or a different transition kind. -/// The result is a second note with the same commitment and the same `rho`, hence the same -/// nullifier: only one of the two can ever be spent. That is the Faerie Gold problem — the -/// recipient's wallet is safe if it merges notes by nullifier, but any wallet or indexer that -/// counts by commitment sees two payments where one is real. /// -/// Binding the tag and the token id makes a copied bundle fail the sighash in any pool or kind -/// but its own. A copy into the *same* pool and kind is not covered: catching that needs the -/// bundle's dummy nullifiers to be recorded and checked, which no outputs-only path does yet, -/// here or in the credit pool. +/// What that copy costs depends on where it lands, because nullifiers are namespaced per pool: +/// +/// - **Another kind, same pool.** The copy shares the original's nullifier tree, so it lands a +/// second note with the same commitment and the same `rho`, hence the same nullifier: only +/// one of the two can ever be spent. That is Faerie Gold. A wallet that merges notes by +/// nullifier is safe, but any wallet or indexer counting by commitment sees two payments +/// where one is real. Bundles of different kinds are otherwise indistinguishable to the +/// proof — same flags, same empty-tree anchor, same value balance — so only the tag tells +/// them apart. +/// - **Another pool.** That pool has its own nullifier tree, so both notes stay spendable and +/// nothing is Faerie Gold. The harm is that the recipient now holds notes derived from one +/// `rho` in two pools, which links their spends across pools, and the copier burns their own +/// tokens doing it. +/// +/// Not covered: a copy into the same pool *and* the same kind, submitted by someone else. That +/// is the larger Faerie Gold vector and this preimage does not reach it, because it binds no +/// owner. Closing it needs the bundle's dummy nullifiers recorded and checked, which no +/// outputs-only path does yet, here or in the credit pool. pub fn token_pool_output_only_extra_sighash_data( action_type: TokenTransitionActionType, token_id: &[u8; 32], @@ -669,6 +686,10 @@ pub fn token_pool_output_only_extra_sighash_data( /// The bytes are written out here rather than derived from the variant's position, because /// `TokenTransitionActionType` is append-only for the sake of clients and nothing about it /// promises a stable ordering to consensus. +/// +/// Every other kind is named rather than swept up by a catch-all: a kind added to the enum must +/// fail to compile here, so that whoever adds it decides whether it carries a bundle. A +/// catch-all would silently answer "no" and surface as a rejected block instead. fn outputs_only_bundle_tag_v0(action_type: TokenTransitionActionType) -> Result { match action_type { TokenTransitionActionType::Shield => Ok(TOKEN_SHIELD_BUNDLE_TAG), @@ -677,9 +698,24 @@ fn outputs_only_bundle_tag_v0(action_type: TokenTransitionActionType) -> Result< TokenTransitionActionType::DirectPurchaseToPool => { Ok(TOKEN_DIRECT_PURCHASE_TO_POOL_BUNDLE_TAG) } - other => Err(ProtocolError::InvalidStateTransitionType(format!( - "{other} is not an outputs-only token pool transition and has no bundle tag" - ))), + other @ (TokenTransitionActionType::Burn + | TokenTransitionActionType::Mint + | TokenTransitionActionType::Transfer + | TokenTransitionActionType::Freeze + | TokenTransitionActionType::Unfreeze + | TokenTransitionActionType::DestroyFrozenFunds + | TokenTransitionActionType::Claim + | TokenTransitionActionType::EmergencyAction + | TokenTransitionActionType::ConfigUpdate + | TokenTransitionActionType::DirectPurchase + | TokenTransitionActionType::SetPriceForDirectPurchase + | TokenTransitionActionType::BurnFromPool + | TokenTransitionActionType::Unshield + | TokenTransitionActionType::ShieldedTransfer) => { + Err(ProtocolError::InvalidStateTransitionType(format!( + "{other} is not an outputs-only token pool transition and has no bundle tag" + ))) + } } } @@ -699,6 +735,7 @@ pub fn token_pool_output_only_extra_sighash_data_v0( mod tests { use super::*; use crate::identity::core_script::CoreScript; + use crate::state_transition::StateTransitionType; use crate::withdrawal::Pooling; // These tests pin the v0 preimage directly (they assert exact bytes), so resolve the bare helper // names to the `_v0` impls rather than the version-dispatching public wrappers. @@ -1055,16 +1092,26 @@ mod tests { #[test] fn outputs_only_token_pool_sighash_data_pins_its_v0_layout() { + // Every byte here is consensus: a kind that changes its tag invalidates every bundle + // already proved for it, so the four are pinned literally, not to the constants. let token_id = [7u8; 32]; - let data = token_pool_output_only_extra_sighash_data_v0(TOKEN_SHIELD_BUNDLE_TAG, &token_id); - - let mut expected = Vec::with_capacity(33); - expected.push(0x80); - expected.extend_from_slice(&token_id); - assert_eq!( - data, expected, - "v0 layout is frozen: tag (1) || token_id (32)" - ); + let version = PlatformVersion::latest(); + for (action_type, tag) in [ + (TokenTransitionActionType::Shield, 0x80u8), + (TokenTransitionActionType::MintToPool, 0x81), + (TokenTransitionActionType::ClaimToPool, 0x82), + (TokenTransitionActionType::DirectPurchaseToPool, 0x83), + ] { + let mut expected = Vec::with_capacity(33); + expected.push(tag); + expected.extend_from_slice(&token_id); + assert_eq!( + token_pool_output_only_extra_sighash_data(action_type, &token_id, version) + .expect("outputs-only kind"), + expected, + "v0 layout is frozen for {action_type}: tag (1) || token_id (32)" + ); + } } #[test] @@ -1122,15 +1169,24 @@ mod tests { // variants. Handing one of those in is a caller bug, and it must be loud: silently // falling back to some default byte would hand two kinds the same preimage. let version = PlatformVersion::latest(); + // All fourteen, not a sample: match exhaustiveness protects against a kind being + // *added* and forgotten, but nothing stops an existing arm being edited into the + // accepting half, and a sample would not notice. for action_type in [ - TokenTransitionActionType::Mint, TokenTransitionActionType::Burn, + TokenTransitionActionType::Mint, TokenTransitionActionType::Transfer, + TokenTransitionActionType::Freeze, + TokenTransitionActionType::Unfreeze, + TokenTransitionActionType::DestroyFrozenFunds, TokenTransitionActionType::Claim, + TokenTransitionActionType::EmergencyAction, + TokenTransitionActionType::ConfigUpdate, TokenTransitionActionType::DirectPurchase, + TokenTransitionActionType::SetPriceForDirectPurchase, + TokenTransitionActionType::BurnFromPool, TokenTransitionActionType::Unshield, TokenTransitionActionType::ShieldedTransfer, - TokenTransitionActionType::BurnFromPool, ] { let error = token_pool_output_only_extra_sighash_data(action_type, &[1u8; 32], version) .expect_err("only outputs-only pool bundles have a tag"); @@ -1143,8 +1199,10 @@ mod tests { #[test] fn outputs_only_token_pool_tags_cannot_collide_with_state_transition_types() { - // The tags share a preimage position with the `StateTransitionType` bytes used by the - // identity-less bundles, and that enum keeps growing. Keep the ranges disjoint. + // The tags share a preimage slot with the `StateTransitionType` byte the identity-less + // token bundles commit to, at the same 1 + 32 length. Asking the enum itself — rather + // than comparing against the three types that exist today — is what makes this break + // on the day someone assigns a transition type inside the tag range. for tag in [ TOKEN_SHIELD_BUNDLE_TAG, TOKEN_MINT_TO_POOL_BUNDLE_TAG, @@ -1152,16 +1210,9 @@ mod tests { TOKEN_DIRECT_PURCHASE_TO_POOL_BUNDLE_TAG, ] { assert!( - tag >= 0x80, - "bundle tags live above the state transition type range, got {tag}" + StateTransitionType::try_from(tag).is_err(), + "state transition type {tag:#04x} now collides with an outputs-only bundle tag" ); } - for state_transition_type in [ - TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE, - TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE, - TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, - ] { - assert!(state_transition_type < 0x80); - } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/mod.rs index a6b8a8869d6..d86057248b8 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/mod.rs @@ -13,8 +13,9 @@ pub use v0::TokenShieldTransitionV0; /// /// The Orchard bundle is outputs-only (spends disabled) with a value balance of `-amount`: the /// new notes are created for whoever the owner addressed them to, and nothing is spent from the -/// pool. The identity signature over the batch binds the bundle, the token and the amount, so -/// the bundle carries no extra sighash data of its own. +/// pool. The identity signature binds the bundle inside this batch only, and an outputs-only +/// bundle has no anchor pinning it to a pool, so the bundle's own sighash binds the shield tag +/// and the token id — see `token_pool_output_only_extra_sighash_data`. #[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From, DecodeUntrusted)] #[cfg_attr( feature = "serde-conversion", diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs index 5afa7835c81..de5e7bcedcc 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs @@ -151,8 +151,10 @@ pub trait DocumentsBatchTransitionMethodsV1: DocumentsBatchTransitionAccessorsV0 /// Creates a `StateTransition` moving `amount` of a token from `owner_id`'s identity token /// balance into the token's shielded pool. /// - /// `bundle` is the outputs-only Orchard bundle (value balance `-amount`) the client proved for - /// the token pool; the identity signature over the batch binds it. + /// `bundle` is the outputs-only Orchard bundle (value balance `-amount`) the client proved + /// for the token pool. Having no spends it carries no anchor, so its sighash must bind the + /// shield tag and the token id — see `token_pool_output_only_extra_sighash_data`; a bundle + /// proved without them is rejected. #[cfg(feature = "state-transition-signing")] #[allow(clippy::too_many_arguments)] async fn new_token_shield_transition>( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs index aacc318fde2..194f0989bd1 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs @@ -121,10 +121,8 @@ impl TokenClaimToPoolTransitionActionStateValidationV0 for TokenClaimToPoolTrans } } - // An outputs-only bundle carries no anchor pinning it to a pool — every token pool - // starts from the same empty-tree anchor — and the identity signature over the batch - // only binds it inside this batch. The extra sighash data is what stops the authorized - // bundle bytes from being lifted into another pool or another transition kind. + // An outputs-only bundle has no anchor of its own, so the sighash is what pins it to + // this pool and this kind — see `token_pool_output_only_extra_sighash_data`. let extra_sighash_data = token_pool_output_only_extra_sighash_data( TokenTransitionActionType::ClaimToPool, token_id.as_bytes(), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs index 575f7fbcb71..81bd6bae73a 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs @@ -122,10 +122,8 @@ impl TokenDirectPurchaseToPoolTransitionActionStateValidationV0 } } - // An outputs-only bundle carries no anchor pinning it to a pool — every token pool - // starts from the same empty-tree anchor — and the identity signature over the batch - // only binds it inside this batch. The extra sighash data is what stops the authorized - // bundle bytes from being lifted into another pool or another transition kind. + // An outputs-only bundle has no anchor of its own, so the sighash is what pins it to + // this pool and this kind — see `token_pool_output_only_extra_sighash_data`. let extra_sighash_data = token_pool_output_only_extra_sighash_data( TokenTransitionActionType::DirectPurchaseToPool, token_id.as_bytes(), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs index 5889c76fc09..29d42a75bc6 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs @@ -197,10 +197,8 @@ impl TokenMintToPoolTransitionActionStateValidationV0 for TokenMintToPoolTransit } } - // An outputs-only bundle carries no anchor pinning it to a pool — every token pool - // starts from the same empty-tree anchor — and the identity signature over the batch - // only binds it inside this batch. The extra sighash data is what stops the authorized - // bundle bytes from being lifted into another pool or another transition kind. + // An outputs-only bundle has no anchor of its own, so the sighash is what pins it to + // this pool and this kind — see `token_pool_output_only_extra_sighash_data`. let extra_sighash_data = token_pool_output_only_extra_sighash_data( TokenTransitionActionType::MintToPool, token_id.as_bytes(), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs index f665048aa4f..3e618cc42bb 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs @@ -115,10 +115,8 @@ impl TokenShieldTransitionActionStateValidationV0 for TokenShieldTransitionActio } // Outputs-only bundle entering the pool: value balance is `-amount`. - // An outputs-only bundle carries no anchor pinning it to a pool — every token pool - // starts from the same empty-tree anchor — and the identity signature over the batch - // only binds it inside this batch. The extra sighash data is what stops the authorized - // bundle bytes from being lifted into another pool or another transition kind. + // An outputs-only bundle has no anchor of its own, so the sighash is what pins it to + // this pool and this kind — see `token_pool_output_only_extra_sighash_data`. let extra_sighash_data = token_pool_output_only_extra_sighash_data( TokenTransitionActionType::Shield, token_id.as_bytes(), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 974a7222e64..35b887c44f3 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -78,8 +78,7 @@ mod token_shielded_pool_tests { ) } - /// An outputs-only bundle paying `amount` to the test wallet: what a token shield carries. - /// The bundle has no spends, so no extra sighash data is bound. + /// An outputs-only bundle paying `amount` to the test wallet. /// Builds the outputs-only bundle of a token pool transition that only creates notes. /// `action_type` and `token_id` are what the bundle's sighash commits to, so a bundle built /// for one pool or one transition kind will not verify as another. @@ -744,20 +743,13 @@ mod token_shielded_pool_tests { let mut platform = platform_with_latest_version(); let mut rng = StdRng::seed_from_u64(9004); - // The identity has to be funded before the contract exists, but the fee depends only on - // the action count, which does not depend on the token. Probe it with a throwaway pool - // id, then assert below that the real bundle has the same count. - let fee_probe = build_shield_bundle( - SHIELD_AMOUNT, - 13, - TokenTransitionActionType::Shield, - Identifier::from([0u8; 32]), - ); - let compute_fee = dpp::shielded::compute_shielded_verification_fee( - fee_probe.actions.len(), - platform_version, - ) - .expect("shielded compute fee"); + // The identity has to be funded before the contract exists, so the fee is computed from + // the action count Orchard pads a single-output bundle to, and the bundle built below + // is asserted to have exactly that many. + const PADDED_ACTIONS: usize = 2; + let compute_fee = + dpp::shielded::compute_shielded_verification_fee(PADDED_ACTIONS, platform_version) + .expect("shielded compute fee"); // Funded well past the preliminary batch minimum — which has no Orchard component — so // the rejection has to come from the full fee estimate, and exactly one compute fee @@ -781,7 +773,7 @@ mod token_shielded_pool_tests { ); assert_eq!( bundle.actions.len(), - fee_probe.actions.len(), + PADDED_ACTIONS, "the funded figure must be this bundle's compute fee" ); @@ -1082,8 +1074,10 @@ mod token_shielded_pool_tests { /// which pool they were built for: the proof and the binding signature verify against every /// token pool, all of which start from the same empty-tree anchor. Only the sighash tells /// them apart. Without it anyone could lift a shield bundle out of the mempool into a shield - /// of their own, funded by their own tokens, and mint a second note with the same commitment - /// and the same nullifier as the original — of which only one can ever be spent. + /// of their own, funded by their own tokens, and land a copy of the note in a pool it was + /// never meant for. Nullifiers are per pool, so both copies stay spendable and this is not + /// Faerie Gold; the harm is that one `rho` now yields notes in two pools, which links the + /// recipient's spends across them. #[tokio::test] async fn test_token_shield_rejects_a_bundle_proved_for_another_tokens_pool() { let platform_version = PlatformVersion::latest(); @@ -1745,6 +1739,10 @@ mod token_pool_mint_burn_claim_purchase_tests { .await .expect("token mint to pool transition"); + // CheckTx rebuilds the preimage independently of block execution, so a kind that + // reached for the wrong tag there would drop every honest transition of this kind + // at admission while block-level tests stayed green. + assert_check_tx_accepts(&platform, &mint); let result = process(&platform, &mint); assert_matches!( result.execution_results().as_slice(), @@ -2782,6 +2780,10 @@ mod token_pool_mint_burn_claim_purchase_tests { ) .await .expect("token direct purchase to pool transition"); + // CheckTx rebuilds the preimage independently of block execution, so a kind that + // reached for the wrong tag there would drop every honest transition of this kind + // at admission while block-level tests stayed green. + assert_check_tx_accepts(&platform, &purchase); let result = process(&platform, &purchase); assert_matches!( result.execution_results().as_slice(), @@ -3422,8 +3424,8 @@ mod document_shielded_token_payment_tests { /// both pools; the credit pool is funded directly in state, the token pool through a shield. mod token_pool_paid_transitions_tests { use super::token_shielded_pool_tests::{ - assert_tokens_conserved, build_shield_bundle, dummy_bundle, enable_shielded_pool, - identity_token_balance, insert_token_pool_anchor, nullifier_is_spent, + assert_check_tx_accepts, assert_tokens_conserved, build_shield_bundle, dummy_bundle, + enable_shielded_pool, identity_token_balance, insert_token_pool_anchor, nullifier_is_spent, platform_with_latest_version, pool_balance, process, spend_keys, spendable_note, OWNER_INITIAL_BALANCE, }; @@ -3442,13 +3444,30 @@ mod token_pool_paid_transitions_tests { use dpp::data_contract::DataContract; use dpp::identity::accessors::IdentityGettersV0; use dpp::identity::{Identity, IdentityPublicKey}; + use dpp::shielded::builder::{ + build_token_claim_to_pool_transition, build_token_direct_purchase_to_pool_transition, + build_token_mint_to_pool_transition, build_token_shield_transition, + }; + use dpp::shielded::token_pool_output_only_extra_sighash_data; + use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; + use crate::execution::validation::state_transition::state_transitions::shielded_common::{ + reconstruct_and_verify_bundle, FLAGS_OUTPUTS_ONLY, + }; + use dpp::state_transition::batch_transition::token_mint_to_pool_transition::v0::v0_methods::TokenMintToPoolTransitionV0Methods; + use dpp::state_transition::batch_transition::token_claim_to_pool_transition::v0::v0_methods::TokenClaimToPoolTransitionV0Methods; + use dpp::state_transition::batch_transition::token_direct_purchase_to_pool_transition::v0::v0_methods::TokenDirectPurchaseToPoolTransitionV0Methods; use dpp::shielded::builder::{ build_token_purchase_from_shielded_pool_transition, build_token_shielded_transfer_with_shielded_fee_transition, build_token_unshield_with_shielded_fee_transition, OrchardProver, ShieldedFeePayer, SpendableNote, TokenPoolSpender, }; + use dpp::shielded::OrchardBundleParams; + use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; + use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransition; use dpp::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRef; + use dpp::state_transition::batch_transition::token_shield_transition::v0::v0_methods::TokenShieldTransitionV0Methods; use dpp::state_transition::token_purchase_from_shielded_pool_transition::v0::TokenPurchaseFromShieldedPoolTransitionV0; use dpp::state_transition::token_purchase_from_shielded_pool_transition::TokenPurchaseFromShieldedPoolTransition; use dpp::state_transition::StateTransition; @@ -3910,4 +3929,270 @@ mod token_pool_paid_transitions_tests { if message.contains("TokenPurchaseFromShieldedPool") && message.contains("not active") ); } + + /// The bundle a client actually ships must be the one consensus accepts. Every other test + /// here hand-builds its bundle through the same helper the verifier's preimage comes from, + /// so a mistake inside the public builder — the wrong kind, a missing field — would be + /// invisible: the suite would stay green while every real shield was rejected at every + /// node. This one goes through `build_token_shield_transition`, shows the bundle admitted + /// by CheckTx and executed, and only then replays that same proven bundle into another + /// pool. The positive half is what makes the negative half mean anything: the replay is + /// refused for the pool it landed in, not because the bundle was never valid anywhere. + #[tokio::test] + async fn test_a_builder_made_shield_is_accepted_and_its_bundle_cannot_be_replayed() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9210); + let amount = 4_200; + + let (owner, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + owner.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + + let shield = build_token_shield_transition( + token_id, + owner.id(), + contract.id(), + 0, + &wallet_address(), + amount, + [0u8; 36], + None, + &key, + 2, + 0, + &signer, + &Prover, + platform_version, + ) + .await + .expect("client-built token shield"); + + // Admission and execution both rebuild the preimage; either disagreeing with the + // builder would reject this. + assert_check_tx_accepts(&platform, &shield); + assert_matches!( + process(&platform, &shield).execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(&platform, token_id), amount); + assert_tokens_conserved(&platform); + + // That exact bundle, proven valid above, is now offered to a second token's pool. + let StateTransition::Batch(batch) = &shield else { + panic!("expected a batch transition"); + }; + let BatchedTransitionRef::Token(TokenTransition::Shield(proven)) = + batch.transitions_iter().next().expect("one transition") + else { + panic!("expected a token shield transition"); + }; + let proven_bundle = OrchardBundleParams { + actions: proven.actions().to_vec(), + anchor: *proven.anchor(), + proof: proven.proof().to_vec(), + binding_signature: *proven.binding_signature(), + }; + + // A second owner, because a contract id derives from its owner: the same owner would + // give back the same contract and the same pool, and the test would prove nothing. + let (other_owner, other_signer, other_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (other_contract, other_token_id) = create_token_contract_with_owner_identity( + &mut platform, + other_owner.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + assert_ne!( + token_id, other_token_id, + "the two tokens must own separate pools" + ); + + let replay = BatchTransition::new_token_shield_transition( + other_token_id, + other_owner.id(), + other_contract.id(), + 0, + amount, + proven_bundle, + &other_key, + 2, + 0, + &other_signer, + platform_version, + None, + ) + .await + .expect("token shield transition"); + + assert_matches!( + process(&platform, &replay).execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::InvalidShieldedProofError(_)), + .. + }] + ); + assert_eq!(pool_balance(&platform, other_token_id), 0); + assert_tokens_conserved(&platform); + } + + /// Each of the four public builders must bind the preimage its own kind's verifier + /// rebuilds. Nothing in the types enforces that: the builders take a + /// `TokenTransitionActionType`, so they cannot pass a non-kind, but any one of them could + /// pass a *different* outputs-only kind and still compile. The failure is total and silent + /// — every honest transition of that kind rejected at every node, with the suite green — + /// so it is checked here directly, against the same verification consensus runs. + #[tokio::test] + async fn test_every_outputs_only_builder_binds_the_preimage_its_verifier_rebuilds() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let (owner, signer, key) = setup_identity(&mut platform, 4242, dash_to_credits!(0.5)); + + let token_id = Identifier::from([7u8; 32]); + let contract_id = Identifier::from([8u8; 32]); + let recipient = wallet_address(); + let amount = 5_000u64; + + let shield = build_token_shield_transition( + token_id, + owner.id(), + contract_id, + 0, + &recipient, + amount, + [0u8; 36], + None, + &key, + 2, + 0, + &signer, + &Prover, + platform_version, + ) + .await + .expect("shield"); + let mint = build_token_mint_to_pool_transition( + token_id, + owner.id(), + contract_id, + 0, + &recipient, + amount, + [0u8; 36], + None, + None, + None, + &key, + 3, + 0, + &signer, + &Prover, + platform_version, + ) + .await + .expect("mint to pool"); + let claim = build_token_claim_to_pool_transition( + token_id, + owner.id(), + contract_id, + 0, + &recipient, + amount, + TokenDistributionType::PreProgrammed, + None, + [0u8; 36], + None, + None, + &key, + 4, + 0, + &signer, + &Prover, + platform_version, + ) + .await + .expect("claim to pool"); + let purchase = build_token_direct_purchase_to_pool_transition( + token_id, + owner.id(), + contract_id, + 0, + &recipient, + amount, + 1_000, + [0u8; 36], + None, + &key, + 5, + 0, + &signer, + &Prover, + platform_version, + ) + .await + .expect("direct purchase to pool"); + + for (built, action_type) in [ + (&shield, TokenTransitionActionType::Shield), + (&mint, TokenTransitionActionType::MintToPool), + (&claim, TokenTransitionActionType::ClaimToPool), + (&purchase, TokenTransitionActionType::DirectPurchaseToPool), + ] { + let StateTransition::Batch(batch) = built else { + panic!("expected a batch transition"); + }; + let transition = batch.transitions_iter().next().expect("one transition"); + // Every one of the four pays `amount` into the pool, so the bundle's value + // balance is the same. A claim states no amount of its own — consensus resolves + // it against state — so it has to come from what the builder was handed. + let value_balance = -(amount as i64); + let (actions, anchor, proof, binding_signature) = match transition { + BatchedTransitionRef::Token(TokenTransition::Shield(t)) => { + (t.actions(), t.anchor(), t.proof(), t.binding_signature()) + } + BatchedTransitionRef::Token(TokenTransition::MintToPool(t)) => { + (t.actions(), t.anchor(), t.proof(), t.binding_signature()) + } + BatchedTransitionRef::Token(TokenTransition::ClaimToPool(t)) => { + (t.actions(), t.anchor(), t.proof(), t.binding_signature()) + } + BatchedTransitionRef::Token(TokenTransition::DirectPurchaseToPool(t)) => { + (t.actions(), t.anchor(), t.proof(), t.binding_signature()) + } + other => panic!("unexpected transition {other:?}"), + }; + + // The preimage the verifier for this kind rebuilds, nothing the builder handed us. + let expected = token_pool_output_only_extra_sighash_data( + action_type, + &token_id.to_buffer(), + platform_version, + ) + .expect("outputs-only kind"); + + reconstruct_and_verify_bundle( + actions, + FLAGS_OUTPUTS_ONLY, + value_balance, + anchor, + proof, + binding_signature, + &expected, + ) + .unwrap_or_else(|error| { + panic!("{action_type} builder does not bind its verifier's preimage: {error:?}") + }); + } + } } diff --git a/packages/wasm-dpp2/src/shielded/mod.rs b/packages/wasm-dpp2/src/shielded/mod.rs index 944a88fb72b..b57230ceb2a 100644 --- a/packages/wasm-dpp2/src/shielded/mod.rs +++ b/packages/wasm-dpp2/src/shielded/mod.rs @@ -34,14 +34,21 @@ use wasm_bindgen::prelude::wasm_bindgen; /// /// `sighash = SHA-256("DashPlatformSighash" || bundleCommitment || extraData)` /// -/// - For shield and shielded_transfer transitions, `extraData` should be empty. -/// - For unshield transitions, `extraData` = serialized `outputAddress` bytes. -/// - For shielded withdrawal transitions, `extraData` = `outputScript` bytes. -/// - For token pool transitions, `extraData` is the layout that transition binds. An -/// outputs-only one — token shield, and mint, claim or direct purchase into the pool — -/// binds its one-byte kind tag followed by the 32-byte token id; leaving it empty is -/// rejected, since all token pools share the empty-tree anchor such a bundle proves -/// against and nothing else would say which pool it was built for. +/// `extraData` is the exact preimage the transition's own layout defines; consensus rebuilds +/// it and compares, so a byte out of place rejects an otherwise valid bundle. Integers are +/// little endian. +/// +/// - Credit pool `Shield` and `ShieldedTransfer`: empty. +/// - `Unshield`: `outputAddress || amount (u64)`. +/// - Shielded withdrawal: `outputScript || amount (u64) || coreFeePerByte (u32) || +/// pooling (1 byte)`. +/// - Token pool transitions that only create notes — token shield, and mint, claim or direct +/// purchase into the pool: `kind tag (1 byte) || tokenId (32 bytes)`, with the tag `0x80`, +/// `0x81`, `0x82` and `0x83` in that order. Empty is rejected: such a bundle carries no +/// anchor, every token pool shares the empty-tree anchor it proves against, and nothing +/// else in the bundle says which pool or which kind it was built for. +/// - Other token pool transitions bind their own fields; see the `extra_sighash_data` +/// builders in `dpp::shielded`. #[wasm_bindgen(js_name = computePlatformSighash)] pub fn compute_platform_sighash_wasm( bundle_commitment: Vec, From c3070c71a5721cf6292b7f73a7b024e5e603add9 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 24 Sep 2026 17:56:45 +0700 Subject: [PATCH 24/94] fix(drive-abci): give the seven document base action constructors the shielded payment field Merging v4.2-dev added `shielded_token_payment` to `DocumentBaseTransitionActionV0`, and seven constructors were left without it: six in `batch/tests/document/` and one in the `document_create_transition_action` advanced structure validator. Nothing caught this. Both merges were verified with `cargo check --workspace --all-features`, which builds lib and bin targets and not test ones, so the only constructors that broke were exactly the ones no non-test code touches. It surfaced when a parallel worker tried to build against this branch. `cargo check -p drive-abci --tests` is clean now. The lesson for the next merge here is to use `--all-targets` whenever the claim is that a merge is clean, because a merge that adds a field to a struct breaks precisely the constructors a feature-only check cannot see. Co-Authored-By: Claude Opus 5 (1M context) --- .../advanced_structure_v1/mod.rs | 1 + .../batch/tests/document/list_element_reference.rs | 1 + .../state_transitions/batch/tests/document/lookup_reference.rs | 1 + .../state_transitions/batch/tests/document/max_bytes.rs | 1 + .../state_transitions/batch/tests/document/owner_reference.rs | 1 + .../batch/tests/document/property_constraints.rs | 1 + .../batch/tests/document/reference_expression.rs | 2 ++ 7 files changed, 8 insertions(+) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/advanced_structure_v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/advanced_structure_v1/mod.rs index 9e6025d1673..62a71fbb016 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/advanced_structure_v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_create_transition_action/advanced_structure_v1/mod.rs @@ -527,6 +527,7 @@ mod tests { gas_fees_paid_by: GasFeesPaidBy::default(), contract_gas_fees_paid_by: GasFeesPaidBy::default(), declared_action_fee: None, + shielded_token_payment: None, }), block_info: BlockInfo::default(), data, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/list_element_reference.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/list_element_reference.rs index 4b8937ca99f..ac3bbf495cf 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/list_element_reference.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/list_element_reference.rs @@ -816,6 +816,7 @@ mod list_element_reference_tests { document_type_name: "resignation".to_string(), data_contract: contract_fetch_info.expect("the contract is in state"), token_cost: None, + shielded_token_payment: None, gas_fees_paid_by: GasFeesPaidBy::default(), contract_gas_fees_paid_by: GasFeesPaidBy::default(), declared_action_fee: None, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/lookup_reference.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/lookup_reference.rs index e949836f875..b86eeceb836 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/lookup_reference.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/lookup_reference.rs @@ -659,6 +659,7 @@ mod lookup_reference_tests { document_type_name: "charter".to_string(), data_contract: contract_fetch_info.expect("the contract is in state"), token_cost: None, + shielded_token_payment: None, gas_fees_paid_by: GasFeesPaidBy::default(), contract_gas_fees_paid_by: GasFeesPaidBy::default(), declared_action_fee: None, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/max_bytes.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/max_bytes.rs index ef11df52322..2b37205a455 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/max_bytes.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/max_bytes.rs @@ -372,6 +372,7 @@ mod max_bytes_tests { document_type_name: "profile".to_string(), data_contract: contract_fetch_info.clone(), token_cost: None, + shielded_token_payment: None, gas_fees_paid_by: GasFeesPaidBy::default(), contract_gas_fees_paid_by: GasFeesPaidBy::default(), declared_action_fee: None, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/owner_reference.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/owner_reference.rs index f84225acdf3..750bc47be7b 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/owner_reference.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/owner_reference.rs @@ -341,6 +341,7 @@ mod owner_reference_tests { document_type_name: type_name.to_string(), data_contract: contract_fetch_info.expect("the contract is in state"), token_cost: None, + shielded_token_payment: None, gas_fees_paid_by: GasFeesPaidBy::default(), contract_gas_fees_paid_by: GasFeesPaidBy::default(), declared_action_fee: None, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/property_constraints.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/property_constraints.rs index 98af6a90cb9..11922aaf6a8 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/property_constraints.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/property_constraints.rs @@ -559,6 +559,7 @@ mod property_constraints_tests { document_type_name: "offer".to_string(), data_contract: contract_fetch_info, token_cost: None, + shielded_token_payment: None, gas_fees_paid_by: GasFeesPaidBy::default(), contract_gas_fees_paid_by: GasFeesPaidBy::default(), declared_action_fee: None, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/reference_expression.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/reference_expression.rs index dd2e37de9be..997881dd417 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/reference_expression.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/reference_expression.rs @@ -699,6 +699,7 @@ mod reference_expression_tests { document_type_name: "resignation".to_string(), data_contract: contract_fetch_info.expect("the contract is in state"), token_cost: None, + shielded_token_payment: None, gas_fees_paid_by: GasFeesPaidBy::default(), contract_gas_fees_paid_by: GasFeesPaidBy::default(), declared_action_fee: None, @@ -866,6 +867,7 @@ mod reference_expression_tests { document_type_name: "resignation".to_string(), data_contract: contract_fetch_info.expect("the contract is in state"), token_cost: None, + shielded_token_payment: None, gas_fees_paid_by: GasFeesPaidBy::default(), contract_gas_fees_paid_by: GasFeesPaidBy::default(), declared_action_fee: None, From d7ebc849216a2b78064cb57bc4ddcc10582bf6e3 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 24 Sep 2026 18:58:07 +0700 Subject: [PATCH 25/94] style(dpp): drop the stray blank line the manual enum tail moves left behind Resolving the v4.2-dev merges meant moving this branch's error variants to the tail of `BasicError` and `StateError` by hand, so the base kept its wire discriminants. Each move left a blank line before the enum's closing brace, which `cargo fmt --check` rejects. This is what turned the branch's CI red. The verification after that merge ran the frozen-discriminant tests and `cargo check --workspace --all-features`, but not `cargo fmt --check --all`, so nothing local saw it. Co-Authored-By: Claude Opus 5 (1M context) --- packages/rs-dpp/src/errors/consensus/basic/basic_error.rs | 1 - packages/rs-dpp/src/errors/consensus/state/state_error.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs b/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs index be6f0bce486..8986ae74b53 100644 --- a/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs +++ b/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs @@ -834,7 +834,6 @@ pub enum BasicError { #[error(transparent)] TokenShieldedPoolIncompatibleRulesError(TokenShieldedPoolIncompatibleRulesError), - } impl From for ConsensusError { diff --git a/packages/rs-dpp/src/errors/consensus/state/state_error.rs b/packages/rs-dpp/src/errors/consensus/state/state_error.rs index 37b37b2185a..659cbc7d449 100644 --- a/packages/rs-dpp/src/errors/consensus/state/state_error.rs +++ b/packages/rs-dpp/src/errors/consensus/state/state_error.rs @@ -633,7 +633,6 @@ pub enum StateError { #[error(transparent)] TokenShieldedPaymentNotRequiredError(TokenShieldedPaymentNotRequiredError), - } impl From for ConsensusError { From 6b1559b09ed2ec866df46e8a286aeefdfda3883b Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 24 Sep 2026 21:48:45 +0700 Subject: [PATCH 26/94] docs(dpp): say what the outputs-only sighash actually binds and what it leaves open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A review of the binding commits found the surrounding documentation claiming more, and other things, than the code does. Seven statements were wrong; six are corrected here. The worst of them said an outputs-only bundle "has no anchor of its own". These transitions do carry an `anchor` field, and every one of those sentences sits two lines above that anchor being passed to bundle verification. What is true is that the anchor is never CHECKED: `validate_token_pool_anchor_exists` is called by the transitions that spend and by none of the four outputs-only ones, which is correct, because an outputs-only bundle has no spends and so no merkle path for an anchor to authenticate. Neither pool writer records the declared anchor either, so an arbitrary one is inert. The wording mattered because "has no anchor" reads as an unavoidable property while "the anchor is not checked against the pool" reads as the deliberate choice it is. Also corrected: four credit pool transitions carry an empty preimage, not the two the comment named (`ShieldFromIdentity` and `ShieldFromAssetLock` pass `&[]` as well), and of those four only `ShieldedTransfer` spends; different kinds share a value balance only when their amounts match, since it is `-amount`; "the copier burns their own tokens" holds for the shield arm alone, because a mint copy creates new supply and needs mint authority while a claim consumes an entitlement and a direct purchase pays credits; the tag bytes are written out explicitly so the preimage does not depend on the enum's layout at all, not because the enum promises nothing about ordering — it carries an append-only marker, which is a convention for clients rather than a guarantee a consensus preimage can rest on; and of the identity-less token bundles only `TokenShieldedTransferWithShieldedFee` writes its type byte at the same 1 + 32 length that could collide with a tag. The wasm binding's list was a regression: its previous wording was loose enough to be true, and naming the transitions made it false in a user-facing document. No behaviour changes, so no test is owed. `cargo test -p dpp --all-features -- shielded` is green at 311, and `cargo fmt --check --all` is clean. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/shielded/builder/token_shield.rs | 2 +- packages/rs-dpp/src/shielded/sighash.rs | 41 +++++++++++-------- .../rs-platform-version/src/version/v14.rs | 3 +- packages/wasm-dpp2/src/shielded/mod.rs | 3 +- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/packages/rs-dpp/src/shielded/builder/token_shield.rs b/packages/rs-dpp/src/shielded/builder/token_shield.rs index a5b1c686ea6..a38ac116373 100644 --- a/packages/rs-dpp/src/shielded/builder/token_shield.rs +++ b/packages/rs-dpp/src/shielded/builder/token_shield.rs @@ -20,7 +20,7 @@ use super::{build_output_only_bundle, serialize_authorized_bundle, OrchardProver /// a batch transition signed by `owner_id`, the identity whose token balance funds the shield. /// /// The identity pays the fee in credits; `amount` tokens leave its balance for the pool at -/// execution. The bundle has no spends, so it carries no anchor pinning it to a pool, and the +/// execution. The bundle has no spends, so its anchor is never checked against a pool, and the /// identity signature over the batch only binds it inside *this* batch — the authorized bundle /// bytes on their own would verify in any pool. The extra sighash data pins them to this token /// and to the shield kind. `sender_ovk` lets the sending wallet recover the note it created diff --git a/packages/rs-dpp/src/shielded/sighash.rs b/packages/rs-dpp/src/shielded/sighash.rs index 7c660b011bd..a706a3ee3ca 100644 --- a/packages/rs-dpp/src/shielded/sighash.rs +++ b/packages/rs-dpp/src/shielded/sighash.rs @@ -55,12 +55,16 @@ pub const TOKEN_DIRECT_PURCHASE_TO_POOL_BUNDLE_TAG: u8 = 0x83; /// valid Orchard bundle. /// /// It also binds a bundle that has no transparent fields to the one context it was proved -/// for, which an outputs-only bundle cannot do on its own: carrying no anchor, it verifies -/// against every pool that shares the empty-tree anchor. +/// for, which an outputs-only bundle cannot do on its own: having no spends, its anchor is +/// never checked against a pool — the client builds it against the empty tree — so it verifies +/// against every pool. /// /// The same computation must be used on both the signing (client) and verification (platform) -/// sides. `extra_data` is empty only for the credit pool's own `Shield` and `ShieldedTransfer`; -/// each other transition has a builder in this module that spells out its layout. +/// sides. `extra_data` is empty for the credit pool's `Shield`, `ShieldFromIdentity`, +/// `ShieldFromAssetLock` and `ShieldedTransfer`; every other transition spells out a layout. +/// Of those four only `ShieldedTransfer` spends, so it carries an anchor and nullifiers that +/// pin it to one pool and one set of notes. The three credit shields are outputs-only and so +/// are pinned by nothing. pub fn compute_platform_sighash(bundle_commitment: &[u8; 32], extra_data: &[u8]) -> [u8; 32] { let mut hasher = Sha256::new(); hasher.update(SIGHASH_DOMAIN); @@ -638,11 +642,14 @@ pub fn token_shielded_transfer_extra_sighash_data_v0( /// Extra sighash data of an outputs-only token pool bundle — `TokenShield`, `TokenMintToPool`, /// `TokenClaimToPool` and `TokenDirectPurchaseToPool`: the bundle's domain tag and the token id. /// -/// These bundles have no spends, so they carry no anchor pinning them to a pool: every token -/// pool starts from the same empty-tree anchor, and the proof and binding signature verify -/// against any of them. Without this data the authorized bundle bytes are a free-standing, +/// These bundles have no spends, so nothing pins them to a pool: their anchor is never checked +/// against one — the client builds it against the empty tree — and the proof and binding +/// signature verify against any pool. Without this data the authorized bundle bytes are a free-standing, /// self-verifying object that anybody can lift out of the mempool into a transition of their -/// own, funded by their own tokens, against a different pool or a different transition kind. +/// own, against a different pool or a different transition kind. The copier supplies whatever +/// that kind demands of them — their own tokens, credits or a distribution entitlement — except +/// for a mint, which demands the token's mint authority and creates new supply rather than +/// moving any. /// /// What that copy costs depends on where it lands, because nullifiers are namespaced per pool: /// @@ -651,12 +658,11 @@ pub fn token_shielded_transfer_extra_sighash_data_v0( /// one of the two can ever be spent. That is Faerie Gold. A wallet that merges notes by /// nullifier is safe, but any wallet or indexer counting by commitment sees two payments /// where one is real. Bundles of different kinds are otherwise indistinguishable to the -/// proof — same flags, same empty-tree anchor, same value balance — so only the tag tells -/// them apart. +/// proof — same flags, same empty-tree anchor, and the same value balance whenever the +/// amounts match — so only the tag tells them apart. /// - **Another pool.** That pool has its own nullifier tree, so both notes stay spendable and /// nothing is Faerie Gold. The harm is that the recipient now holds notes derived from one -/// `rho` in two pools, which links their spends across pools, and the copier burns their own -/// tokens doing it. +/// `rho` in two pools, which links their spends across pools. /// /// Not covered: a copy into the same pool *and* the same kind, submitted by someone else. That /// is the larger Faerie Gold vector and this preimage does not reach it, because it binds no @@ -683,9 +689,10 @@ pub fn token_pool_output_only_extra_sighash_data( /// The v0 domain tag of an outputs-only token pool bundle. Frozen: the mapping is part of the /// sighash preimage, so a kind keeps its byte forever and a new kind takes a new one. /// -/// The bytes are written out here rather than derived from the variant's position, because -/// `TokenTransitionActionType` is append-only for the sake of clients and nothing about it -/// promises a stable ordering to consensus. +/// The bytes are written out here rather than derived from the variant's position, so the +/// preimage does not depend on the enum's layout at all. `TokenTransitionActionType` is marked +/// append-only, but that marker is a convention for clients; resting a consensus preimage on it +/// would turn an accidental reordering into a silent wire change. /// /// Every other kind is named rather than swept up by a catch-all: a kind added to the enum must /// fail to compile here, so that whoever adds it decides whether it carries a bundle. A @@ -1200,7 +1207,9 @@ mod tests { #[test] fn outputs_only_token_pool_tags_cannot_collide_with_state_transition_types() { // The tags share a preimage slot with the `StateTransitionType` byte the identity-less - // token bundles commit to, at the same 1 + 32 length. Asking the enum itself — rather + // token bundles commit to, and one of them — `TokenShieldedTransferWithShieldedFee` — + // writes it at the same 1 + 32 length, so a colliding tag would leave the two preimages + // indistinguishable. Asking the enum itself — rather // than comparing against the three types that exist today — is what makes this break // on the day someone assigns a transition type inside the tag range. for tag in [ diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index 210acad9643..96e90afa957 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -757,7 +757,8 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// proposer of a group action), plus the recipient and amount where tokens /// leave the pool; an outputs-only bundle (`TokenShield`, /// `TokenMintToPool`, `TokenClaimToPool`, `TokenDirectPurchaseToPool`), -/// which has no anchor of its own, binds a per-kind tag and the token id. +/// whose anchor is never checked against a pool, binds a per-kind tag and +/// the token id. /// The pool balances are a term of the token conservation check /// (`calculate_total_tokens_balance` v1 in `DRIVE_TOKEN_METHOD_VERSIONS_V2`). /// `record_token_shielded_pool_anchors` diff --git a/packages/wasm-dpp2/src/shielded/mod.rs b/packages/wasm-dpp2/src/shielded/mod.rs index b57230ceb2a..b730e244fdd 100644 --- a/packages/wasm-dpp2/src/shielded/mod.rs +++ b/packages/wasm-dpp2/src/shielded/mod.rs @@ -38,7 +38,8 @@ use wasm_bindgen::prelude::wasm_bindgen; /// it and compares, so a byte out of place rejects an otherwise valid bundle. Integers are /// little endian. /// -/// - Credit pool `Shield` and `ShieldedTransfer`: empty. +/// - Credit pool `Shield`, `ShieldFromIdentity`, `ShieldFromAssetLock` and `ShieldedTransfer`: +/// empty. /// - `Unshield`: `outputAddress || amount (u64)`. /// - Shielded withdrawal: `outputScript || amount (u64) || coreFeePerByte (u32) || /// pooling (1 byte)`. From 0658194a1a50a4258a5ac47e9683113bbb4ce272 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 24 Sep 2026 22:29:49 +0700 Subject: [PATCH 27/94] fix(platform)!: record and check the dummy nullifiers of outputs-only token pool bundles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Binding a kind tag and the token id into these bundles' sighash stopped a third party lifting one into another pool or another kind. It could not stop the same authorized bundle landing twice where the preimage still matches: the sender resubmitting it, an SDK retrying with a fresh nonce, or a copier who satisfies the binding. Each of those produced a second note with the same commitment and the same `rho`, hence the same nullifier, of which only one is ever spendable — the wallet that merges by nullifier is fine, but a wallet or indexer counting by commitment sees two payments where one is real. An outputs-only bundle has no real spends, but its dummy spends carry nullifiers all the same. Recording those in the pool's nullifier tree and refusing a bundle whose nullifier is already there closes the duplicate whatever route it takes. Both halves were needed and only one was present. The checks existed; nothing recorded, so they had nothing to find. The two writers that can add to a token pool now store each note's dummy nullifier, and since all five pool-entry paths lower into exactly those two operations, all five are covered — including the identity-less `TokenPurchaseFromShieldedPool`, which had been missed because it reaches the pool through the mint writer rather than a batch transition of its own. The intra-bundle duplicate check is load-bearing beyond replay. Once these nullifiers are written, it is the only thing keeping two identical `insert_only_known_to_not_already_exist_op` entries out of a single GroveDB batch. A node running the shipped default folds byte-identical ops silently while a node with batching consistency verification refuses the batch, so a duplicate reaching the write would make two nodes disagree on one block with nothing visible on either. The check runs first, on every path, ahead of any state read, and a comment at each write site records that the writer relies on it. Fees needed no change and none is present: the inserts are built before the estimate/apply branch, so the operation count cannot diverge between the estimate and the write, and the nullifier path was already registered as a worst-case tree for the spend paths. The estimate models sixteen levels and an empty pool, so `estimated >= actual` keeps its margin. Scoped to token pools. The credit pool is a separate change. Every negative test of the new behaviour was shown red by reverting consensus code alone: with the checks removed the copies execute successfully, because the insert overwrites in silence, and with the recording removed there is nothing to find. One test is not of that kind — the unit test for a nullifier repeated inside a single bundle exercises a check that predates this change, so it is green either way. It is kept because that check now carries weight it did not before, being the only thing that keeps two identical inserts out of one batch, but it pins nothing this commit adds. Green after: drive-abci 220, drive 169, dpp 311, frozen discriminants 3, workspace clippy clean. Two independent reviews found nothing blocking. Co-Authored-By: Claude Opus 5 (1M context) --- book/src/data-model/token-shielded-pools.md | 20 +- book/src/fees/shielded-fees.md | 4 +- packages/rs-dpp/src/shielded/sighash.rs | 10 +- .../state_v0/mod.rs | 16 +- .../state_v0/mod.rs | 16 +- .../state_v0/mod.rs | 16 +- .../state_v0/mod.rs | 16 +- .../token/token_shielded_pool_common/mod.rs | 109 +- .../batch/tests/token/shielded/mod.rs | 1501 ++++++++++++++++- .../token_pool_paid_common/mod.rs | 31 + .../transform_into_action/v0/mod.rs | 20 +- .../shielded_pool/mint_to_pool/v0/mod.rs | 11 +- .../src/drive/tokens/shielded_pool/mod.rs | 12 +- .../tokens/shielded_pool/shield/v0/mod.rs | 11 +- .../src/util/batch/drive_op_batch/token.rs | 6 +- 15 files changed, 1756 insertions(+), 43 deletions(-) diff --git a/book/src/data-model/token-shielded-pools.md b/book/src/data-model/token-shielded-pools.md index 5d5d5aa03d3..74274dbc964 100644 --- a/book/src/data-model/token-shielded-pools.md +++ b/book/src/data-model/token-shielded-pools.md @@ -101,6 +101,15 @@ Outputs-only bundles (shield, mint, claim, purchase) have no spends, so their an checked against the pool; the client builds them against the empty tree. Spending bundles must name an anchor the pool has recorded. +Because the preimage of an outputs-only bundle binds no owner, anybody can lift one out of the +mempool and submit it again as the same kind into the same pool. The copy would land a second +note with the same commitment and the same `rho`, hence the same nullifier, and only one of the +two could ever be spent. The pool refuses it on the state side: each action's dummy nullifier, +from which its note takes its `rho`, is recorded in the pool's nullifier tree when the bundle +enters, and a bundle whose dummy nullifier is already there is rejected with +`NullifierAlreadySpentError`. `TokenPurchaseFromShieldedPool` carries an outputs-only token +bundle too and is checked the same way. + A mint or burn into the pool that goes through a group action stores `TokenEvent::MintToPool` / `TokenEvent::BurnFromPool` with a digest of the serialized actions (`serialized_actions_digest`), so every signer commits to exactly the same notes. A burn's @@ -195,7 +204,8 @@ State validation runs in this order, and the first failure is returned: schedule and the max supply. 4. Spending bundles: the anchor is recorded in the pool (`InvalidAnchorError`), no nullifier repeats within the bundle or is already spent (`NullifierAlreadySpentError`), and for an - unshield the pool holds `amount`. + unshield the pool holds `amount`. Outputs-only bundles: no dummy nullifier repeats within + the bundle or is already recorded in the pool (`NullifierAlreadySpentError`). 5. Proof verification. The fee for it, `compute_shielded_verification_fee(actions)`, is added as a precalculated operation before the Halo 2 proof and binding signature are checked, so a failed proof is a paid failure: the identity is charged, its nonce advances, and nothing @@ -213,13 +223,13 @@ batch carrying any of them with `StateTransitionNotActiveError`. The drive operations are composites of the credit pool primitives re-rooted under the token: -- shield: remove `amount` from the owner's token balance, append the notes, add `amount` to - the pool's `TOTAL_BALANCE`; +- shield: remove `amount` from the owner's token balance, insert the bundle's dummy + nullifiers, append the notes, add `amount` to the pool's `TOTAL_BALANCE`; - unshield: insert the nullifiers, append the notes, subtract `amount` from the pool balance, add `amount` to the recipient's token balance; - shielded transfer: insert the nullifiers, append the notes; -- mint, claim and purchase to pool: append the notes, add `amount` to the pool balance and to - the total supply (`TokenMintToPool`); +- mint, claim and purchase to pool: insert the bundle's dummy nullifiers, append the notes, + add `amount` to the pool balance and to the total supply (`TokenMintToPool`); - burn from pool: insert the nullifiers, append the change notes, subtract `amount` from the pool balance and from the total supply (`TokenBurnFromPool`). diff --git a/book/src/fees/shielded-fees.md b/book/src/fees/shielded-fees.md index 73c4d97c857..8b3acbb76ca 100644 --- a/book/src/fees/shielded-fees.md +++ b/book/src/fees/shielded-fees.md @@ -55,10 +55,10 @@ signing identity pays in credits through the standard identity-paid path. | Transition | Fee Formula | Explanation | |---|---|---| -| **TokenShield** | `fee = metered(storage + processing) + shielded_verification_fee`, paid by the signing identity | Same model as `ShieldFromIdentity`: the note appends, the identity token balance write and the pool balance write are metered, and `compute_shielded_verification_fee(num_actions)` is added as a precalculated operation before the proof is verified. `value_balance` is `-amount` in tokens and carries no fee. | +| **TokenShield** | `fee = metered(storage + processing) + shielded_verification_fee`, paid by the signing identity | Same model as `ShieldFromIdentity`: the dummy nullifier inserts, the note appends, the identity token balance write and the pool balance write are metered, and `compute_shielded_verification_fee(num_actions)` is added as a precalculated operation before the proof is verified. `value_balance` is `-amount` in tokens and carries no fee. | | **TokenUnshield** | `fee = metered(storage + processing) + shielded_verification_fee`, paid by the signing identity | `value_balance` equals the unshielded token amount exactly; the recipient receives the full amount. Nullifier inserts, note appends and the two balance writes are metered. | | **TokenShieldedTransfer** | `fee = metered(storage + processing) + shielded_verification_fee`, paid by the signing identity | `value_balance` is exactly zero; consensus rejects any other value. Only the nullifier inserts and note appends are metered. | -| **TokenMintToPool**, **TokenClaimToPool**, **TokenDirectPurchaseToPool** | same model | Outputs-only bundles; the note appends and the supply and pool balance writes are metered, the verification fee is charged by the action transformer so CheckTx and block execution price the bundle identically. | +| **TokenMintToPool**, **TokenClaimToPool**, **TokenDirectPurchaseToPool** | same model | Outputs-only bundles; the dummy nullifier inserts, the note appends and the supply and pool balance writes are metered, the verification fee is charged by the action transformer so CheckTx and block execution price the bundle identically. | | **TokenBurnFromPool** | same model | Nullifier inserts, change note appends and the supply and pool balance writes are metered. | | Document with a **TokenPaymentInfo::V1** | same model, on top of the document's own fee | The shielded payment bundle's verification fee is added by the batch transformer before anything can fail; the pool writes it causes are metered. | diff --git a/packages/rs-dpp/src/shielded/sighash.rs b/packages/rs-dpp/src/shielded/sighash.rs index a706a3ee3ca..ae3a5288c7c 100644 --- a/packages/rs-dpp/src/shielded/sighash.rs +++ b/packages/rs-dpp/src/shielded/sighash.rs @@ -664,10 +664,12 @@ pub fn token_shielded_transfer_extra_sighash_data_v0( /// nothing is Faerie Gold. The harm is that the recipient now holds notes derived from one /// `rho` in two pools, which links their spends across pools. /// -/// Not covered: a copy into the same pool *and* the same kind, submitted by someone else. That -/// is the larger Faerie Gold vector and this preimage does not reach it, because it binds no -/// owner. Closing it needs the bundle's dummy nullifiers recorded and checked, which no -/// outputs-only path does yet, here or in the credit pool. +/// Not covered by this preimage: a copy into the same pool *and* the same kind, submitted by +/// someone else. That is the larger Faerie Gold vector, and the preimage cannot reach it +/// because it binds no owner. Consensus closes it on the state side instead: every token pool +/// write that takes an outputs-only bundle records the bundle's dummy nullifiers in the pool's +/// nullifier tree, and every such transition is refused with `NullifierAlreadySpentError` when +/// one of them is already there. The credit pool's outputs-only paths do not do this. pub fn token_pool_output_only_extra_sighash_data( action_type: TokenTransitionActionType, token_id: &[u8; 32], diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs index 194f0989bd1..9c5fe4c9073 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs @@ -13,7 +13,8 @@ use drive::query::TransactionArg; use crate::execution::types::execution_operation::ValidationOperation; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContextMethodsV0; use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_pool_common::{ - validate_token_not_paused, validate_token_shielded_pool_enabled, verify_token_pool_bundle, + validate_token_not_paused, validate_token_pool_output_nullifiers, + validate_token_shielded_pool_enabled, verify_token_pool_bundle, }; use crate::execution::validation::state_transition::state_transitions::shielded_common::FLAGS_OUTPUTS_ONLY; use dpp::consensus::state::state_error::StateError; @@ -121,6 +122,19 @@ impl TokenClaimToPoolTransitionActionStateValidationV0 for TokenClaimToPoolTrans } } + let validation_result = validate_token_pool_output_nullifiers( + platform, + &token_id.to_buffer(), + &self.nullifiers(), + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + // An outputs-only bundle has no anchor of its own, so the sighash is what pins it to // this pool and this kind — see `token_pool_output_only_extra_sighash_data`. let extra_sighash_data = token_pool_output_only_extra_sighash_data( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs index 81bd6bae73a..5e90c60d586 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs @@ -13,7 +13,8 @@ use drive::query::TransactionArg; use crate::execution::types::execution_operation::ValidationOperation; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContextMethodsV0; use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_pool_common::{ - validate_token_not_paused, validate_token_shielded_pool_enabled, verify_token_pool_bundle, + validate_token_not_paused, validate_token_pool_output_nullifiers, + validate_token_shielded_pool_enabled, verify_token_pool_bundle, }; use crate::execution::validation::state_transition::state_transitions::shielded_common::FLAGS_OUTPUTS_ONLY; use dpp::consensus::state::state_error::StateError; @@ -122,6 +123,19 @@ impl TokenDirectPurchaseToPoolTransitionActionStateValidationV0 } } + let validation_result = validate_token_pool_output_nullifiers( + platform, + &token_id.to_buffer(), + &self.nullifiers(), + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + // An outputs-only bundle has no anchor of its own, so the sighash is what pins it to // this pool and this kind — see `token_pool_output_only_extra_sighash_data`. let extra_sighash_data = token_pool_output_only_extra_sighash_data( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs index 29d42a75bc6..9fb49f50621 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs @@ -13,7 +13,8 @@ use drive::query::TransactionArg; use crate::execution::types::execution_operation::ValidationOperation; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContextMethodsV0; use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_pool_common::{ - validate_token_not_paused, validate_token_shielded_pool_enabled, verify_token_pool_bundle, + validate_token_not_paused, validate_token_pool_output_nullifiers, + validate_token_shielded_pool_enabled, verify_token_pool_bundle, }; use crate::execution::validation::state_transition::state_transitions::shielded_common::FLAGS_OUTPUTS_ONLY; use dpp::consensus::basic::token::ChoosingTokenMintRecipientNotAllowedError; @@ -197,6 +198,19 @@ impl TokenMintToPoolTransitionActionStateValidationV0 for TokenMintToPoolTransit } } + let validation_result = validate_token_pool_output_nullifiers( + platform, + &token_id.to_buffer(), + &self.nullifiers(), + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + // An outputs-only bundle has no anchor of its own, so the sighash is what pins it to // this pool and this kind — see `token_pool_output_only_extra_sighash_data`. let extra_sighash_data = token_pool_output_only_extra_sighash_data( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs index 3e618cc42bb..4798f47fb21 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs @@ -5,7 +5,8 @@ use crate::execution::types::state_transition_execution_context::{ }; use crate::execution::validation::state_transition::batch::action_validation::token::token_base_transition_action::TokenBaseTransitionActionValidation; use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_pool_common::{ - validate_token_not_paused, validate_token_shielded_pool_enabled, verify_token_pool_bundle, + validate_token_not_paused, validate_token_pool_output_nullifiers, + validate_token_shielded_pool_enabled, verify_token_pool_bundle, }; use crate::execution::validation::state_transition::state_transitions::shielded_common::FLAGS_OUTPUTS_ONLY; use crate::execution::validation::state_transition::ValidationMode; @@ -114,6 +115,19 @@ impl TokenShieldTransitionActionStateValidationV0 for TokenShieldTransitionActio return Ok(validation_result); } + let validation_result = validate_token_pool_output_nullifiers( + platform, + &token_id.to_buffer(), + &self.nullifiers(), + block_info, + execution_context, + transaction, + platform_version, + )?; + if !validation_result.is_valid() { + return Ok(validation_result); + } + // Outputs-only bundle entering the pool: value balance is `-amount`. // An outputs-only bundle has no anchor of its own, so the sighash is what pins it to // this pool and this kind — see `token_pool_output_only_extra_sighash_data`. diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs index 7ffbfe7c39e..a608294713f 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs @@ -1,5 +1,4 @@ -//! Stateful checks shared by the three token shielded pool validators (`TokenShield`, -//! `TokenUnshield`, `TokenShieldedTransfer`). +//! Stateful checks shared by the token shielded pool validators. //! //! These mirror the credit pool helpers in `shielded_common`, re-rooted under the token's //! pool, and meter every read into the execution context so the batch owner pays for them. @@ -166,6 +165,45 @@ pub(crate) fn validate_token_pool_nullifiers( Ok(SimpleConsensusValidationResult::new()) } +/// An outputs-only bundle's dummy nullifiers must not repeat within the bundle or already be +/// recorded in the token pool. The reads are charged to the batch owner. +/// +/// Such a bundle binds no owner and no anchor, so the same authorized bytes can be submitted +/// again, by anybody, as the same kind into the same pool. Each of its notes takes its `rho` +/// from its action's dummy nullifier, so the copy would land a second note with the same +/// commitment and the same nullifier, of which only one could ever be spent. Every token pool +/// write that takes an outputs-only bundle records these nullifiers; this is the check that +/// makes the record refuse anything, since the insert itself does not look for an existing +/// entry. +#[allow(clippy::too_many_arguments)] +pub(crate) fn validate_token_pool_output_nullifiers( + platform: &PlatformStateRef, + token_id: &[u8; 32], + nullifiers: &[[u8; 32]], + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + transaction: TransactionArg, + platform_version: &PlatformVersion, +) -> Result { + let mut drive_operations = vec![]; + let validation_result = validate_token_pool_nullifiers( + platform.drive, + token_id, + nullifiers, + transaction, + &mut drive_operations, + platform_version, + )?; + charge_drive_operations( + platform.drive, + block_info, + execution_context, + drive_operations, + platform_version, + )?; + Ok(validation_result) +} + /// The anonymity-set floor for outflows with an observable destination /// (`minimum_token_pool_notes_for_outgoing`, 0 at introduction). pub(crate) fn validate_minimum_token_pool_notes( @@ -240,3 +278,70 @@ pub(crate) fn verify_token_pool_bundle( Ok(SimpleConsensusValidationResult::new()) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::execution::validation::state_transition::state_transitions::test_helpers::setup_platform; + use assert_matches::assert_matches; + + const TOKEN_ID: [u8; 32] = [7u8; 32]; + + /// A bundle naming one nullifier in two actions would land two notes of which only one + /// could be spent, and an outputs-only bundle's pool write would insert that nullifier + /// twice in one batch. The pool here has recorded nothing, so only the within-bundle + /// check can refuse it. + #[test] + fn a_nullifier_repeated_within_one_bundle_is_refused_by_a_pool_that_has_not_seen_it() { + let platform = setup_platform(); + let platform_version = PlatformVersion::latest(); + let operations = platform + .drive + .create_token_shielded_pool_trees_operations( + TOKEN_ID, + false, + &mut None, + None, + platform_version, + ) + .expect("pool tree operations"); + platform + .drive + .apply_batch_low_level_drive_operations( + None, + None, + operations, + &mut vec![], + &platform_version.drive, + ) + .expect("create pool trees"); + + let nullifier = [1u8; 32]; + let result = validate_token_pool_nullifiers( + &platform.drive, + &TOKEN_ID, + &[nullifier, nullifier], + None, + &mut vec![], + platform_version, + ) + .expect("no execution error"); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::StateError(StateError::NullifierAlreadySpentError(error))] + if error.nullifier() == nullifier + ); + + // Named once, the same nullifier is fresh: what was refused is the repeat. + let result = validate_token_pool_nullifiers( + &platform.drive, + &TOKEN_ID, + &[nullifier], + None, + &mut vec![], + platform_version, + ) + .expect("no execution error"); + assert!(result.is_valid()); + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 35b887c44f3..bdcd0b229b7 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -3478,10 +3478,10 @@ mod token_pool_paid_transitions_tests { use simple_signer::signer::SimpleSigner; /// A credit pool note the test wallet holds: enough for a purchase at 0.01 DASH a token. - const CREDIT_NOTE: u64 = 5_000_000_000; + pub(super) const CREDIT_NOTE: u64 = 5_000_000_000; const SHIELDED: u64 = 15; - struct Prover; + pub(super) struct Prover; impl OrchardProver for Prover { fn proving_key(&self) -> &ProvingKey { @@ -3489,19 +3489,19 @@ mod token_pool_paid_transitions_tests { } } - fn wallet_address() -> OrchardAddress { + pub(super) fn wallet_address() -> OrchardAddress { let (_, _, address) = spend_keys(); OrchardAddress::from_raw_bytes(&address.to_raw_address_bytes()) .expect("valid orchard address bytes") } - fn spendable(note: Note, merkle_path: MerklePath) -> SpendableNote { + pub(super) fn spendable(note: Note, merkle_path: MerklePath) -> SpendableNote { SpendableNote { note, merkle_path } } /// Puts a `CREDIT_NOTE` note of the test wallet into the credit pool: its anchor is /// recorded and the pool total covers it. - fn fund_credit_pool( + pub(super) fn fund_credit_pool( platform: &TempPlatform, tag: u8, ) -> (Note, Anchor, MerklePath) { @@ -3511,7 +3511,7 @@ mod token_pool_paid_transitions_tests { (note, anchor, merkle_path) } - fn credit_pool_balance(platform: &TempPlatform) -> u64 { + pub(super) fn credit_pool_balance(platform: &TempPlatform) -> u64 { read_pool_total_balance( &platform.drive, None, @@ -4048,11 +4048,13 @@ mod token_pool_paid_transitions_tests { } /// Each of the four public builders must bind the preimage its own kind's verifier - /// rebuilds. Nothing in the types enforces that: the builders take a - /// `TokenTransitionActionType`, so they cannot pass a non-kind, but any one of them could - /// pass a *different* outputs-only kind and still compile. The failure is total and silent - /// — every honest transition of that kind rejected at every node, with the suite green — - /// so it is checked here directly, against the same verification consensus runs. + /// rebuilds. Nothing in the types enforces that: each builder names its kind as a + /// `TokenTransitionActionType` literal when it computes the preimage + /// (`token_pool_output_only_extra_sighash_data`), so it cannot name a non-kind, but any one + /// of them could name a *different* outputs-only kind and still compile. The failure is + /// total and silent — every honest transition of that kind rejected at every node, with the + /// suite green — so it is checked here directly, against the same verification consensus + /// runs. #[tokio::test] async fn test_every_outputs_only_builder_binds_the_preimage_its_verifier_rebuilds() { let platform_version = PlatformVersion::latest(); @@ -4196,3 +4198,1480 @@ mod token_pool_paid_transitions_tests { } } } + +/// An outputs-only bundle copied into the same pool, as the same kind, by somebody else. +/// +/// The sighash of such a bundle binds its kind and its token but not who submits it, so the +/// copy verifies. It would land a second note with the same commitment and the same `rho`, +/// hence the same nullifier, and only one of the two could ever be spent: a wallet counting by +/// commitment would see a payment that does not exist. The pool records each bundle's dummy +/// nullifiers when it enters and refuses a bundle whose dummy nullifier is already there. +/// +/// Every test first builds the original through the public builder and shows it admitted by +/// CheckTx and executed in a block. Without that, a refused copy would prove nothing: a bundle +/// the verifier never accepted anywhere is refused for any reason at all. The copy must then +/// fail on the original's first dummy nullifier, and nothing it carries may move. +mod token_pool_outputs_only_copy_tests { + use super::token_pool_paid_transitions_tests::{ + credit_pool_balance, fund_credit_pool, spendable, wallet_address, Prover, CREDIT_NOTE, + }; + use super::token_shielded_pool_tests::{ + assert_check_tx_accepts, assert_check_tx_rejects, assert_tokens_conserved, + build_spend_bundle, + enable_shielded_pool, identity_token_balance, nullifier_is_spent, + platform_with_latest_version, pool_balance, pool_notes_count, process, spend_keys, + OWNER_INITIAL_BALANCE, + }; + use super::*; + use crate::platform_types::state_transitions_processing_result::StateTransitionsProcessingResult; + use crate::rpc::core::MockCoreRPCLike; + use crate::test::helpers::fast_forward_to_block::fast_forward_to_block; + use crate::execution::validation::state_transition::state_transitions::test_helpers::set_pool_total_balance; + use crate::test::helpers::setup::TempPlatform; + use dpp::block::epoch::Epoch; + use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters; + use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; + use dpp::data_contract::associated_token::token_pre_programmed_distribution::v0::TokenPreProgrammedDistributionV0; + use dpp::data_contract::associated_token::token_pre_programmed_distribution::TokenPreProgrammedDistribution; + use dpp::fee::Credits; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::identity_nonce::IDENTITY_NONCE_VALUE_FILTER; + use dpp::shielded::serialized_actions_digest; + use dpp::shielded::builder::{ + build_token_claim_to_pool_transition, build_token_direct_purchase_to_pool_transition, + build_token_mint_to_pool_transition, build_token_purchase_from_shielded_pool_transition, + build_token_shield_transition, ShieldedFeePayer, + }; + use dpp::shielded::{ + compute_token_purchase_from_shielded_pool_fee, token_pool_fee_bundle_extra_sighash_data, + OrchardBundleParams, TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, + }; + use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; + use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransition; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRef; + use dpp::state_transition::batch_transition::token_claim_to_pool_transition::v0::v0_methods::TokenClaimToPoolTransitionV0Methods; + use dpp::state_transition::batch_transition::token_direct_purchase_to_pool_transition::v0::v0_methods::TokenDirectPurchaseToPoolTransitionV0Methods; + use dpp::state_transition::batch_transition::token_mint_to_pool_transition::v0::v0_methods::TokenMintToPoolTransitionV0Methods; + use dpp::state_transition::batch_transition::TokenMintToPoolTransition; + use dpp::state_transition::batch_transition::token_shield_transition::v0::v0_methods::TokenShieldTransitionV0Methods; + use dpp::state_transition::token_purchase_from_shielded_pool_transition::v0::TokenPurchaseFromShieldedPoolTransitionV0; + use dpp::state_transition::token_purchase_from_shielded_pool_transition::TokenPurchaseFromShieldedPoolTransition; + use dpp::state_transition::StateTransition; + use dpp::tokens::token_pricing_schedule::TokenPricingSchedule; + use platform_version::version::PlatformVersion; + + fn total_supply(platform: &TempPlatform, token_id: Identifier) -> u64 { + platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, PlatformVersion::latest()) + .expect("total supply") + .expect("supply exists") + } + + /// The proven bundle a builder put into an outputs-only batch token transition. + fn proven_bundle(transition: &StateTransition) -> OrchardBundleParams { + let StateTransition::Batch(batch) = transition else { + panic!("expected a batch transition"); + }; + let (actions, anchor, proof, binding_signature) = + match batch.transitions_iter().next().expect("one transition") { + BatchedTransitionRef::Token(TokenTransition::Shield(t)) => { + (t.actions(), t.anchor(), t.proof(), t.binding_signature()) + } + BatchedTransitionRef::Token(TokenTransition::MintToPool(t)) => { + (t.actions(), t.anchor(), t.proof(), t.binding_signature()) + } + BatchedTransitionRef::Token(TokenTransition::ClaimToPool(t)) => { + (t.actions(), t.anchor(), t.proof(), t.binding_signature()) + } + BatchedTransitionRef::Token(TokenTransition::DirectPurchaseToPool(t)) => { + (t.actions(), t.anchor(), t.proof(), t.binding_signature()) + } + other => panic!("not an outputs-only token pool transition: {other:?}"), + }; + OrchardBundleParams { + actions: actions.to_vec(), + anchor: *anchor, + proof: proof.to_vec(), + binding_signature: *binding_signature, + } + } + + /// The original was admitted and executed: every one of its dummy nullifiers is now in the + /// pool, which is the record a copy is checked against. + fn assert_dummy_nullifiers_recorded( + platform: &TempPlatform, + token_id: Identifier, + bundle: &OrchardBundleParams, + ) { + for action in &bundle.actions { + assert!( + nullifier_is_spent(platform, token_id, &action.nullifier), + "an outputs-only bundle's dummy nullifiers must be recorded when it enters the pool" + ); + } + } + + fn credits(platform: &TempPlatform, identity_id: Identifier) -> Credits { + platform + .drive + .fetch_identity_balance(identity_id.to_buffer(), None, PlatformVersion::latest()) + .expect("balance") + .expect("identity exists") + } + + /// Submits a batch copy and returns what the copier was charged for it. + /// + /// CheckTx admits the copy: it verifies a batch's token bundles statelessly and does not + /// read the pool, so the admission itself shows the copied bundle is valid as the copier's + /// transition. Block execution must then refuse it on the original's first dummy nullifier, + /// as a paid failure: the copier's nonce advances and its fee is charged, nothing else moves. + fn assert_copy_refused_in_block( + platform: &TempPlatform, + copy: &StateTransition, + copier_id: Identifier, + contract_id: Identifier, + original: &OrchardBundleParams, + block_info: Option<&BlockInfo>, + ) -> Credits { + assert_check_tx_accepts(platform, copy); + let credits_before = credits(platform, copier_id); + let result = match block_info { + Some(block_info) => process_at(platform, copy, block_info), + None => process(platform, copy), + }; + let first = original.actions[0].nullifier; + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::NullifierAlreadySpentError(error)), + .. + }] if error.nullifier() == first + ); + let nonce = platform + .drive + .fetch_identity_contract_nonce( + copier_id.to_buffer(), + contract_id.to_buffer(), + true, + None, + PlatformVersion::latest(), + ) + .expect("identity contract nonce") + .expect("the refused copy bumps the copier's nonce"); + assert_eq!(nonce & IDENTITY_NONCE_VALUE_FILTER, 1); + let charged = credits_before - credits(platform, copier_id); + assert!(charged > 0, "a refused copy is a paid failure"); + charged + } + + /// Processes one transition at `block_info`, for a claim whose release has to be due. + fn process_at( + platform: &TempPlatform, + transition: &StateTransition, + block_info: &BlockInfo, + ) -> StateTransitionsProcessingResult { + process_in_one_block(platform, &[transition], block_info) + } + + /// Processes `transitions` in order in one block transaction, then commits it: each is + /// validated against what the ones before it wrote, not only against committed state. + fn process_in_one_block( + platform: &TempPlatform, + transitions: &[&StateTransition], + block_info: &BlockInfo, + ) -> StateTransitionsProcessingResult { + let platform_version = PlatformVersion::latest(); + let platform_state = platform.state.load(); + let serialized: Vec> = transitions + .iter() + .map(|transition| transition.serialize_to_bytes().expect("serialize")) + .collect(); + let transaction = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &serialized, + &platform_state, + block_info, + &transaction, + platform_version, + false, + None, + ) + .expect("process state transition"); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("commit"); + result + } + + #[tokio::test] + async fn test_a_shield_bundle_copied_into_the_same_pool_by_another_holder_is_refused() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9501); + let amount = 4_200; + + let (owner, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (copier, copier_signer, copier_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + owner.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + + // The copier holds as many tokens as the copy shields, so a refusal cannot be its + // balance. + let transfer = BatchTransition::new_token_transfer_transition( + token_id, + owner.id(), + contract.id(), + 0, + amount, + copier.id(), + None, + None, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token transfer transition"); + assert_matches!( + process(&platform, &transfer).execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + let shield = build_token_shield_transition( + token_id, + owner.id(), + contract.id(), + 0, + &wallet_address(), + amount, + [0u8; 36], + None, + &key, + 3, + 0, + &signer, + &Prover, + platform_version, + ) + .await + .expect("client-built token shield"); + assert_check_tx_accepts(&platform, &shield); + assert_matches!( + process(&platform, &shield).execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!( + identity_token_balance(&platform, token_id, owner.id()), + Some(OWNER_INITIAL_BALANCE - 2 * amount) + ); + let bundle = proven_bundle(&shield); + assert_dummy_nullifiers_recorded(&platform, token_id, &bundle); + let notes_after_original = pool_notes_count(&platform, token_id); + + let copy = BatchTransition::new_token_shield_transition( + token_id, + copier.id(), + contract.id(), + 0, + amount, + bundle.clone(), + &copier_key, + 1, + 0, + &copier_signer, + platform_version, + None, + ) + .await + .expect("token shield transition"); + assert_copy_refused_in_block(&platform, ©, copier.id(), contract.id(), &bundle, None); + + assert_eq!(pool_balance(&platform, token_id), amount); + assert_eq!(pool_notes_count(&platform, token_id), notes_after_original); + assert_eq!( + identity_token_balance(&platform, token_id, copier.id()), + Some(amount) + ); + + // A pool that already holds recorded dummy nullifiers still takes a bundle of its own: + // what is refused is the repeat, not every bundle after the first. + let own_shield = build_token_shield_transition( + token_id, + copier.id(), + contract.id(), + 0, + &wallet_address(), + amount, + [0u8; 36], + None, + &copier_key, + 2, + 0, + &copier_signer, + &Prover, + platform_version, + ) + .await + .expect("client-built token shield"); + assert_check_tx_accepts(&platform, &own_shield); + assert_matches!( + process(&platform, &own_shield) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(&platform, token_id), 2 * amount); + assert_dummy_nullifiers_recorded(&platform, token_id, &proven_bundle(&own_shield)); + assert_tokens_conserved(&platform); + } + + /// A copy lifted from the mempool reaches the same block as its original. CheckTx admits + /// both, since neither has entered the pool yet, so what refuses the copy is the check + /// reading the original's write in the block's own transaction. + #[tokio::test] + async fn test_a_shield_bundle_and_its_copy_in_one_block_land_only_once() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9508); + let amount = 3_100; + + let (owner, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (copier, copier_signer, copier_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + owner.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + + // The copier holds as many tokens as the copy shields, so a refusal cannot be its + // balance. + let transfer = BatchTransition::new_token_transfer_transition( + token_id, + owner.id(), + contract.id(), + 0, + amount, + copier.id(), + None, + None, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token transfer transition"); + assert_matches!( + process(&platform, &transfer).execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + let shield = build_token_shield_transition( + token_id, + owner.id(), + contract.id(), + 0, + &wallet_address(), + amount, + [0u8; 36], + None, + &key, + 3, + 0, + &signer, + &Prover, + platform_version, + ) + .await + .expect("client-built token shield"); + let bundle = proven_bundle(&shield); + let copy = BatchTransition::new_token_shield_transition( + token_id, + copier.id(), + contract.id(), + 0, + amount, + bundle.clone(), + &copier_key, + 1, + 0, + &copier_signer, + platform_version, + None, + ) + .await + .expect("token shield transition"); + assert_check_tx_accepts(&platform, &shield); + assert_check_tx_accepts(&platform, ©); + + let first = bundle.actions[0].nullifier; + assert_matches!( + process_in_one_block(&platform, &[&shield, ©], &BlockInfo::default()) + .execution_results() + .as_slice(), + [ + StateTransitionExecutionResult::SuccessfulExecution { .. }, + StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::NullifierAlreadySpentError(error)), + .. + }, + ] if error.nullifier() == first + ); + + assert_dummy_nullifiers_recorded(&platform, token_id, &bundle); + assert_eq!(pool_balance(&platform, token_id), amount); + assert_eq!( + pool_notes_count(&platform, token_id), + bundle.actions.len() as u64 + ); + assert_eq!( + identity_token_balance(&platform, token_id, owner.id()), + Some(OWNER_INITIAL_BALANCE - 2 * amount) + ); + assert_eq!( + identity_token_balance(&platform, token_id, copier.id()), + Some(amount) + ); + assert_tokens_conserved(&platform); + } + + #[tokio::test] + async fn test_a_mint_to_pool_bundle_copied_by_another_minter_is_refused() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9502); + let amount = 1_337; + + let (minter, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (copier, copier_signer, copier_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + // Either member of the group mints alone, so the copier is as entitled to mint as the + // minter and only the bundle can be why its copy is refused. + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + minter.id(), + Some(|configuration: &mut TokenConfiguration| { + enable_shielded_pool(configuration); + configuration.set_manual_minting_rules(ChangeControlRules::V0( + ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::Group(0), + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }, + )); + }), + None, + Some( + [( + 0, + Group::V0(GroupV0 { + members: [(minter.id(), 1), (copier.id(), 1)].into(), + required_power: 1, + }), + )] + .into(), + ), + None, + platform_version, + ); + + let mint = build_token_mint_to_pool_transition( + token_id, + minter.id(), + contract.id(), + 0, + &wallet_address(), + amount, + [0u8; 36], + None, + None, + Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(0)), + &key, + 2, + 0, + &signer, + &Prover, + platform_version, + ) + .await + .expect("client-built token mint to pool"); + assert_check_tx_accepts(&platform, &mint); + assert_matches!( + process(&platform, &mint).execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(&platform, token_id), amount); + let bundle = proven_bundle(&mint); + assert_dummy_nullifiers_recorded(&platform, token_id, &bundle); + let notes_after_original = pool_notes_count(&platform, token_id); + + let copy = BatchTransition::new_token_mint_to_pool_transition( + token_id, + copier.id(), + contract.id(), + 0, + amount, + bundle.clone(), + None, + Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(0)), + &copier_key, + 1, + 0, + &copier_signer, + platform_version, + None, + ) + .await + .expect("token mint to pool transition"); + assert_copy_refused_in_block(&platform, ©, copier.id(), contract.id(), &bundle, None); + + assert_eq!(pool_balance(&platform, token_id), amount); + assert_eq!(pool_notes_count(&platform, token_id), notes_after_original); + assert_eq!( + total_supply(&platform, token_id), + OWNER_INITIAL_BALANCE + amount, + "a refused copy must not print supply" + ); + assert_tokens_conserved(&platform); + } + + /// A group mint carries one bundle through several signers: every confirmer resubmits the + /// proposer's bundle unchanged, so its dummy nullifiers are offered to the pool once per + /// signer. Only the signature that closes the action mints, and only it may record them; + /// were they recorded earlier, the closing signature would be refused as a copy and no + /// group mint into a pool could ever complete. + #[tokio::test] + async fn test_a_group_mint_to_pool_closes_with_the_proposers_bundle_resubmitted() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9507); + let amount = 2_024; + + let (proposer, proposer_signer, proposer_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (confirmer, confirmer_signer, confirmer_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + proposer.id(), + Some(|configuration: &mut TokenConfiguration| { + enable_shielded_pool(configuration); + configuration.set_manual_minting_rules(ChangeControlRules::V0( + ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::Group(0), + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }, + )); + }), + None, + Some( + [( + 0, + Group::V0(GroupV0 { + members: [(proposer.id(), 1), (confirmer.id(), 1)].into(), + required_power: 2, + }), + )] + .into(), + ), + None, + platform_version, + ); + + let proposer_nonce = 2; + let proposal = build_token_mint_to_pool_transition( + token_id, + proposer.id(), + contract.id(), + 0, + &wallet_address(), + amount, + [0u8; 36], + None, + None, + Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(0)), + &proposer_key, + proposer_nonce, + 0, + &proposer_signer, + &Prover, + platform_version, + ) + .await + .expect("client-built token mint to pool proposal"); + assert_check_tx_accepts(&platform, &proposal); + assert_matches!( + process(&platform, &proposal).execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + let bundle = proven_bundle(&proposal); + // One signature of two: nothing is minted and nothing is recorded yet. + assert_eq!(pool_balance(&platform, token_id), 0); + assert!(!nullifier_is_spent( + &platform, + token_id, + &bundle.actions[0].nullifier + )); + + let action_id = TokenMintToPoolTransition::calculate_action_id_with_fields( + token_id.as_bytes(), + proposer.id().as_bytes(), + proposer_nonce, + amount, + &serialized_actions_digest(&bundle.actions), + ); + let confirmation = BatchTransition::new_token_mint_to_pool_transition( + token_id, + confirmer.id(), + contract.id(), + 0, + amount, + bundle.clone(), + None, + Some( + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: 0, + action_id, + action_is_proposer: false, + }, + ), + ), + &confirmer_key, + 1, + 0, + &confirmer_signer, + platform_version, + None, + ) + .await + .expect("token mint to pool confirmation"); + assert_check_tx_accepts(&platform, &confirmation); + assert_matches!( + process(&platform, &confirmation) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(&platform, token_id), amount); + assert_eq!( + total_supply(&platform, token_id), + OWNER_INITIAL_BALANCE + amount + ); + assert_dummy_nullifiers_recorded(&platform, token_id, &bundle); + assert_tokens_conserved(&platform); + } + + /// Two group actions can carry the same bundle: each member may propose, and the group + /// action pins only the bundle's digest. Once one of them closes and mints, the signature + /// that would close the other is a copy like any other and must be refused, or the group + /// would mint the amount twice into notes of which only one of each pair could be spent. + /// Confirmers go through the check as proposers do. + #[tokio::test] + async fn test_a_second_group_mint_to_pool_carrying_a_minted_bundle_cannot_close() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9509); + let amount = 777; + + let (first, first_signer, first_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (second, second_signer, second_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (third, third_signer, third_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + first.id(), + Some(|configuration: &mut TokenConfiguration| { + enable_shielded_pool(configuration); + configuration.set_manual_minting_rules(ChangeControlRules::V0( + ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::Group(0), + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }, + )); + }), + None, + Some( + [( + 0, + Group::V0(GroupV0 { + members: [(first.id(), 1), (second.id(), 1), (third.id(), 1)].into(), + required_power: 2, + }), + )] + .into(), + ), + None, + platform_version, + ); + + let first_nonce = 2; + let first_proposal = build_token_mint_to_pool_transition( + token_id, + first.id(), + contract.id(), + 0, + &wallet_address(), + amount, + [0u8; 36], + None, + None, + Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(0)), + &first_key, + first_nonce, + 0, + &first_signer, + &Prover, + platform_version, + ) + .await + .expect("client-built token mint to pool proposal"); + assert_matches!( + process(&platform, &first_proposal) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + let bundle = proven_bundle(&first_proposal); + let digest = serialized_actions_digest(&bundle.actions); + + // A second proposal of the same amount and the same bundle, by another member. + let second_nonce = 1; + let second_proposal = BatchTransition::new_token_mint_to_pool_transition( + token_id, + second.id(), + contract.id(), + 0, + amount, + bundle.clone(), + None, + Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(0)), + &second_key, + second_nonce, + 0, + &second_signer, + platform_version, + None, + ) + .await + .expect("token mint to pool proposal"); + assert_matches!( + process(&platform, &second_proposal) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(&platform, token_id), 0); + + let confirmation = |proposer: Identifier, proposer_nonce| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: 0, + action_id: TokenMintToPoolTransition::calculate_action_id_with_fields( + token_id.as_bytes(), + proposer.as_bytes(), + proposer_nonce, + amount, + &digest, + ), + action_is_proposer: false, + }, + ) + }; + + // The third member closes the second proposal: it mints and records the bundle. + let close_second = BatchTransition::new_token_mint_to_pool_transition( + token_id, + third.id(), + contract.id(), + 0, + amount, + bundle.clone(), + None, + Some(confirmation(second.id(), second_nonce)), + &third_key, + 1, + 0, + &third_signer, + platform_version, + None, + ) + .await + .expect("token mint to pool confirmation"); + assert_matches!( + process(&platform, &close_second) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(&platform, token_id), amount); + assert_dummy_nullifiers_recorded(&platform, token_id, &bundle); + let notes_after_mint = pool_notes_count(&platform, token_id); + + // The second member's signature would close the first proposal with the same bundle. + let close_first = BatchTransition::new_token_mint_to_pool_transition( + token_id, + second.id(), + contract.id(), + 0, + amount, + bundle.clone(), + None, + Some(confirmation(first.id(), first_nonce)), + &second_key, + 2, + 0, + &second_signer, + platform_version, + None, + ) + .await + .expect("token mint to pool confirmation"); + assert_check_tx_accepts(&platform, &close_first); + let first_nullifier = bundle.actions[0].nullifier; + assert_matches!( + process(&platform, &close_first) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::NullifierAlreadySpentError(error)), + .. + }] if error.nullifier() == first_nullifier + ); + + assert_eq!(pool_balance(&platform, token_id), amount); + assert_eq!(pool_notes_count(&platform, token_id), notes_after_mint); + assert_eq!( + total_supply(&platform, token_id), + OWNER_INITIAL_BALANCE + amount, + "the group must mint the bundle once" + ); + assert_tokens_conserved(&platform); + } + + #[tokio::test] + async fn test_a_claim_to_pool_bundle_copied_by_another_claimant_is_refused() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9503); + let release = 445; + + let (owner, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (claimant, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (copier, copier_signer, copier_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (claimant_id, copier_id) = (claimant.id(), copier.id()); + // Both are due the same release, so the copier's claim resolves to exactly the amount + // the copied bundle carries. + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + owner.id(), + Some(move |configuration: &mut TokenConfiguration| { + configuration.set_has_shielded_pool(true); + configuration + .distribution_rules_mut() + .set_pre_programmed_distribution(Some(TokenPreProgrammedDistribution::V0( + TokenPreProgrammedDistributionV0 { + distributions: [( + 100, + [(claimant_id, release), (copier_id, release)].into(), + )] + .into(), + }, + ))); + }), + None, + None, + None, + platform_version, + ); + + fast_forward_to_block(&platform, 100, 40, 42, 1, false); + let block_info = BlockInfo { + time_ms: 200, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }; + + let claim = build_token_claim_to_pool_transition( + token_id, + claimant_id, + contract.id(), + 0, + &wallet_address(), + release, + TokenDistributionType::PreProgrammed, + None, + [0u8; 36], + None, + None, + &key, + 2, + 0, + &signer, + &Prover, + platform_version, + ) + .await + .expect("client-built token claim to pool"); + assert_check_tx_accepts(&platform, &claim); + assert_matches!( + process_at(&platform, &claim, &block_info) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(&platform, token_id), release); + let bundle = proven_bundle(&claim); + assert_dummy_nullifiers_recorded(&platform, token_id, &bundle); + let notes_after_original = pool_notes_count(&platform, token_id); + + let copy = BatchTransition::new_token_claim_to_pool_transition( + token_id, + copier_id, + contract.id(), + 0, + TokenDistributionType::PreProgrammed, + None, + bundle.clone(), + None, + &copier_key, + 1, + 0, + &copier_signer, + platform_version, + None, + ) + .await + .expect("token claim to pool transition"); + assert_copy_refused_in_block( + &platform, + ©, + copier_id, + contract.id(), + &bundle, + Some(&block_info), + ); + assert_eq!(pool_balance(&platform, token_id), release); + assert_eq!(pool_notes_count(&platform, token_id), notes_after_original); + assert_eq!( + total_supply(&platform, token_id), + OWNER_INITIAL_BALANCE + release + ); + + // The copier was due the release all along and can still take it with a bundle of its + // own: what was refused is the copied bundle, not the claim. + let own_claim = build_token_claim_to_pool_transition( + token_id, + copier_id, + contract.id(), + 0, + &wallet_address(), + release, + TokenDistributionType::PreProgrammed, + None, + [0u8; 36], + None, + None, + &copier_key, + 2, + 0, + &copier_signer, + &Prover, + platform_version, + ) + .await + .expect("client-built token claim to pool"); + assert_check_tx_accepts(&platform, &own_claim); + assert_matches!( + process_at(&platform, &own_claim, &block_info) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(&platform, token_id), 2 * release); + assert_eq!( + total_supply(&platform, token_id), + OWNER_INITIAL_BALANCE + 2 * release + ); + assert_tokens_conserved(&platform); + } + + #[tokio::test] + async fn test_a_direct_purchase_to_pool_bundle_copied_by_another_buyer_is_refused() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9504); + let (token_count, price) = (3, dash_to_credits!(0.03)); + + let (seller, seller_signer, seller_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (buyer, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(1.0)); + let (copier, copier_signer, copier_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(1.0)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + seller.id(), + Some(|configuration: &mut TokenConfiguration| { + configuration.set_has_shielded_pool(true); + configuration + .distribution_rules_mut() + .set_change_direct_purchase_pricing_rules(ChangeControlRules::V0( + ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::ContractOwner, + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }, + )); + }), + None, + None, + None, + platform_version, + ); + let set_price = BatchTransition::new_token_change_direct_purchase_price_transition( + token_id, + seller.id(), + contract.id(), + 0, + Some(TokenPricingSchedule::SinglePrice(dash_to_credits!(0.01))), + None, + None, + &seller_key, + 2, + 0, + &seller_signer, + platform_version, + None, + ) + .await + .expect("set price transition"); + assert_matches!( + process(&platform, &set_price) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + let seller_credits_before = credits(&platform, seller.id()); + let purchase = build_token_direct_purchase_to_pool_transition( + token_id, + buyer.id(), + contract.id(), + 0, + &wallet_address(), + token_count, + price, + [0u8; 36], + None, + &key, + 1, + 0, + &signer, + &Prover, + platform_version, + ) + .await + .expect("client-built token direct purchase to pool"); + assert_check_tx_accepts(&platform, &purchase); + assert_matches!( + process(&platform, &purchase).execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(&platform, token_id), token_count); + let bundle = proven_bundle(&purchase); + assert_dummy_nullifiers_recorded(&platform, token_id, &bundle); + let notes_after_original = pool_notes_count(&platform, token_id); + let seller_credits_after_original = credits(&platform, seller.id()); + assert_eq!(seller_credits_after_original, seller_credits_before + price); + + let copy = BatchTransition::new_token_direct_purchase_to_pool_transition( + token_id, + copier.id(), + contract.id(), + 0, + token_count, + price, + bundle.clone(), + &copier_key, + 1, + 0, + &copier_signer, + platform_version, + None, + ) + .await + .expect("token direct purchase to pool transition"); + let charged = assert_copy_refused_in_block( + &platform, + ©, + copier.id(), + contract.id(), + &bundle, + None, + ); + assert!( + charged < price, + "a refused copy costs its fee, never the price" + ); + + assert_eq!(pool_balance(&platform, token_id), token_count); + assert_eq!(pool_notes_count(&platform, token_id), notes_after_original); + assert_eq!( + total_supply(&platform, token_id), + OWNER_INITIAL_BALANCE + token_count + ); + assert_eq!( + credits(&platform, seller.id()), + seller_credits_after_original, + "a refused copy must not pay the seller" + ); + assert_tokens_conserved(&platform); + } + + /// A token that owns a pool, sold directly at `price_per_token` by its contract owner. + /// Returns the seller's, the contract's and the token's ids. + async fn token_for_sale( + platform: &mut TempPlatform, + seller_seed: u64, + price_per_token: Credits, + ) -> (Identifier, Identifier, Identifier) { + let platform_version = PlatformVersion::latest(); + let (seller, seller_signer, seller_key) = + setup_identity(platform, seller_seed, dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + platform, + seller.id(), + Some(|configuration: &mut TokenConfiguration| { + configuration.set_has_shielded_pool(true); + configuration + .distribution_rules_mut() + .set_change_direct_purchase_pricing_rules(ChangeControlRules::V0( + ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::ContractOwner, + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }, + )); + }), + None, + None, + None, + platform_version, + ); + let set_price = BatchTransition::new_token_change_direct_purchase_price_transition( + token_id, + seller.id(), + contract.id(), + 0, + Some(TokenPricingSchedule::SinglePrice(price_per_token)), + None, + None, + &seller_key, + 2, + 0, + &seller_signer, + platform_version, + None, + ) + .await + .expect("set price transition"); + assert_matches!( + process(platform, &set_price).execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + (seller.id(), contract.id(), token_id) + } + + /// A client-built purchase of `token_count` tokens for `price`, paid from a fresh credit + /// pool note (`tag`). Returns the transition and the fee it carries. + fn build_purchase( + platform: &TempPlatform, + token_id: Identifier, + contract_id: Identifier, + token_count: u64, + price: Credits, + tag: u8, + ) -> (StateTransition, Credits) { + let (fvk, ask, _) = spend_keys(); + let address = wallet_address(); + let (credit_note, credit_anchor, credit_path) = fund_credit_pool(platform, tag); + build_token_purchase_from_shielded_pool_transition( + token_id, + contract_id, + 0, + &address, + &fvk, + token_count, + price, + [0u8; 36], + ShieldedFeePayer { + spends: vec![spendable(credit_note, credit_path)], + change_address: &address, + fvk: &fvk, + ask: &ask, + anchor: credit_anchor, + }, + &Prover, + PlatformVersion::latest(), + ) + .expect("build transition") + } + + fn purchase_v0(purchase: &StateTransition) -> &TokenPurchaseFromShieldedPoolTransitionV0 { + let StateTransition::TokenPurchaseFromShieldedPool( + TokenPurchaseFromShieldedPoolTransition::V0(v0), + ) = purchase + else { + panic!("expected a token purchase from the shielded pool"); + }; + v0 + } + + /// The original's token bundle verbatim, paid for by another payer: a fresh fee bundle + /// spending another credit note (`tag`), bound to the original's token actions and carrying + /// the same price and fee out of the credit pool. Returns the copy and its fee nullifier. + fn purchase_copy_with_another_fee_bundle( + platform: &TempPlatform, + original: &TokenPurchaseFromShieldedPoolTransitionV0, + price: Credits, + fee: Credits, + tag: u8, + seed: u64, + ) -> (StateTransition, [u8; 32]) { + let platform_version = PlatformVersion::latest(); + let (other_note, other_anchor, other_path) = fund_credit_pool(platform, tag); + let fee_extra = token_pool_fee_bundle_extra_sighash_data( + TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, + &original.token_id.to_buffer(), + &original.token_actions, + platform_version, + ) + .expect("fee bundle sighash data"); + let credits_leaving = price + fee; + let (fee_bundle, fee_value_balance) = build_spend_bundle( + other_note, + other_path, + other_anchor, + credits_leaving, + &fee_extra, + seed, + ); + assert_eq!(fee_value_balance, credits_leaving as i64); + assert_eq!( + compute_token_purchase_from_shielded_pool_fee( + original.token_actions.len(), + fee_bundle.actions.len(), + platform_version, + ) + .expect("fee"), + fee, + "the copy must pay exactly the fee the original paid" + ); + let copy_fee_nullifier = fee_bundle.actions[0].nullifier; + let mut copy = original.clone(); + copy.fee_actions = fee_bundle.actions; + copy.fee_anchor = fee_bundle.anchor; + copy.fee_proof = fee_bundle.proof; + copy.fee_binding_signature = fee_bundle.binding_signature; + ( + StateTransition::TokenPurchaseFromShieldedPool( + TokenPurchaseFromShieldedPoolTransition::V0(copy), + ), + copy_fee_nullifier, + ) + } + + /// The identity-less purchase (state transition type 28) mints into the pool through the + /// same drive operation as a batch mint, and its token bundle is outputs-only too. Its + /// sighash binds the token, the count and the price but no payer, and its fee bundle binds + /// only the type, the token and a digest of the token bundle's actions — which anybody can + /// reproduce with credit notes of their own. So the copy here keeps the original's token + /// bundle verbatim and pays for it with a fresh fee bundle from another note. + #[tokio::test] + async fn test_a_purchase_from_shielded_pool_token_bundle_copied_with_another_fee_bundle_is_refused( + ) { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let price_per_token = dash_to_credits!(0.01); + let (seller_id, contract_id, token_id) = + token_for_sale(&mut platform, 9505, price_per_token).await; + let token_count = 3; + let price = price_per_token * token_count; + + let (purchase, fee) = + build_purchase(&platform, token_id, contract_id, token_count, price, 15); + assert_check_tx_accepts(&platform, &purchase); + assert_matches!( + process(&platform, &purchase).execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(&platform, token_id), token_count); + assert_eq!(credit_pool_balance(&platform), CREDIT_NOTE - price - fee); + + let original = purchase_v0(&purchase); + let token_bundle = OrchardBundleParams { + actions: original.token_actions.clone(), + anchor: original.token_anchor, + proof: original.token_proof.clone(), + binding_signature: original.token_binding_signature, + }; + assert_dummy_nullifiers_recorded(&platform, token_id, &token_bundle); + let notes_after_original = pool_notes_count(&platform, token_id); + + let (copy, copy_fee_nullifier) = + purchase_copy_with_another_fee_bundle(&platform, original, price, fee, 16, 9506); + + let seller_credits_after_original = credits(&platform, seller_id); + let credit_pool_before_copy = credit_pool_balance(&platform); + let first = token_bundle.actions[0].nullifier; + + // Unlike a batch, this transition's pools are checked at admission, so the copy never + // reaches the mempool. A proposer can still put it in a block, where it must fail too. + assert_matches!( + assert_check_tx_rejects(&platform, ©).as_slice(), + [ConsensusError::StateError(StateError::NullifierAlreadySpentError(error))] + if error.nullifier() == first + ); + assert_matches!( + process(&platform, ©).execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::StateError(StateError::NullifierAlreadySpentError(error)) + )] if error.nullifier() == first + ); + + assert_eq!(pool_balance(&platform, token_id), token_count); + assert_eq!(pool_notes_count(&platform, token_id), notes_after_original); + assert_eq!( + total_supply(&platform, token_id), + OWNER_INITIAL_BALANCE + token_count + ); + assert_eq!( + credit_pool_balance(&platform), + credit_pool_before_copy, + "a refused copy must not spend the payer's credit note" + ); + assert!( + !platform + .drive + .has_nullifier(©_fee_nullifier, None, &mut vec![], platform_version) + .expect("credit pool nullifier lookup"), + "a refused copy must leave the payer's credit note unspent" + ); + assert_eq!( + credits(&platform, seller_id), + seller_credits_after_original, + "a refused copy must not pay the seller" + ); + assert_tokens_conserved(&platform); + } + + /// The type 28 copy, lifted from the mempool before its original executed, reaches the + /// same block. CheckTx admits both, since neither token bundle is in the pool yet; the + /// block must land the original and refuse the copy on the original's write in the block's + /// own transaction. + #[tokio::test] + async fn test_a_purchase_from_shielded_pool_and_its_copy_in_one_block_land_only_once() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let price_per_token = dash_to_credits!(0.01); + let (seller_id, contract_id, token_id) = + token_for_sale(&mut platform, 9510, price_per_token).await; + let token_count = 3; + let price = price_per_token * token_count; + + let (purchase, fee) = + build_purchase(&platform, token_id, contract_id, token_count, price, 15); + let original = purchase_v0(&purchase); + let (copy, copy_fee_nullifier) = + purchase_copy_with_another_fee_bundle(&platform, original, price, fee, 16, 9511); + // Both payers' notes are in the credit pool and its total covers both purchases, so + // only the token bundle can tell the two apart. + set_pool_total_balance(&platform, 2 * CREDIT_NOTE); + assert_check_tx_accepts(&platform, &purchase); + assert_check_tx_accepts(&platform, ©); + + let seller_credits_before = credits(&platform, seller_id); + let first = original.token_actions[0].nullifier; + assert_matches!( + process_in_one_block(&platform, &[&purchase, ©], &BlockInfo::default()) + .execution_results() + .as_slice(), + [ + StateTransitionExecutionResult::SuccessfulExecution { .. }, + StateTransitionExecutionResult::UnpaidConsensusError(ConsensusError::StateError( + StateError::NullifierAlreadySpentError(error) + )), + ] if error.nullifier() == first + ); + + assert_eq!(pool_balance(&platform, token_id), token_count); + assert_eq!( + pool_notes_count(&platform, token_id), + original.token_actions.len() as u64 + ); + assert_eq!( + total_supply(&platform, token_id), + OWNER_INITIAL_BALANCE + token_count + ); + assert_eq!( + credit_pool_balance(&platform), + 2 * CREDIT_NOTE - price - fee + ); + assert!( + !platform + .drive + .has_nullifier(©_fee_nullifier, None, &mut vec![], platform_version) + .expect("credit pool nullifier lookup"), + "a refused copy must leave the payer's credit note unspent" + ); + assert_eq!( + credits(&platform, seller_id), + seller_credits_before + price, + "the seller is paid once" + ); + assert_tokens_conserved(&platform); + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_pool_paid_common/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_pool_paid_common/mod.rs index 3728545c48e..7d2e057f86c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_pool_paid_common/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_pool_paid_common/mod.rs @@ -135,6 +135,37 @@ pub(crate) fn validate_token_pool_spend( Ok(None) } +/// An outputs-only token bundle entering the token pool: its dummy nullifiers must not repeat +/// within the bundle or already be recorded there. Returns the rejection, if any. +/// +/// The bundle binds nobody, so its authorized bytes can be put into a second transition of the +/// same kind with another fee bundle. That copy would land notes with the same commitments and +/// the same nullifiers as the original's, of which only one of each could ever be spent. The +/// pool records these nullifiers when the bundle enters; this is what refuses the copy. +pub(crate) fn validate_token_pool_outputs( + drive: &Drive, + token_id: Identifier, + nullifiers: &[[u8; 32]], + transaction: TransactionArg, + platform_version: &PlatformVersion, +) -> Result>, Error> { + let mut drive_operations = vec![]; + let result = validate_token_pool_nullifiers( + drive, + &token_id.to_buffer(), + nullifiers, + transaction, + &mut drive_operations, + platform_version, + )?; + if !result.is_valid() { + return Ok(Some(ConsensusValidationResult::new_with_errors( + result.errors, + ))); + } + Ok(None) +} + /// The credit pool side of the fee bundle: anchor recorded, nullifiers unspent and the pool /// holding `credits_leaving`. Returns the pool's current total on success. pub(crate) fn validate_credit_pool_fee_spend( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs index 35443ec10aa..1080f82e1dd 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs @@ -1,6 +1,6 @@ use crate::error::Error; use crate::execution::validation::state_transition::state_transitions::token_pool_paid_common::{ - resolve_pooled_token, validate_credit_pool_fee_spend, + resolve_pooled_token, validate_credit_pool_fee_spend, validate_token_pool_outputs, }; use dpp::consensus::basic::state_transition::ShieldedInvalidValueBalanceError; use dpp::consensus::basic::BasicError; @@ -31,9 +31,10 @@ impl TokenPurchaseFromShieldedPoolStateTransitionTransformIntoActionValidationV0 for TokenPurchaseFromShieldedPoolTransition { /// Both Orchard proofs and the fee floor were checked by the processor. Here the token - /// must own a pool and not be paused, the token bundle must spend recorded and unspent - /// notes of that pool, the fee bundle must spend recorded and unspent notes of the credit - /// pool which must hold what leaves it, and the transition's own rules apply. + /// must own a pool and not be paused, the outputs-only token bundle's dummy nullifiers + /// must not already be recorded in that pool, the fee bundle must spend recorded and + /// unspent notes of the credit pool which must hold what leaves it, and the transition's + /// own rules apply. fn transform_into_action_v0( &self, drive: &Drive, @@ -114,6 +115,17 @@ impl TokenPurchaseFromShieldedPoolStateTransitionTransformIntoActionValidationV0 .into(), )); }; + let token_nullifiers: Vec<[u8; 32]> = + v0.token_actions.iter().map(|a| a.nullifier).collect(); + if let Some(rejection) = validate_token_pool_outputs( + drive, + v0.token_id, + &token_nullifiers, + transaction, + platform_version, + )? { + return Ok(rejection); + } let fee_nullifiers: Vec<[u8; 32]> = v0.fee_actions.iter().map(|a| a.nullifier).collect(); let current_credit_pool_balance = match validate_credit_pool_fee_spend( drive, diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/mint_to_pool/v0/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/mint_to_pool/v0/mod.rs index f49eb9af6a9..216c1057279 100644 --- a/packages/rs-drive/src/drive/tokens/shielded_pool/mint_to_pool/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/mint_to_pool/v0/mod.rs @@ -10,7 +10,8 @@ use grovedb::{EstimatedLayerInformation, TransactionArg}; use std::collections::HashMap; impl Drive { - /// Version 0: raise the total supply, then append the notes and credit the pool balance. + /// Version 0: raise the total supply, then record the dummy nullifiers, append the notes + /// and credit the pool balance. /// Conservation holds because the supply and the pool grow by the same amount. #[allow(clippy::too_many_arguments)] pub(super) fn token_mint_to_pool_operations_v0( @@ -35,11 +36,15 @@ impl Drive { platform_version, )?; - // An outputs-only bundle spends nothing: its dummy nullifiers are never recorded. + // An outputs-only bundle spends nothing, but its dummy nullifiers are recorded all the + // same. Each note takes its `rho` from its action's dummy nullifier, so the same bundle + // entering this pool again would land notes with the same commitments and nullifiers; + // the record is what lets validation refuse it. + let dummy_nullifiers: Vec<[u8; 32]> = notes.iter().map(|note| note.nullifier).collect(); drive_operations.extend(self.token_shielded_pool_update_operations( token_id, TokenPoolBalanceChange::Add(amount), - &[], + &dummy_nullifiers, notes, estimated_costs_only_with_layer_info, transaction, diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs index 01067c351aa..ec69ec27ab9 100644 --- a/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs @@ -380,8 +380,10 @@ mod tests { assert_eq!(token_balance(&drive, identity_id), Some(600)); assert_eq!(pool_balance(&drive), 400); assert_eq!(notes_count(&drive), 2); - // Outputs-only bundles never spend: their dummy nullifiers are not recorded. - assert!(!nullifier_is_spent(&drive, ¬es[0].nullifier)); + // An outputs-only bundle spends nothing, yet its dummy nullifiers are recorded: they are + // what lets validation refuse the same bundle entering this pool again. + assert!(nullifier_is_spent(&drive, ¬es[0].nullifier)); + assert!(nullifier_is_spent(&drive, ¬es[1].nullifier)); } #[test] @@ -524,12 +526,13 @@ mod tests { let (drive, identity_id) = setup(1_000); let platform_version = PlatformVersion::latest(); + let notes = [note(1), note(2)]; let operations = drive .token_mint_to_pool_operations( TOKEN_ID, 250, false, - &[note(1), note(2)], + ¬es, &mut None, None, platform_version, @@ -547,6 +550,9 @@ mod tests { ); // The minter's own balance is not involved. assert_eq!(token_balance(&drive, identity_id), Some(1_000)); + // The bundle's dummy nullifiers are recorded, as for a shield. + assert!(nullifier_is_spent(&drive, ¬es[0].nullifier)); + assert!(nullifier_is_spent(&drive, ¬es[1].nullifier)); assert!(drive .has_token_shielded_pool(TOKEN_ID, None, &mut vec![], platform_version) .expect("pool lookup")); diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/shield/v0/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/shield/v0/mod.rs index 5e77312f960..e0109e45ab4 100644 --- a/packages/rs-drive/src/drive/tokens/shielded_pool/shield/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/shield/v0/mod.rs @@ -10,7 +10,8 @@ use grovedb::{EstimatedLayerInformation, TransactionArg}; use std::collections::HashMap; impl Drive { - /// Version 0: debit the identity, then append the notes and credit the pool balance. + /// Version 0: debit the identity, then record the dummy nullifiers, append the notes and + /// credit the pool balance. /// The token's total supply is unchanged: the pool balance is a conservation-side term. #[allow(clippy::too_many_arguments)] pub(super) fn token_shield_operations_v0( @@ -34,11 +35,15 @@ impl Drive { platform_version, )?; - // An outputs-only bundle spends nothing: its dummy nullifiers are never recorded. + // An outputs-only bundle spends nothing, but its dummy nullifiers are recorded all the + // same. Each note takes its `rho` from its action's dummy nullifier, so the same bundle + // entering this pool again would land notes with the same commitments and nullifiers; + // the record is what lets validation refuse it. + let dummy_nullifiers: Vec<[u8; 32]> = notes.iter().map(|note| note.nullifier).collect(); drive_operations.extend(self.token_shielded_pool_update_operations( token_id, TokenPoolBalanceChange::Add(amount), - &[], + &dummy_nullifiers, notes, estimated_costs_only_with_layer_info, transaction, diff --git a/packages/rs-drive/src/util/batch/drive_op_batch/token.rs b/packages/rs-drive/src/util/batch/drive_op_batch/token.rs index 6541e9cf0d6..f21df7c710a 100644 --- a/packages/rs-drive/src/util/batch/drive_op_batch/token.rs +++ b/packages/rs-drive/src/util/batch/drive_op_batch/token.rs @@ -142,7 +142,8 @@ pub enum TokenOperationType { identity_id: Identifier, /// The amount entering the pool amount: TokenAmount, - /// The bundle's output notes, appended to the pool's commitment tree + /// The bundle's output notes, appended to the pool's commitment tree; their dummy + /// nullifiers are recorded in the pool's nullifier set notes: Vec, }, /// Moves tokens from the token's shielded pool into an identity balance. @@ -175,7 +176,8 @@ pub enum TokenOperationType { amount: TokenAmount, /// Should we allow this to be the first ever mint allow_first_mint: bool, - /// The output notes of the bundle + /// The output notes of the bundle; their dummy nullifiers are recorded in the pool's + /// nullifier set notes: Vec, }, /// Burns tokens held in the token's shielded pool (pool balance and supply shrink). From ac0661be3aa7ba52355114cf11708ab41dfe93f1 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 24 Sep 2026 21:23:53 +0700 Subject: [PATCH 28/94] feat(platform)!: let a token set how few notes its shielded pool may be left holding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A shielded pool with very few notes offers its holders little to hide among: the smaller the anonymity set, the more an observer learns from each movement. The threshold below which an outflow is refused was a single constant for every token, which cannot be right for tokens with wildly different holder counts. It becomes a per-token field on `TokenConfigurationV1`, with the cap in `SystemLimits`. The default is 0, not the floor one might expect. A non-zero floor traps the first depositors: until enough notes exist nobody can leave, and no count of commitments guarantees distinct holders anyway, since one bundle carries up to sixteen actions and a single depositor clears a floor of ten alone. The cap of 250 is the real protection, because a threshold set absurdly high strands a pool permanently. An issuer who wants a floor opts into one deliberately. It changes through `TokenConfigUpdate` under the token's own `ChangeControlRules`, like every other token setting, so no new DataContract `validate_update` generation exists here and none is needed — a contract update that tries to change the threshold is refused by the rule that already refuses every other token config edit, which is the behaviour we want. Immutability comes free where it is wanted: the change control rules default to permitting no one, so an issuer who never opens them has a threshold nothing can move. Reaching a field that lives on `TokenConfigurationV1` outside its V0 base needed real V1 arms in the apply, permission and action-taker dispatchers, which previously delegated everything to the base and so could not see such a field at all. Those arms are exhaustive: a macro spells out all thirty-two V0 items by name, so a future item fails to compile in the V1 matches too. That property is the point — without it the natural fix at the compile error is a no-op arm in V0, and a no-op means a transition that validates, is accepted, charges a fee and changes nothing. `TokenUnshieldWithShieldedFee` is closed here as well. It is the only identity-less outflow and it checked the threshold zero times, so it was a way around the whole mechanism. The type 27 test was red with the fix removed — CheckTx admitted an unshield below the threshold — and green with it restored. Green after: dpp 4995, drive-abci token 482, drive-abci shielded 232, frozen discriminants 4, formatting clean, workspace clippy without warnings. Reviews found nothing Critical, High or Medium. BREAKING CHANGE: `TokenConfigurationV1`'s bincode layout changes in place, so any network holding contracts with pooled tokens must be reset rather than upgraded. Co-Authored-By: Claude Opus 5 (1M context) --- .../token_configuration/accessors/mod.rs | 17 +- .../token_configuration/accessors/v1/mod.rs | 8 +- .../apply_token_configuration_item/mod.rs | 32 +- .../apply_token_configuration_item/v0/mod.rs | 5 + .../mod.rs | 68 +- .../v0/mod.rs | 6 + .../can_apply_token_configuration_item/mod.rs | 81 +- .../v0/mod.rs | 4 + .../token_configuration/mod.rs | 337 +++- .../token_configuration/v0/mod.rs | 2 +- .../token_configuration/v1/accessors/mod.rs | 29 +- .../token_configuration/v1/mod.rs | 39 +- .../token_configuration_item.rs | 179 +- .../methods/validate_update/v0/mod.rs | 65 + .../validate_structure/v0/mod.rs | 18 + .../processor/traits/is_allowed.rs | 19 +- .../document_shielded_token_payment/mod.rs | 51 +- .../state_v0/mod.rs | 1 + .../state_v0/mod.rs | 18 + .../token/token_shielded_pool_common/mod.rs | 13 +- .../state_v0/mod.rs | 2 + .../token/shielded/minimum_pool_notes.rs | 1492 +++++++++++++++++ .../batch/tests/token/shielded/mod.rs | 23 +- .../transform_into_action/v0/mod.rs | 24 +- .../document_base_transition_action/mod.rs | 5 +- .../document_base_transition_action/v0/mod.rs | 18 +- .../v0/transformer.rs | 10 +- .../drive_abci_validation_versions/mod.rs | 9 +- .../src/version/mocks/v2_test.rs | 1 + .../src/version/system_limits/mod.rs | 7 + .../src/version/system_limits/v1.rs | 1 + .../src/version/system_limits/v2.rs | 1 + .../src/version/system_limits/v3.rs | 1 + .../src/version/system_limits/v4.rs | 5 + .../token_configuration_change_item.rs | 18 + 35 files changed, 2532 insertions(+), 77 deletions(-) create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/minimum_pool_notes.rs diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/mod.rs index 310cfd1a448..f8cd03fcdd7 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/mod.rs @@ -117,12 +117,18 @@ impl TokenConfigurationV0Getters for TokenConfiguration { /// Returns all group positions used in the token configuration fn all_used_group_positions(&self) -> (BTreeSet, bool) { - self.as_v0().all_used_group_positions() + match self { + TokenConfiguration::V0(v0) => v0.all_used_group_positions(), + TokenConfiguration::V1(v1) => v1.all_used_group_positions(), + } } /// Returns all the change contract rules, including those from the distribution rules fn all_change_control_rules(&self) -> Vec<(&str, &ChangeControlRules)> { - self.as_v0().all_change_control_rules() + match self { + TokenConfiguration::V0(v0) => v0.all_change_control_rules(), + TokenConfiguration::V1(v1) => v1.all_change_control_rules(), + } } /// Returns the token description. @@ -242,6 +248,13 @@ impl TokenConfigurationV1Getters for TokenConfiguration { TokenConfiguration::V1(v1) => v1.has_shielded_pool(), } } + + fn minimum_pool_notes_for_outgoing(&self) -> u64 { + match self { + TokenConfiguration::V0(_) => 0, + TokenConfiguration::V1(v1) => v1.minimum_pool_notes_for_outgoing(), + } + } } impl TokenConfigurationV1Setters for TokenConfiguration { diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/v1/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/v1/mod.rs index 4ccdffa4c18..c31da7f2b65 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/accessors/v1/mod.rs @@ -1,10 +1,14 @@ /// Getters introduced by `TokenConfigurationV1`. /// -/// Implemented for every configuration version: a V0 configuration answers `false`, since -/// the shielded pool opt-in did not exist before V1. +/// Implemented for every configuration version: a V0 configuration answers `false` and 0, +/// since neither the shielded pool opt-in nor its threshold existed before V1. pub trait TokenConfigurationV1Getters { /// Whether the token has its own shielded pool. fn has_shielded_pool(&self) -> bool; + + /// The fewest notes the token's shielded pool must hold before tokens may leave it for a + /// visible destination; 0 when the configuration sets none. + fn minimum_pool_notes_for_outgoing(&self) -> u64; } /// Setters introduced by `TokenConfigurationV1`. diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/mod.rs index ffc9186eac1..135349428c3 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/mod.rs @@ -1,5 +1,8 @@ +use crate::data_contract::associated_token::token_configuration::v1::TokenConfigurationV1; use crate::data_contract::associated_token::token_configuration::TokenConfiguration; -use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem; +use crate::data_contract::associated_token::token_configuration_item::{ + token_configuration_v0_change_items, TokenConfigurationChangeItem, +}; mod v0; @@ -13,7 +16,32 @@ impl TokenConfiguration { pub fn apply_token_configuration_item(&mut self, change_item: TokenConfigurationChangeItem) { match self { TokenConfiguration::V0(v0) => v0.apply_token_configuration_item(change_item), - TokenConfiguration::V1(v1) => v1.base.apply_token_configuration_item(change_item), + TokenConfiguration::V1(v1) => v1.apply_token_configuration_item(change_item), + } + } +} + +impl TokenConfigurationV1 { + /// Applies a `TokenConfigurationChangeItem`: the shielded pool's threshold items to this + /// configuration, every other item to the nested V0 configuration. + pub fn apply_token_configuration_item(&mut self, change_item: TokenConfigurationChangeItem) { + match change_item { + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(minimum_pool_notes) => { + self.minimum_pool_notes_for_outgoing = Some(minimum_pool_notes); + } + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup( + control_group, + ) => { + self.minimum_pool_notes_for_outgoing_change_rules + .set_authorized_to_make_change_action_takers(control_group); + } + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(admin_group) => { + self.minimum_pool_notes_for_outgoing_change_rules + .set_admin_action_takers(admin_group); + } + token_configuration_v0_change_items!() => { + self.base.apply_token_configuration_item(change_item) + } } } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/v0/mod.rs index 5e5ff7dcc86..d146de9f2ef 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/v0/mod.rs @@ -151,6 +151,11 @@ impl TokenConfigurationV0 { .trade_mode_change_rules_mut() .set_admin_action_takers(admin_group); } + // A V0 configuration has no shielded pool and so no threshold to change; + // `can_apply_token_configuration_item` refuses these items for it. + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(_) + | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup(_) + | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(_) => {} } } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/mod.rs index 1ef16659c4d..18ddc7a5b80 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/mod.rs @@ -1,5 +1,8 @@ +use crate::data_contract::associated_token::token_configuration::v1::TokenConfigurationV1; use crate::data_contract::associated_token::token_configuration::TokenConfiguration; -use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem; +use crate::data_contract::associated_token::token_configuration_item::{ + token_configuration_v0_change_items, TokenConfigurationChangeItem, +}; use crate::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; mod v0; @@ -10,8 +13,14 @@ impl TokenConfiguration { &self, change_item: &TokenConfigurationChangeItem, ) -> AuthorizedActionTakers { - self.as_v0() - .controlling_action_takers_for_configuration_item(change_item) + match self { + TokenConfiguration::V0(v0) => { + v0.controlling_action_takers_for_configuration_item(change_item) + } + TokenConfiguration::V1(v1) => { + v1.controlling_action_takers_for_configuration_item(change_item) + } + } } /// Returns the authorized action takers for a specific `TokenConfigurationChangeItem`. @@ -25,7 +34,56 @@ impl TokenConfiguration { &self, change_item: &TokenConfigurationChangeItem, ) -> AuthorizedActionTakers { - self.as_v0() - .authorized_action_takers_for_configuration_item(change_item) + match self { + TokenConfiguration::V0(v0) => { + v0.authorized_action_takers_for_configuration_item(change_item) + } + TokenConfiguration::V1(v1) => { + v1.authorized_action_takers_for_configuration_item(change_item) + } + } + } +} + +impl TokenConfigurationV1 { + /// Returns the current authority that controls a configuration item: the shielded pool's + /// threshold items are controlled like any other rule-governed item, every other item as + /// the nested V0 configuration says. + pub fn controlling_action_takers_for_configuration_item( + &self, + change_item: &TokenConfigurationChangeItem, + ) -> AuthorizedActionTakers { + match change_item { + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(_) + | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup(_) + | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(_) => { + self.authorized_action_takers_for_configuration_item(change_item) + } + token_configuration_v0_change_items!() => self + .base + .controlling_action_takers_for_configuration_item(change_item), + } + } + + /// Returns the authorized action takers for a specific `TokenConfigurationChangeItem`: the + /// shielded pool's threshold items from `minimum_pool_notes_for_outgoing_change_rules`, + /// every other item from the nested V0 configuration. + pub fn authorized_action_takers_for_configuration_item( + &self, + change_item: &TokenConfigurationChangeItem, + ) -> AuthorizedActionTakers { + let rules = &self.minimum_pool_notes_for_outgoing_change_rules; + match change_item { + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(_) => { + *rules.authorized_to_make_change_action_takers() + } + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup(_) + | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(_) => { + *rules.admin_action_takers() + } + token_configuration_v0_change_items!() => self + .base + .authorized_action_takers_for_configuration_item(change_item), + } } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/v0/mod.rs index 4f028952b3a..4bdd68490a9 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/v0/mod.rs @@ -134,6 +134,12 @@ impl TokenConfigurationV0 { .marketplace_rules .trade_mode_change_rules() .admin_action_takers(), + // A V0 configuration has no shielded pool and so no threshold for anyone to change. + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(_) + | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup(_) + | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(_) => { + AuthorizedActionTakers::NoOne + } } } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/mod.rs index b203f78bc96..c4af63982e5 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/mod.rs @@ -1,5 +1,8 @@ +use crate::data_contract::associated_token::token_configuration::v1::TokenConfigurationV1; use crate::data_contract::associated_token::token_configuration::TokenConfiguration; -use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem; +use crate::data_contract::associated_token::token_configuration_item::{ + token_configuration_v0_change_items, TokenConfigurationChangeItem, +}; use crate::data_contract::group::Group; use crate::data_contract::GroupContractPosition; use crate::group::action_taker::{ActionGoal, ActionTaker}; @@ -24,13 +27,73 @@ impl TokenConfiguration { action_taker: &ActionTaker, goal: ActionGoal, ) -> bool { - self.as_v0().can_apply_token_configuration_item( - change_item, - contract_owner_id, - main_group, - groups, - action_taker, - goal, - ) + match self { + TokenConfiguration::V0(v0) => v0.can_apply_token_configuration_item( + change_item, + contract_owner_id, + main_group, + groups, + action_taker, + goal, + ), + TokenConfiguration::V1(v1) => v1.can_apply_token_configuration_item( + change_item, + contract_owner_id, + main_group, + groups, + action_taker, + goal, + ), + } + } +} + +impl TokenConfigurationV1 { + /// Determines whether a `TokenConfigurationChangeItem` can be applied: the shielded pool's + /// threshold items under `minimum_pool_notes_for_outgoing_change_rules`, every other item + /// under the nested V0 configuration's rules. + pub fn can_apply_token_configuration_item( + &self, + change_item: &TokenConfigurationChangeItem, + contract_owner_id: &Identifier, + main_group: Option, + groups: &BTreeMap, + action_taker: &ActionTaker, + goal: ActionGoal, + ) -> bool { + let rules = &self.minimum_pool_notes_for_outgoing_change_rules; + match change_item { + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(_) => { + rules.can_make_change(contract_owner_id, main_group, groups, action_taker, goal) + } + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup( + control_group, + ) => rules.can_change_authorized_action_takers( + control_group, + contract_owner_id, + main_group, + groups, + action_taker, + goal, + ), + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(admin_group) => { + rules.can_change_admin_action_takers( + admin_group, + contract_owner_id, + main_group, + groups, + action_taker, + goal, + ) + } + token_configuration_v0_change_items!() => self.base.can_apply_token_configuration_item( + change_item, + contract_owner_id, + main_group, + groups, + action_taker, + goal, + ), + } } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/v0/mod.rs index 8fc5deebc1a..7e46417ba91 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/v0/mod.rs @@ -317,6 +317,10 @@ impl TokenConfigurationV0 { action_taker, goal, ), + // A V0 configuration has no shielded pool and so no threshold for anyone to change. + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(_) + | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup(_) + | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(_) => false, } } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs index 269d290605f..b9e2858b2f3 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs @@ -6,6 +6,7 @@ use crate::data_contract::associated_token::token_configuration::v0::TokenConfig use crate::data_contract::associated_token::token_configuration::v1::TokenConfigurationV1; use crate::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; use crate::data_contract::change_control_rules::ChangeControlRules; +use crate::data_contract::errors::DataContractError; use crate::data_contract::TokenContractPosition; #[cfg(feature = "json-conversion")] use crate::serialization::JsonConvertible; @@ -120,12 +121,53 @@ impl TokenConfiguration { } SimpleConsensusValidationResult::new() } + + /// The pool's outgoing notes threshold may not exceed + /// `max_token_pool_notes_for_outgoing`: a threshold the pool never reaches would refuse + /// every outflow and strand every shielded balance, irreversibly on a readonly contract. + /// Checked on contract create and update and on the configuration a `TokenConfigUpdate` + /// proposes. + pub fn validate_minimum_pool_notes_for_outgoing( + &self, + token_contract_position: TokenContractPosition, + platform_version: &PlatformVersion, + ) -> SimpleConsensusValidationResult { + validate_minimum_pool_notes_for_outgoing_bound( + self.minimum_pool_notes_for_outgoing(), + token_contract_position, + platform_version, + ) + } +} + +/// The bound on a token shielded pool's outgoing notes threshold: at most +/// `max_token_pool_notes_for_outgoing`, refused as `KeyWrongBounds`. Shared by the token +/// configuration validation and the `TokenConfigUpdate` that changes the threshold. +pub fn validate_minimum_pool_notes_for_outgoing_bound( + minimum_pool_notes: u64, + token_contract_position: TokenContractPosition, + platform_version: &PlatformVersion, +) -> SimpleConsensusValidationResult { + let max_minimum_pool_notes = platform_version + .system_limits + .max_token_pool_notes_for_outgoing; + if minimum_pool_notes > max_minimum_pool_notes { + return SimpleConsensusValidationResult::new_with_error( + DataContractError::KeyWrongBounds(format!( + "token at position {token_contract_position}: minimumPoolNotesForOutgoing \ + {minimum_pool_notes} is above the maximum {max_minimum_pool_notes}" + )) + .into(), + ); + } + SimpleConsensusValidationResult::new() } /// Validates every token configuration of a contract for `platform_version`: the format -/// version must be admitted and a pooled token's rules must be compatible with a pool. Returns -/// the first error. Shared by the contract create and update basic structure generations and -/// by the pre-activation gate, so the three cannot drift. +/// version must be admitted, a pooled token's rules must be compatible with a pool and its +/// outgoing notes threshold within bounds. Returns the first error. Shared by the contract +/// create and update basic structure generations and by the pre-activation gate, so the three +/// cannot drift. pub fn validate_token_configurations( tokens: &BTreeMap, platform_version: &PlatformVersion, @@ -139,6 +181,11 @@ pub fn validate_token_configurations( if !result.is_valid() { return result; } + let result = + configuration.validate_minimum_pool_notes_for_outgoing(*position, platform_version); + if !result.is_valid() { + return result; + } } SimpleConsensusValidationResult::new() } @@ -385,3 +432,287 @@ mod json_convertible_tests { } } } + +#[cfg(test)] +mod minimum_pool_notes_tests { + use super::*; + use crate::consensus::basic::BasicError; + use crate::consensus::codes::ErrorWithCode; + use crate::consensus::ConsensusError; + use crate::data_contract::associated_token::token_configuration::accessors::v0::{ + TokenConfigurationV0Getters, TokenConfigurationV0Setters, + }; + use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters; + use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem; + use crate::data_contract::change_control_rules::v0::ChangeControlRulesV0; + use crate::group::action_taker::{ActionGoal, ActionTaker}; + use crate::prelude::Identifier; + + fn pooled() -> TokenConfiguration { + let mut configuration = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + configuration.set_has_shielded_pool(true); + configuration + } + + fn pooled_with_threshold(minimum_pool_notes: u64) -> TokenConfiguration { + let mut configuration = pooled(); + let TokenConfiguration::V1(v1) = &mut configuration else { + panic!("a pooled configuration is V1"); + }; + v1.minimum_pool_notes_for_outgoing = Some(minimum_pool_notes); + configuration + } + + fn rules(authorized: AuthorizedActionTakers) -> ChangeControlRules { + ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: authorized, + admin_action_takers: authorized, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }) + } + + #[test] + fn should_read_a_configuration_without_a_threshold_as_zero() { + assert_eq!( + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()) + .minimum_pool_notes_for_outgoing(), + 0 + ); + let configuration = pooled(); + let TokenConfiguration::V1(v1) = &configuration else { + panic!("a pooled configuration is V1"); + }; + assert_eq!(v1.minimum_pool_notes_for_outgoing, None); + assert_eq!(configuration.minimum_pool_notes_for_outgoing(), 0); + // Nobody may change a threshold the issuer did not open to change. + assert_eq!( + configuration.authorized_action_takers_for_configuration_item( + &TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(1) + ), + AuthorizedActionTakers::NoOne + ); + } + + #[test] + fn should_bound_the_threshold_by_the_system_limit_with_error_10241() { + let platform_version = PlatformVersion::latest(); + let max = platform_version + .system_limits + .max_token_pool_notes_for_outgoing; + assert_eq!(max, 250); + + let at_the_limit = BTreeMap::from([(0, pooled_with_threshold(max))]); + let result = validate_token_configurations(&at_the_limit, platform_version); + assert!(result.is_valid(), "unexpected errors: {:?}", result.errors); + + let over_the_limit = BTreeMap::from([(3, pooled_with_threshold(max + 1))]); + let result = validate_token_configurations(&over_the_limit, platform_version); + assert_matches::assert_matches!( + result.errors.as_slice(), + [error @ ConsensusError::BasicError(BasicError::ContractError( + DataContractError::KeyWrongBounds(message) + ))] if error.code() == 10241 && message.contains("position 3") + ); + } + + #[test] + fn should_govern_the_threshold_by_its_own_rules_on_a_pooled_token_only() { + let owner = Identifier::from([1; 32]); + let other = Identifier::from([2; 32]); + let groups = BTreeMap::new(); + let change = TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(12); + let can_apply = |configuration: &TokenConfiguration, action_taker: Identifier| { + configuration.can_apply_token_configuration_item( + &change, + &owner, + None, + &groups, + &ActionTaker::SingleIdentity(action_taker), + ActionGoal::ActionCompletion, + ) + }; + + let mut governed = pooled(); + let TokenConfiguration::V1(v1) = &mut governed else { + panic!("a pooled configuration is V1"); + }; + v1.minimum_pool_notes_for_outgoing_change_rules = + rules(AuthorizedActionTakers::ContractOwner); + assert!(can_apply(&governed, owner)); + assert!(!can_apply(&governed, other)); + assert_eq!( + governed.controlling_action_takers_for_configuration_item(&change), + AuthorizedActionTakers::ContractOwner + ); + governed.apply_token_configuration_item(change.clone()); + assert_eq!(governed.minimum_pool_notes_for_outgoing(), 12); + + // A token without a pool has no threshold, whoever asks and whatever its other rules. + let mut unpooled = TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + unpooled.set_max_supply_change_rules(rules(AuthorizedActionTakers::ContractOwner)); + assert!(!can_apply(&unpooled, owner)); + let before = unpooled.clone(); + unpooled.apply_token_configuration_item(change); + assert_eq!(unpooled, before); + } + + /// The control item moves who may set the threshold and the admin item moves who + /// administers that, each under the threshold's own admin rule and neither touching the + /// other or any rule of the nested V0 configuration. + #[test] + fn should_route_the_threshold_control_and_admin_items_to_their_own_rules() { + let owner = Identifier::from([1; 32]); + let heir = Identifier::from([3; 32]); + let groups = BTreeMap::new(); + let can_apply = |configuration: &TokenConfiguration, + change: &TokenConfigurationChangeItem, + action_taker: Identifier| { + configuration.can_apply_token_configuration_item( + change, + &owner, + None, + &groups, + &ActionTaker::SingleIdentity(action_taker), + ActionGoal::ActionCompletion, + ) + }; + let threshold_rules = |configuration: &TokenConfiguration| { + let TokenConfiguration::V1(v1) = configuration else { + panic!("a pooled configuration is V1"); + }; + v1.minimum_pool_notes_for_outgoing_change_rules.clone() + }; + + let mut configuration = pooled(); + let TokenConfiguration::V1(v1) = &mut configuration else { + panic!("a pooled configuration is V1"); + }; + v1.minimum_pool_notes_for_outgoing_change_rules = + ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::NoOne, + admin_action_takers: AuthorizedActionTakers::ContractOwner, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: true, + }); + let max_supply_rules_before = configuration.max_supply_change_rules().clone(); + let set_threshold = TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(8); + let control = TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup( + AuthorizedActionTakers::Identity(heir), + ); + let admin = TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup( + AuthorizedActionTakers::Identity(heir), + ); + + for item in [&control, &admin] { + assert_eq!( + configuration.authorized_action_takers_for_configuration_item(item), + AuthorizedActionTakers::ContractOwner + ); + assert!(can_apply(&configuration, item, owner)); + assert!(!can_apply(&configuration, item, heir)); + } + assert!(!can_apply(&configuration, &set_threshold, owner)); + + configuration.apply_token_configuration_item(control); + let rules = threshold_rules(&configuration); + assert_eq!( + *rules.authorized_to_make_change_action_takers(), + AuthorizedActionTakers::Identity(heir) + ); + assert_eq!( + *rules.admin_action_takers(), + AuthorizedActionTakers::ContractOwner + ); + assert!(can_apply(&configuration, &set_threshold, heir)); + assert!(!can_apply(&configuration, &set_threshold, owner)); + + configuration.apply_token_configuration_item(admin); + let rules = threshold_rules(&configuration); + assert_eq!( + *rules.authorized_to_make_change_action_takers(), + AuthorizedActionTakers::Identity(heir) + ); + assert_eq!( + *rules.admin_action_takers(), + AuthorizedActionTakers::Identity(heir) + ); + assert_eq!( + configuration.max_supply_change_rules(), + &max_supply_rules_before + ); + } + + #[test] + fn should_hand_every_other_item_of_a_pooled_token_to_its_v0_rules() { + let owner = Identifier::from([1; 32]); + let groups = BTreeMap::new(); + let mut configuration = pooled(); + configuration.set_max_supply_change_rules(rules(AuthorizedActionTakers::ContractOwner)); + let change = TokenConfigurationChangeItem::MaxSupply(Some(5_000)); + assert!(configuration.can_apply_token_configuration_item( + &change, + &owner, + None, + &groups, + &ActionTaker::SingleIdentity(owner), + ActionGoal::ActionCompletion, + )); + configuration.apply_token_configuration_item(change); + assert_eq!(configuration.max_supply(), Some(5_000)); + assert!(configuration.has_shielded_pool()); + } + + #[test] + fn should_count_the_threshold_rules_among_the_tokens_rules_and_groups() { + let mut configuration = pooled(); + let TokenConfiguration::V1(v1) = &mut configuration else { + panic!("a pooled configuration is V1"); + }; + v1.minimum_pool_notes_for_outgoing_change_rules = rules(AuthorizedActionTakers::Group(4)); + let (group_positions, _) = configuration.all_used_group_positions(); + assert!(group_positions.contains(&4)); + assert!(configuration + .all_change_control_rules() + .iter() + .any( + |(name, rules)| *name == "minimum_pool_notes_for_outgoing_change_rules" + && *rules.authorized_to_make_change_action_takers() + == AuthorizedActionTakers::Group(4) + )); + } +} + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod minimum_pool_notes_json_tests { + use super::*; + use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters; + use crate::serialization::JsonConvertible; + + /// A pooled configuration written before the threshold existed, or by a client that leaves + /// it out, reads with no threshold and no one allowed to change it. + #[test] + fn should_read_a_pooled_configuration_without_the_threshold_keys_as_none() { + let mut configuration = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + configuration.set_has_shielded_pool(true); + let mut json = configuration.to_json().expect("to_json"); + let object = json.as_object_mut().expect("configuration object"); + assert!(object.remove("minimumPoolNotesForOutgoing").is_some()); + assert!(object + .remove("minimumPoolNotesForOutgoingChangeRules") + .is_some()); + + let decoded = TokenConfiguration::from_json(json).expect("from_json"); + assert_eq!(decoded, configuration); + assert_eq!(decoded.minimum_pool_notes_for_outgoing(), 0); + } +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/mod.rs index 5a7e57c97d2..6a0a4bb667a 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/mod.rs @@ -194,7 +194,7 @@ fn default_token_marketplace_rules() -> TokenMarketplaceRules { }) } -fn default_change_control_rules() -> ChangeControlRules { +pub(crate) fn default_change_control_rules() -> ChangeControlRules { ChangeControlRules::V0(ChangeControlRulesV0 { authorized_to_make_change: AuthorizedActionTakers::NoOne, admin_action_takers: AuthorizedActionTakers::NoOne, diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v1/accessors/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v1/accessors/mod.rs index 79b17a16c7d..b87f40b9e85 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v1/accessors/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v1/accessors/mod.rs @@ -97,11 +97,32 @@ impl TokenConfigurationV0Getters for TokenConfigurationV1 { } fn all_used_group_positions(&self) -> (BTreeSet, bool) { - self.base.all_used_group_positions() + let (mut group_positions, mut uses_main_group) = self.base.all_used_group_positions(); + let rules = &self.minimum_pool_notes_for_outgoing_change_rules; + for action_takers in [ + rules.authorized_to_make_change_action_takers(), + rules.admin_action_takers(), + ] { + match action_takers { + AuthorizedActionTakers::Group(group_position) => { + group_positions.insert(*group_position); + } + AuthorizedActionTakers::MainGroup => uses_main_group = true, + AuthorizedActionTakers::NoOne + | AuthorizedActionTakers::ContractOwner + | AuthorizedActionTakers::Identity(_) => {} + } + } + (group_positions, uses_main_group) } fn all_change_control_rules(&self) -> Vec<(&str, &ChangeControlRules)> { - self.base.all_change_control_rules() + let mut rules = self.base.all_change_control_rules(); + rules.push(( + "minimum_pool_notes_for_outgoing_change_rules", + &self.minimum_pool_notes_for_outgoing_change_rules, + )); + rules } fn description(&self) -> &Option { @@ -185,6 +206,10 @@ impl TokenConfigurationV1Getters for TokenConfigurationV1 { fn has_shielded_pool(&self) -> bool { self.has_shielded_pool } + + fn minimum_pool_notes_for_outgoing(&self) -> u64 { + self.minimum_pool_notes_for_outgoing.unwrap_or(0) + } } impl TokenConfigurationV1Setters for TokenConfigurationV1 { diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v1/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v1/mod.rs index a7b32af0f7a..eb5f0d9d4d1 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v1/mod.rs @@ -1,6 +1,9 @@ mod accessors; -use crate::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; +use crate::data_contract::associated_token::token_configuration::v0::{ + default_change_control_rules, TokenConfigurationV0, +}; +use crate::data_contract::change_control_rules::ChangeControlRules; #[cfg(feature = "json-conversion")] use crate::serialization::json_safe_fields; use bincode::{Decode, DecodeUntrusted, Encode}; @@ -8,12 +11,13 @@ use serde::{Deserialize, Serialize}; use std::fmt; /// Version 1 of the token configuration: everything `TokenConfigurationV0` carries, plus the -/// per-token shielded pool opt-in. +/// per-token shielded pool opt-in and the pool's outgoing notes threshold. /// /// The V0 fields are nested as `base` and flattened on the JSON / Value wire, so a V1 /// configuration reads exactly like a V0 one with `$formatVersion: "1"` and the extra -/// `hasShieldedPool` key. On the bincode wire it is the V0 bytes followed by the flag, under -/// the enum's variant index 1, so stored V0 configurations decode unchanged. +/// `hasShieldedPool`, `minimumPoolNotesForOutgoing` and `minimumPoolNotesForOutgoingChangeRules` +/// keys. On the bincode wire it is the V0 bytes followed by the flag, the threshold and its +/// rules, under the enum's variant index 1, so stored V0 configurations decode unchanged. /// /// `has_shielded_pool` is decided at token creation and is immutable afterwards: the pool /// subtree (an Orchard note commitment tree, nullifier set, anchors and a balance) is created @@ -31,6 +35,24 @@ pub struct TokenConfigurationV1 { /// and move balance back out to an identity (`TokenUnshield`). #[serde(default)] pub has_shielded_pool: bool, + /// The fewest notes the token's shielded pool must hold before tokens may leave it for a + /// visible destination (unshielding to an identity, burning from the pool, paying a + /// document's token cost from the pool). Transfers inside the pool are not limited. + /// `None` reads as 0, no threshold. + /// + /// The count is of note commitments, not of holders: one bundle carries several actions, + /// so a single depositor can reach a threshold alone. It tells holders how busy the pool + /// should be before they leave it and guarantees no anonymity set. A threshold above 0 + /// also traps a new pool's first depositors until enough notes accumulate, so the default + /// is none. At most `max_token_pool_notes_for_outgoing`, so an issuer cannot set one no + /// pool reaches and strand every shielded balance. Changed through `TokenConfigUpdate` + /// under `minimum_pool_notes_for_outgoing_change_rules`. + #[serde(default)] + pub minimum_pool_notes_for_outgoing: Option, + /// Change control rules governing who can modify `minimum_pool_notes_for_outgoing`. No one + /// when absent. + #[serde(default = "default_change_control_rules")] + pub minimum_pool_notes_for_outgoing_change_rules: ChangeControlRules, } impl TokenConfigurationV1 { @@ -39,6 +61,8 @@ impl TokenConfigurationV1 { Self { base, has_shielded_pool, + minimum_pool_notes_for_outgoing: None, + minimum_pool_notes_for_outgoing_change_rules: default_change_control_rules(), } } } @@ -47,8 +71,11 @@ impl fmt::Display for TokenConfigurationV1 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "TokenConfigurationV1 {{\n base: {},\n has_shielded_pool: {}\n}}", - self.base, self.has_shielded_pool + "TokenConfigurationV1 {{\n base: {},\n has_shielded_pool: {},\n minimum_pool_notes_for_outgoing: {:?},\n minimum_pool_notes_for_outgoing_change_rules: {:?}\n}}", + self.base, + self.has_shielded_pool, + self.minimum_pool_notes_for_outgoing, + self.minimum_pool_notes_for_outgoing_change_rules ) } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs index 0476a43c43c..0037526040d 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration_item.rs @@ -71,8 +71,56 @@ pub enum TokenConfigurationChangeItem { MarketplaceTradeModeControlGroup(AuthorizedActionTakers), MarketplaceTradeModeAdminGroup(AuthorizedActionTakers), MainControlGroup(Option), + /// The token shielded pool's outgoing notes threshold. Only a `TokenConfiguration::V1` + /// (a token with a pool) carries one, so only it can apply these three items. + MinimumPoolNotesForOutgoing(u64), + MinimumPoolNotesForOutgoingControlGroup(AuthorizedActionTakers), + MinimumPoolNotesForOutgoingAdminGroup(AuthorizedActionTakers), } +/// Every change item a `TokenConfigurationV0` carries the field for, as one pattern. A +/// `TokenConfigurationV1` hands these to its nested V0 configuration and handles its own items +/// itself; spelling the list out keeps each of its matches exhaustive, so an item added later +/// cannot fall through to the V0 handling unnoticed. Expects `TokenConfigurationChangeItem` +/// in scope. +macro_rules! token_configuration_v0_change_items { + () => { + TokenConfigurationChangeItem::TokenConfigurationNoChange + | TokenConfigurationChangeItem::Conventions(_) + | TokenConfigurationChangeItem::ConventionsControlGroup(_) + | TokenConfigurationChangeItem::ConventionsAdminGroup(_) + | TokenConfigurationChangeItem::MaxSupply(_) + | TokenConfigurationChangeItem::MaxSupplyControlGroup(_) + | TokenConfigurationChangeItem::MaxSupplyAdminGroup(_) + | TokenConfigurationChangeItem::PerpetualDistribution(_) + | TokenConfigurationChangeItem::PerpetualDistributionControlGroup(_) + | TokenConfigurationChangeItem::PerpetualDistributionAdminGroup(_) + | TokenConfigurationChangeItem::NewTokensDestinationIdentity(_) + | TokenConfigurationChangeItem::NewTokensDestinationIdentityControlGroup(_) + | TokenConfigurationChangeItem::NewTokensDestinationIdentityAdminGroup(_) + | TokenConfigurationChangeItem::MintingAllowChoosingDestination(_) + | TokenConfigurationChangeItem::MintingAllowChoosingDestinationControlGroup(_) + | TokenConfigurationChangeItem::MintingAllowChoosingDestinationAdminGroup(_) + | TokenConfigurationChangeItem::ManualMinting(_) + | TokenConfigurationChangeItem::ManualMintingAdminGroup(_) + | TokenConfigurationChangeItem::ManualBurning(_) + | TokenConfigurationChangeItem::ManualBurningAdminGroup(_) + | TokenConfigurationChangeItem::Freeze(_) + | TokenConfigurationChangeItem::FreezeAdminGroup(_) + | TokenConfigurationChangeItem::Unfreeze(_) + | TokenConfigurationChangeItem::UnfreezeAdminGroup(_) + | TokenConfigurationChangeItem::DestroyFrozenFunds(_) + | TokenConfigurationChangeItem::DestroyFrozenFundsAdminGroup(_) + | TokenConfigurationChangeItem::EmergencyAction(_) + | TokenConfigurationChangeItem::EmergencyActionAdminGroup(_) + | TokenConfigurationChangeItem::MarketplaceTradeMode(_) + | TokenConfigurationChangeItem::MarketplaceTradeModeControlGroup(_) + | TokenConfigurationChangeItem::MarketplaceTradeModeAdminGroup(_) + | TokenConfigurationChangeItem::MainControlGroup(_) + }; +} +pub(crate) use token_configuration_v0_change_items; + // Internal-`$type` serde shape via a struct-variant Repr (the outer enum mixes // AuthorizedActionTakers/struct variants with primitive/Option/unit variants // that serde cannot auto-internal-tag). Unit -> `{"$type":"..."}`; data -> @@ -151,6 +199,9 @@ token_configuration_change_item_repr! { MarketplaceTradeModeControlGroup: AuthorizedActionTakers; MarketplaceTradeModeAdminGroup: AuthorizedActionTakers; MainControlGroup: Option; + MinimumPoolNotesForOutgoing: u64, with = "crate::serialization::json_safe_u64"; + MinimumPoolNotesForOutgoingControlGroup: AuthorizedActionTakers; + MinimumPoolNotesForOutgoingAdminGroup: AuthorizedActionTakers; } impl TokenConfigurationChangeItem { @@ -184,7 +235,11 @@ impl TokenConfigurationChangeItem { | TokenConfigurationChangeItem::EmergencyAction(a) | TokenConfigurationChangeItem::EmergencyActionAdminGroup(a) | TokenConfigurationChangeItem::MarketplaceTradeModeControlGroup(a) - | TokenConfigurationChangeItem::MarketplaceTradeModeAdminGroup(a) => Some(a.to_bytes()), + | TokenConfigurationChangeItem::MarketplaceTradeModeAdminGroup(a) + | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup(a) + | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(a) => { + Some(a.to_bytes()) + } TokenConfigurationChangeItem::MaxSupply(max_supply) => { max_supply.map(|amount| amount.to_be_bytes().to_vec()) } @@ -208,6 +263,9 @@ impl TokenConfigurationChangeItem { TokenConfigurationChangeItem::MainControlGroup(position) => { position.map(|pos| pos.to_be_bytes().to_vec()) } + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(minimum_pool_notes) => { + Some(minimum_pool_notes.to_be_bytes().to_vec()) + } }) } pub fn u8_item_index(&self) -> u8 { @@ -244,6 +302,9 @@ impl TokenConfigurationChangeItem { TokenConfigurationChangeItem::MarketplaceTradeModeControlGroup(_) => 29, TokenConfigurationChangeItem::MarketplaceTradeModeAdminGroup(_) => 30, TokenConfigurationChangeItem::MainControlGroup(_) => 31, + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(_) => 32, + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup(_) => 33, + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(_) => 34, } } } @@ -376,6 +437,25 @@ impl fmt::Display for TokenConfigurationChangeItem { TokenConfigurationChangeItem::MarketplaceTradeModeAdminGroup(admin_group) => { write!(f, "Marketplace Trade Mode Admin Group: {}", admin_group) } + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(minimum_pool_notes) => { + write!(f, "Minimum Pool Notes For Outgoing: {}", minimum_pool_notes) + } + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup( + control_group, + ) => { + write!( + f, + "Minimum Pool Notes For Outgoing Control Group: {}", + control_group + ) + } + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(admin_group) => { + write!( + f, + "Minimum Pool Notes For Outgoing Admin Group: {}", + admin_group + ) + } } } } @@ -425,10 +505,64 @@ mod tests { TokenConfigurationChangeItem::MarketplaceTradeModeControlGroup(aat), TokenConfigurationChangeItem::MarketplaceTradeModeAdminGroup(aat), TokenConfigurationChangeItem::MainControlGroup(None), + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(0), + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup(aat), + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(aat), ] } - // ---- u8_item_index returns unique values 0..=31 ---- + /// `TokenConfigurationChangeItem` is encoded by variant position, so inserting a variant + /// anywhere but the end silently reassigns the bincode discriminant of every variant after + /// it: a stored group action or a transition in flight would then decode as a different + /// change. `u8_item_index` is an explicit table that a reordered declaration leaves + /// untouched, so holding every variant's discriminant to it catches a reorder anywhere. + fn discriminant_of(item: &TokenConfigurationChangeItem) -> u8 { + let bytes = bincode::encode_to_vec(item, bincode::config::standard()) + .expect("expected to encode the change item"); + // Discriminants below 251 are a single byte under bincode's varint. + bytes[0] + } + + #[test] + fn change_item_discriminants_are_frozen() { + let aat = AuthorizedActionTakers::NoOne; + assert_eq!( + discriminant_of(&TokenConfigurationChangeItem::TokenConfigurationNoChange), + 0 + ); + assert_eq!( + discriminant_of(&TokenConfigurationChangeItem::MainControlGroup(None)), + 31 + ); + assert_eq!( + discriminant_of(&TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing( + 0 + )), + 32 + ); + assert_eq!( + discriminant_of( + &TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup(aat) + ), + 33 + ); + assert_eq!( + discriminant_of( + &TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(aat) + ), + 34 + ); + for item in all_variants() { + assert_eq!( + discriminant_of(&item), + item.u8_item_index(), + "bincode discriminant of {:?} moved", + item + ); + } + } + + // ---- u8_item_index returns unique values 0..=34 ---- #[test] fn u8_item_index_values_are_unique() { @@ -444,10 +578,10 @@ mod tests { } #[test] - fn u8_item_index_covers_0_through_31() { + fn u8_item_index_covers_0_through_34() { let variants = all_variants(); let indices: BTreeSet = variants.iter().map(|v| v.u8_item_index()).collect(); - for i in 0u8..=31 { + for i in 0u8..=34 { assert!(indices.contains(&i), "Missing u8_item_index value: {}", i); } } @@ -457,7 +591,7 @@ mod tests { let variants = all_variants(); for v in &variants { let idx = v.u8_item_index(); - assert!(idx <= 31, "Index {} exceeds expected max of 31", idx); + assert!(idx <= 34, "Index {} exceeds expected max of 34", idx); } } @@ -480,13 +614,31 @@ mod tests { TokenConfigurationChangeItem::MainControlGroup(Some(5)).u8_item_index(), 31 ); + assert_eq!( + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(10).u8_item_index(), + 32 + ); + assert_eq!( + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup( + AuthorizedActionTakers::NoOne + ) + .u8_item_index(), + 33 + ); + assert_eq!( + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup( + AuthorizedActionTakers::NoOne + ) + .u8_item_index(), + 34 + ); } #[test] fn u8_item_index_variant_count() { - // We expect exactly 32 variants (indices 0..=31) + // We expect exactly 35 variants (indices 0..=34) let variants = all_variants(); - assert_eq!(variants.len(), 32); + assert_eq!(variants.len(), 35); } // --- payload_serialization --- @@ -894,6 +1046,19 @@ mod json_convertible_tests { assert_eq!(original, recovered); } + #[test] + fn json_round_trip_of_the_pool_notes_threshold() { + use crate::serialization::JsonConvertible; + let original = TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(10); + let json = original.to_json().expect("to_json"); + assert_eq!( + json, + json!({"$type": "minimumPoolNotesForOutgoing", "value": 10}) + ); + let recovered = TokenConfigurationChangeItem::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + #[test] fn json_large_max_supply_serializes_as_string_for_js_safety() { use crate::serialization::JsonConvertible; diff --git a/packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs b/packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs index 6cea9ac97b6..03628b5341d 100644 --- a/packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/methods/validate_update/v0/mod.rs @@ -87,7 +87,9 @@ mod tests { use crate::data_contract::associated_token::token_configuration::accessors::v0::{ TokenConfigurationV0Getters, TokenConfigurationV0Setters, }; + use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters; use crate::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; + use crate::data_contract::associated_token::token_configuration::v1::TokenConfigurationV1; use crate::data_contract::associated_token::token_configuration_convention::v0::TokenConfigurationConventionV0; use crate::data_contract::associated_token::token_configuration_convention::TokenConfigurationConvention; use crate::data_contract::associated_token::token_configuration_localization::v0::TokenConfigurationLocalizationV0; @@ -95,6 +97,7 @@ mod tests { use crate::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Setters; use crate::data_contract::associated_token::token_pre_programmed_distribution::v0::TokenPreProgrammedDistributionV0; use crate::data_contract::associated_token::token_pre_programmed_distribution::TokenPreProgrammedDistribution; + use crate::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; use crate::data_contract::document_type::DocumentTypeMutRef; use crate::data_contract::group::accessors::v0::{GroupV0Getters, GroupV0Setters}; use crate::data_contract::group::v0::GroupV0; @@ -552,6 +555,68 @@ mod tests { ); } + /// An existing token is immutable through a contract update because its whole + /// configuration is compared, so every field `TokenConfigurationV1` carries is immutable + /// with no code of its own. The destructuring names each field without `..`, so a field + /// appended later stops this test compiling until the pattern names it too; that is the + /// moment to add a case changing it to the list below. + #[test] + fn should_return_invalid_result_when_any_token_configuration_v1_field_is_updated() { + let platform_version = PlatformVersion::latest(); + let mut pooled = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + pooled.set_has_shielded_pool(true); + let TokenConfiguration::V1(original) = &pooled else { + panic!("a pooled configuration is V1"); + }; + let TokenConfigurationV1 { + base: _, + has_shielded_pool: _, + minimum_pool_notes_for_outgoing: _, + minimum_pool_notes_for_outgoing_change_rules: _, + } = original; + + type Change = fn(&mut TokenConfigurationV1); + let changes: [(&str, Change); 3] = [ + ("has_shielded_pool", |v1| v1.has_shielded_pool = false), + ("minimum_pool_notes_for_outgoing", |v1| { + v1.minimum_pool_notes_for_outgoing = Some(1) + }), + ("minimum_pool_notes_for_outgoing_change_rules", |v1| { + v1.minimum_pool_notes_for_outgoing_change_rules + .set_authorized_to_make_change_action_takers( + AuthorizedActionTakers::ContractOwner, + ) + }), + ]; + for (field, change) in changes { + let mut old_data_contract = + get_data_contract_fixture(None, IdentityNonce::default(), 9) + .data_contract_owned(); + old_data_contract.set_tokens(BTreeMap::from([(0, pooled.clone())])); + + let mut changed = original.clone(); + change(&mut changed); + assert_ne!(&changed, original, "{field} must actually change"); + let mut new_data_contract = old_data_contract.clone(); + new_data_contract.set_version(old_data_contract.version() + 1); + new_data_contract + .set_tokens(BTreeMap::from([(0, TokenConfiguration::V1(changed))])); + + let result = old_data_contract + .validate_update(&new_data_contract, &BlockInfo::default(), platform_version) + .expect("failed validate update"); + + assert_matches!( + result.errors.as_slice(), + [ConsensusError::StateError( + StateError::DataContractUpdateActionNotAllowedError(e) + )] if e.action() == "update token at position 0", + "a contract update changing {field} must be refused" + ); + } + } + #[test] fn should_return_invalid_result_when_token_is_added_with_past_timestamp() { let platform_version = PlatformVersion::latest(); diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/validate_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/validate_structure/v0/mod.rs index 6ffaae914f8..a0fafc61b19 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/validate_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_config_update_transition/validate_structure/v0/mod.rs @@ -2,6 +2,7 @@ use platform_version::version::PlatformVersion; use crate::consensus::basic::token::{InvalidTokenConfigUpdateNoChangeError, InvalidTokenNoteTooBigError, TokenNoteOnlyAllowedWhenProposerError}; use crate::consensus::basic::{BasicError, UnsupportedFeatureError}; use crate::consensus::ConsensusError; +use crate::data_contract::associated_token::token_configuration::validate_minimum_pool_notes_for_outgoing_bound; use crate::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem; use crate::state_transition::batch_transition::token_config_update_transition::v0::v0_methods::TokenConfigUpdateTransitionV0Methods; use crate::state_transition::batch_transition::TokenConfigUpdateTransition; @@ -47,6 +48,23 @@ impl TokenConfigUpdateTransitionStructureValidationV0 for TokenConfigUpdateTrans )); } + // The pool's outgoing notes threshold is bounded like the one a contract declares. The + // item exists from protocol version 14 only: before it the batch pre-activation gate + // refuses the transition unpaid, and software older than it cannot even decode the + // item, so this check is never reached there. + if let TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(minimum_pool_notes) = + self.update_token_configuration_item() + { + let result = validate_minimum_pool_notes_for_outgoing_bound( + *minimum_pool_notes, + self.base().token_contract_position(), + platform_version, + ); + if !result.is_valid() { + return Ok(result); + } + } + if let Some(public_note) = self.public_note() { if public_note.len() > MAX_TOKEN_NOTE_LEN { return Ok(SimpleConsensusValidationResult::new_with_error( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs index 173c75a2eac..8c703c4b288 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs @@ -4,12 +4,14 @@ use crate::platform_types::platform::PlatformRef; use crate::rpc::core::CoreRPCLike; use dpp::consensus::basic::state_transition::StateTransitionNotActiveError; use dpp::data_contract::associated_token::token_configuration::validate_token_configurations; +use dpp::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem; use dpp::prelude::ConsensusValidationResult; use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransition; use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRef; use dpp::state_transition::batch_transition::document_base_transition::v1::v1_methods::DocumentBaseTransitionV1Methods; +use dpp::state_transition::batch_transition::token_config_update_transition::v0::v0_methods::TokenConfigUpdateTransitionV0Methods; use dpp::state_transition::data_contract_create_transition::accessors::DataContractCreateTransitionAccessorsV0; use dpp::state_transition::data_contract_update_transition::accessors::DataContractUpdateTransitionAccessorsV0; use dpp::state_transition::StateTransition; @@ -101,8 +103,8 @@ impl StateTransitionIsAllowedValidationV0 for StateTransition { } match self { StateTransition::Batch(st) => { - // Token shielded pools (the batch transitions that use them and a document token - // cost paid from one) are a + // Token shielded pools (the batch transitions that use them, a document token + // cost paid from one and the configuration items of a pool's threshold) are a // protocol-version feature, not a table-versioned validator, so the gate is // applied to the batch as a whole before its own `is_allowed` runs. if platform_version.protocol_version < TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION @@ -131,6 +133,19 @@ impl StateTransitionIsAllowedValidationV0 for StateTransition { BatchedTransitionRef::Token( TokenTransition::DirectPurchaseToPool(_), ) => Some("TokenDirectPurchaseToPool"), + // Only a token with a pool has an outgoing notes threshold, + // and software older than it cannot decode these items. + BatchedTransitionRef::Token(TokenTransition::ConfigUpdate( + config_update, + )) if matches!( + config_update.update_token_configuration_item(), + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(_) + | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup(_) + | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(_) + ) => + { + Some("TokenConfigUpdateMinimumPoolNotesForOutgoing") + } BatchedTransitionRef::Document(document_transition) if document_transition .base() diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_shielded_token_payment/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_shielded_token_payment/mod.rs index d255e76de1b..1288d9a780f 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_shielded_token_payment/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_shielded_token_payment/mod.rs @@ -4,12 +4,17 @@ //! The document action itself is validated first by its own validator; this runs afterwards, //! from the batch state validation, and mirrors the pool side of `TokenUnshield` (the notes //! leave the pool; the cost lands in the contract owner's balance or leaves the supply): the -//! pool must exist, the token must not be paused, the anchor must be recorded, the nullifiers +//! pool must exist, the token must not be paused, the pool must hold the notes the token's +//! configuration requires before tokens leave it, the anchor must be recorded, the nullifiers //! unspent, the pool must hold the amount, and the spend bundle must verify with the token id, //! the batch owner, the document's contract and id and the amount bound into its sighash. +use crate::error::execution::ExecutionError; use crate::error::Error; -use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::types::execution_operation::ValidationOperation; +use crate::execution::types::state_transition_execution_context::{ + StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0, +}; use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_pool_common::{ charge_drive_operations, validate_minimum_token_pool_notes, validate_token_not_paused, validate_token_pool_anchor_exists, validate_token_pool_nullifiers, verify_token_pool_bundle, @@ -21,14 +26,16 @@ use dpp::block::block_info::BlockInfo; use dpp::consensus::state::shielded::invalid_shielded_proof_error::InvalidShieldedProofError; use dpp::consensus::state::state_error::StateError; use dpp::consensus::state::token::TokenShieldedPoolNotEnabledError; +use dpp::data_contract::accessors::v1::DataContractV1Getters; use dpp::prelude::Identifier; use dpp::shielded::document_token_payment_extra_sighash_data; -use dpp::tokens::token_payment_info::v1::TokenShieldedPayment; use dpp::validation::SimpleConsensusValidationResult; use dpp::version::PlatformVersion; +use drive::error::drive::DriveError; use drive::query::TransactionArg; use drive::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::{ DocumentBaseTransitionAction, DocumentBaseTransitionActionAccessorsV0, + DocumentShieldedTokenPayment, }; /// Validates the pool side of a document action's shielded token payment and verifies its @@ -36,7 +43,7 @@ use drive::state_transition_action::batch::batched_transition::document_transiti #[allow(clippy::too_many_arguments)] pub(in crate::execution::validation::state_transition::state_transitions::batch) fn validate_document_shielded_token_payment( base: &DocumentBaseTransitionAction, - payment: &TokenShieldedPayment, + shielded_payment: &DocumentShieldedTokenPayment, platform: &PlatformStateRef, owner_id: Identifier, block_info: &BlockInfo, @@ -51,6 +58,11 @@ pub(in crate::execution::validation::state_transition::state_transitions::batch) return Ok(SimpleConsensusValidationResult::new()); }; let token_id_bytes = token_id.to_buffer(); + let DocumentShieldedTokenPayment { + payment, + token_contract_id, + token_contract_position, + } = shielded_payment; let mut drive_operations = vec![]; @@ -93,9 +105,40 @@ pub(in crate::execution::validation::state_transition::state_transitions::batch) return Ok(validation_result); } + // The token's configuration holds its pool's outgoing notes threshold. Its contract is the + // document's own, already loaded, or the one the document type's token cost names, fetched + // and billed here. The pool exists, so its token's contract does too. + let token_contract = if *token_contract_id == base.data_contract_id() { + base.data_contract_fetch_info() + } else { + let (fee, token_contract) = platform.drive.get_contract_with_fetch_info_and_fee( + token_contract_id.to_buffer(), + Some(&block_info.epoch), + false, + transaction, + platform_version, + )?; + let fee = fee.ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution( + "fee must exist when fetching a token's contract with an epoch", + )))?; + execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee)); + token_contract.ok_or_else(|| { + Error::Drive(drive::error::Error::Drive(DriveError::CorruptedDriveState( + format!( + "contract {} of token {} is missing although the token has a shielded pool", + token_contract_id, token_id + ), + ))) + })? + }; + let token_configuration = token_contract + .contract + .expected_token_configuration(*token_contract_position)?; + let validation_result = validate_minimum_token_pool_notes( platform.drive, &token_id_bytes, + token_configuration, transaction, &mut drive_operations, platform_version, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_burn_from_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_burn_from_pool_transition_action/state_v0/mod.rs index ac4f969d997..664eb256ce1 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_burn_from_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_burn_from_pool_transition_action/state_v0/mod.rs @@ -152,6 +152,7 @@ impl TokenBurnFromPoolTransitionActionStateValidationV0 for TokenBurnFromPoolTra let validation_result = validate_minimum_token_pool_notes( platform.drive, &token_id_bytes, + token_configuration, transaction, &mut drive_operations, platform_version, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_config_update_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_config_update_transition_action/state_v0/mod.rs index 2326a0af90d..0df28c2d0ac 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_config_update_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_config_update_transition_action/state_v0/mod.rs @@ -312,6 +312,12 @@ impl TokenConfigUpdateTransitionActionStateValidationV0 for TokenConfigUpdateTra )) | TokenConfigurationChangeItem::EmergencyActionAdminGroup( AuthorizedActionTakers::Identity(identity_id), + ) + | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup( + AuthorizedActionTakers::Identity(identity_id), + ) + | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup( + AuthorizedActionTakers::Identity(identity_id), ) => { let (identity_balance, fee) = platform.drive.fetch_identity_balance_with_costs( identity_id.to_buffer(), @@ -392,6 +398,12 @@ impl TokenConfigUpdateTransitionActionStateValidationV0 for TokenConfigUpdateTra )) | TokenConfigurationChangeItem::EmergencyActionAdminGroup( AuthorizedActionTakers::Group(group_contract_position), + ) + | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup( + AuthorizedActionTakers::Group(group_contract_position), + ) + | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup( + AuthorizedActionTakers::Group(group_contract_position), ) => { if !self .data_contract_fetch_info() @@ -453,6 +465,12 @@ impl TokenConfigUpdateTransitionActionStateValidationV0 for TokenConfigUpdateTra | TokenConfigurationChangeItem::EmergencyAction(AuthorizedActionTakers::MainGroup) | TokenConfigurationChangeItem::EmergencyActionAdminGroup( AuthorizedActionTakers::MainGroup, + ) + | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup( + AuthorizedActionTakers::MainGroup, + ) + | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup( + AuthorizedActionTakers::MainGroup, ) if token_configuration.main_control_group().is_none() => { return Ok(SimpleConsensusValidationResult::new_with_error( ConsensusError::StateError( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs index a608294713f..1e4231aec1a 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs @@ -24,6 +24,7 @@ use dpp::consensus::state::state_error::StateError; use dpp::consensus::state::token::{TokenIsPausedError, TokenShieldedPoolNotEnabledError}; use dpp::consensus::ConsensusError; use dpp::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters; +use dpp::data_contract::associated_token::token_configuration::TokenConfiguration; use dpp::prelude::Identifier; use dpp::shielded::{SerializedAction}; use dpp::tokens::status::v0::TokenStatusV0Accessors; @@ -204,20 +205,18 @@ pub(crate) fn validate_token_pool_output_nullifiers( Ok(validation_result) } -/// The anonymity-set floor for outflows with an observable destination -/// (`minimum_token_pool_notes_for_outgoing`, 0 at introduction). +/// The token's configured notes threshold for outflows with an observable destination +/// (`minimumPoolNotesForOutgoing`, none unless the configuration sets one). The pool's notes +/// are counted only when there is a threshold to compare them with. pub(crate) fn validate_minimum_token_pool_notes( drive: &Drive, token_id: &[u8; 32], + token_configuration: &TokenConfiguration, transaction: TransactionArg, drive_operations: &mut Vec, platform_version: &PlatformVersion, ) -> Result { - let min_notes = platform_version - .drive_abci - .validation_and_processing - .event_constants - .minimum_token_pool_notes_for_outgoing; + let min_notes = token_configuration.minimum_pool_notes_for_outgoing(); if min_notes > 0 { let notes_count = drive.token_shielded_pool_notes_count( token_id, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/state_v0/mod.rs index 0baf159f971..ca55fe222bb 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/state_v0/mod.rs @@ -19,6 +19,7 @@ use dpp::shielded::token_unshield_extra_sighash_data; use dpp::validation::SimpleConsensusValidationResult; use dpp::version::PlatformVersion; use drive::query::TransactionArg; +use drive::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::TokenBaseTransitionActionAccessorsV0; use drive::state_transition_action::batch::batched_transition::token_transition::token_unshield_transition_action::{ TokenUnshieldTransitionAction, TokenUnshieldTransitionActionAccessorsV0, }; @@ -105,6 +106,7 @@ impl TokenUnshieldTransitionActionStateValidationV0 for TokenUnshieldTransitionA let validation_result = validate_minimum_token_pool_notes( platform.drive, &token_id_bytes, + self.base().token_configuration()?, transaction, &mut drive_operations, platform_version, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/minimum_pool_notes.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/minimum_pool_notes.rs new file mode 100644 index 00000000000..f61e95e14e3 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/minimum_pool_notes.rs @@ -0,0 +1,1492 @@ +//! A token shielded pool's outgoing notes threshold, `minimumPoolNotesForOutgoing`. +//! +//! The issuer sets it per token, at most `max_token_pool_notes_for_outgoing`, and changes it +//! only through `TokenConfigUpdate` under the token's own rules. While the pool holds fewer +//! notes, every path that moves tokens out of the pool to a visible destination is refused: +//! unshielding to an identity, with the identity's fee or with a fee from the credit pool, +//! burning from the pool, and paying a document's token cost from it. Transfers inside the +//! pool are never refused, since they are how the count grows. + +use super::document_shielded_token_payment_tests as cards; +use super::token_pool_paid_transitions_tests::{ + credit_pool_balance, fund_credit_pool, fund_token_pool, spendable, wallet_address, Prover, + CREDIT_NOTE, SHIELDED, +}; +use super::token_shielded_pool_tests::{ + assert_tokens_conserved, build_shield_bundle, build_spend_bundle, identity_token_balance, + insert_token_pool_anchor, nullifier_is_spent, platform_with_latest_version, pool_balance, + pool_notes_count, process, shielded_token_contract, spend_keys, spendable_note, SHIELD_AMOUNT, +}; +use super::*; +use crate::execution::check_tx::CheckTxLevel; +use crate::execution::validation::state_transition::tests::create_card_game_external_token_contract_with_owner_identity; +use crate::platform_types::platform::PlatformRef; +use crate::rpc::core::MockCoreRPCLike; +use crate::test::helpers::setup::TempPlatform; +use dpp::consensus::codes::ErrorWithCode; +use dpp::data_contract::accessors::v0::DataContractV0Setters; +use dpp::data_contract::accessors::v1::DataContractV1Setters; +use dpp::data_contract::associated_token::token_configuration::accessors::v1::{ + TokenConfigurationV1Getters, TokenConfigurationV1Setters, +}; +use dpp::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem; +use dpp::data_contract::change_control_rules::ChangeControlRules; +use dpp::data_contract::config::v0::DataContractConfigSettersV0; +use dpp::data_contract::errors::DataContractError; +use dpp::data_contract::DataContract; +use dpp::identity::accessors::IdentityGettersV0; +use dpp::identity::identity_public_key::accessors::v0::IdentityPublicKeyGettersV0; +use dpp::identity::{Identity, IdentityPublicKey}; +use dpp::shielded::builder::{ + build_token_shielded_transfer_with_shielded_fee_transition, + build_token_unshield_with_shielded_fee_transition, ShieldedFeePayer, TokenPoolSpender, +}; +use dpp::shielded::{ + document_token_payment_extra_sighash_data_v0, token_burn_from_pool_extra_sighash_data_v0, + token_shielded_transfer_extra_sighash_data_v0, token_unshield_extra_sighash_data_v0, +}; +use dpp::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType; +use dpp::state_transition::data_contract_create_transition::methods::DataContractCreateTransitionMethodsV0; +use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; +use dpp::state_transition::data_contract_update_transition::methods::DataContractUpdateTransitionMethodsV0; +use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; +use dpp::state_transition::StateTransition; +use dpp::tests::fixtures::get_data_contract_fixture; +use dpp::tokens::calculate_token_id; +use dpp::tokens::gas_fees_paid_by::GasFeesPaidBy; +use dpp::tokens::token_payment_info::v1::TokenPaymentInfoV1; +use dpp::tokens::token_payment_info::TokenPaymentInfo; +use dpp::version::feature_initial_protocol_versions::TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION; +use drive::util::test_helpers::setup_contract; +use platform_version::version::PlatformVersion; +use simple_signer::signer::SimpleSigner; + +/// One shield puts two notes in an empty pool and one spend with its change two more, so a +/// threshold of 4 refuses the first outflow and admits it once one transfer happened inside +/// the pool. Each test asserts the counts it relies on rather than trusting this arithmetic. +const THRESHOLD: u64 = 4; + +/// Gives the token a shielded pool whose outflows wait for `minimum_pool_notes` notes. +fn pooled_with_threshold(configuration: &mut TokenConfiguration, minimum_pool_notes: u64) { + configuration.set_has_shielded_pool(true); + let TokenConfiguration::V1(v1) = configuration else { + panic!("a token with a pool has a V1 configuration"); + }; + v1.minimum_pool_notes_for_outgoing = Some(minimum_pool_notes); +} + +fn contract_owner_rules() -> ChangeControlRules { + ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::ContractOwner, + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }) +} + +fn max_threshold() -> u64 { + PlatformVersion::latest() + .system_limits + .max_token_pool_notes_for_outgoing +} + +/// The threshold of token 0 as the contract stored in Drive declares it. +fn stored_threshold(platform: &TempPlatform, contract_id: Identifier) -> u64 { + platform + .drive + .fetch_contract( + contract_id.to_buffer(), + None, + None, + None, + PlatformVersion::latest(), + ) + .unwrap() + .expect("fetch contract") + .expect("contract exists") + .contract + .expected_token_configuration(0) + .expect("token 0") + .minimum_pool_notes_for_outgoing() +} + +fn assert_refused_below(errors: &[ConsensusError], notes: u64, minimum_required: u64) { + assert_matches!( + errors, + [ConsensusError::StateError(StateError::InsufficientPoolNotesError(error))] + if error.current_count() == notes && error.minimum_required() == minimum_required + ); +} + +fn assert_refused_below_threshold(errors: &[ConsensusError], notes: u64) { + assert_refused_below(errors, notes, THRESHOLD); +} + +/// The errors CheckTx answers with at mempool admission. +fn check_tx_errors( + platform: &TempPlatform, + transition: &StateTransition, +) -> Vec { + let state = platform.state.load(); + let platform_ref = PlatformRef { + drive: &platform.drive, + state: &state, + config: &platform.config, + core_rpc: &platform.core_rpc, + }; + platform + .check_tx( + &transition.serialize_to_bytes().expect("serialize"), + CheckTxLevel::FirstTimeCheck, + &platform_ref, + PlatformVersion::latest(), + ) + .expect("check tx") + .errors +} + +/// Shields `SHIELD_AMOUNT` of the owner's tokens into the pool. +#[allow(clippy::too_many_arguments)] +async fn shield( + platform: &TempPlatform, + contract: &DataContract, + token_id: Identifier, + owner: &Identity, + key: &IdentityPublicKey, + signer: &SimpleSigner, + nonce: u64, + seed: u64, +) { + let shield = BatchTransition::new_token_shield_transition( + token_id, + owner.id(), + contract.id(), + 0, + SHIELD_AMOUNT, + build_shield_bundle( + SHIELD_AMOUNT, + seed, + TokenTransitionActionType::Shield, + token_id, + ), + key, + nonce, + 0, + signer, + PlatformVersion::latest(), + None, + ) + .await + .expect("token shield transition"); + let result = process(platform, &shield); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); +} + +/// A token pool written without a threshold, the way every pool was before the setting +/// existed, reads as 0 and lets its first holder leave: the default must not trap the first +/// depositors of a new pool. +#[tokio::test] +async fn should_read_a_pooled_token_without_a_threshold_as_zero_and_let_its_first_holder_leave() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9501); + let (identity, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (recipient, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.1)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|configuration: &mut TokenConfiguration| configuration.set_has_shielded_pool(true)), + None, + None, + None, + platform_version, + ); + let TokenConfiguration::V1(v1) = contract.expected_token_configuration(0).expect("token 0") + else { + panic!("a token with a pool has a V1 configuration"); + }; + assert_eq!(v1.minimum_pool_notes_for_outgoing, None); + assert_eq!(stored_threshold(&platform, contract.id()), 0); + + shield( + &platform, &contract, token_id, &identity, &key, &signer, 2, 91, + ) + .await; + assert_eq!(pool_notes_count(&platform, token_id), 2); + + let token = token_id.to_buffer(); + let (note, anchor, merkle_path) = spendable_note(6_000, 1); + insert_token_pool_anchor(&platform, token_id, &anchor); + let extra = token_unshield_extra_sighash_data_v0( + &token, + &identity.id().to_buffer(), + &recipient.id().to_buffer(), + 4_000, + ); + let (bundle, _) = build_spend_bundle(note, merkle_path, anchor, 4_000, &extra, 92); + let unshield = BatchTransition::new_token_unshield_transition( + token_id, + identity.id(), + contract.id(), + 0, + 4_000, + recipient.id(), + bundle, + &key, + 3, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token unshield transition"); + let result = process(&platform, &unshield); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!( + identity_token_balance(&platform, token_id, recipient.id()), + Some(4_000) + ); + assert_tokens_conserved(&platform); +} + +/// An unshield to an identity waits for the threshold, a transfer inside the pool does not, +/// and the very unshield that was refused goes through once that transfer lifted the count: +/// the threshold was the only reason it was refused. +#[tokio::test] +async fn should_refuse_an_unshield_below_the_threshold_but_never_a_transfer_inside_the_pool() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9502); + let (identity, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (recipient, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.1)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|configuration: &mut TokenConfiguration| { + pooled_with_threshold(configuration, THRESHOLD) + }), + None, + None, + None, + platform_version, + ); + let token = token_id.to_buffer(); + shield( + &platform, &contract, token_id, &identity, &key, &signer, 2, 93, + ) + .await; + let notes = pool_notes_count(&platform, token_id); + assert!(notes < THRESHOLD); + + let unshield_amount = 4_000; + let (note, anchor, merkle_path) = spendable_note(6_000, 1); + insert_token_pool_anchor(&platform, token_id, &anchor); + let extra = token_unshield_extra_sighash_data_v0( + &token, + &identity.id().to_buffer(), + &recipient.id().to_buffer(), + unshield_amount, + ); + let (unshield_bundle, _) = + build_spend_bundle(note, merkle_path, anchor, unshield_amount, &extra, 94); + let unshield_with_nonce = |nonce| { + BatchTransition::new_token_unshield_transition( + token_id, + identity.id(), + contract.id(), + 0, + unshield_amount, + recipient.id(), + unshield_bundle.clone(), + &key, + nonce, + 0, + &signer, + platform_version, + None, + ) + }; + + let unshield = unshield_with_nonce(3).await.expect("token unshield"); + let result = process(&platform, &unshield); + let [StateTransitionExecutionResult::PaidConsensusError { error, .. }] = + result.execution_results().as_slice() + else { + panic!( + "expected a paid refusal, got {:?}", + result.execution_results() + ); + }; + assert_refused_below_threshold(std::slice::from_ref(error), notes); + assert_eq!(pool_balance(&platform, token_id), SHIELD_AMOUNT); + assert_eq!( + identity_token_balance(&platform, token_id, recipient.id()), + None + ); + for action in &unshield_bundle.actions { + assert!(!nullifier_is_spent(&platform, token_id, &action.nullifier)); + } + + let (note, anchor, merkle_path) = spendable_note(3_000, 2); + insert_token_pool_anchor(&platform, token_id, &anchor); + let extra = token_shielded_transfer_extra_sighash_data_v0(&token, &identity.id().to_buffer()); + let (transfer_bundle, _) = build_spend_bundle(note, merkle_path, anchor, 0, &extra, 95); + let transfer = BatchTransition::new_token_shielded_transfer_transition( + token_id, + identity.id(), + contract.id(), + 0, + transfer_bundle, + &key, + 4, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token shielded transfer"); + let result = process(&platform, &transfer); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_notes_count(&platform, token_id), THRESHOLD); + + let unshield = unshield_with_nonce(5).await.expect("token unshield"); + let result = process(&platform, &unshield); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!( + identity_token_balance(&platform, token_id, recipient.id()), + Some(unshield_amount) + ); + assert_eq!( + pool_balance(&platform, token_id), + SHIELD_AMOUNT - unshield_amount + ); + assert_tokens_conserved(&platform); +} + +#[tokio::test] +async fn should_refuse_a_burn_from_the_pool_below_the_threshold() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9503); + let (identity, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|configuration: &mut TokenConfiguration| { + pooled_with_threshold(configuration, THRESHOLD) + }), + None, + None, + None, + platform_version, + ); + shield( + &platform, &contract, token_id, &identity, &key, &signer, 2, 96, + ) + .await; + let notes = pool_notes_count(&platform, token_id); + assert!(notes < THRESHOLD); + + let burn_amount = 4_000; + let (note, anchor, merkle_path) = spendable_note(6_000, 3); + insert_token_pool_anchor(&platform, token_id, &anchor); + let extra = token_burn_from_pool_extra_sighash_data_v0( + &token_id.to_buffer(), + &identity.id().to_buffer(), + burn_amount, + ); + let (burn_bundle, _) = build_spend_bundle(note, merkle_path, anchor, burn_amount, &extra, 97); + let burn = BatchTransition::new_token_burn_from_pool_transition( + token_id, + identity.id(), + contract.id(), + 0, + burn_amount, + burn_bundle.clone(), + None, + None, + &key, + 3, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token burn from pool transition"); + let result = process(&platform, &burn); + let [StateTransitionExecutionResult::PaidConsensusError { error, .. }] = + result.execution_results().as_slice() + else { + panic!( + "expected a paid refusal, got {:?}", + result.execution_results() + ); + }; + assert_refused_below_threshold(std::slice::from_ref(error), notes); + assert_eq!(pool_balance(&platform, token_id), SHIELD_AMOUNT); + for action in &burn_bundle.actions { + assert!(!nullifier_is_spent(&platform, token_id, &action.nullifier)); + } + assert_tokens_conserved(&platform); +} + +/// The card game contract with a burned creation cost in its own token 0, which has a pool +/// with the test threshold. +fn card_game_contract_with_threshold( + platform: &TempPlatform, + owner_id: Identifier, + platform_version: &PlatformVersion, +) -> (DataContract, Identifier) { + let data_contract_id = DataContract::generate_data_contract_id_v0(owner_id, 1); + let contract = setup_contract( + &platform.drive, + "tests/supporting_files/contract/crypto-card-game/crypto-card-game-in-game-currency-burn-tokens.json", + Some(data_contract_id.to_buffer()), + Some(owner_id.to_buffer()), + Some(|data_contract: &mut DataContract| { + data_contract.set_created_at_epoch(Some(0)); + data_contract.set_created_at(Some(0)); + data_contract.set_created_at_block_height(Some(0)); + let configuration = data_contract + .tokens_mut() + .and_then(|tokens| tokens.get_mut(&0)) + .expect("token 0"); + pooled_with_threshold(configuration, THRESHOLD); + }), + None, + Some(platform_version), + ); + ( + contract, + calculate_token_id(data_contract_id.as_bytes(), 0).into(), + ) +} + +#[tokio::test] +async fn should_refuse_a_document_token_cost_paid_from_the_pool_below_the_threshold() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9504); + let (contract_owner, _, _) = setup_identity(&mut platform, 961, dash_to_credits!(0.1)); + let (buyer, signer, key) = setup_identity(&mut platform, 237, dash_to_credits!(0.5)); + let (contract, token_id) = + card_game_contract_with_threshold(&platform, contract_owner.id(), platform_version); + cards::fund_and_shield( + &mut platform, + &contract, + token_id, + &buyer, + &key, + &signer, + 98, + platform_version, + ) + .await; + let notes = pool_notes_count(&platform, token_id); + assert!(notes < THRESHOLD); + + let card_document_type = contract + .document_type_for_name("card") + .expect("card document type"); + let (document, entropy) = cards::random_card( + &mut rng, + card_document_type, + buyer.id(), + 2, + platform_version, + ); + let (note, anchor, merkle_path) = spendable_note(cards::SHIELDED, 7); + insert_token_pool_anchor(&platform, token_id, &anchor); + let extra = document_token_payment_extra_sighash_data_v0( + &token_id.to_buffer(), + &buyer.id().to_buffer(), + &contract.id().to_buffer(), + &document.id().to_buffer(), + cards::CARD_COST, + ); + let (bundle, _) = build_spend_bundle(note, merkle_path, anchor, cards::CARD_COST, &extra, 99); + let payment = cards::shielded_payment(bundle, cards::CARD_COST); + let transition = BatchTransition::new_document_creation_transition_from_document( + document, + card_document_type, + entropy.0, + &key, + 2, + 0, + Some(cards::payment_info(payment.clone())), + &signer, + platform_version, + None, + ) + .await + .expect("document create transition"); + + let result = process(&platform, &transition); + let [StateTransitionExecutionResult::PaidConsensusError { error, .. }] = + result.execution_results().as_slice() + else { + panic!( + "expected a paid refusal, got {:?}", + result.execution_results() + ); + }; + assert_refused_below_threshold(std::slice::from_ref(error), notes); + assert_eq!(pool_balance(&platform, token_id), cards::SHIELDED); + assert!(!nullifier_is_spent( + &platform, + token_id, + &payment.actions[0].nullifier + )); + assert_tokens_conserved(&platform); +} + +/// The document's cost is in a token another contract declares, so the threshold is read from +/// that contract, which the document's own does not carry; once the pool holds enough notes +/// the same cost is paid from it. +#[tokio::test] +async fn should_read_the_threshold_of_a_token_declared_by_another_contract() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9505); + let (holder, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (card_owner, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.1)); + let (token_contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + holder.id(), + Some(|configuration: &mut TokenConfiguration| { + pooled_with_threshold(configuration, THRESHOLD) + }), + None, + None, + None, + platform_version, + ); + let card_contract = create_card_game_external_token_contract_with_owner_identity( + &mut platform, + token_contract.id(), + 0, + cards::CARD_COST, + GasFeesPaidBy::DocumentOwner, + card_owner.id(), + platform_version, + ); + shield( + &platform, + &token_contract, + token_id, + &holder, + &key, + &signer, + 2, + 100, + ) + .await; + let notes = pool_notes_count(&platform, token_id); + assert!(notes < THRESHOLD); + + let card_document_type = card_contract + .document_type_for_name("card") + .expect("card document type"); + let (note, anchor, merkle_path) = spendable_note(6_000, 8); + insert_token_pool_anchor(&platform, token_id, &anchor); + // A card paid from the pool, created under `nonce`. + let mut rng_for_cards = StdRng::seed_from_u64(9515); + let mut paid_card = |nonce: u64, seed: u64| { + let (document, entropy) = cards::random_card( + &mut rng_for_cards, + card_document_type, + holder.id(), + nonce, + platform_version, + ); + let extra = document_token_payment_extra_sighash_data_v0( + &token_id.to_buffer(), + &holder.id().to_buffer(), + &card_contract.id().to_buffer(), + &document.id().to_buffer(), + cards::CARD_COST, + ); + let (bundle, _) = build_spend_bundle( + note, + merkle_path.clone(), + anchor, + cards::CARD_COST, + &extra, + seed, + ); + let payment_info = TokenPaymentInfo::V1(TokenPaymentInfoV1 { + payment_token_contract_id: Some(token_contract.id()), + token_contract_position: 0, + minimum_token_cost: None, + maximum_token_cost: Some(cards::CARD_COST), + gas_fees_paid_by: GasFeesPaidBy::DocumentOwner, + shielded_payment: Box::new(cards::shielded_payment(bundle, cards::CARD_COST)), + }); + (document, entropy, payment_info) + }; + + let (document, entropy, payment_info) = paid_card(1, 101); + let transition = BatchTransition::new_document_creation_transition_from_document( + document, + card_document_type, + entropy.0, + &key, + 1, + 0, + Some(payment_info), + &signer, + platform_version, + None, + ) + .await + .expect("document create transition"); + let result = process(&platform, &transition); + let [StateTransitionExecutionResult::PaidConsensusError { error, .. }] = + result.execution_results().as_slice() + else { + panic!( + "expected a paid refusal, got {:?}", + result.execution_results() + ); + }; + assert_refused_below_threshold(std::slice::from_ref(error), notes); + assert_eq!(pool_balance(&platform, token_id), SHIELD_AMOUNT); + + let (transfer_note, transfer_anchor, transfer_path) = spendable_note(3_000, 10); + insert_token_pool_anchor(&platform, token_id, &transfer_anchor); + let extra = token_shielded_transfer_extra_sighash_data_v0( + &token_id.to_buffer(), + &holder.id().to_buffer(), + ); + let (transfer_bundle, _) = build_spend_bundle( + transfer_note, + transfer_path, + transfer_anchor, + 0, + &extra, + 105, + ); + let transfer = BatchTransition::new_token_shielded_transfer_transition( + token_id, + holder.id(), + token_contract.id(), + 0, + transfer_bundle, + &key, + 3, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token shielded transfer"); + let result = process(&platform, &transfer); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_notes_count(&platform, token_id), THRESHOLD); + + let (document, entropy, payment_info) = paid_card(2, 106); + let transition = BatchTransition::new_document_creation_transition_from_document( + document, + card_document_type, + entropy.0, + &key, + 2, + 0, + Some(payment_info), + &signer, + platform_version, + None, + ) + .await + .expect("document create transition"); + let result = process(&platform, &transition); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!( + pool_balance(&platform, token_id), + SHIELD_AMOUNT - cards::CARD_COST + ); + assert_eq!( + identity_token_balance(&platform, token_id, card_owner.id()), + Some(cards::CARD_COST) + ); + assert_tokens_conserved(&platform); +} + +/// `TokenUnshieldWithShieldedFee` moves tokens out of the pool to a visible identity like +/// `TokenUnshield` does; only its fee comes from the credit pool. It is refused below the +/// threshold at mempool admission and in the block, while `TokenShieldedTransferWithShieldedFee`, +/// which moves nothing out, is not; and the refused transition itself goes through once that +/// transfer lifted the count. +#[tokio::test] +async fn should_refuse_an_unshield_with_a_shielded_fee_below_the_threshold() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9506); + let (owner, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (recipient, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.1)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + owner.id(), + Some(|configuration: &mut TokenConfiguration| { + pooled_with_threshold(configuration, THRESHOLD) + }), + None, + None, + None, + platform_version, + ); + let (token_note, token_anchor, token_path) = fund_token_pool( + &mut platform, + &contract, + token_id, + &owner, + &key, + &signer, + 102, + 21, + platform_version, + ) + .await; + let notes = pool_notes_count(&platform, token_id); + assert!(notes < THRESHOLD); + + let (credit_note, credit_anchor, credit_path) = fund_credit_pool(&platform, 22); + let (fvk, ask, _) = spend_keys(); + let address = wallet_address(); + let (unshield, fee) = build_token_unshield_with_shielded_fee_transition( + token_id, + contract.id(), + 0, + TokenPoolSpender { + spends: vec![spendable(token_note, token_path)], + change_address: &address, + fvk: &fvk, + ask: &ask, + anchor: token_anchor, + }, + recipient.id(), + 10, + [0u8; 36], + ShieldedFeePayer { + spends: vec![spendable(credit_note, credit_path)], + change_address: &address, + fvk: &fvk, + ask: &ask, + anchor: credit_anchor, + }, + &Prover, + platform_version, + ) + .expect("build transition"); + + assert_refused_below_threshold(&check_tx_errors(&platform, &unshield), notes); + let result = process(&platform, &unshield); + let [StateTransitionExecutionResult::UnpaidConsensusError(error)] = + result.execution_results().as_slice() + else { + panic!( + "expected an unpaid refusal, got {:?}", + result.execution_results() + ); + }; + assert_refused_below_threshold(std::slice::from_ref(error), notes); + assert_eq!(pool_balance(&platform, token_id), SHIELDED); + assert_eq!( + identity_token_balance(&platform, token_id, recipient.id()), + None + ); + assert_eq!(credit_pool_balance(&platform), CREDIT_NOTE); + + let (transfer_note, transfer_anchor, transfer_path) = spendable_note(SHIELDED, 23); + insert_token_pool_anchor(&platform, token_id, &transfer_anchor); + let (credit_note, credit_anchor, credit_path) = fund_credit_pool(&platform, 24); + let (transfer, transfer_fee) = build_token_shielded_transfer_with_shielded_fee_transition( + token_id, + contract.id(), + 0, + TokenPoolSpender { + spends: vec![spendable(transfer_note, transfer_path)], + change_address: &address, + fvk: &fvk, + ask: &ask, + anchor: transfer_anchor, + }, + &address, + 6, + [0u8; 36], + ShieldedFeePayer { + spends: vec![spendable(credit_note, credit_path)], + change_address: &address, + fvk: &fvk, + ask: &ask, + anchor: credit_anchor, + }, + &Prover, + platform_version, + ) + .expect("build transition"); + let result = process(&platform, &transfer); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_notes_count(&platform, token_id), THRESHOLD); + + let result = process(&platform, &unshield); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(&platform, token_id), SHIELDED - 10); + assert_eq!( + identity_token_balance(&platform, token_id, recipient.id()), + Some(10) + ); + assert_eq!( + credit_pool_balance(&platform), + CREDIT_NOTE - transfer_fee - fee + ); + assert_tokens_conserved(&platform); +} + +fn assert_refused_as_out_of_bounds(result: &[StateTransitionExecutionResult]) { + assert_matches!( + result, + [StateTransitionExecutionResult::UnpaidConsensusError( + error @ ConsensusError::BasicError(BasicError::ContractError( + DataContractError::KeyWrongBounds(_) + )) + )] if error.code() == 10241 + ); +} + +/// An issuer could otherwise set a threshold no pool ever reaches and strand every shielded +/// balance, irreversibly on a readonly contract. +#[tokio::test] +async fn should_refuse_a_threshold_above_the_limit_at_contract_create() { + let platform_version = PlatformVersion::latest(); + for (minimum_pool_notes, accepted) in [(max_threshold(), true), (max_threshold() + 1, false)] { + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9507); + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let mut contract = shielded_token_contract(identity.id(), platform_version); + let contract_id = contract.id(); + pooled_with_threshold( + contract.token_configuration_mut(0).expect("token 0"), + minimum_pool_notes, + ); + let create = DataContractCreateTransition::new_from_data_contract( + contract, + 1, + &identity.clone().into_partial_identity_info(), + key.id(), + &signer, + platform_version, + None, + ) + .await + .expect("contract create transition"); + + let result = process(&platform, &create); + if accepted { + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(stored_threshold(&platform, contract_id), minimum_pool_notes); + } else { + assert_refused_as_out_of_bounds(result.execution_results()); + } + } +} + +#[tokio::test] +async fn should_refuse_a_threshold_above_the_limit_on_a_token_a_contract_update_adds() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let (identity, signer, key) = setup_identity(&mut platform, 9508, dash_to_credits!(1.0)); + let mut contract = + get_data_contract_fixture(None, 0, platform_version.protocol_version).data_contract_owned(); + contract.set_owner_id(identity.id()); + contract.config_mut().set_readonly(false); + platform + .drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("store original contract"); + + let mut configuration = shielded_token_contract(identity.id(), platform_version) + .expected_token_configuration(0) + .expect("token 0") + .clone(); + pooled_with_threshold(&mut configuration, max_threshold() + 1); + contract.add_token(0, configuration); + contract.increment_version(); + let update = DataContractUpdateTransition::new_from_data_contract( + contract, + &identity.into_partial_identity_info(), + key.id(), + 1, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("contract update transition"); + + let result = process(&platform, &update); + assert_refused_as_out_of_bounds(result.execution_results()); +} + +/// A `TokenConfigUpdate` of token 0 applying `item`, signed by `identity`. +#[allow(clippy::too_many_arguments)] +async fn token_config_update( + token_id: Identifier, + contract_id: Identifier, + identity: &Identity, + key: &IdentityPublicKey, + signer: &SimpleSigner, + item: TokenConfigurationChangeItem, + nonce: u64, + platform_version: &PlatformVersion, +) -> StateTransition { + BatchTransition::new_token_config_update_transition( + token_id, + identity.id(), + contract_id, + 0, + item, + None, + None, + key, + nonce, + 0, + signer, + platform_version, + None, + ) + .await + .expect("token config update transition") +} + +/// The threshold changes the way any other token parameter does: through `TokenConfigUpdate`, +/// bounded by the same limit and authorized by its own rules, and the outflow checks read the +/// value it set. A contract update that tries to change it directly is still refused, as for +/// every other field of an existing token. +#[tokio::test] +async fn should_change_the_threshold_through_token_config_update_and_never_by_contract_update() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9509); + let (owner, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (stranger, stranger_signer, stranger_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (recipient, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.1)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + owner.id(), + Some(|configuration: &mut TokenConfiguration| { + configuration.set_has_shielded_pool(true); + let TokenConfiguration::V1(v1) = configuration else { + panic!("a token with a pool has a V1 configuration"); + }; + v1.minimum_pool_notes_for_outgoing_change_rules = contract_owner_rules(); + }), + None, + None, + None, + platform_version, + ); + assert_eq!(stored_threshold(&platform, contract.id()), 0); + let set_threshold = |minimum_pool_notes| { + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(minimum_pool_notes) + }; + + let over_the_limit = token_config_update( + token_id, + contract.id(), + &owner, + &key, + &signer, + set_threshold(max_threshold() + 1), + 2, + platform_version, + ) + .await; + let result = process(&platform, &over_the_limit); + assert_refused_as_out_of_bounds(result.execution_results()); + + let by_a_stranger = token_config_update( + token_id, + contract.id(), + &stranger, + &stranger_key, + &stranger_signer, + set_threshold(5), + 2, + platform_version, + ) + .await; + let result = process(&platform, &by_a_stranger); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::UnauthorizedTokenActionError(_)), + .. + }] + ); + assert_eq!(stored_threshold(&platform, contract.id()), 0); + + let raise = token_config_update( + token_id, + contract.id(), + &owner, + &key, + &signer, + set_threshold(5), + 3, + platform_version, + ) + .await; + let result = process(&platform, &raise); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(stored_threshold(&platform, contract.id()), 5); + + // The raised threshold is the one an outflow now meets. + shield( + &platform, &contract, token_id, &owner, &key, &signer, 4, 103, + ) + .await; + let notes = pool_notes_count(&platform, token_id); + assert!(notes < 5); + let token = token_id.to_buffer(); + let (note, anchor, merkle_path) = spendable_note(6_000, 9); + insert_token_pool_anchor(&platform, token_id, &anchor); + let extra = token_unshield_extra_sighash_data_v0( + &token, + &owner.id().to_buffer(), + &recipient.id().to_buffer(), + 4_000, + ); + let (unshield_bundle, _) = build_spend_bundle(note, merkle_path, anchor, 4_000, &extra, 104); + let unshield_with_nonce = |nonce| { + BatchTransition::new_token_unshield_transition( + token_id, + owner.id(), + contract.id(), + 0, + 4_000, + recipient.id(), + unshield_bundle.clone(), + &key, + nonce, + 0, + &signer, + platform_version, + None, + ) + }; + let unshield = unshield_with_nonce(5).await.expect("token unshield"); + let result = process(&platform, &unshield); + let [StateTransitionExecutionResult::PaidConsensusError { error, .. }] = + result.execution_results().as_slice() + else { + panic!( + "expected a paid refusal, got {:?}", + result.execution_results() + ); + }; + assert_refused_below(std::slice::from_ref(error), notes, 5); + + // Lowering it back to 0 releases the pool. + let lower = token_config_update( + token_id, + contract.id(), + &owner, + &key, + &signer, + set_threshold(0), + 6, + platform_version, + ) + .await; + let result = process(&platform, &lower); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(stored_threshold(&platform, contract.id()), 0); + let unshield = unshield_with_nonce(7).await.expect("token unshield"); + let result = process(&platform, &unshield); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!( + identity_token_balance(&platform, token_id, recipient.id()), + Some(4_000) + ); + + let mut updated = platform + .drive + .fetch_contract( + contract.id().to_buffer(), + None, + None, + None, + platform_version, + ) + .unwrap() + .expect("fetch contract") + .expect("contract exists") + .contract + .clone(); + pooled_with_threshold(updated.token_configuration_mut(0).expect("token 0"), 7); + updated.increment_version(); + let direct = DataContractUpdateTransition::new_from_data_contract( + updated, + &owner.into_partial_identity_info(), + key.id(), + 8, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("contract update transition"); + let result = process(&platform, &direct); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError( + StateError::DataContractUpdateActionNotAllowedError(error) + ), + .. + }] if error.action() == "update token at position 0" + ); + assert_eq!(stored_threshold(&platform, contract.id()), 0); +} + +/// Control of the threshold moves like control of any other token parameter: its admin hands +/// it to an identity, which has to exist, and that identity then sets the threshold in place +/// of the one that handed it over. +#[tokio::test] +async fn should_hand_control_of_the_threshold_only_to_an_identity_that_exists() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9511); + let (owner, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (heir, heir_signer, heir_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + owner.id(), + Some(|configuration: &mut TokenConfiguration| { + configuration.set_has_shielded_pool(true); + let TokenConfiguration::V1(v1) = configuration else { + panic!("a token with a pool has a V1 configuration"); + }; + v1.minimum_pool_notes_for_outgoing_change_rules = + ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::ContractOwner, + admin_action_takers: AuthorizedActionTakers::ContractOwner, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }); + }), + None, + None, + None, + platform_version, + ); + let hand_control_to = |identity_id| { + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup( + AuthorizedActionTakers::Identity(identity_id), + ) + }; + + let missing = Identifier::from([0xAB; 32]); + let to_nobody = token_config_update( + token_id, + contract.id(), + &owner, + &key, + &signer, + hand_control_to(missing), + 2, + platform_version, + ) + .await; + let result = process(&platform, &to_nobody); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError( + StateError::NewAuthorizedActionTakerIdentityDoesNotExistError(_) + ), + .. + }] + ); + + let to_the_heir = token_config_update( + token_id, + contract.id(), + &owner, + &key, + &signer, + hand_control_to(heir.id()), + 3, + platform_version, + ) + .await; + let result = process(&platform, &to_the_heir); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + let by_the_owner = token_config_update( + token_id, + contract.id(), + &owner, + &key, + &signer, + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(5), + 4, + platform_version, + ) + .await; + let result = process(&platform, &by_the_owner); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::UnauthorizedTokenActionError(_)), + .. + }] + ); + + let by_the_heir = token_config_update( + token_id, + contract.id(), + &heir, + &heir_key, + &heir_signer, + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(5), + 2, + platform_version, + ) + .await; + let result = process(&platform, &by_the_heir); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(stored_threshold(&platform, contract.id()), 5); +} + +/// A token without a pool has a V0 configuration, which has no threshold to change. Its +/// threshold items are refused as unauthorized, never accepted and applied to nothing. +#[tokio::test] +async fn should_refuse_the_threshold_items_for_a_token_without_a_pool() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9512); + let (owner, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + owner.id(), + None::, + None, + None, + None, + platform_version, + ); + + for (item, nonce) in [ + ( + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(5), + 2, + ), + ( + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup( + AuthorizedActionTakers::ContractOwner, + ), + 3, + ), + ( + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup( + AuthorizedActionTakers::ContractOwner, + ), + 4, + ), + ] { + let transition = token_config_update( + token_id, + contract.id(), + &owner, + &key, + &signer, + item, + nonce, + platform_version, + ) + .await; + let result = process(&platform, &transition); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::UnauthorizedTokenActionError(_)), + .. + }] + ); + } + + let stored = platform + .drive + .fetch_contract( + contract.id().to_buffer(), + None, + None, + None, + platform_version, + ) + .unwrap() + .expect("fetch contract") + .expect("contract exists"); + assert_matches!( + stored.contract.expected_token_configuration(0), + Ok(TokenConfiguration::V0(_)) + ); + assert_eq!(stored_threshold(&platform, contract.id()), 0); +} + +/// Before token shielded pools activate, no token has a threshold and software older than the +/// setting cannot decode its items, so they are refused unpaid like the pool transitions. Any +/// other configuration item still passes the gate. +#[tokio::test] +async fn should_refuse_the_threshold_items_before_protocol_version_14() { + let platform_version = PlatformVersion::get(TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION - 1) + .expect("previous protocol version"); + let mut platform = TestPlatformBuilder::new() + .with_initial_protocol_version(platform_version.protocol_version) + .build_with_mock_rpc() + .set_genesis_state(); + let mut rng = StdRng::seed_from_u64(9510); + let (identity, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(|configuration: &mut TokenConfiguration| { + configuration.set_max_supply_change_rules(contract_owner_rules()) + }), + None, + None, + None, + platform_version, + ); + let process_at_version_13 = |transition: StateTransition| { + let platform_state = platform.state.load(); + let transaction = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &[transition.serialize_to_bytes().expect("serialize")], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("process state transition"); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("commit"); + result + }; + + for item in [ + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(1), + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup( + AuthorizedActionTakers::ContractOwner, + ), + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup( + AuthorizedActionTakers::ContractOwner, + ), + ] { + let transition = token_config_update( + token_id, + contract.id(), + &identity, + &key, + &signer, + item, + 2, + platform_version, + ) + .await; + let result = process_at_version_13(transition); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::BasicError(BasicError::StateTransitionNotActiveError(_)) + )] + ); + } + + let max_supply = token_config_update( + token_id, + contract.id(), + &identity, + &key, + &signer, + TokenConfigurationChangeItem::MaxSupply(Some(1_000_000)), + 2, + platform_version, + ) + .await; + let result = process_at_version_13(max_supply); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index bdcd0b229b7..6c2e20576a8 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -1,5 +1,7 @@ use super::*; +mod minimum_pool_notes; + /// Token shielded pool transitions: shield, unshield and shielded transfer inside a batch. /// /// The bundles are real Orchard bundles proven with the shared proving key, so the tests cover @@ -1320,7 +1322,7 @@ mod token_shielded_pool_tests { } /// A contract carrying a token configuration with a shielded pool. - fn shielded_token_contract( + pub(super) fn shielded_token_contract( owner_id: Identifier, platform_version: &PlatformVersion, ) -> DataContract { @@ -2920,8 +2922,8 @@ mod document_shielded_token_payment_tests { use platform_version::version::PlatformVersion; use simple_signer::signer::SimpleSigner; - const CARD_COST: u64 = 10; - const SHIELDED: u64 = 15; + pub(super) const CARD_COST: u64 = 10; + pub(super) const SHIELDED: u64 = 15; /// The card game contract with an in-game currency: creating a `card` costs `CARD_COST` of /// token 0, burned (`burn`) or paid to the contract owner. `pool` gives token 0 a shielded pool. @@ -2964,7 +2966,7 @@ mod document_shielded_token_payment_tests { ) } - fn random_card( + pub(super) fn random_card( rng: &mut StdRng, card_document_type: DocumentTypeRef, owner_id: Identifier, @@ -2996,7 +2998,10 @@ mod document_shielded_token_payment_tests { (document, entropy) } - fn shielded_payment(bundle: OrchardBundleParams, amount: u64) -> TokenShieldedPayment { + pub(super) fn shielded_payment( + bundle: OrchardBundleParams, + amount: u64, + ) -> TokenShieldedPayment { TokenShieldedPayment { amount, actions: bundle.actions, @@ -3006,7 +3011,7 @@ mod document_shielded_token_payment_tests { } } - fn payment_info(shielded_payment: TokenShieldedPayment) -> TokenPaymentInfo { + pub(super) fn payment_info(shielded_payment: TokenShieldedPayment) -> TokenPaymentInfo { TokenPaymentInfo::V1(TokenPaymentInfoV1 { payment_token_contract_id: None, token_contract_position: 0, @@ -3026,7 +3031,7 @@ mod document_shielded_token_payment_tests { } /// Funds the buyer with `SHIELDED` tokens and shields all of them (identity contract nonce 1). - async fn fund_and_shield( + pub(super) async fn fund_and_shield( platform: &mut TempPlatform, contract: &DataContract, token_id: Identifier, @@ -3479,7 +3484,7 @@ mod token_pool_paid_transitions_tests { /// A credit pool note the test wallet holds: enough for a purchase at 0.01 DASH a token. pub(super) const CREDIT_NOTE: u64 = 5_000_000_000; - const SHIELDED: u64 = 15; + pub(super) const SHIELDED: u64 = 15; pub(super) struct Prover; @@ -3524,7 +3529,7 @@ mod token_pool_paid_transitions_tests { /// Funds the buyer with `SHIELDED` tokens and shields all of them (identity contract nonce /// 2, the contract creation used 1), then records an anchor for a `SHIELDED` note of the /// test wallet in the token pool. - async fn fund_token_pool( + pub(super) async fn fund_token_pool( platform: &mut TempPlatform, contract: &DataContract, token_id: Identifier, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_unshield_with_shielded_fee/transform_into_action/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_unshield_with_shielded_fee/transform_into_action/v0/mod.rs index 756543d25e7..091651be6a8 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_unshield_with_shielded_fee/transform_into_action/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_unshield_with_shielded_fee/transform_into_action/v0/mod.rs @@ -1,4 +1,5 @@ use crate::error::Error; +use crate::execution::validation::state_transition::batch::action_validation::token::token_shielded_pool_common::validate_minimum_token_pool_notes; use crate::execution::validation::state_transition::state_transitions::token_pool_paid_common::{ resolve_pooled_token, validate_credit_pool_fee_spend, validate_token_pool_holds, validate_token_pool_spend, @@ -26,9 +27,10 @@ impl TokenUnshieldWithShieldedFeeStateTransitionTransformIntoActionValidationV0 for TokenUnshieldWithShieldedFeeTransition { /// Both Orchard proofs and the fee floor were checked by the processor. Here the token - /// must own a pool and not be paused, the token bundle must spend recorded and unspent - /// notes of that pool, the fee bundle must spend recorded and unspent notes of the credit - /// pool which must hold what leaves it, and the transition's own rules apply. + /// must own a pool and not be paused, the pool must hold the notes the token's + /// configuration requires before tokens leave it, the token bundle must spend recorded and + /// unspent notes of that pool, the fee bundle must spend recorded and unspent notes of the + /// credit pool which must hold what leaves it, and the transition's own rules apply. fn transform_into_action_v0( &self, drive: &Drive, @@ -36,7 +38,7 @@ impl TokenUnshieldWithShieldedFeeStateTransitionTransformIntoActionValidationV0 platform_version: &PlatformVersion, ) -> Result, Error> { let TokenUnshieldWithShieldedFeeTransition::V0(v0) = self; - let (_contract, _configuration) = match resolve_pooled_token( + let (_contract, configuration) = match resolve_pooled_token( drive, v0.data_contract_id, v0.token_contract_position, @@ -57,6 +59,20 @@ impl TokenUnshieldWithShieldedFeeStateTransitionTransformIntoActionValidationV0 TokenTransferRecipientIdentityNotExistError::new(v0.recipient_id).into(), )); } + // The tokens leave the pool for a visible identity, as with `TokenUnshield`; only the + // fee comes from the credit pool. So the token's outgoing notes threshold applies here + // too, or a holder with credit pool notes could leave a pool below it. + let result = validate_minimum_token_pool_notes( + drive, + &v0.token_id.to_buffer(), + &configuration, + transaction, + &mut vec![], + platform_version, + )?; + if !result.is_valid() { + return Ok(ConsensusValidationResult::new_with_errors(result.errors)); + } let token_nullifiers: Vec<[u8; 32]> = v0.token_actions.iter().map(|a| a.nullifier).collect(); if let Some(rejection) = validate_token_pool_spend( diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/mod.rs index 281dbb2a997..6d035f63b43 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/mod.rs @@ -14,7 +14,6 @@ use dpp::data_contract::document_type::DocumentTypeRef; use dpp::prelude::IdentityNonce; use dpp::tokens::gas_fees_paid_by::GasFeesPaidBy; use dpp::tokens::token_amount_on_contract_token::DocumentActionTokenEffect; -use dpp::tokens::token_payment_info::v1::TokenShieldedPayment; use dpp::ProtocolError; use std::sync::Arc; @@ -110,7 +109,7 @@ impl DocumentBaseTransitionActionAccessorsV0 for DocumentBaseTransitionAction { } } - fn shielded_token_payment(&self) -> Option<&TokenShieldedPayment> { + fn shielded_token_payment(&self) -> Option<&DocumentShieldedTokenPayment> { match self { DocumentBaseTransitionAction::V0(v0) => v0.shielded_token_payment.as_deref(), } @@ -144,7 +143,7 @@ impl DocumentBaseTransitionAction { return vec![]; }; let contract_owner_id = self.data_contract_fetch_info_ref().contract.owner_id(); - if let Some(payment) = self.shielded_token_payment() { + if let Some(DocumentShieldedTokenPayment { payment, .. }) = self.shielded_token_payment() { let nullifiers = payment .actions .iter() diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/mod.rs index 7251d7796ff..f853d092136 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/mod.rs @@ -6,6 +6,7 @@ use dpp::balances::credits::TokenAmount; use dpp::data_contract::document_type::action_fees::agreement::DocumentActionFeeAgreement; use dpp::data_contract::document_type::action_fees::{ActionFeePricing, DocumentActionFee}; use dpp::data_contract::document_type::DocumentTypeRef; +use dpp::data_contract::TokenContractPosition; use dpp::identifier::Identifier; use dpp::prelude::IdentityNonce; use dpp::tokens::gas_fees_paid_by::GasFeesPaidBy; @@ -47,6 +48,19 @@ impl DeclaredDocumentActionFee { } } +/// A document action's token cost paid out of the token's shielded pool, with the token it is +/// paid in: the contract declaring the token (the document's own or the one its document type's +/// token cost names) and the token's position there. +#[derive(Debug, Clone)] +pub struct DocumentShieldedTokenPayment { + /// The spend bundle paying the cost (`TokenPaymentInfo::V1`) + pub payment: TokenShieldedPayment, + /// The contract declaring the token the cost is paid in + pub token_contract_id: Identifier, + /// The token's position in that contract + pub token_contract_position: TokenContractPosition, +} + #[derive(Debug, Clone)] /// document base transition action v0 pub struct DocumentBaseTransitionActionV0 { @@ -72,7 +86,7 @@ pub struct DocumentBaseTransitionActionV0 { pub declared_action_fee: Option>, /// The spend bundle paying `token_cost` out of the token's shielded pool instead of the /// owner's token balance (`TokenPaymentInfo::V1`). Only set when there is a token cost. - pub shielded_token_payment: Option>, + pub shielded_token_payment: Option>, } /// document base transition action accessors v0 @@ -113,7 +127,7 @@ pub trait DocumentBaseTransitionActionAccessorsV0 { /// to pay fn declared_action_fee_with_agreement(&self) -> Option; /// The shielded payment of the token cost, when the cost is paid out of the token's pool - fn shielded_token_payment(&self) -> Option<&TokenShieldedPayment>; + fn shielded_token_payment(&self) -> Option<&DocumentShieldedTokenPayment>; /// Whether the transition agrees to a discounted moderators part on a document type an /// elected contract moderates: the only place a discount may come from, the share of the /// contract's seated moderation charter. The batch transformer reads that share for every diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/transformer.rs index e5813dcb552..d033249678c 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/transformer.rs @@ -21,7 +21,7 @@ use dpp::tokens::token_payment_info::methods::v0::TokenPaymentInfoMethodsV0; use dpp::tokens::token_payment_info::v1::v1_accessors::TokenPaymentInfoAccessorsV1; use crate::drive::contract::DataContractFetchInfo; use crate::error::Error; -use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::{DeclaredDocumentActionFee, DocumentBaseTransitionActionV0}; +use crate::state_transition_action::batch::batched_transition::document_transition::document_base_transition_action::{DeclaredDocumentActionFee, DocumentBaseTransitionActionV0, DocumentShieldedTokenPayment}; impl DocumentBaseTransitionActionV0 { /// try from borrowed base transition with contract lookup @@ -131,7 +131,13 @@ impl DocumentBaseTransitionActionV0 { ), )); } - shielded_token_payment = Some(Box::new(payment.clone())); + shielded_token_payment = Some(Box::new(DocumentShieldedTokenPayment { + payment: payment.clone(), + token_contract_id: document_action_token_cost + .contract_id + .unwrap_or(data_contract_id), + token_contract_position: document_action_token_cost.token_contract_position, + })); } } else if let Some(token_payment_info) = value.token_payment_info_ref() { // A bundle with nothing to pay would be verified for free and never applied. diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs index 174eef77715..a8b3e0dd219 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs @@ -33,11 +33,10 @@ pub struct DriveAbciValidationConstants { /// transitions (Unshield, ShieldedWithdrawal) are allowed. This ensures a /// sufficient anonymity set before funds can leave the pool. pub minimum_pool_notes_for_outgoing: u64, - /// Minimum number of notes a TOKEN shielded pool must hold before a `TokenUnshield` - /// (the outflow with an observable destination) is allowed. Token pools start empty - /// and are small, so this is 0 at introduction: a floor would trap the first - /// depositors until enough other holders shielded. The knob exists so a later version - /// can raise it once per-token anonymity sets are meaningful. + /// Superseded and read by nothing: a token shielded pool's outgoing notes threshold is + /// the token configuration's `minimumPoolNotesForOutgoing`, set per token by its issuer + /// and bounded by `SystemLimits::max_token_pool_notes_for_outgoing`. Kept only so the + /// version tables stay as they are; do not read it for a token pool. pub minimum_token_pool_notes_for_outgoing: u64, /// Number of blocks of anchors to retain. Anchors older than this are /// pruned at the end of each block. Clients must use an anchor no older diff --git a/packages/rs-platform-version/src/version/mocks/v2_test.rs b/packages/rs-platform-version/src/version/mocks/v2_test.rs index 65e7732a2cb..8d342ced597 100644 --- a/packages/rs-platform-version/src/version/mocks/v2_test.rs +++ b/packages/rs-platform-version/src/version/mocks/v2_test.rs @@ -604,6 +604,7 @@ pub const TEST_PLATFORM_V2: PlatformVersion = PlatformVersion { max_contract_moderation_added_moderators: 15, max_token_redemption_cycles: 128, max_shielded_transition_actions: 16, + max_token_pool_notes_for_outgoing: 250, max_time_range_overlap_factor: None, max_time_range_ttl_seconds: None, min_time_range_ttl_drop_operations_per_write: None, diff --git a/packages/rs-platform-version/src/version/system_limits/mod.rs b/packages/rs-platform-version/src/version/system_limits/mod.rs index 06ff5101075..9c510aa65df 100644 --- a/packages/rs-platform-version/src/version/system_limits/mod.rs +++ b/packages/rs-platform-version/src/version/system_limits/mod.rs @@ -203,6 +203,13 @@ pub struct SystemLimits { // do this that much pub max_token_redemption_cycles: u32, pub max_shielded_transition_actions: u16, + /// Highest `minimumPoolNotesForOutgoing` a token's configuration may set. The threshold + /// refuses outflows from the token's shielded pool while the pool holds fewer notes, so + /// without an upper bound an issuer could set one no pool ever reaches and strand every + /// holder's shielded balance, irreversibly on a readonly contract. Read by the token + /// configuration validation of contract create and update and by `TokenConfigUpdate` + /// (protocol version 14), which never reach it before. + pub max_token_pool_notes_for_outgoing: u64, /// Maximum overlap factor (`range / step`) a `timeRange` index transform /// may declare, enforced at contract registration. /// diff --git a/packages/rs-platform-version/src/version/system_limits/v1.rs b/packages/rs-platform-version/src/version/system_limits/v1.rs index 067d537b3c5..2f9bb4b79ad 100644 --- a/packages/rs-platform-version/src/version/system_limits/v1.rs +++ b/packages/rs-platform-version/src/version/system_limits/v1.rs @@ -72,6 +72,7 @@ pub const SYSTEM_LIMITS_V1: SystemLimits = SystemLimits { // only becomes reachable if the size limit is raised. Pinned by dpp's // `seed_pool_batch_fits_max_state_transition_size` signing test. max_shielded_transition_actions: 16, + max_token_pool_notes_for_outgoing: 250, max_time_range_overlap_factor: None, max_time_range_ttl_seconds: None, min_time_range_ttl_drop_operations_per_write: None, diff --git a/packages/rs-platform-version/src/version/system_limits/v2.rs b/packages/rs-platform-version/src/version/system_limits/v2.rs index e4711614dd6..6f87814a013 100644 --- a/packages/rs-platform-version/src/version/system_limits/v2.rs +++ b/packages/rs-platform-version/src/version/system_limits/v2.rs @@ -53,6 +53,7 @@ pub const SYSTEM_LIMITS_V2: SystemLimits = SystemLimits { // only becomes reachable if the size limit is raised. Pinned by dpp's // `seed_pool_batch_fits_max_state_transition_size` signing test. max_shielded_transition_actions: 16, + max_token_pool_notes_for_outgoing: 250, max_time_range_overlap_factor: None, max_time_range_ttl_seconds: None, min_time_range_ttl_drop_operations_per_write: None, diff --git a/packages/rs-platform-version/src/version/system_limits/v3.rs b/packages/rs-platform-version/src/version/system_limits/v3.rs index 38769cc4c19..dc4072c8f4e 100644 --- a/packages/rs-platform-version/src/version/system_limits/v3.rs +++ b/packages/rs-platform-version/src/version/system_limits/v3.rs @@ -55,6 +55,7 @@ pub const SYSTEM_LIMITS_V3: SystemLimits = SystemLimits { // only becomes reachable if the size limit is raised. Pinned by dpp's // `seed_pool_batch_fits_max_state_transition_size` signing test. max_shielded_transition_actions: 16, + max_token_pool_notes_for_outgoing: 250, max_time_range_overlap_factor: None, max_time_range_ttl_seconds: None, min_time_range_ttl_drop_operations_per_write: None, diff --git a/packages/rs-platform-version/src/version/system_limits/v4.rs b/packages/rs-platform-version/src/version/system_limits/v4.rs index 3a224c6eda3..9f50533c9a4 100644 --- a/packages/rs-platform-version/src/version/system_limits/v4.rs +++ b/packages/rs-platform-version/src/version/system_limits/v4.rs @@ -72,6 +72,10 @@ use crate::version::system_limits::SystemLimits; /// seated team's leader add at most 15 members (`max_contract_moderation_added_moderators`), /// which joined this table in place while protocol version 14 was unreleased. A charter's /// description cap is the charter schema's own `maxBytes`, not a limit here. +/// * Token pool notes threshold (protocol version 14): a token configuration's +/// `minimumPoolNotesForOutgoing` is at most 250 (`max_token_pool_notes_for_outgoing`), the +/// credit pool's own threshold, backfilled into the earlier tables, which admit no token +/// configuration able to carry it. pub const SYSTEM_LIMITS_V4: SystemLimits = SystemLimits { estimated_contract_max_serialized_size: 16384, max_field_value_size: 5120, //5 KiB @@ -121,6 +125,7 @@ pub const SYSTEM_LIMITS_V4: SystemLimits = SystemLimits { // only becomes reachable if the size limit is raised. Pinned by dpp's // `seed_pool_batch_fits_max_state_transition_size` signing test. max_shielded_transition_actions: 16, + max_token_pool_notes_for_outgoing: 250, max_time_range_overlap_factor: Some(24), max_time_range_ttl_seconds: Some(604_800), // one week min_time_range_ttl_drop_operations_per_write: Some(32), diff --git a/packages/wasm-dpp2/src/tokens/configuration_change_item/token_configuration_change_item.rs b/packages/wasm-dpp2/src/tokens/configuration_change_item/token_configuration_change_item.rs index e43f8786948..c06ed5d72b3 100644 --- a/packages/wasm-dpp2/src/tokens/configuration_change_item/token_configuration_change_item.rs +++ b/packages/wasm-dpp2/src/tokens/configuration_change_item/token_configuration_change_item.rs @@ -133,6 +133,15 @@ impl TokenConfigurationChangeItemWasm { String::from("MarketplaceTradeModeAdminGroup") } TokenConfigurationChangeItem::MainControlGroup(_) => String::from("MainControlGroup"), + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(_) => { + String::from("MinimumPoolNotesForOutgoing") + } + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup(_) => { + String::from("MinimumPoolNotesForOutgoingControlGroup") + } + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(_) => { + String::from("MinimumPoolNotesForOutgoingAdminGroup") + } } } @@ -241,6 +250,15 @@ impl TokenConfigurationChangeItemWasm { TokenConfigurationChangeItem::MainControlGroup(group_contract_position) => { JsValue::from(group_contract_position) } + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(minimum_pool_notes) => { + JsValue::from(minimum_pool_notes) + } + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup( + action_takers, + ) => JsValue::from(AuthorizedActionTakersWasm::from(action_takers)), + TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(action_takers) => { + JsValue::from(AuthorizedActionTakersWasm::from(action_takers)) + } }; js_value.into() } From b84ed91ba9425bcae45efdd67a35c5afbaa8ee9e Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 24 Sep 2026 21:36:03 +0700 Subject: [PATCH 29/94] test(wallet): pin that two notes sharing a nullifier count once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A nullifier is what lets a note be spent exactly once. If two notes in a pool carry the same one, only one of them is ever spendable and the other's value is unreachable — so a wallet that added both to its balance would show funds the holder cannot move and would build transitions that fail against the chain. `save_note` already gives the right answer: it keys on the nullifier and overwrites. But that branch was written to make rescanning idempotent, and the comment justifying it says nullifiers are globally unique — which is precisely the assumption that does not hold when two notes collide. The protection is therefore incidental, and nothing recorded the outcome it produces. Someone later making rescans idempotent by other means could remove the overwrite as redundant, on exactly the reasoning the comment offers, and silently make the wallet double-count. This pins the outcome instead of the reasoning, so the guarantee survives a change to why the code is shaped that way. Test would have caught the regression: removing the overwrite branch fails it with "a second note sharing a nullifier must replace the first, not join it" (✖ without, ✔ with, ✔ again after restoring). The wallet suite runs with `--all-features` in CI, so the `shielded` feature this module needs is enabled there. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/wallet/shielded/store.rs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/packages/rs-platform-wallet/src/wallet/shielded/store.rs b/packages/rs-platform-wallet/src/wallet/shielded/store.rs index 3ead87dbe1f..6e20a0ae1fc 100644 --- a/packages/rs-platform-wallet/src/wallet/shielded/store.rs +++ b/packages/rs-platform-wallet/src/wallet/shielded/store.rs @@ -1285,6 +1285,63 @@ mod tests { assert!(!store.mark_spent(id, &nullifier).unwrap()); } + /// Two notes carrying one nullifier must count once. + /// + /// A nullifier is what makes a note spendable exactly once, so if two notes in a pool + /// share one, only one of them can ever be spent and the other's value is unreachable. + /// A wallet that added both to its balance would show funds it cannot move and would + /// build transitions that fail against the chain. + /// + /// `save_note` keys on the nullifier and overwrites, which gives the right answer. That + /// behaviour was written to make rescanning idempotent, and its reasoning — that + /// nullifiers are unique — is exactly what does not hold here. This pins the outcome + /// rather than the reasoning, so the guarantee survives a future rescan mechanism that + /// makes the original justification obsolete. + #[test] + fn should_count_two_notes_sharing_a_nullifier_once() { + let mut store = InMemoryShieldedStore::new(); + let id = test_id(0); + let nullifier = [7u8; 32]; + let note = ShieldedNote { + position: 3, + cmx: [9u8; 32], + nullifier, + block_height: 10, + is_spent: false, + value: 1_000, + note_data: vec![0u8; 115], + }; + // The same note observed again at a later tree position: same commitment, same + // nullifier, different provenance. + let twin = ShieldedNote { + position: 11, + block_height: 12, + ..note.clone() + }; + + store.save_note(id, ¬e).unwrap(); + store.save_note(id, &twin).unwrap(); + + assert_eq!( + store.get_all_notes(id).unwrap().len(), + 1, + "a second note sharing a nullifier must replace the first, not join it" + ); + assert_eq!( + store.spendable_balance(id).unwrap(), + 1_000, + "BALANCE INFLATION: the wallet counted both notes, but only one is spendable, \ + so it would offer the holder funds no transition can move" + ); + + // And the single entry really is the one note: spending it leaves nothing behind. + assert!(store.mark_spent(id, &nullifier).unwrap()); + assert!( + store.get_unspent_notes(id).unwrap().is_empty(), + "spending the shared nullifier must leave no second note live" + ); + } + /// Resolving any of a redrive's nullifiers — landing (`mark_spent`) /// or release (`clear_pending`) — drops the whole record: a /// transition lands or dies atomically for all its nullifiers. From 45c9ff8d5488fe99b094bd607b79cd72c93c42f0 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 24 Sep 2026 22:29:24 +0700 Subject: [PATCH 30/94] fix(platform): name what the batch cap protects and stop a group check resting on one call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three hazards a review found, none of which changes behaviour today. Each is a property that holds now and would be lost quietly later. **The documents-batch cap protects token shielded pools twice, and said so nowhere.** A test already refuses any table that raises `max_transitions_in_documents_batch` above one, so it cannot be raised in silence. But the field's documentation listed only phantom index groups and the two document-operation cases, so someone raising the cap would fail that test, read the list, fix what it names and ship — with both pool properties untouched. A guard that points the wrong way is worse than none, because it is believed. Both are now named there, with the shape of the real work. The pool's balance write is absolute rather than a delta, so two pool operations in one batch discard the first: value lost uniformly, with no disagreement between nodes to notice it. And an outputs-only bundle binds neither owner nor anchor, so the same authorized bytes fit in two shields of one batch — state validation runs per transition against the transaction, so the second cannot see the first's pending insert, and the within-bundle check covers one action set. The two inserts are then identical in path, key and value, which a node on the shipped batching default folds silently while a node verifying batch consistency refuses. The two disagree on one block and neither can say why. **A group check rested on a single call in the generation it was meant to supersede.** The threshold rule's authorized-takers were reached only through `all_used_group_positions` in v0, because v1 narrowed to the V0 base before its own loop. v1 is not a superset of v0 — it exists because v0 misses the distribution pricing rules — so the file reads as though v1 were the thorough one and v0 legacy. Making v1 stand alone, which is the natural reading, would have dropped the check with nothing failing to compile. It now iterates through the enum, so the coverage is doubled rather than single-threaded. **The nullifier tree's estimate ceiling is a halting condition, not a rounding one.** Sixteen levels model roughly 65 000 keys, and the tree now fills about twice as fast as that depth was chosen for: spends contribute one nullifier each, but every inflow bundle records a dummy per action, up to sixteen. Past the model the estimate falls below the actual cost and fee validation refuses the block. Written down where the level is set. Also: four call sites on consensus paths copied a 32-byte token id where a borrow of the same bytes was already used fifteen lines below in the same function. Green: dpp 4995, drive-abci token 482, platform-version 23, formatting clean. Co-Authored-By: Claude Opus 5 (1M context) --- .../v1/mod.rs | 13 +++++++------ .../state_v0/mod.rs | 2 +- .../state_v0/mod.rs | 2 +- .../state_v0/mod.rs | 2 +- .../state_v0/mod.rs | 2 +- .../tokens/shielded_pool/estimated_costs.rs | 8 ++++++++ .../src/version/system_limits/mod.rs | 19 +++++++++++++++++-- 7 files changed, 36 insertions(+), 12 deletions(-) diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_groups_exist/v1/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_groups_exist/v1/mod.rs index 88732208e9e..7a39e2b69f2 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_groups_exist/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_groups_exist/v1/mod.rs @@ -20,9 +20,12 @@ impl TokenConfiguration { return legacy_result; } - let configuration = self.as_v0(); - - for (_, rules) in configuration.all_change_control_rules() { + // Iterate through the enum rather than narrowing to the V0 base first. The narrowed + // view cannot see rules a later configuration version adds, and the v0 pass above is + // the only other place those are reached — through `all_used_group_positions`. Reading + // them here too means the coverage is doubled rather than resting on that one call, so + // making v0 stand alone, or making this loop stand alone, cannot silently drop a rule. + for (_, rules) in self.all_change_control_rules() { for action_takers in [ rules.authorized_to_make_change_action_takers(), rules.admin_action_takers(), @@ -35,9 +38,7 @@ impl TokenConfiguration { GroupPositionDoesNotExistError::new(*group_position).into(), ); } - AuthorizedActionTakers::MainGroup - if configuration.main_control_group().is_none() => - { + AuthorizedActionTakers::MainGroup if self.main_control_group().is_none() => { return SimpleConsensusValidationResult::new_with_error( MainGroupIsNotDefinedError::new().into(), ); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs index 9c5fe4c9073..6688799e1af 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs @@ -124,7 +124,7 @@ impl TokenClaimToPoolTransitionActionStateValidationV0 for TokenClaimToPoolTrans let validation_result = validate_token_pool_output_nullifiers( platform, - &token_id.to_buffer(), + token_id.as_bytes(), &self.nullifiers(), block_info, execution_context, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs index 5e90c60d586..f4bc1bb739e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs @@ -125,7 +125,7 @@ impl TokenDirectPurchaseToPoolTransitionActionStateValidationV0 let validation_result = validate_token_pool_output_nullifiers( platform, - &token_id.to_buffer(), + token_id.as_bytes(), &self.nullifiers(), block_info, execution_context, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs index 9fb49f50621..399054bd08e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs @@ -200,7 +200,7 @@ impl TokenMintToPoolTransitionActionStateValidationV0 for TokenMintToPoolTransit let validation_result = validate_token_pool_output_nullifiers( platform, - &token_id.to_buffer(), + token_id.as_bytes(), &self.nullifiers(), block_info, execution_context, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs index 4798f47fb21..9f0ff1f2ff2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs @@ -117,7 +117,7 @@ impl TokenShieldTransitionActionStateValidationV0 for TokenShieldTransitionActio let validation_result = validate_token_pool_output_nullifiers( platform, - &token_id.to_buffer(), + token_id.as_bytes(), &self.nullifiers(), block_info, execution_context, diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/estimated_costs.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/estimated_costs.rs index 4f2caecd8fe..84febc90ddd 100644 --- a/packages/rs-drive/src/drive/tokens/shielded_pool/estimated_costs.rs +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/estimated_costs.rs @@ -122,6 +122,14 @@ impl Drive { ); // Nullifiers tree: [16, 224, token_id, 64] + // + // The sixteen levels model roughly 65 000 keys, and the estimate stays at or above the + // real cost only while the tree is shallower than that. It fills about twice as fast as + // the depth was chosen for: spends contribute one nullifier each, but every inflow + // bundle now also records a dummy nullifier per action, up to sixteen. A pool that + // outgrows the model does not merely misprice — the estimate falls below the actual + // cost and fee validation refuses the block, so the ceiling is a halting condition and + // not a rounding one. Raise the level before a pool approaches it. estimated_costs_only_with_layer_info.insert( KeyInfoPath::from_known_owned_path(token_shielded_pool_nullifiers_path_vec(token_id)), EstimatedLayerInformation { diff --git a/packages/rs-platform-version/src/version/system_limits/mod.rs b/packages/rs-platform-version/src/version/system_limits/mod.rs index 9c510aa65df..3f52e58c304 100644 --- a/packages/rs-platform-version/src/version/system_limits/mod.rs +++ b/packages/rs-platform-version/src/version/system_limits/mod.rs @@ -99,6 +99,19 @@ pub struct SystemLimits { /// their siblings — which is why the withdrawal paths batch many documents safely. It is /// not a drop-in for batch transitions: it carries no delete variant. /// + /// * A token shielded pool leans on the cap twice, and neither is visible from the pool's + /// own code. Its balance write is absolute rather than a delta, so two pool operations in + /// one batch would silently discard the first — value lost on every node, no disagreement + /// to notice. And an outputs-only bundle binds neither owner nor anchor, so the same + /// authorized bytes can sit in two shields of one batch: state validation runs per + /// transition against the transaction before any operation applies, so the second cannot + /// see the first's pending insert, and the within-bundle check is scoped to one action + /// set. The two inserts are then byte-identical in path, key and value, which a node + /// running the shipped batching default folds in silence while a node verifying batch + /// consistency refuses — the two disagree on one block and neither shows why. Raising the + /// cap means batch-scoped nullifier deduplication and a delta-based pool balance write, + /// not just making the ignored cases above pass. + /// /// Five cases in `rs-drive`'s `batched_group_drain` suite are `#[ignore]`d for exactly this /// reason; the rest of that suite runs. Anyone raising this cap should un-ignore those five /// first and make them pass. @@ -292,7 +305,8 @@ mod tests { .max_transitions_in_documents_batch, 1, "protocol version {} allows more than one transition per documents batch; \ - see the documentation on SystemLimits::max_transitions_in_documents_batch \ + token shielded pools rely on this cap for two separate properties, so read \ + the documentation on SystemLimits::max_transitions_in_documents_batch \ for what that exposes", platform_version.protocol_version ); @@ -329,7 +343,8 @@ mod tests { .max_transitions_in_documents_batch, 1, "mock platform version {} allows more than one transition per documents \ - batch; see SystemLimits::max_transitions_in_documents_batch", + batch; token shielded pools rely on this cap for two separate properties, so \ + read SystemLimits::max_transitions_in_documents_batch", platform_version.protocol_version ); } From 8b4c29f0654f4fae9f1e6db665fd8e6649126b2a Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 24 Sep 2026 23:44:03 +0700 Subject: [PATCH 31/94] feat(platform)!: bind the owner into outputs-only token pool bundle sighashes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kind tag and token id stopped a bundle being lifted into another pool or another kind. They could not stop it being lifted into the same pool and the same kind by somebody else, because the preimage named nobody. The preimage now carries the owner: `tag (1) || token_id (32) || owner_id (32)`. That closes the last shape the binding can reach. What it leaves open is written down rather than implied: the owner cannot stop the owner, so a sender resubmitting its own bundle is still refused — by the nullifier record, as a paid failure — and an honest client retrying with a fresh nonce pays its fee a second time. A test pins that rather than leaving it to be discovered. A group mint binds the group action's proposer, not the batch owner, because the bundle a confirmer submits is the proposer's. Stateless admission cannot read the stored group action to learn who that is, so it skips the bundle of a non-proposing signer and leaves it to block execution, exactly as a group burn already does. This costs real admission-time checking: `action_is_proposer` is the submitter's own field, so anyone can set it false with invented group data and have an unverified bundle admitted to the mempool. Block validation then refuses it as a paid failure, and refuses a batch whose owner is not a member of the group, so the cost is bounded by fees — the same bound the existing burn and claim skips rely on. The comment at the skip says this, instead of reading as though only honest confirmers arrive there. The skip is safe only because block execution keeps no memory of what admission decided. A comment at the verification entry point now says so, and names the test that goes red if someone adds an admission cache: the stateless check reports a batch valid while skipping a confirmer's bundle entirely, so such a cache would mark an unverified bundle verified. `TokenPurchaseFromShieldedPool` is untouched. It is identity-less and has no owner to bind, so a copy of its token bundle remains constructible; the honest payer's refusal is unpaid and the copier pays, which a test states plainly rather than leaving as a gap. Both construction sites changed together — block validation and stateless admission build the preimage through one helper, so they cannot drift — and the four builders with them. Every negative was shown red by reverting consensus code alone, four different ways, each breaking a different set: dropping the owner from the layout; removing the confirmer skip; binding the batch owner instead of the proposer; and skipping the confirmer's verification at block execution. The same-owner repeat tests stay green under all four, which is what distinguishes them from the owner-binding tests. Green: dpp shielded 311, drive-abci shielded 239, drive-abci token 489, frozen discriminants 4, formatting clean, workspace clippy clean. Co-Authored-By: Claude Opus 5 (1M context) --- book/src/data-model/token-shielded-pools.md | 52 +- .../shielded/builder/token_claim_to_pool.rs | 1 + .../builder/token_direct_purchase_to_pool.rs | 1 + .../shielded/builder/token_mint_to_pool.rs | 6 + .../src/shielded/builder/token_shield.rs | 7 +- packages/rs-dpp/src/shielded/sighash.rs | 108 +- .../token_shield_transition/mod.rs | 4 +- .../batch_transition/methods/v1/mod.rs | 9 +- .../processor/traits/shielded_proof.rs | 17 + .../state_v0/mod.rs | 3 +- .../state_v0/mod.rs | 3 +- .../state_v0/mod.rs | 10 +- .../state_v0/mod.rs | 3 +- .../token/token_shielded_pool_common/mod.rs | 19 +- .../token/shielded/minimum_pool_notes.rs | 1 + .../batch/tests/token/shielded/mod.rs | 1875 ++++++++++++++--- .../rs-platform-version/src/version/v14.rs | 5 +- packages/wasm-dpp2/src/shielded/mod.rs | 10 +- 18 files changed, 1716 insertions(+), 418 deletions(-) diff --git a/book/src/data-model/token-shielded-pools.md b/book/src/data-model/token-shielded-pools.md index 74274dbc964..35a684ce5b0 100644 --- a/book/src/data-model/token-shielded-pools.md +++ b/book/src/data-model/token-shielded-pools.md @@ -78,13 +78,13 @@ fees, so unlike the credit pool nothing is carved from the bundle's value balanc | Transition | Flags | Value balance | Extra sighash data | Effect | |---|---|---|---|---| -| `TokenShield` | outputs only | `-amount` | `tag(0x80), token_id` | `amount` leaves the owner's balance and enters the pool as new notes. | +| `TokenShield` | outputs only | `-amount` | `tag(0x80), token_id, owner_id` | `amount` leaves the owner's balance and enters the pool as new notes. | | `TokenUnshield` | spends and outputs | `+amount` | `token_id, owner_id, recipient_id, amount` | Notes are spent; `amount` is credited to `recipient_id`; change comes back as new notes. | | `TokenShieldedTransfer` | spends and outputs | `0` | `token_id, owner_id` | Notes are spent and recreated; the pool balance is unchanged. | -| `TokenMintToPool` | outputs only | `-amount` | `tag(0x81), token_id` | An authorized minter (manual minting rules, group actions supported) mints `amount` into new notes; the supply and the pool balance grow. Allowed only where `mintingAllowChoosingDestination` is set, since the notes' recipients are the minter's choice. | +| `TokenMintToPool` | outputs only | `-amount` | `tag(0x81), token_id, minter_id` | An authorized minter (manual minting rules, group actions supported) mints `amount` into new notes; the supply and the pool balance grow. Allowed only where `mintingAllowChoosingDestination` is set, since the notes' recipients are the minter's choice. | | `TokenBurnFromPool` | spends and outputs | `+amount` | `token_id, burner_id, amount` | An authorized burner (manual burning rules, group actions supported) spends notes and destroys `amount`; the supply and the pool balance shrink. `burner_id` is the batch owner, or the proposer of a group action. | -| `TokenClaimToPool` | outputs only | `-amount` | `tag(0x82), token_id` | A distribution claim released into new notes instead of the claimant's balance; a perpetual claim names the cycle-aligned moment it claims up to so the amount is predictable. | -| `TokenDirectPurchaseToPool` | outputs only | `-token_count` | `tag(0x83), token_id` | The buyer pays credits at the direct purchase price and the tokens are minted into new notes. | +| `TokenClaimToPool` | outputs only | `-amount` | `tag(0x82), token_id, owner_id` | A distribution claim released into new notes instead of the claimant's balance; a perpetual claim names the cycle-aligned moment it claims up to so the amount is predictable. | +| `TokenDirectPurchaseToPool` | outputs only | `-token_count` | `tag(0x83), token_id, owner_id` | The buyer pays credits at the direct purchase price and the tokens are minted into new notes. | Each transition carries the Orchard bundle (`actions`, `anchor`, `proof`, `binding_signature`) next to the token base transition (`token_id`, contract id, contract position, identity @@ -92,7 +92,7 @@ contract nonce). The extra sighash data is bound into the Orchard sighash by the recomputed by consensus from the transition's own fields, so a bundle cannot be replayed against whatever its layout names. The layouts differ: the ones that spend bind the token, the owner and, where tokens leave the pool, the recipient and amount; the four that only create -notes bind their kind and the token, and nothing about who submits them. The layouts are in +notes bind their kind, the token and the owner. The layouts are in `dpp::shielded::sighash` (`token_unshield_extra_sighash_data`, `token_shielded_transfer_extra_sighash_data`, `token_burn_from_pool_extra_sighash_data` and `token_pool_output_only_extra_sighash_data`). @@ -101,24 +101,30 @@ Outputs-only bundles (shield, mint, claim, purchase) have no spends, so their an checked against the pool; the client builds them against the empty tree. Spending bundles must name an anchor the pool has recorded. -Because the preimage of an outputs-only bundle binds no owner, anybody can lift one out of the -mempool and submit it again as the same kind into the same pool. The copy would land a second -note with the same commitment and the same `rho`, hence the same nullifier, and only one of the -two could ever be spent. The pool refuses it on the state side: each action's dummy nullifier, -from which its note takes its `rho`, is recorded in the pool's nullifier tree when the bundle -enters, and a bundle whose dummy nullifier is already there is rejected with -`NullifierAlreadySpentError`. `TokenPurchaseFromShieldedPool` carries an outputs-only token -bundle too and is checked the same way. - -A mint or burn into the pool that goes through a group action stores -`TokenEvent::MintToPool` / `TokenEvent::BurnFromPool` with a digest of the serialized actions -(`serialized_actions_digest`), so every signer commits to exactly the same notes. A burn's -bundle is therefore proven once, by the proposer: the digest covers the spend authorization -signatures, so the sighash cannot depend on which signer's batch carries the bundle. It binds -the group action's proposer as `burner_id` (the batch owner for a direct burn) and every other -signer submits the proposer's bundle unchanged. No token history document is written for pool -operations, so the ledger records no actor for them. The operations still leave public traces: -spent nullifiers, the pool's note count and its anchors are all in state. +The preimage of an outputs-only bundle binds its owner — the batch owner, or for a group action +mint its proposer, whose bundle every other signer submits unchanged — so nobody else's +transition can land a bundle lifted out of the mempool ahead of its author. What the preimage +cannot stop is its owner submitting the same bundle again in a new transition. That repeat +would land a second note with the same commitment and the same `rho`, hence the same nullifier, +and only one of the two could ever be spent. The pool refuses it on the state side: each +action's dummy nullifier, from which its note takes its `rho`, is recorded in the pool's +nullifier tree when the bundle enters, and a bundle whose dummy nullifier is already there is +rejected with `NullifierAlreadySpentError`. `TokenPurchaseFromShieldedPool` carries an +outputs-only token bundle too and is checked the same way. + +A mint or burn into the pool that goes through a group action stores `TokenEvent::MintToPool` / +`TokenEvent::BurnFromPool` with a digest of the serialized actions +(`serialized_actions_digest`), so every signer commits to exactly the same notes. Either bundle +is therefore proven once, by the proposer: the digest fixes the bundle's actions and only the +bundle's builder can sign it, so every other signer submits the proposer's bundle as it is and +the sighash cannot depend on which signer's batch carries it. It binds the group action's +proposer — as `burner_id` for a burn, as `minter_id` for a mint, and the batch owner when there +is no group action. CheckTx verifies a batch's token bundles statelessly, seeing only the +transition and not the stored group action, so it skips the bundle of a signer other than the +proposer; state validation in the block verifies it against the proposer. No token history +document is written for pool operations, so the ledger records no actor for them. The +operations still leave public traces: spent nullifiers, the pool's note count and its anchors +are all in state. ## Documents paid from the pool diff --git a/packages/rs-dpp/src/shielded/builder/token_claim_to_pool.rs b/packages/rs-dpp/src/shielded/builder/token_claim_to_pool.rs index 19e1ba10b63..a17e5e93202 100644 --- a/packages/rs-dpp/src/shielded/builder/token_claim_to_pool.rs +++ b/packages/rs-dpp/src/shielded/builder/token_claim_to_pool.rs @@ -68,6 +68,7 @@ pub async fn build_token_claim_to_pool_transition< let extra_sighash_data = token_pool_output_only_extra_sighash_data( TokenTransitionActionType::ClaimToPool, token_id.as_bytes(), + owner_id.as_bytes(), platform_version, )?; let bundle = build_output_only_bundle( diff --git a/packages/rs-dpp/src/shielded/builder/token_direct_purchase_to_pool.rs b/packages/rs-dpp/src/shielded/builder/token_direct_purchase_to_pool.rs index 3d0f599f7fe..2d93c4c685c 100644 --- a/packages/rs-dpp/src/shielded/builder/token_direct_purchase_to_pool.rs +++ b/packages/rs-dpp/src/shielded/builder/token_direct_purchase_to_pool.rs @@ -57,6 +57,7 @@ pub async fn build_token_direct_purchase_to_pool_transition< let extra_sighash_data = token_pool_output_only_extra_sighash_data( TokenTransitionActionType::DirectPurchaseToPool, token_id.as_bytes(), + owner_id.as_bytes(), platform_version, )?; let bundle = build_output_only_bundle( diff --git a/packages/rs-dpp/src/shielded/builder/token_mint_to_pool.rs b/packages/rs-dpp/src/shielded/builder/token_mint_to_pool.rs index d5668e21be0..ef1f86a3b6b 100644 --- a/packages/rs-dpp/src/shielded/builder/token_mint_to_pool.rs +++ b/packages/rs-dpp/src/shielded/builder/token_mint_to_pool.rs @@ -20,6 +20,11 @@ use super::{build_output_only_bundle, serialize_authorized_bundle, OrchardProver /// `amount` of the token as a note for `recipient` inside the token's shielded pool, then wraps /// it in a batch transition signed by `owner_id`, who must be authorized to mint and pays the /// fee in credits. `using_group_info` sets up a group action exactly like a transparent mint. +/// +/// The bundle's sighash binds `owner_id` as the minter, which is right for a direct mint and for +/// the proposal of a group action. Every other signer of a group action must submit the +/// proposer's bundle unchanged, through `new_token_mint_to_pool_transition`, rather than prove +/// a bundle of its own here. #[allow(clippy::too_many_arguments)] pub async fn build_token_mint_to_pool_transition, P: OrchardProver>( token_id: Identifier, @@ -55,6 +60,7 @@ pub async fn build_token_mint_to_pool_transition, P let extra_sighash_data = token_pool_output_only_extra_sighash_data( TokenTransitionActionType::MintToPool, token_id.as_bytes(), + owner_id.as_bytes(), platform_version, )?; let bundle = build_output_only_bundle( diff --git a/packages/rs-dpp/src/shielded/builder/token_shield.rs b/packages/rs-dpp/src/shielded/builder/token_shield.rs index a38ac116373..968520ecac0 100644 --- a/packages/rs-dpp/src/shielded/builder/token_shield.rs +++ b/packages/rs-dpp/src/shielded/builder/token_shield.rs @@ -22,9 +22,9 @@ use super::{build_output_only_bundle, serialize_authorized_bundle, OrchardProver /// The identity pays the fee in credits; `amount` tokens leave its balance for the pool at /// execution. The bundle has no spends, so its anchor is never checked against a pool, and the /// identity signature over the batch only binds it inside *this* batch — the authorized bundle -/// bytes on their own would verify in any pool. The extra sighash data pins them to this token -/// and to the shield kind. `sender_ovk` lets the sending wallet recover the note it created -/// (recipient, value, memo) from chain data. +/// bytes on their own would verify in any pool. The extra sighash data pins them to this token, +/// to the shield kind and to `owner_id`. `sender_ovk` lets the sending wallet recover the note +/// it created (recipient, value, memo) from chain data. #[allow(clippy::too_many_arguments)] pub async fn build_token_shield_transition, P: OrchardProver>( token_id: Identifier, @@ -58,6 +58,7 @@ pub async fn build_token_shield_transition, P: Orch let extra_sighash_data = token_pool_output_only_extra_sighash_data( TokenTransitionActionType::Shield, token_id.as_bytes(), + owner_id.as_bytes(), platform_version, )?; let bundle = build_output_only_bundle( diff --git a/packages/rs-dpp/src/shielded/sighash.rs b/packages/rs-dpp/src/shielded/sighash.rs index ae3a5288c7c..c354a7af9a8 100644 --- a/packages/rs-dpp/src/shielded/sighash.rs +++ b/packages/rs-dpp/src/shielded/sighash.rs @@ -640,45 +640,53 @@ pub fn token_shielded_transfer_extra_sighash_data_v0( } /// Extra sighash data of an outputs-only token pool bundle — `TokenShield`, `TokenMintToPool`, -/// `TokenClaimToPool` and `TokenDirectPurchaseToPool`: the bundle's domain tag and the token id. +/// `TokenClaimToPool` and `TokenDirectPurchaseToPool`: the bundle's domain tag, the token id +/// and the identity the bundle is attributed to. /// /// These bundles have no spends, so nothing pins them to a pool: their anchor is never checked /// against one — the client builds it against the empty tree — and the proof and binding -/// signature verify against any pool. Without this data the authorized bundle bytes are a free-standing, -/// self-verifying object that anybody can lift out of the mempool into a transition of their -/// own, against a different pool or a different transition kind. The copier supplies whatever -/// that kind demands of them — their own tokens, credits or a distribution entitlement — except -/// for a mint, which demands the token's mint authority and creates new supply rather than -/// moving any. +/// signature verify against any pool. Without this data the authorized bundle bytes are a +/// free-standing, self-verifying object that anybody can lift out of the mempool into a +/// transition of their own. Each of the three fields closes one direction of that: /// -/// What that copy costs depends on where it lands, because nullifiers are namespaced per pool: +/// - **The token id** keeps a bundle out of every other token's pool. That pool has its own +/// nullifier tree, so a copy there would leave both notes spendable; the harm is that the +/// recipient would hold notes derived from one `rho` in two pools, which links their spends +/// across pools. +/// - **The tag** keeps the kinds' preimages disjoint in the same pool. Bundles of different +/// kinds are otherwise indistinguishable to the proof — same flags, same empty-tree anchor, +/// and the same value balance whenever the amounts match. A same-pool reuse is also caught by +/// the nullifier record described below. +/// - **The owner** keeps it out of every other identity's transition of the same kind into the +/// same pool. Without it a copier could take a bundle from the mempool and land it ahead of +/// its author, funding the notes the author built with its own tokens, credits or claim, or +/// minting them with its own authority. The author's own transition would then be refused on +/// its recorded dummy nullifier and charged its fee. /// -/// - **Another kind, same pool.** The copy shares the original's nullifier tree, so it lands a -/// second note with the same commitment and the same `rho`, hence the same nullifier: only -/// one of the two can ever be spent. That is Faerie Gold. A wallet that merges notes by -/// nullifier is safe, but any wallet or indexer counting by commitment sees two payments -/// where one is real. Bundles of different kinds are otherwise indistinguishable to the -/// proof — same flags, same empty-tree anchor, and the same value balance whenever the -/// amounts match — so only the tag tells them apart. -/// - **Another pool.** That pool has its own nullifier tree, so both notes stay spendable and -/// nothing is Faerie Gold. The harm is that the recipient now holds notes derived from one -/// `rho` in two pools, which links their spends across pools. +/// `owner_id` is the batch owner, except for a group action mint, where it is the group +/// action's proposer: a group action pins the digest of the bundle's actions, so every other +/// signer submits the proposer's bundle unchanged and the preimage must not depend on whose +/// batch carries it. `TokenShield`, `TokenClaimToPool` and `TokenDirectPurchaseToPool` are +/// never group actions. /// -/// Not covered by this preimage: a copy into the same pool *and* the same kind, submitted by -/// someone else. That is the larger Faerie Gold vector, and the preimage cannot reach it -/// because it binds no owner. Consensus closes it on the state side instead: every token pool -/// write that takes an outputs-only bundle records the bundle's dummy nullifiers in the pool's -/// nullifier tree, and every such transition is refused with `NullifierAlreadySpentError` when -/// one of them is already there. The credit pool's outputs-only paths do not do this. +/// The preimage does not stop the owner from submitting its own bundle twice. That, and any +/// other repeat of a bundle into the same pool, consensus refuses on the state side: every +/// token pool write that takes an outputs-only bundle records the bundle's dummy nullifiers in +/// the pool's nullifier tree, and a bundle whose dummy nullifier is already there is refused +/// with `NullifierAlreadySpentError`. A repeat that got through would land a second note with +/// the same commitment and the same `rho`, hence the same nullifier, of which only one could +/// ever be spent. pub fn token_pool_output_only_extra_sighash_data( action_type: TokenTransitionActionType, token_id: &[u8; 32], + owner_id: &[u8; 32], platform_version: &PlatformVersion, ) -> Result, ProtocolError> { match platform_version.dpp.methods.shielded_extra_sighash_data { 0 => Ok(token_pool_output_only_extra_sighash_data_v0( outputs_only_bundle_tag_v0(action_type)?, token_id, + owner_id, )), version => Err(ProtocolError::UnknownVersionMismatch { method: "token_pool_output_only_extra_sighash_data".to_string(), @@ -728,15 +736,17 @@ fn outputs_only_bundle_tag_v0(action_type: TokenTransitionActionType) -> Result< } } -/// Version 0 layout: `bundle tag (1) || token_id (32)`. Frozen: never mutate; a layout change -/// requires a new `_v1` + version bump. +/// Version 0 layout: `bundle tag (1) || token_id (32) || owner_id (32)`. Frozen: never +/// mutate; a layout change requires a new `_v1` + version bump. pub fn token_pool_output_only_extra_sighash_data_v0( bundle_tag: u8, token_id: &[u8; 32], + owner_id: &[u8; 32], ) -> Vec { - let mut data = Vec::with_capacity(1 + 32); + let mut data = Vec::with_capacity(1 + 32 + 32); data.push(bundle_tag); data.extend_from_slice(token_id); + data.extend_from_slice(owner_id); data } @@ -1104,6 +1114,7 @@ mod tests { // Every byte here is consensus: a kind that changes its tag invalidates every bundle // already proved for it, so the four are pinned literally, not to the constants. let token_id = [7u8; 32]; + let owner_id = [5u8; 32]; let version = PlatformVersion::latest(); for (action_type, tag) in [ (TokenTransitionActionType::Shield, 0x80u8), @@ -1111,14 +1122,20 @@ mod tests { (TokenTransitionActionType::ClaimToPool, 0x82), (TokenTransitionActionType::DirectPurchaseToPool, 0x83), ] { - let mut expected = Vec::with_capacity(33); + let mut expected = Vec::with_capacity(65); expected.push(tag); expected.extend_from_slice(&token_id); + expected.extend_from_slice(&owner_id); assert_eq!( - token_pool_output_only_extra_sighash_data(action_type, &token_id, version) - .expect("outputs-only kind"), + token_pool_output_only_extra_sighash_data( + action_type, + &token_id, + &owner_id, + version + ) + .expect("outputs-only kind"), expected, - "v0 layout is frozen for {action_type}: tag (1) || token_id (32)" + "v0 layout is frozen for {action_type}: tag (1) || token_id (32) || owner_id (32)" ); } } @@ -1131,12 +1148,14 @@ mod tests { let a = token_pool_output_only_extra_sighash_data( TokenTransitionActionType::Shield, &[1u8; 32], + &[5u8; 32], version, ) .expect("shield tag"); let b = token_pool_output_only_extra_sighash_data( TokenTransitionActionType::Shield, &[2u8; 32], + &[5u8; 32], version, ) .expect("shield tag"); @@ -1148,10 +1167,12 @@ mod tests { #[test] fn outputs_only_token_pool_sighash_data_separates_transition_kinds() { - // All four kinds bind the same token id, so only the tag keeps a shield bundle from - // being resubmitted as a mint, a claim or a purchase into the very same pool. + // All four kinds bind the same token id and the same owner, so only the tag keeps a + // shield bundle from being resubmitted by its owner as a mint, a claim or a purchase + // into the very same pool. let version = PlatformVersion::latest(); let token_id = [9u8; 32]; + let owner_id = [5u8; 32]; let built: Vec> = [ TokenTransitionActionType::Shield, TokenTransitionActionType::MintToPool, @@ -1160,7 +1181,7 @@ mod tests { ] .into_iter() .map(|action_type| { - token_pool_output_only_extra_sighash_data(action_type, &token_id, version) + token_pool_output_only_extra_sighash_data(action_type, &token_id, &owner_id, version) .expect("outputs-only kind") }) .collect(); @@ -1197,8 +1218,13 @@ mod tests { TokenTransitionActionType::Unshield, TokenTransitionActionType::ShieldedTransfer, ] { - let error = token_pool_output_only_extra_sighash_data(action_type, &[1u8; 32], version) - .expect_err("only outputs-only pool bundles have a tag"); + let error = token_pool_output_only_extra_sighash_data( + action_type, + &[1u8; 32], + &[5u8; 32], + version, + ) + .expect_err("only outputs-only pool bundles have a tag"); assert!( matches!(error, ProtocolError::InvalidStateTransitionType(_)), "{action_type} must be refused, got {error:?}" @@ -1209,11 +1235,11 @@ mod tests { #[test] fn outputs_only_token_pool_tags_cannot_collide_with_state_transition_types() { // The tags share a preimage slot with the `StateTransitionType` byte the identity-less - // token bundles commit to, and one of them — `TokenShieldedTransferWithShieldedFee` — - // writes it at the same 1 + 32 length, so a colliding tag would leave the two preimages - // indistinguishable. Asking the enum itself — rather - // than comparing against the three types that exist today — is what makes this break - // on the day someone assigns a transition type inside the tag range. + // token bundles commit to, and one of those layouts — the credit pool fee bundle's + // `type || token_id || token actions digest` — has the same 1 + 32 + 32 length, so a + // colliding tag would make the two layouts byte-for-byte the same shape. Asking the enum + // itself — rather than comparing against the three types that exist today — is what + // makes this break on the day someone assigns a transition type inside the tag range. for tag in [ TOKEN_SHIELD_BUNDLE_TAG, TOKEN_MINT_TO_POOL_BUNDLE_TAG, diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/mod.rs index d86057248b8..1e57bc1a747 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_shield_transition/mod.rs @@ -14,8 +14,8 @@ pub use v0::TokenShieldTransitionV0; /// The Orchard bundle is outputs-only (spends disabled) with a value balance of `-amount`: the /// new notes are created for whoever the owner addressed them to, and nothing is spent from the /// pool. The identity signature binds the bundle inside this batch only, and an outputs-only -/// bundle has no anchor pinning it to a pool, so the bundle's own sighash binds the shield tag -/// and the token id — see `token_pool_output_only_extra_sighash_data`. +/// bundle has no anchor pinning it to a pool, so the bundle's own sighash binds the shield tag, +/// the token id and the batch owner — see `token_pool_output_only_extra_sighash_data`. #[derive(Debug, Clone, Encode, Decode, PartialEq, Display, From, DecodeUntrusted)] #[cfg_attr( feature = "serde-conversion", diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs index de5e7bcedcc..dc0108a3b67 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/methods/v1/mod.rs @@ -153,8 +153,8 @@ pub trait DocumentsBatchTransitionMethodsV1: DocumentsBatchTransitionAccessorsV0 /// /// `bundle` is the outputs-only Orchard bundle (value balance `-amount`) the client proved /// for the token pool. Having no spends it carries no anchor, so its sighash must bind the - /// shield tag and the token id — see `token_pool_output_only_extra_sighash_data`; a bundle - /// proved without them is rejected. + /// shield tag, the token id and `owner_id` — see `token_pool_output_only_extra_sighash_data`; + /// a bundle proved without them is rejected. #[cfg(feature = "state-transition-signing")] #[allow(clippy::too_many_arguments)] async fn new_token_shield_transition>( @@ -175,6 +175,11 @@ pub trait DocumentsBatchTransitionMethodsV1: DocumentsBatchTransitionAccessorsV0 /// Creates a `StateTransition` minting `amount` of a token straight into the token's shielded /// pool as the notes in `bundle`. `owner_id` must be authorized to mint; it signs and pays the /// credits fee. A group action is set up exactly like a transparent mint. + /// + /// The bundle's sighash must bind the mint tag, the token id and the minter — see + /// `token_pool_output_only_extra_sighash_data`. The minter is `owner_id` for a direct mint + /// and the proposer for a group action, so every other signer passes the proposer's bundle + /// unchanged. #[cfg(feature = "state-transition-signing")] #[allow(clippy::too_many_arguments)] async fn new_token_mint_to_pool_transition>( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs index f7eed742dc2..ce95fbea8fa 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs @@ -932,6 +932,7 @@ fn validate_batch_token_shielded_proofs( let extra_sighash_data = token_pool_output_only_extra_sighash_data( TokenTransitionActionType::Shield, &t.base().token_id().to_buffer(), + &owner_id, platform_version, )?; reconstruct_and_verify_bundle( @@ -979,9 +980,24 @@ fn validate_batch_token_shielded_proofs( ) } BatchedTransitionRef::Token(TokenTransition::MintToPool(t)) => { + if t.base() + .using_group_info() + .is_some_and(|info| !info.action_is_proposer) + { + // A confirmer reuses the proposer's bundle, whose sighash binds the + // proposer. The proposer is only in the stored group action, which this + // stateless check cannot see, so binding this batch's owner here would refuse + // every honest confirmer. Whether a signer is the proposer, though, + // is the submitter's own field, so anyone can set it false and reach this skip + // with any bundle. That costs nothing here because state validation verifies + // this bundle against the stored proposer regardless of what admission did, and + // refuses a batch whose owner is not a member of the group. + continue; + } let extra_sighash_data = token_pool_output_only_extra_sighash_data( TokenTransitionActionType::MintToPool, &t.base().token_id().to_buffer(), + &owner_id, platform_version, )?; reconstruct_and_verify_bundle( @@ -1026,6 +1042,7 @@ fn validate_batch_token_shielded_proofs( let extra_sighash_data = token_pool_output_only_extra_sighash_data( TokenTransitionActionType::DirectPurchaseToPool, &t.base().token_id().to_buffer(), + &owner_id, platform_version, )?; reconstruct_and_verify_bundle( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs index 6688799e1af..1dd3b19e48e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs @@ -136,10 +136,11 @@ impl TokenClaimToPoolTransitionActionStateValidationV0 for TokenClaimToPoolTrans } // An outputs-only bundle has no anchor of its own, so the sighash is what pins it to - // this pool and this kind — see `token_pool_output_only_extra_sighash_data`. + // this pool, this kind and this owner — see `token_pool_output_only_extra_sighash_data`. let extra_sighash_data = token_pool_output_only_extra_sighash_data( TokenTransitionActionType::ClaimToPool, token_id.as_bytes(), + owner_id.as_bytes(), platform_version, )?; verify_token_pool_bundle( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs index f4bc1bb739e..9ee6d3f9761 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs @@ -137,10 +137,11 @@ impl TokenDirectPurchaseToPoolTransitionActionStateValidationV0 } // An outputs-only bundle has no anchor of its own, so the sighash is what pins it to - // this pool and this kind — see `token_pool_output_only_extra_sighash_data`. + // this pool, this kind and this owner — see `token_pool_output_only_extra_sighash_data`. let extra_sighash_data = token_pool_output_only_extra_sighash_data( TokenTransitionActionType::DirectPurchaseToPool, token_id.as_bytes(), + owner_id.as_bytes(), platform_version, )?; verify_token_pool_bundle( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs index 399054bd08e..4d77bcc752c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs @@ -212,10 +212,18 @@ impl TokenMintToPoolTransitionActionStateValidationV0 for TokenMintToPoolTransit } // An outputs-only bundle has no anchor of its own, so the sighash is what pins it to - // this pool and this kind — see `token_pool_output_only_extra_sighash_data`. + // this pool, this kind and its minter — see `token_pool_output_only_extra_sighash_data`. + // A group action's bundle is proven once by the proposer and submitted unchanged by every + // other signer (the digest check above pins it), so the sighash binds the proposer rather + // than the batch owner. A direct mint binds the batch owner. + let minter_id = match self.base().original_group_action() { + Some(original_group_action) => original_group_action.proposer_id(), + None => owner_id, + }; let extra_sighash_data = token_pool_output_only_extra_sighash_data( TokenTransitionActionType::MintToPool, token_id.as_bytes(), + minter_id.as_bytes(), platform_version, )?; verify_token_pool_bundle( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs index 9f0ff1f2ff2..7264b41f63f 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs @@ -130,10 +130,11 @@ impl TokenShieldTransitionActionStateValidationV0 for TokenShieldTransitionActio // Outputs-only bundle entering the pool: value balance is `-amount`. // An outputs-only bundle has no anchor of its own, so the sighash is what pins it to - // this pool and this kind — see `token_pool_output_only_extra_sighash_data`. + // this pool, this kind and this owner — see `token_pool_output_only_extra_sighash_data`. let extra_sighash_data = token_pool_output_only_extra_sighash_data( TokenTransitionActionType::Shield, token_id.as_bytes(), + owner_id.as_bytes(), platform_version, )?; verify_token_pool_bundle( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs index 1e4231aec1a..551ce489b0f 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shielded_pool_common/mod.rs @@ -169,13 +169,13 @@ pub(crate) fn validate_token_pool_nullifiers( /// An outputs-only bundle's dummy nullifiers must not repeat within the bundle or already be /// recorded in the token pool. The reads are charged to the batch owner. /// -/// Such a bundle binds no owner and no anchor, so the same authorized bytes can be submitted -/// again, by anybody, as the same kind into the same pool. Each of its notes takes its `rho` -/// from its action's dummy nullifier, so the copy would land a second note with the same -/// commitment and the same nullifier, of which only one could ever be spent. Every token pool -/// write that takes an outputs-only bundle records these nullifiers; this is the check that -/// makes the record refuse anything, since the insert itself does not look for an existing -/// entry. +/// Such a bundle has no anchor, and its sighash binds the pool, the kind and the owner but not +/// a single use, so its owner can submit the same authorized bytes again in a new transition. +/// Each of its notes takes its `rho` from its action's dummy nullifier, so the repeat would +/// land a second note with the same commitment and the same nullifier, of which only one could +/// ever be spent. Every token pool write that takes an outputs-only bundle records these +/// nullifiers; this is the check that makes the record refuse anything, since the insert itself +/// does not look for an existing entry. #[allow(clippy::too_many_arguments)] pub(crate) fn validate_token_pool_output_nullifiers( platform: &PlatformStateRef, @@ -257,6 +257,11 @@ pub(crate) fn verify_token_pool_bundle( ) -> Result { // The verification fee was charged when the action was built (see the token pool action // transformers), so CheckTx admission and block execution price it once and identically. + // Block execution verifies every bundle, with no memory of what admission decided. That is + // load-bearing, not redundant: the stateless admission check skips the bundle of a group + // action's non-proposing signer entirely and still reports the batch valid, so a cache of + // "admission accepted this" would mark an unverified bundle verified. Pinned by + // `test_a_group_mint_to_pool_confirmation_with_a_tampered_proof_is_refused`. if !matches!(validation_mode, ValidationMode::Validator) { return Ok(SimpleConsensusValidationResult::new()); } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/minimum_pool_notes.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/minimum_pool_notes.rs index f61e95e14e3..391173bab93 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/minimum_pool_notes.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/minimum_pool_notes.rs @@ -169,6 +169,7 @@ async fn shield( seed, TokenTransitionActionType::Shield, token_id, + owner.id(), ), key, nonce, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 6c2e20576a8..759f60c6a06 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -80,15 +80,16 @@ mod token_shielded_pool_tests { ) } - /// An outputs-only bundle paying `amount` to the test wallet. - /// Builds the outputs-only bundle of a token pool transition that only creates notes. - /// `action_type` and `token_id` are what the bundle's sighash commits to, so a bundle built - /// for one pool or one transition kind will not verify as another. + /// Builds the outputs-only bundle of a token pool transition that only creates notes, + /// paying `amount` to the test wallet. `action_type`, `token_id` and `owner_id` are what the + /// bundle's sighash commits to, so a bundle built for one pool, one transition kind or one + /// owner will not verify as another. pub(super) fn build_shield_bundle( amount: u64, seed: u64, action_type: TokenTransitionActionType, token_id: Identifier, + owner_id: Identifier, ) -> OrchardBundleParams { let mut rng = StdRng::seed_from_u64(seed); let (fvk, _, address) = spend_keys(); @@ -112,6 +113,7 @@ mod token_shielded_pool_tests { let extra_sighash_data = token_pool_output_only_extra_sighash_data( action_type, &token_id.to_buffer(), + &owner_id.to_buffer(), PlatformVersion::latest(), ) .expect("outputs-only token pool sighash data"); @@ -354,11 +356,13 @@ mod token_shielded_pool_tests { ); } - /// The counterpart of [`assert_check_tx_accepts`] for an identity that cannot pay. + /// The counterpart of [`assert_check_tx_accepts`]: CheckTx must refuse the transition, and + /// the errors it refuses it with are returned. /// - /// The shielded compute fee is charged when the action is built, which happens inside - /// CheckTx, so an identity that cannot cover it is refused at admission rather than after - /// the node has already run the Halo 2 verification the batch asks for. + /// One case is an identity that cannot pay. The shielded compute fee is charged when the + /// action is built, which happens inside CheckTx, so an identity that cannot cover it is + /// refused at admission rather than after the node has already run the Halo 2 verification + /// the batch asks for. pub(super) fn assert_check_tx_rejects( platform: &TempPlatform, transition: &StateTransition, @@ -382,7 +386,7 @@ mod token_shielded_pool_tests { .expect("check tx"); assert!( !result.is_valid(), - "CheckTx admitted a transition the identity cannot pay for" + "CheckTx admitted a transition it must refuse" ); result.errors } @@ -426,6 +430,7 @@ mod token_shielded_pool_tests { 11, TokenTransitionActionType::Shield, token_id, + identity.id(), ); let shield = BatchTransition::new_token_shield_transition( token_id, @@ -772,6 +777,7 @@ mod token_shielded_pool_tests { 13, TokenTransitionActionType::Shield, token_id, + identity.id(), ); assert_eq!( bundle.actions.len(), @@ -1075,11 +1081,11 @@ mod token_shielded_pool_tests { /// An outputs-only bundle carries no anchor, so nothing in the proved bytes themselves says /// which pool they were built for: the proof and the binding signature verify against every /// token pool, all of which start from the same empty-tree anchor. Only the sighash tells - /// them apart. Without it anyone could lift a shield bundle out of the mempool into a shield - /// of their own, funded by their own tokens, and land a copy of the note in a pool it was - /// never meant for. Nullifiers are per pool, so both copies stay spendable and this is not - /// Faerie Gold; the harm is that one `rho` now yields notes in two pools, which links the - /// recipient's spends across them. + /// them apart. Without the token id in it, whoever the bundle names as its owner could submit + /// it again as a shield of another token, funded by that token, and land a copy of the note + /// in a pool it was never meant for. Nullifiers are per pool, so both copies stay spendable + /// and this is not Faerie Gold; the harm is that one `rho` now yields notes in two pools, + /// which links the recipient's spends across them. #[tokio::test] async fn test_token_shield_rejects_a_bundle_proved_for_another_tokens_pool() { let platform_version = PlatformVersion::latest(); @@ -1112,12 +1118,14 @@ mod token_shielded_pool_tests { assert_ne!(token_a, token_b, "the two tokens must own separate pools"); // Proved for the victim's pool, then replayed into the attacker's own, paid for with - // the attacker's own tokens. + // the attacker's own tokens. The bundle names the attacker as its owner, so the pool is + // the only thing about it that does not match the replay. let stolen_bundle = build_shield_bundle( SHIELD_AMOUNT, 31, TokenTransitionActionType::Shield, token_a, + attacker.id(), ); let replay = BatchTransition::new_token_shield_transition( token_b, @@ -1196,6 +1204,7 @@ mod token_shielded_pool_tests { 23, TokenTransitionActionType::Shield, token_a, + identity.id(), ), &key, 2, @@ -1664,8 +1673,13 @@ mod token_pool_mint_burn_claim_purchase_tests { let supply_before = total_supply(&platform, token_id); // Same pool, same amount, same flags — only the transition kind differs. - let shield_bundle = - build_shield_bundle(1_337, 21, TokenTransitionActionType::Shield, token_id); + let shield_bundle = build_shield_bundle( + 1_337, + 21, + TokenTransitionActionType::Shield, + token_id, + identity.id(), + ); let mint = BatchTransition::new_token_mint_to_pool_transition( token_id, identity.id(), @@ -1720,8 +1734,13 @@ mod token_pool_mint_burn_claim_purchase_tests { platform_version, ); - let bundle = - build_shield_bundle(1_337, 21, TokenTransitionActionType::MintToPool, token_id); + let bundle = build_shield_bundle( + 1_337, + 21, + TokenTransitionActionType::MintToPool, + token_id, + identity.id(), + ); let mint = BatchTransition::new_token_mint_to_pool_transition( token_id, identity.id(), @@ -1800,7 +1819,13 @@ mod token_pool_mint_burn_claim_purchase_tests { contract.id(), 0, 1_337, - build_shield_bundle(1_337, 27, TokenTransitionActionType::MintToPool, token_id), + build_shield_bundle( + 1_337, + 27, + TokenTransitionActionType::MintToPool, + token_id, + identity.id(), + ), None, None, &key, @@ -1948,8 +1973,13 @@ mod token_pool_mint_burn_claim_purchase_tests { let token = token_id.to_buffer(); // Fund the pool first. - let shield_bundle = - build_shield_bundle(10_000, 22, TokenTransitionActionType::Shield, token_id); + let shield_bundle = build_shield_bundle( + 10_000, + 22, + TokenTransitionActionType::Shield, + token_id, + identity.id(), + ); let shield = BatchTransition::new_token_shield_transition( token_id, identity.id(), @@ -2102,7 +2132,13 @@ mod token_pool_mint_burn_claim_purchase_tests { contract.id(), 0, 10_000, - build_shield_bundle(10_000, 24, TokenTransitionActionType::Shield, token_id), + build_shield_bundle( + 10_000, + 24, + TokenTransitionActionType::Shield, + token_id, + proposer.id(), + ), &proposer_key, 2, 0, @@ -2382,6 +2418,7 @@ mod token_pool_mint_burn_claim_purchase_tests { 27, TokenTransitionActionType::ClaimToPool, token_id, + owner.id(), ), None, &key, @@ -2427,6 +2464,7 @@ mod token_pool_mint_burn_claim_purchase_tests { 28, TokenTransitionActionType::ClaimToPool, token_id, + owner.id(), ); let pinned = BatchTransition::new_token_claim_to_pool_transition( token_id, @@ -2545,7 +2583,13 @@ mod token_pool_mint_burn_claim_purchase_tests { 0, distribution_type, None, - build_shield_bundle(444, 24, TokenTransitionActionType::ClaimToPool, token_id), + build_shield_bundle( + 444, + 24, + TokenTransitionActionType::ClaimToPool, + token_id, + claimant_id, + ), None, &key, 2, @@ -2583,8 +2627,13 @@ mod token_pool_mint_burn_claim_purchase_tests { .unwrap() .expect("commit"); - let claim_bundle = - build_shield_bundle(445, 25, TokenTransitionActionType::ClaimToPool, token_id); + let claim_bundle = build_shield_bundle( + 445, + 25, + TokenTransitionActionType::ClaimToPool, + token_id, + claimant_id, + ); let claim = BatchTransition::new_token_claim_to_pool_transition( token_id, claimant_id, @@ -2650,7 +2699,13 @@ mod token_pool_mint_burn_claim_purchase_tests { 0, distribution_type, None, - build_shield_bundle(445, 26, TokenTransitionActionType::ClaimToPool, token_id), + build_shield_bundle( + 445, + 26, + TokenTransitionActionType::ClaimToPool, + token_id, + claimant_id, + ), None, &key, 4, @@ -2764,6 +2819,7 @@ mod token_pool_mint_burn_claim_purchase_tests { 27, TokenTransitionActionType::DirectPurchaseToPool, token_id, + buyer.id(), ); let purchase = BatchTransition::new_token_direct_purchase_to_pool_transition( token_id, @@ -2826,6 +2882,7 @@ mod token_pool_mint_burn_claim_purchase_tests { 28, TokenTransitionActionType::DirectPurchaseToPool, token_id, + buyer.id(), ), &key, 3, @@ -2865,6 +2922,7 @@ mod token_pool_mint_burn_claim_purchase_tests { 29, TokenTransitionActionType::DirectPurchaseToPool, token_id, + poor_buyer.id(), ), &poor_key, 1, @@ -3048,7 +3106,13 @@ mod document_shielded_token_payment_tests { contract.id(), 0, SHIELDED, - build_shield_bundle(SHIELDED, seed, TokenTransitionActionType::Shield, token_id), + build_shield_bundle( + SHIELDED, + seed, + TokenTransitionActionType::Shield, + token_id, + buyer.id(), + ), key, 1, 0, @@ -3546,7 +3610,13 @@ mod token_pool_paid_transitions_tests { contract.id(), 0, SHIELDED, - build_shield_bundle(SHIELDED, seed, TokenTransitionActionType::Shield, token_id), + build_shield_bundle( + SHIELDED, + seed, + TokenTransitionActionType::Shield, + token_id, + holder.id(), + ), key, 2, 0, @@ -4184,6 +4254,7 @@ mod token_pool_paid_transitions_tests { let expected = token_pool_output_only_extra_sighash_data( action_type, &token_id.to_buffer(), + &owner.id().to_buffer(), platform_version, ) .expect("outputs-only kind"); @@ -4204,25 +4275,32 @@ mod token_pool_paid_transitions_tests { } } -/// An outputs-only bundle copied into the same pool, as the same kind, by somebody else. +/// An outputs-only bundle offered to the same pool, as the same kind, a second time. /// -/// The sighash of such a bundle binds its kind and its token but not who submits it, so the -/// copy verifies. It would land a second note with the same commitment and the same `rho`, -/// hence the same nullifier, and only one of the two could ever be spent: a wallet counting by -/// commitment would see a payment that does not exist. The pool records each bundle's dummy -/// nullifiers when it enters and refuses a bundle whose dummy nullifier is already there. +/// The sighash of such a bundle binds its pool, its kind and its owner, but nothing that makes +/// it single-use, so its owner can submit the same bundle again in a new transition. The repeat +/// would land a second note with the same commitment and the same `rho`, hence the same +/// nullifier, and only one of the two could ever be spent: a wallet counting by commitment +/// would see a payment that does not exist. The pool records each bundle's dummy nullifiers +/// when it enters and refuses a bundle whose dummy nullifier is already there. +/// +/// Somebody else's copy is refused by the sighash instead, before it can land; those tests are +/// the front-running ones below. Where the pool's record is read first, the record refuses a +/// copy too: block validation checks the dummy nullifiers before it verifies the bundle, and +/// CheckTx cannot verify a claim's bundle at all, so another claimant's copy of a claim that +/// has already landed is refused on its nullifier. /// /// Every test first builds the original through the public builder and shows it admitted by -/// CheckTx and executed in a block. Without that, a refused copy would prove nothing: a bundle -/// the verifier never accepted anywhere is refused for any reason at all. The copy must then -/// fail on the original's first dummy nullifier, and nothing it carries may move. +/// CheckTx and executed in a block. Without that, a refused repeat would prove nothing: a +/// bundle the verifier never accepted anywhere is refused for any reason at all. The repeat +/// must then fail on the original's first dummy nullifier, and nothing it carries may move. mod token_pool_outputs_only_copy_tests { use super::token_pool_paid_transitions_tests::{ credit_pool_balance, fund_credit_pool, spendable, wallet_address, Prover, CREDIT_NOTE, }; use super::token_shielded_pool_tests::{ assert_check_tx_accepts, assert_check_tx_rejects, assert_tokens_conserved, - build_spend_bundle, + build_shield_bundle, build_spend_bundle, enable_shielded_pool, identity_token_balance, nullifier_is_spent, platform_with_latest_version, pool_balance, pool_notes_count, process, spend_keys, OWNER_INITIAL_BALANCE, @@ -4252,7 +4330,10 @@ mod token_pool_outputs_only_copy_tests { OrchardBundleParams, TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, }; use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; - use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransition; + use dpp::state_transition::batch_transition::batched_transition::token_transition::{ + TokenTransition, TokenTransitionV0Methods, + }; + use dpp::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType; use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRef; use dpp::state_transition::batch_transition::token_claim_to_pool_transition::v0::v0_methods::TokenClaimToPoolTransitionV0Methods; use dpp::state_transition::batch_transition::token_direct_purchase_to_pool_transition::v0::v0_methods::TokenDirectPurchaseToPoolTransitionV0Methods; @@ -4325,51 +4406,154 @@ mod token_pool_outputs_only_copy_tests { .expect("identity exists") } - /// Submits a batch copy and returns what the copier was charged for it. + /// The identity contract nonce `identity_id` has used last on `contract_id`. + fn contract_nonce( + platform: &TempPlatform, + identity_id: Identifier, + contract_id: Identifier, + ) -> u64 { + platform + .drive + .fetch_identity_contract_nonce( + identity_id.to_buffer(), + contract_id.to_buffer(), + true, + None, + PlatformVersion::latest(), + ) + .expect("identity contract nonce") + .expect("the identity has used the contract") + & IDENTITY_NONCE_VALUE_FILTER + } + + /// Submits a bundle again after it has landed and returns what the submitter was charged for + /// it. + /// + /// CheckTx admits it: it does not read the pool. Where CheckTx verifies the kind's bundle and + /// the submitter is the bundle's owner, that admission also shows the bundle is valid as the + /// submitter's transition; a claim's bundle CheckTx does not verify at all. Block execution + /// must then refuse it on the original's first dummy nullifier, which it checks before the + /// proof, as a paid failure: the submitter's nonce advances to `nonce` and its fee is + /// charged, nothing else moves. + fn assert_repeat_refused_in_block( + platform: &TempPlatform, + repeat: &StateTransition, + submitter_id: Identifier, + contract_id: Identifier, + nonce: u64, + original: &OrchardBundleParams, + block_info: Option<&BlockInfo>, + ) -> Credits { + assert_check_tx_accepts(platform, repeat); + let credits_before = credits(platform, submitter_id); + let result = match block_info { + Some(block_info) => process_at(platform, repeat, block_info), + None => process(platform, repeat), + }; + let first = original.actions[0].nullifier; + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::NullifierAlreadySpentError(error)), + .. + }] if error.nullifier() == first + ); + assert_eq!( + contract_nonce(platform, submitter_id, contract_id), + nonce, + "the refused repeat bumps the submitter's nonce" + ); + let charged = credits_before - credits(platform, submitter_id); + assert!(charged > 0, "a refused repeat is a paid failure"); + charged + } + + /// Submits another identity's bundle, taken from the mempool before the original landed, + /// and returns what the copier was charged for it. + /// + /// The bundle's sighash names its author, so the copy is invalid as the copier's transition + /// wherever the bundle is verified: CheckTx refuses it when `verified_at_check_tx`, and + /// block execution refuses it as a paid failure — the copier's nonce advances to `nonce` and + /// its fee is charged. + /// + /// The two nullifier assertions do different jobs, and neither pins the order of checks: + /// + /// - Before the copy, none of the bundle's dummy nullifiers is in the pool. This is what + /// anchors the negative: at the moment of refusal the pool's record holds nothing to + /// refuse with, so the refusal can only be the preimage. + /// - After the copy, still none is. A refused transition never reaches the pool write, so + /// this does not catch a binding check moved after the nullifier check. It guards a + /// different regression: recording the nullifiers during validation, ahead of the write, + /// which would let a copier burn the author's nullifiers and block the author for good. /// - /// CheckTx admits the copy: it verifies a batch's token bundles statelessly and does not - /// read the pool, so the admission itself shows the copied bundle is valid as the copier's - /// transition. Block execution must then refuse it on the original's first dummy nullifier, - /// as a paid failure: the copier's nonce advances and its fee is charged, nothing else moves. - fn assert_copy_refused_in_block( + /// What shows nothing of the copy persisted is the caller's next step: the original then + /// executes successfully. + fn assert_front_run_refused( platform: &TempPlatform, copy: &StateTransition, copier_id: Identifier, contract_id: Identifier, - original: &OrchardBundleParams, + nonce: u64, + bundle: &OrchardBundleParams, + verified_at_check_tx: bool, block_info: Option<&BlockInfo>, ) -> Credits { - assert_check_tx_accepts(platform, copy); + let token_id = copied_token_id(copy); + assert!( + bundle.actions.iter().all(|action| !nullifier_is_spent( + platform, + token_id, + &action.nullifier + )), + "a front-run lands before the original, so none of its dummy nullifiers is recorded" + ); + if verified_at_check_tx { + assert_matches!( + assert_check_tx_rejects(platform, copy).as_slice(), + [ConsensusError::StateError( + StateError::InvalidShieldedProofError(_) + )] + ); + } else { + assert_check_tx_accepts(platform, copy); + } let credits_before = credits(platform, copier_id); let result = match block_info { Some(block_info) => process_at(platform, copy, block_info), None => process(platform, copy), }; - let first = original.actions[0].nullifier; assert_matches!( result.execution_results().as_slice(), [StateTransitionExecutionResult::PaidConsensusError { - error: ConsensusError::StateError(StateError::NullifierAlreadySpentError(error)), + error: ConsensusError::StateError(StateError::InvalidShieldedProofError(_)), .. - }] if error.nullifier() == first + }] ); - let nonce = platform - .drive - .fetch_identity_contract_nonce( - copier_id.to_buffer(), - contract_id.to_buffer(), - true, - None, - PlatformVersion::latest(), - ) - .expect("identity contract nonce") - .expect("the refused copy bumps the copier's nonce"); - assert_eq!(nonce & IDENTITY_NONCE_VALUE_FILTER, 1); + assert_eq!(contract_nonce(platform, copier_id, contract_id), nonce); let charged = credits_before - credits(platform, copier_id); assert!(charged > 0, "a refused copy is a paid failure"); + assert!( + bundle.actions.iter().all(|action| !nullifier_is_spent( + platform, + token_id, + &action.nullifier + )), + "a refused copy must not record the bundle's dummy nullifiers" + ); charged } + /// The token an outputs-only batch token transition pays into. + fn copied_token_id(transition: &StateTransition) -> Identifier { + let StateTransition::Batch(batch) = transition else { + panic!("expected a batch transition"); + }; + match batch.transitions_iter().next().expect("one transition") { + BatchedTransitionRef::Token(token_transition) => token_transition.token_id(), + other => panic!("not a token transition: {other:?}"), + } + } + /// Processes one transition at `block_info`, for a claim whose release has to be due. fn process_at( platform: &TempPlatform, @@ -4414,16 +4598,18 @@ mod token_pool_outputs_only_copy_tests { result } + /// The owner submits its own landed bundle again with a fresh nonce, as a wallet retrying a + /// submission it believes lost would. The owner binding cannot tell the two apart, so the + /// pool's record refuses the repeat, and it is a paid failure: the retry is charged its fee, + /// though nothing else moves. #[tokio::test] - async fn test_a_shield_bundle_copied_into_the_same_pool_by_another_holder_is_refused() { + async fn test_a_shield_bundle_resubmitted_by_its_owner_is_refused() { let platform_version = PlatformVersion::latest(); let mut platform = platform_with_latest_version(); let mut rng = StdRng::seed_from_u64(9501); let amount = 4_200; let (owner, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); - let (copier, copier_signer, copier_key) = - setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); let (contract, token_id) = create_token_contract_with_owner_identity( &mut platform, owner.id(), @@ -4434,32 +4620,6 @@ mod token_pool_outputs_only_copy_tests { platform_version, ); - // The copier holds as many tokens as the copy shields, so a refusal cannot be its - // balance. - let transfer = BatchTransition::new_token_transfer_transition( - token_id, - owner.id(), - contract.id(), - 0, - amount, - copier.id(), - None, - None, - None, - &key, - 2, - 0, - &signer, - platform_version, - None, - ) - .await - .expect("token transfer transition"); - assert_matches!( - process(&platform, &transfer).execution_results().as_slice(), - [StateTransitionExecutionResult::SuccessfulExecution { .. }] - ); - let shield = build_token_shield_transition( token_id, owner.id(), @@ -4470,7 +4630,7 @@ mod token_pool_outputs_only_copy_tests { [0u8; 36], None, &key, - 3, + 2, 0, &signer, &Prover, @@ -4485,52 +4645,62 @@ mod token_pool_outputs_only_copy_tests { ); assert_eq!( identity_token_balance(&platform, token_id, owner.id()), - Some(OWNER_INITIAL_BALANCE - 2 * amount) + Some(OWNER_INITIAL_BALANCE - amount) ); let bundle = proven_bundle(&shield); assert_dummy_nullifiers_recorded(&platform, token_id, &bundle); let notes_after_original = pool_notes_count(&platform, token_id); - let copy = BatchTransition::new_token_shield_transition( + // The owner still holds far more than the repeat shields, so a refusal cannot be its + // balance. + let repeat = BatchTransition::new_token_shield_transition( token_id, - copier.id(), + owner.id(), contract.id(), 0, amount, bundle.clone(), - &copier_key, - 1, + &key, + 3, 0, - &copier_signer, + &signer, platform_version, None, ) .await .expect("token shield transition"); - assert_copy_refused_in_block(&platform, ©, copier.id(), contract.id(), &bundle, None); + assert_repeat_refused_in_block( + &platform, + &repeat, + owner.id(), + contract.id(), + 3, + &bundle, + None, + ); assert_eq!(pool_balance(&platform, token_id), amount); assert_eq!(pool_notes_count(&platform, token_id), notes_after_original); assert_eq!( - identity_token_balance(&platform, token_id, copier.id()), - Some(amount) + identity_token_balance(&platform, token_id, owner.id()), + Some(OWNER_INITIAL_BALANCE - amount) ); - // A pool that already holds recorded dummy nullifiers still takes a bundle of its own: - // what is refused is the repeat, not every bundle after the first. + // A pool that already holds recorded dummy nullifiers still takes a new bundle from the + // same owner: what is refused is the repeat, not every bundle after the first. let own_shield = build_token_shield_transition( token_id, - copier.id(), + owner.id(), contract.id(), 0, &wallet_address(), amount, [0u8; 36], None, - &copier_key, - 2, + &key, + 4, 0, - &copier_signer, + &signer, &Prover, platform_version, ) @@ -4548,19 +4718,17 @@ mod token_pool_outputs_only_copy_tests { assert_tokens_conserved(&platform); } - /// A copy lifted from the mempool reaches the same block as its original. CheckTx admits - /// both, since neither has entered the pool yet, so what refuses the copy is the check - /// reading the original's write in the block's own transaction. + /// A repeat submitted before its original has landed reaches the same block. CheckTx admits + /// both, since neither has entered the pool yet, so what refuses the second is the check + /// reading the first one's write in the block's own transaction. #[tokio::test] - async fn test_a_shield_bundle_and_its_copy_in_one_block_land_only_once() { + async fn test_a_shield_bundle_and_its_repeat_in_one_block_land_only_once() { let platform_version = PlatformVersion::latest(); let mut platform = platform_with_latest_version(); let mut rng = StdRng::seed_from_u64(9508); let amount = 3_100; let (owner, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); - let (copier, copier_signer, copier_key) = - setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); let (contract, token_id) = create_token_contract_with_owner_identity( &mut platform, owner.id(), @@ -4571,32 +4739,6 @@ mod token_pool_outputs_only_copy_tests { platform_version, ); - // The copier holds as many tokens as the copy shields, so a refusal cannot be its - // balance. - let transfer = BatchTransition::new_token_transfer_transition( - token_id, - owner.id(), - contract.id(), - 0, - amount, - copier.id(), - None, - None, - None, - &key, - 2, - 0, - &signer, - platform_version, - None, - ) - .await - .expect("token transfer transition"); - assert_matches!( - process(&platform, &transfer).execution_results().as_slice(), - [StateTransitionExecutionResult::SuccessfulExecution { .. }] - ); - let shield = build_token_shield_transition( token_id, owner.id(), @@ -4607,7 +4749,7 @@ mod token_pool_outputs_only_copy_tests { [0u8; 36], None, &key, - 3, + 2, 0, &signer, &Prover, @@ -4616,28 +4758,28 @@ mod token_pool_outputs_only_copy_tests { .await .expect("client-built token shield"); let bundle = proven_bundle(&shield); - let copy = BatchTransition::new_token_shield_transition( + let repeat = BatchTransition::new_token_shield_transition( token_id, - copier.id(), + owner.id(), contract.id(), 0, amount, bundle.clone(), - &copier_key, - 1, + &key, + 3, 0, - &copier_signer, + &signer, platform_version, None, ) .await .expect("token shield transition"); assert_check_tx_accepts(&platform, &shield); - assert_check_tx_accepts(&platform, ©); + assert_check_tx_accepts(&platform, &repeat); let first = bundle.actions[0].nullifier; assert_matches!( - process_in_one_block(&platform, &[&shield, ©], &BlockInfo::default()) + process_in_one_block(&platform, &[&shield, &repeat], &BlockInfo::default()) .execution_results() .as_slice(), [ @@ -4657,27 +4799,21 @@ mod token_pool_outputs_only_copy_tests { ); assert_eq!( identity_token_balance(&platform, token_id, owner.id()), - Some(OWNER_INITIAL_BALANCE - 2 * amount) - ); - assert_eq!( - identity_token_balance(&platform, token_id, copier.id()), - Some(amount) + Some(OWNER_INITIAL_BALANCE - amount) ); assert_tokens_conserved(&platform); } #[tokio::test] - async fn test_a_mint_to_pool_bundle_copied_by_another_minter_is_refused() { + async fn test_a_mint_to_pool_bundle_resubmitted_by_its_minter_is_refused() { let platform_version = PlatformVersion::latest(); let mut platform = platform_with_latest_version(); let mut rng = StdRng::seed_from_u64(9502); let amount = 1_337; let (minter, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); - let (copier, copier_signer, copier_key) = - setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); - // Either member of the group mints alone, so the copier is as entitled to mint as the - // minter and only the bundle can be why its copy is refused. + // A group the minter alone can close, so each proposal mints at once and the repeat is + // a complete mint of its own. let (contract, token_id) = create_token_contract_with_owner_identity( &mut platform, minter.id(), @@ -4698,7 +4834,7 @@ mod token_pool_outputs_only_copy_tests { [( 0, Group::V0(GroupV0 { - members: [(minter.id(), 1), (copier.id(), 1)].into(), + members: [(minter.id(), 1)].into(), required_power: 1, }), )] @@ -4738,46 +4874,213 @@ mod token_pool_outputs_only_copy_tests { assert_dummy_nullifiers_recorded(&platform, token_id, &bundle); let notes_after_original = pool_notes_count(&platform, token_id); - let copy = BatchTransition::new_token_mint_to_pool_transition( + let repeat = BatchTransition::new_token_mint_to_pool_transition( token_id, - copier.id(), + minter.id(), contract.id(), 0, amount, bundle.clone(), None, Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(0)), - &copier_key, - 1, + &key, + 3, 0, - &copier_signer, + &signer, platform_version, None, ) .await .expect("token mint to pool transition"); - assert_copy_refused_in_block(&platform, ©, copier.id(), contract.id(), &bundle, None); + assert_repeat_refused_in_block( + &platform, + &repeat, + minter.id(), + contract.id(), + 3, + &bundle, + None, + ); assert_eq!(pool_balance(&platform, token_id), amount); assert_eq!(pool_notes_count(&platform, token_id), notes_after_original); assert_eq!( total_supply(&platform, token_id), OWNER_INITIAL_BALANCE + amount, - "a refused copy must not print supply" + "a refused repeat must not print supply" ); assert_tokens_conserved(&platform); } - /// A group mint carries one bundle through several signers: every confirmer resubmits the - /// proposer's bundle unchanged, so its dummy nullifiers are offered to the pool once per - /// signer. Only the signature that closes the action mints, and only it may record them; - /// were they recorded earlier, the closing signature would be refused as a copy and no - /// group mint into a pool could ever complete. + /// Another member of the group takes a proposal's bundle and proposes it as its own. That + /// member is as entitled to mint as the proposer, and the proposal it copied has not closed, + /// so no dummy nullifier is on record to refuse it: only the minter the bundle's sighash + /// names can. Were it accepted, the group could close either proposal, and whichever closed + /// second would be refused as a repeat. #[tokio::test] - async fn test_a_group_mint_to_pool_closes_with_the_proposers_bundle_resubmitted() { + async fn test_a_mint_to_pool_bundle_proposed_again_by_another_member_is_refused() { let platform_version = PlatformVersion::latest(); let mut platform = platform_with_latest_version(); - let mut rng = StdRng::seed_from_u64(9507); + let mut rng = StdRng::seed_from_u64(9512); + let amount = 1_111; + + let (proposer, proposer_signer, proposer_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (copier, copier_signer, copier_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (confirmer, confirmer_signer, confirmer_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + proposer.id(), + Some(|configuration: &mut TokenConfiguration| { + enable_shielded_pool(configuration); + configuration.set_manual_minting_rules(ChangeControlRules::V0( + ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::Group(0), + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }, + )); + }), + None, + Some( + [( + 0, + Group::V0(GroupV0 { + members: [(proposer.id(), 1), (copier.id(), 1), (confirmer.id(), 1)].into(), + required_power: 2, + }), + )] + .into(), + ), + None, + platform_version, + ); + let supply_before = total_supply(&platform, token_id); + + let proposer_nonce = 2; + let proposal = build_token_mint_to_pool_transition( + token_id, + proposer.id(), + contract.id(), + 0, + &wallet_address(), + amount, + [0u8; 36], + None, + None, + Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(0)), + &proposer_key, + proposer_nonce, + 0, + &proposer_signer, + &Prover, + platform_version, + ) + .await + .expect("client-built token mint to pool proposal"); + assert_check_tx_accepts(&platform, &proposal); + assert_matches!( + process(&platform, &proposal).execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + let bundle = proven_bundle(&proposal); + + let copy = BatchTransition::new_token_mint_to_pool_transition( + token_id, + copier.id(), + contract.id(), + 0, + amount, + bundle.clone(), + None, + Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(0)), + &copier_key, + 1, + 0, + &copier_signer, + platform_version, + None, + ) + .await + .expect("token mint to pool proposal"); + assert_front_run_refused( + &platform, + ©, + copier.id(), + contract.id(), + 1, + &bundle, + true, + None, + ); + // One signature of two could not mint either way, so the pool and the supply cannot tell + // a refused proposal from an accepted one; the error asserted above is what does. + + // The proposal the copier took from still closes with its own bundle. + let confirmation = BatchTransition::new_token_mint_to_pool_transition( + token_id, + confirmer.id(), + contract.id(), + 0, + amount, + bundle.clone(), + None, + Some( + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: 0, + action_id: TokenMintToPoolTransition::calculate_action_id_with_fields( + token_id.as_bytes(), + proposer.id().as_bytes(), + proposer_nonce, + amount, + &serialized_actions_digest(&bundle.actions), + ), + action_is_proposer: false, + }, + ), + ), + &confirmer_key, + 1, + 0, + &confirmer_signer, + platform_version, + None, + ) + .await + .expect("token mint to pool confirmation"); + assert_check_tx_accepts(&platform, &confirmation); + assert_matches!( + process(&platform, &confirmation) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(&platform, token_id), amount); + assert_eq!(total_supply(&platform, token_id), supply_before + amount); + assert_dummy_nullifiers_recorded(&platform, token_id, &bundle); + assert_tokens_conserved(&platform); + } + + /// A group mint carries one bundle through several signers: every confirmer resubmits the + /// proposer's bundle unchanged, so its dummy nullifiers are offered to the pool once per + /// signer. Only the signature that closes the action mints, and only it may record them; + /// were they recorded earlier, the closing signature would be refused as a copy and no + /// group mint into a pool could ever complete. + /// + /// The bundle's sighash names the proposer, not the confirmer whose batch carries it here. + /// Admission must therefore leave a confirmer's bundle to state validation, which reads the + /// proposer from the stored group action; were either of the two to bind the batch owner + /// instead, this confirmation would be refused and no group mint could ever close. + #[tokio::test] + async fn test_a_group_mint_to_pool_closes_with_the_proposers_bundle_resubmitted() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9507); let amount = 2_024; let (proposer, proposer_signer, proposer_key) = @@ -4898,27 +5201,27 @@ mod token_pool_outputs_only_copy_tests { assert_tokens_conserved(&platform); } - /// Two group actions can carry the same bundle: each member may propose, and the group - /// action pins only the bundle's digest. Once one of them closes and mints, the signature - /// that would close the other is a copy like any other and must be refused, or the group - /// would mint the amount twice into notes of which only one of each pair could be spent. - /// Confirmers go through the check as proposers do. + /// The group action pins only the digest of the bundle's actions, not its proof or binding + /// signature, and the stateless proof check skips a confirmer's bundle because it cannot + /// see the proposer the bundle is bound to. State validation is therefore the only place a + /// confirmer's bundle is verified: a confirmer that keeps the proposer's actions but swaps + /// the proof must be refused there, or the group would mint against a bundle nobody proved. #[tokio::test] - async fn test_a_second_group_mint_to_pool_carrying_a_minted_bundle_cannot_close() { + async fn test_a_group_mint_to_pool_confirmation_with_a_tampered_proof_is_refused() { let platform_version = PlatformVersion::latest(); let mut platform = platform_with_latest_version(); - let mut rng = StdRng::seed_from_u64(9509); - let amount = 777; + let mut rng = StdRng::seed_from_u64(9515); + let amount = 3_003; - let (first, first_signer, first_key) = + let (proposer, proposer_signer, proposer_key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); - let (second, second_signer, second_key) = + let (tamperer, tamperer_signer, tamperer_key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); - let (third, third_signer, third_key) = + let (confirmer, confirmer_signer, confirmer_key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); let (contract, token_id) = create_token_contract_with_owner_identity( &mut platform, - first.id(), + proposer.id(), Some(|configuration: &mut TokenConfiguration| { enable_shielded_pool(configuration); configuration.set_manual_minting_rules(ChangeControlRules::V0( @@ -4936,7 +5239,8 @@ mod token_pool_outputs_only_copy_tests { [( 0, Group::V0(GroupV0 { - members: [(first.id(), 1), (second.id(), 1), (third.id(), 1)].into(), + members: [(proposer.id(), 1), (tamperer.id(), 1), (confirmer.id(), 1)] + .into(), required_power: 2, }), )] @@ -4945,11 +5249,12 @@ mod token_pool_outputs_only_copy_tests { None, platform_version, ); + let supply_before = total_supply(&platform, token_id); - let first_nonce = 2; - let first_proposal = build_token_mint_to_pool_transition( + let proposer_nonce = 2; + let proposal = build_token_mint_to_pool_transition( token_id, - first.id(), + proposer.id(), contract.id(), 0, &wallet_address(), @@ -4958,143 +5263,1000 @@ mod token_pool_outputs_only_copy_tests { None, None, Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(0)), - &first_key, - first_nonce, + &proposer_key, + proposer_nonce, 0, - &first_signer, + &proposer_signer, &Prover, platform_version, ) .await .expect("client-built token mint to pool proposal"); + assert_check_tx_accepts(&platform, &proposal); assert_matches!( - process(&platform, &first_proposal) + process(&platform, &proposal).execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + let bundle = proven_bundle(&proposal); + let closing_signature = || { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: 0, + action_id: TokenMintToPoolTransition::calculate_action_id_with_fields( + token_id.as_bytes(), + proposer.id().as_bytes(), + proposer_nonce, + amount, + &serialized_actions_digest(&bundle.actions), + ), + action_is_proposer: false, + }, + ) + }; + + // Same actions, so the group action's digest still matches; only the proof differs. + let mut tampered = bundle.clone(); + tampered.proof[0] ^= 0x01; + let tampered_confirmation = BatchTransition::new_token_mint_to_pool_transition( + token_id, + tamperer.id(), + contract.id(), + 0, + amount, + tampered, + None, + Some(closing_signature()), + &tamperer_key, + 1, + 0, + &tamperer_signer, + platform_version, + None, + ) + .await + .expect("token mint to pool confirmation"); + // CheckTx cannot tell whose sighash a confirmer's bundle is bound to, so it does not + // verify it at all: a bundle that no verifier would accept is admitted to the mempool, + // and the block is where it is refused and charged. + assert_check_tx_accepts(&platform, &tampered_confirmation); + assert_matches!( + process(&platform, &tampered_confirmation) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::InvalidShieldedProofError(_)), + .. + }] + ); + assert_eq!(pool_balance(&platform, token_id), 0); + assert_eq!( + total_supply(&platform, token_id), + supply_before, + "a refused confirmation would otherwise have closed the action and minted" + ); + assert!(!nullifier_is_spent( + &platform, + token_id, + &bundle.actions[0].nullifier + )); + + // The untampered bundle still closes the same action. + let confirmation = BatchTransition::new_token_mint_to_pool_transition( + token_id, + confirmer.id(), + contract.id(), + 0, + amount, + bundle.clone(), + None, + Some(closing_signature()), + &confirmer_key, + 1, + 0, + &confirmer_signer, + platform_version, + None, + ) + .await + .expect("token mint to pool confirmation"); + assert_check_tx_accepts(&platform, &confirmation); + assert_matches!( + process(&platform, &confirmation) .execution_results() .as_slice(), [StateTransitionExecutionResult::SuccessfulExecution { .. }] ); - let bundle = proven_bundle(&first_proposal); - let digest = serialized_actions_digest(&bundle.actions); + assert_eq!(pool_balance(&platform, token_id), amount); + assert_eq!(total_supply(&platform, token_id), supply_before + amount); + assert_dummy_nullifiers_recorded(&platform, token_id, &bundle); + assert_tokens_conserved(&platform); + } + + /// A signer other than the proposer proves the proposer's notes again, from the same + /// randomness, but bound to itself. It cannot then match the group action: every action of + /// an outputs-only bundle carries a spend authorization signature over the sighash, and the + /// group action's digest covers those signatures, so a bundle bound to another owner has + /// other actions. Keeping the proposer's actions verbatim and swapping in the rest of its own + /// bundle matches the digest instead, but then the actions' signatures only verify against + /// the proposer's sighash and the binding signature only against its own. Either way the + /// only bundle a signer other than the proposer can close the action with is the + /// proposer's own. + #[tokio::test] + async fn test_a_group_mint_to_pool_confirmation_with_a_bundle_proved_for_the_confirmer_is_refused( + ) { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9518); + let amount = 2_718; + let bundle_seed = 77; + + let (proposer, proposer_signer, proposer_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (rebinder, rebinder_signer, rebinder_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (confirmer, confirmer_signer, confirmer_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + proposer.id(), + Some(|configuration: &mut TokenConfiguration| { + enable_shielded_pool(configuration); + configuration.set_manual_minting_rules(ChangeControlRules::V0( + ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::Group(0), + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }, + )); + }), + None, + Some( + [( + 0, + Group::V0(GroupV0 { + members: [(proposer.id(), 1), (rebinder.id(), 1), (confirmer.id(), 1)] + .into(), + required_power: 2, + }), + )] + .into(), + ), + None, + platform_version, + ); + let supply_before = total_supply(&platform, token_id); + + // Seeded, so the same notes can be proven a second time; the public builder draws fresh + // randomness and could not reproduce them. + let bundle = build_shield_bundle( + amount, + bundle_seed, + TokenTransitionActionType::MintToPool, + token_id, + proposer.id(), + ); + let proposer_nonce = 2; + let proposal = BatchTransition::new_token_mint_to_pool_transition( + token_id, + proposer.id(), + contract.id(), + 0, + amount, + bundle.clone(), + None, + Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(0)), + &proposer_key, + proposer_nonce, + 0, + &proposer_signer, + platform_version, + None, + ) + .await + .expect("token mint to pool proposal"); + assert_check_tx_accepts(&platform, &proposal); + assert_matches!( + process(&platform, &proposal).execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + let rebound = build_shield_bundle( + amount, + bundle_seed, + TokenTransitionActionType::MintToPool, + token_id, + rebinder.id(), + ); + assert_eq!(rebound.actions.len(), bundle.actions.len()); + for (own, proposers) in rebound.actions.iter().zip(&bundle.actions) { + assert_eq!( + ( + own.nullifier, + own.rk, + own.cmx, + &own.encrypted_note, + own.cv_net + ), + ( + proposers.nullifier, + proposers.rk, + proposers.cmx, + &proposers.encrypted_note, + proposers.cv_net + ), + "the same randomness proves the same notes" + ); + assert_ne!( + own.spend_auth_sig, proposers.spend_auth_sig, + "each action signs the sighash, which names the owner" + ); + } + assert_ne!( + serialized_actions_digest(&rebound.actions), + serialized_actions_digest(&bundle.actions) + ); + + let closing_signature = || { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: 0, + action_id: TokenMintToPoolTransition::calculate_action_id_with_fields( + token_id.as_bytes(), + proposer.id().as_bytes(), + proposer_nonce, + amount, + &serialized_actions_digest(&bundle.actions), + ), + action_is_proposer: false, + }, + ) + }; + let confirm_with = |bundle: OrchardBundleParams, nonce| { + BatchTransition::new_token_mint_to_pool_transition( + token_id, + rebinder.id(), + contract.id(), + 0, + amount, + bundle, + None, + Some(closing_signature()), + &rebinder_key, + nonce, + 0, + &rebinder_signer, + platform_version, + None, + ) + }; + + // Its own bundle, whole: the group action refuses other actions. + let own_bundle = confirm_with(rebound.clone(), 1) + .await + .expect("token mint to pool confirmation"); + assert_check_tx_accepts(&platform, &own_bundle); + assert_matches!( + process(&platform, &own_bundle) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError( + StateError::ModificationOfGroupActionMainParametersNotPermittedError(_) + ), + .. + }] + ); + + // The proposer's actions with its own proof and binding signature: the digest matches, + // and the bundle verifies against neither owner. + let spliced = OrchardBundleParams { + actions: bundle.actions.clone(), + anchor: rebound.anchor, + proof: rebound.proof.clone(), + binding_signature: rebound.binding_signature, + }; + let spliced_confirmation = confirm_with(spliced, 2) + .await + .expect("token mint to pool confirmation"); + assert_check_tx_accepts(&platform, &spliced_confirmation); + assert_matches!( + process(&platform, &spliced_confirmation) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::InvalidShieldedProofError(_)), + .. + }] + ); + + assert_eq!(pool_balance(&platform, token_id), 0); + assert_eq!( + total_supply(&platform, token_id), + supply_before, + "either confirmation would otherwise have closed the action and minted" + ); + assert!(!nullifier_is_spent( + &platform, + token_id, + &bundle.actions[0].nullifier + )); + + // The proposer's bundle, unchanged, still closes the action. + let confirmation = BatchTransition::new_token_mint_to_pool_transition( + token_id, + confirmer.id(), + contract.id(), + 0, + amount, + bundle.clone(), + None, + Some(closing_signature()), + &confirmer_key, + 1, + 0, + &confirmer_signer, + platform_version, + None, + ) + .await + .expect("token mint to pool confirmation"); + assert_check_tx_accepts(&platform, &confirmation); + assert_matches!( + process(&platform, &confirmation) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(&platform, token_id), amount); + assert_eq!(total_supply(&platform, token_id), supply_before + amount); + assert_dummy_nullifiers_recorded(&platform, token_id, &bundle); + assert_tokens_conserved(&platform); + } + + /// Two group actions can carry the same bundle: its proposer may propose it twice, and the + /// group action pins only the bundle's digest. Once one of them closes and mints, the + /// signature that would close the other is a repeat like any other and must be refused, or + /// the group would mint the amount twice into notes of which only one of each pair could be + /// spent. Confirmers go through the check as proposers do. + /// + /// Both proposals come from the same member on purpose: another member proposing the same + /// bundle is refused on the owner the bundle's sighash names before it could reach the + /// nullifier check, and that case lives in + /// `test_a_mint_to_pool_bundle_proposed_again_by_another_member_is_refused`. + #[tokio::test] + async fn test_a_second_group_mint_to_pool_carrying_a_minted_bundle_cannot_close() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9509); + let amount = 777; + + let (first, first_signer, first_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (second, second_signer, second_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (third, third_signer, third_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + first.id(), + Some(|configuration: &mut TokenConfiguration| { + enable_shielded_pool(configuration); + configuration.set_manual_minting_rules(ChangeControlRules::V0( + ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::Group(0), + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }, + )); + }), + None, + Some( + [( + 0, + Group::V0(GroupV0 { + members: [(first.id(), 1), (second.id(), 1), (third.id(), 1)].into(), + required_power: 2, + }), + )] + .into(), + ), + None, + platform_version, + ); + + let first_nonce = 2; + let first_proposal = build_token_mint_to_pool_transition( + token_id, + first.id(), + contract.id(), + 0, + &wallet_address(), + amount, + [0u8; 36], + None, + None, + Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(0)), + &first_key, + first_nonce, + 0, + &first_signer, + &Prover, + platform_version, + ) + .await + .expect("client-built token mint to pool proposal"); + assert_matches!( + process(&platform, &first_proposal) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + let bundle = proven_bundle(&first_proposal); + let digest = serialized_actions_digest(&bundle.actions); + + // A second proposal of the same amount and the same bundle, by the same member. + let second_nonce = 3; + let second_proposal = BatchTransition::new_token_mint_to_pool_transition( + token_id, + first.id(), + contract.id(), + 0, + amount, + bundle.clone(), + None, + Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(0)), + &first_key, + second_nonce, + 0, + &first_signer, + platform_version, + None, + ) + .await + .expect("token mint to pool proposal"); + assert_matches!( + process(&platform, &second_proposal) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(&platform, token_id), 0); + + let confirmation = |proposer: Identifier, proposer_nonce| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: 0, + action_id: TokenMintToPoolTransition::calculate_action_id_with_fields( + token_id.as_bytes(), + proposer.as_bytes(), + proposer_nonce, + amount, + &digest, + ), + action_is_proposer: false, + }, + ) + }; + + // The third member closes the second proposal: it mints and records the bundle. + let close_second = BatchTransition::new_token_mint_to_pool_transition( + token_id, + third.id(), + contract.id(), + 0, + amount, + bundle.clone(), + None, + Some(confirmation(first.id(), second_nonce)), + &third_key, + 1, + 0, + &third_signer, + platform_version, + None, + ) + .await + .expect("token mint to pool confirmation"); + assert_matches!( + process(&platform, &close_second) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(&platform, token_id), amount); + assert_dummy_nullifiers_recorded(&platform, token_id, &bundle); + let notes_after_mint = pool_notes_count(&platform, token_id); + + // The second member's signature would close the first proposal with the same bundle. + let close_first = BatchTransition::new_token_mint_to_pool_transition( + token_id, + second.id(), + contract.id(), + 0, + amount, + bundle.clone(), + None, + Some(confirmation(first.id(), first_nonce)), + &second_key, + 1, + 0, + &second_signer, + platform_version, + None, + ) + .await + .expect("token mint to pool confirmation"); + assert_check_tx_accepts(&platform, &close_first); + let first_nullifier = bundle.actions[0].nullifier; + assert_matches!( + process(&platform, &close_first) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::NullifierAlreadySpentError(error)), + .. + }] if error.nullifier() == first_nullifier + ); + + assert_eq!(pool_balance(&platform, token_id), amount); + assert_eq!(pool_notes_count(&platform, token_id), notes_after_mint); + assert_eq!( + total_supply(&platform, token_id), + OWNER_INITIAL_BALANCE + amount, + "the group must mint the bundle once" + ); + assert_tokens_conserved(&platform); + } + + /// CheckTx cannot verify a claim's bundle, whose amount is only known against state, so it + /// admits another claimant's copy; in the block, the original's recorded dummy nullifier + /// refuses the copy before its proof is looked at. + #[tokio::test] + async fn test_a_claim_to_pool_bundle_copied_by_another_claimant_after_it_landed_is_refused() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9503); + let release = 445; + + let (owner, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (claimant, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (copier, copier_signer, copier_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (claimant_id, copier_id) = (claimant.id(), copier.id()); + // Both are due the same release, so the copier's claim resolves to exactly the amount + // the copied bundle carries. + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + owner.id(), + Some(move |configuration: &mut TokenConfiguration| { + configuration.set_has_shielded_pool(true); + configuration + .distribution_rules_mut() + .set_pre_programmed_distribution(Some(TokenPreProgrammedDistribution::V0( + TokenPreProgrammedDistributionV0 { + distributions: [( + 100, + [(claimant_id, release), (copier_id, release)].into(), + )] + .into(), + }, + ))); + }), + None, + None, + None, + platform_version, + ); + + fast_forward_to_block(&platform, 100, 40, 42, 1, false); + let block_info = BlockInfo { + time_ms: 200, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }; + + let claim = build_token_claim_to_pool_transition( + token_id, + claimant_id, + contract.id(), + 0, + &wallet_address(), + release, + TokenDistributionType::PreProgrammed, + None, + [0u8; 36], + None, + None, + &key, + 2, + 0, + &signer, + &Prover, + platform_version, + ) + .await + .expect("client-built token claim to pool"); + assert_check_tx_accepts(&platform, &claim); + assert_matches!( + process_at(&platform, &claim, &block_info) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(&platform, token_id), release); + let bundle = proven_bundle(&claim); + assert_dummy_nullifiers_recorded(&platform, token_id, &bundle); + let notes_after_original = pool_notes_count(&platform, token_id); + + let copy = BatchTransition::new_token_claim_to_pool_transition( + token_id, + copier_id, + contract.id(), + 0, + TokenDistributionType::PreProgrammed, + None, + bundle.clone(), + None, + &copier_key, + 1, + 0, + &copier_signer, + platform_version, + None, + ) + .await + .expect("token claim to pool transition"); + assert_repeat_refused_in_block( + &platform, + ©, + copier_id, + contract.id(), + 1, + &bundle, + Some(&block_info), + ); + assert_eq!(pool_balance(&platform, token_id), release); + assert_eq!(pool_notes_count(&platform, token_id), notes_after_original); + assert_eq!( + total_supply(&platform, token_id), + OWNER_INITIAL_BALANCE + release + ); + + // The copier was due the release all along and can still take it with a bundle of its + // own: what was refused is the copied bundle, not the claim. + let own_claim = build_token_claim_to_pool_transition( + token_id, + copier_id, + contract.id(), + 0, + &wallet_address(), + release, + TokenDistributionType::PreProgrammed, + None, + [0u8; 36], + None, + None, + &copier_key, + 2, + 0, + &copier_signer, + &Prover, + platform_version, + ) + .await + .expect("client-built token claim to pool"); + assert_check_tx_accepts(&platform, &own_claim); + assert_matches!( + process_at(&platform, &own_claim, &block_info) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(&platform, token_id), 2 * release); + assert_eq!( + total_supply(&platform, token_id), + OWNER_INITIAL_BALANCE + 2 * release + ); + assert_tokens_conserved(&platform); + } + + #[tokio::test] + async fn test_a_direct_purchase_to_pool_bundle_resubmitted_by_its_buyer_is_refused() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9504); + let (token_count, price) = (3, dash_to_credits!(0.03)); + + let (seller, seller_signer, seller_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (buyer, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(1.0)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + seller.id(), + Some(|configuration: &mut TokenConfiguration| { + configuration.set_has_shielded_pool(true); + configuration + .distribution_rules_mut() + .set_change_direct_purchase_pricing_rules(ChangeControlRules::V0( + ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::ContractOwner, + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }, + )); + }), + None, + None, + None, + platform_version, + ); + let set_price = BatchTransition::new_token_change_direct_purchase_price_transition( + token_id, + seller.id(), + contract.id(), + 0, + Some(TokenPricingSchedule::SinglePrice(dash_to_credits!(0.01))), + None, + None, + &seller_key, + 2, + 0, + &seller_signer, + platform_version, + None, + ) + .await + .expect("set price transition"); + assert_matches!( + process(&platform, &set_price) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + let seller_credits_before = credits(&platform, seller.id()); + let purchase = build_token_direct_purchase_to_pool_transition( + token_id, + buyer.id(), + contract.id(), + 0, + &wallet_address(), + token_count, + price, + [0u8; 36], + None, + &key, + 1, + 0, + &signer, + &Prover, + platform_version, + ) + .await + .expect("client-built token direct purchase to pool"); + assert_check_tx_accepts(&platform, &purchase); + assert_matches!( + process(&platform, &purchase).execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert_eq!(pool_balance(&platform, token_id), token_count); + let bundle = proven_bundle(&purchase); + assert_dummy_nullifiers_recorded(&platform, token_id, &bundle); + let notes_after_original = pool_notes_count(&platform, token_id); + let seller_credits_after_original = credits(&platform, seller.id()); + assert_eq!(seller_credits_after_original, seller_credits_before + price); + + // The buyer can still afford the price, so a refusal cannot be its credits. + let repeat = BatchTransition::new_token_direct_purchase_to_pool_transition( + token_id, + buyer.id(), + contract.id(), + 0, + token_count, + price, + bundle.clone(), + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token direct purchase to pool transition"); + let charged = assert_repeat_refused_in_block( + &platform, + &repeat, + buyer.id(), + contract.id(), + 2, + &bundle, + None, + ); + assert!( + charged < price, + "a refused repeat costs its fee, never the price" + ); + + assert_eq!(pool_balance(&platform, token_id), token_count); + assert_eq!(pool_notes_count(&platform, token_id), notes_after_original); + assert_eq!( + total_supply(&platform, token_id), + OWNER_INITIAL_BALANCE + token_count + ); + assert_eq!( + credits(&platform, seller.id()), + seller_credits_after_original, + "a refused repeat must not pay the seller" + ); + assert_tokens_conserved(&platform); + } + + /// A shield taken from the mempool and submitted by another holder ahead of its author. + /// + /// Before the author's shield lands its dummy nullifiers are on record nowhere, so the + /// pool's record cannot refuse the copy; only the owner in the sighash can. Had the copy + /// landed, it would have paid the author's recipient out of the copier's tokens and left the + /// author's own shield to be refused as a repeat, charged its fee. The author's shield is + /// admitted by CheckTx before the copy and executes after it, which is what shows the bundle + /// was valid all along and the copy was refused for its owner alone. + #[tokio::test] + async fn test_a_shield_bundle_front_run_by_another_holder_is_refused_and_the_original_lands() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9511); + let amount = 4_200; + + let (owner, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (copier, copier_signer, copier_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + owner.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + + // The copier holds as many tokens as the copy shields, so a refusal cannot be its + // balance. + let transfer = BatchTransition::new_token_transfer_transition( + token_id, + owner.id(), + contract.id(), + 0, + amount, + copier.id(), + None, + None, + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token transfer transition"); + assert_matches!( + process(&platform, &transfer).execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); - // A second proposal of the same amount and the same bundle, by another member. - let second_nonce = 1; - let second_proposal = BatchTransition::new_token_mint_to_pool_transition( + let shield = build_token_shield_transition( token_id, - second.id(), + owner.id(), contract.id(), 0, + &wallet_address(), amount, - bundle.clone(), + [0u8; 36], None, - Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(0)), - &second_key, - second_nonce, + &key, + 3, 0, - &second_signer, + &signer, + &Prover, platform_version, - None, ) .await - .expect("token mint to pool proposal"); - assert_matches!( - process(&platform, &second_proposal) - .execution_results() - .as_slice(), - [StateTransitionExecutionResult::SuccessfulExecution { .. }] - ); - assert_eq!(pool_balance(&platform, token_id), 0); - - let confirmation = |proposer: Identifier, proposer_nonce| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: 0, - action_id: TokenMintToPoolTransition::calculate_action_id_with_fields( - token_id.as_bytes(), - proposer.as_bytes(), - proposer_nonce, - amount, - &digest, - ), - action_is_proposer: false, - }, - ) - }; + .expect("client-built token shield"); + assert_check_tx_accepts(&platform, &shield); + let bundle = proven_bundle(&shield); - // The third member closes the second proposal: it mints and records the bundle. - let close_second = BatchTransition::new_token_mint_to_pool_transition( + let copy = BatchTransition::new_token_shield_transition( token_id, - third.id(), + copier.id(), contract.id(), 0, amount, bundle.clone(), - None, - Some(confirmation(second.id(), second_nonce)), - &third_key, + &copier_key, 1, 0, - &third_signer, + &copier_signer, platform_version, None, ) .await - .expect("token mint to pool confirmation"); + .expect("token shield transition"); + assert_front_run_refused( + &platform, + ©, + copier.id(), + contract.id(), + 1, + &bundle, + true, + None, + ); + assert_eq!(pool_balance(&platform, token_id), 0); + assert_eq!(pool_notes_count(&platform, token_id), 0); + assert_eq!( + identity_token_balance(&platform, token_id, copier.id()), + Some(amount), + "a refused copy must not take the copier's tokens" + ); + assert_matches!( - process(&platform, &close_second) - .execution_results() - .as_slice(), + process(&platform, &shield).execution_results().as_slice(), [StateTransitionExecutionResult::SuccessfulExecution { .. }] ); assert_eq!(pool_balance(&platform, token_id), amount); assert_dummy_nullifiers_recorded(&platform, token_id, &bundle); - let notes_after_mint = pool_notes_count(&platform, token_id); + assert_eq!( + identity_token_balance(&platform, token_id, owner.id()), + Some(OWNER_INITIAL_BALANCE - 2 * amount) + ); - // The second member's signature would close the first proposal with the same bundle. - let close_first = BatchTransition::new_token_mint_to_pool_transition( + // The bundle that just executed, offered again under the copier: CheckTx, which never + // reads the pool's record, still refuses it on the owner. + let late_copy = BatchTransition::new_token_shield_transition( token_id, - second.id(), + copier.id(), contract.id(), 0, amount, - bundle.clone(), - None, - Some(confirmation(first.id(), first_nonce)), - &second_key, + bundle, + &copier_key, 2, 0, - &second_signer, + &copier_signer, platform_version, None, ) .await - .expect("token mint to pool confirmation"); - assert_check_tx_accepts(&platform, &close_first); - let first_nullifier = bundle.actions[0].nullifier; + .expect("token shield transition"); assert_matches!( - process(&platform, &close_first) - .execution_results() - .as_slice(), - [StateTransitionExecutionResult::PaidConsensusError { - error: ConsensusError::StateError(StateError::NullifierAlreadySpentError(error)), - .. - }] if error.nullifier() == first_nullifier - ); - - assert_eq!(pool_balance(&platform, token_id), amount); - assert_eq!(pool_notes_count(&platform, token_id), notes_after_mint); - assert_eq!( - total_supply(&platform, token_id), - OWNER_INITIAL_BALANCE + amount, - "the group must mint the bundle once" + assert_check_tx_rejects(&platform, &late_copy).as_slice(), + [ConsensusError::StateError( + StateError::InvalidShieldedProofError(_) + )] ); assert_tokens_conserved(&platform); } + /// A claim into the pool taken from the mempool and submitted by another identity due the + /// same release, ahead of its author. CheckTx cannot verify a claim's bundle, so it admits + /// the copy; block validation refuses it on the owner the bundle names, before the author's + /// claim has recorded anything the pool could refuse it with. #[tokio::test] - async fn test_a_claim_to_pool_bundle_copied_by_another_claimant_is_refused() { + async fn test_a_claim_to_pool_bundle_front_run_by_another_claimant_is_refused_and_the_original_lands( + ) { let platform_version = PlatformVersion::latest(); let mut platform = platform_with_latest_version(); - let mut rng = StdRng::seed_from_u64(9503); + let mut rng = StdRng::seed_from_u64(9513); let release = 445; let (owner, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); @@ -5104,7 +6266,7 @@ mod token_pool_outputs_only_copy_tests { setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); let (claimant_id, copier_id) = (claimant.id(), copier.id()); // Both are due the same release, so the copier's claim resolves to exactly the amount - // the copied bundle carries. + // the copied bundle carries and only the bundle's owner can be why it is refused. let (contract, token_id) = create_token_contract_with_owner_identity( &mut platform, owner.id(), @@ -5135,6 +6297,7 @@ mod token_pool_outputs_only_copy_tests { core_height: 42, epoch: Epoch::new(1).unwrap(), }; + let supply_before = total_supply(&platform, token_id); let claim = build_token_claim_to_pool_transition( token_id, @@ -5158,16 +6321,7 @@ mod token_pool_outputs_only_copy_tests { .await .expect("client-built token claim to pool"); assert_check_tx_accepts(&platform, &claim); - assert_matches!( - process_at(&platform, &claim, &block_info) - .execution_results() - .as_slice(), - [StateTransitionExecutionResult::SuccessfulExecution { .. }] - ); - assert_eq!(pool_balance(&platform, token_id), release); let bundle = proven_bundle(&claim); - assert_dummy_nullifiers_recorded(&platform, token_id, &bundle); - let notes_after_original = pool_notes_count(&platform, token_id); let copy = BatchTransition::new_token_claim_to_pool_transition( token_id, @@ -5187,64 +6341,44 @@ mod token_pool_outputs_only_copy_tests { ) .await .expect("token claim to pool transition"); - assert_copy_refused_in_block( + assert_front_run_refused( &platform, ©, copier_id, contract.id(), + 1, &bundle, + false, Some(&block_info), ); - assert_eq!(pool_balance(&platform, token_id), release); - assert_eq!(pool_notes_count(&platform, token_id), notes_after_original); + assert_eq!(pool_balance(&platform, token_id), 0); assert_eq!( total_supply(&platform, token_id), - OWNER_INITIAL_BALANCE + release + supply_before, + "a refused copy must not release anything" ); - // The copier was due the release all along and can still take it with a bundle of its - // own: what was refused is the copied bundle, not the claim. - let own_claim = build_token_claim_to_pool_transition( - token_id, - copier_id, - contract.id(), - 0, - &wallet_address(), - release, - TokenDistributionType::PreProgrammed, - None, - [0u8; 36], - None, - None, - &copier_key, - 2, - 0, - &copier_signer, - &Prover, - platform_version, - ) - .await - .expect("client-built token claim to pool"); - assert_check_tx_accepts(&platform, &own_claim); assert_matches!( - process_at(&platform, &own_claim, &block_info) + process_at(&platform, &claim, &block_info) .execution_results() .as_slice(), [StateTransitionExecutionResult::SuccessfulExecution { .. }] ); - assert_eq!(pool_balance(&platform, token_id), 2 * release); - assert_eq!( - total_supply(&platform, token_id), - OWNER_INITIAL_BALANCE + 2 * release - ); + assert_eq!(pool_balance(&platform, token_id), release); + assert_eq!(total_supply(&platform, token_id), supply_before + release); + assert_dummy_nullifiers_recorded(&platform, token_id, &bundle); assert_tokens_conserved(&platform); } + /// A purchase into the pool taken from the mempool and submitted by another buyer ahead of + /// its author. Had it landed, the copier would have paid the price for tokens minted to the + /// author's recipient and the author's own purchase would have been refused as a repeat. #[tokio::test] - async fn test_a_direct_purchase_to_pool_bundle_copied_by_another_buyer_is_refused() { + async fn test_a_direct_purchase_to_pool_bundle_front_run_by_another_buyer_is_refused_and_the_original_lands( + ) { let platform_version = PlatformVersion::latest(); let mut platform = platform_with_latest_version(); - let mut rng = StdRng::seed_from_u64(9504); + let mut rng = StdRng::seed_from_u64(9514); let (token_count, price) = (3, dash_to_credits!(0.03)); let (seller, seller_signer, seller_key) = @@ -5297,8 +6431,9 @@ mod token_pool_outputs_only_copy_tests { .as_slice(), [StateTransitionExecutionResult::SuccessfulExecution { .. }] ); - + let supply_before = total_supply(&platform, token_id); let seller_credits_before = credits(&platform, seller.id()); + let purchase = build_token_direct_purchase_to_pool_transition( token_id, buyer.id(), @@ -5319,17 +6454,9 @@ mod token_pool_outputs_only_copy_tests { .await .expect("client-built token direct purchase to pool"); assert_check_tx_accepts(&platform, &purchase); - assert_matches!( - process(&platform, &purchase).execution_results().as_slice(), - [StateTransitionExecutionResult::SuccessfulExecution { .. }] - ); - assert_eq!(pool_balance(&platform, token_id), token_count); let bundle = proven_bundle(&purchase); - assert_dummy_nullifiers_recorded(&platform, token_id, &bundle); - let notes_after_original = pool_notes_count(&platform, token_id); - let seller_credits_after_original = credits(&platform, seller.id()); - assert_eq!(seller_credits_after_original, seller_credits_before + price); + // The copier can afford the price, so a refusal cannot be its credits. let copy = BatchTransition::new_token_direct_purchase_to_pool_transition( token_id, copier.id(), @@ -5347,30 +6474,42 @@ mod token_pool_outputs_only_copy_tests { ) .await .expect("token direct purchase to pool transition"); - let charged = assert_copy_refused_in_block( + let charged = assert_front_run_refused( &platform, ©, copier.id(), contract.id(), + 1, &bundle, + true, None, ); assert!( charged < price, "a refused copy costs its fee, never the price" ); + assert_eq!(pool_balance(&platform, token_id), 0); + assert_eq!(total_supply(&platform, token_id), supply_before); + assert_eq!( + credits(&platform, seller.id()), + seller_credits_before, + "a refused copy must not pay the seller" + ); + assert_matches!( + process(&platform, &purchase).execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); assert_eq!(pool_balance(&platform, token_id), token_count); - assert_eq!(pool_notes_count(&platform, token_id), notes_after_original); assert_eq!( total_supply(&platform, token_id), - OWNER_INITIAL_BALANCE + token_count + supply_before + token_count ); assert_eq!( credits(&platform, seller.id()), - seller_credits_after_original, - "a refused copy must not pay the seller" + seller_credits_before + price ); + assert_dummy_nullifiers_recorded(&platform, token_id, &bundle); assert_tokens_conserved(&platform); } @@ -5679,4 +6818,80 @@ mod token_pool_outputs_only_copy_tests { ); assert_tokens_conserved(&platform); } + + /// The same two type 28 transitions in the other order: the copy is sequenced ahead of the + /// original, which the block proposer can do with anything it lifted from the mempool. + /// Nothing in the token bundle's sighash names who pays, so the copy lands and pays for the + /// original's notes. What matters is who bears the refusal that follows: the original is + /// refused on the dummy nullifier the copy recorded, and that refusal must be unpaid — the + /// honest payer's credit note stays unspent — or a stranger could make the honest payer pay + /// for a purchase it never got to make. + #[tokio::test] + async fn test_a_purchase_from_shielded_pool_copy_sequenced_first_does_not_charge_the_original_payer( + ) { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let price_per_token = dash_to_credits!(0.01); + let (seller_id, contract_id, token_id) = + token_for_sale(&mut platform, 9516, price_per_token).await; + let token_count = 3; + let price = price_per_token * token_count; + + let (purchase, fee) = + build_purchase(&platform, token_id, contract_id, token_count, price, 15); + let original = purchase_v0(&purchase); + let original_fee_nullifier = original.fee_actions[0].nullifier; + let (copy, copy_fee_nullifier) = + purchase_copy_with_another_fee_bundle(&platform, original, price, fee, 16, 9517); + // Both payers' notes are in the credit pool and its total covers both purchases, so + // only the token bundle can tell the two apart. + set_pool_total_balance(&platform, 2 * CREDIT_NOTE); + assert_check_tx_accepts(&platform, &purchase); + assert_check_tx_accepts(&platform, ©); + + let seller_credits_before = credits(&platform, seller_id); + let first = original.token_actions[0].nullifier; + assert_matches!( + process_in_one_block(&platform, &[©, &purchase], &BlockInfo::default()) + .execution_results() + .as_slice(), + [ + StateTransitionExecutionResult::SuccessfulExecution { .. }, + StateTransitionExecutionResult::UnpaidConsensusError(ConsensusError::StateError( + StateError::NullifierAlreadySpentError(error) + )), + ] if error.nullifier() == first + ); + + assert!( + !platform + .drive + .has_nullifier(&original_fee_nullifier, None, &mut vec![], platform_version) + .expect("credit pool nullifier lookup"), + "the refused original must leave its payer's credit note unspent" + ); + assert!( + platform + .drive + .has_nullifier(©_fee_nullifier, None, &mut vec![], platform_version) + .expect("credit pool nullifier lookup"), + "the copy that landed is paid from the copier's own credit note" + ); + assert_eq!( + credit_pool_balance(&platform), + 2 * CREDIT_NOTE - price - fee, + "one purchase is paid for, once" + ); + assert_eq!(pool_balance(&platform, token_id), token_count); + assert_eq!( + total_supply(&platform, token_id), + OWNER_INITIAL_BALANCE + token_count + ); + assert_eq!( + credits(&platform, seller_id), + seller_credits_before + price, + "the seller is paid once" + ); + assert_tokens_conserved(&platform); + } } diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index 96e90afa957..0d680b5adbb 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -757,8 +757,9 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// proposer of a group action), plus the recipient and amount where tokens /// leave the pool; an outputs-only bundle (`TokenShield`, /// `TokenMintToPool`, `TokenClaimToPool`, `TokenDirectPurchaseToPool`), -/// whose anchor is never checked against a pool, binds a per-kind tag and -/// the token id. +/// whose anchor is never checked against a pool, binds a per-kind tag, +/// the token id and the batch owner (for a group action mint, the +/// proposer). /// The pool balances are a term of the token conservation check /// (`calculate_total_tokens_balance` v1 in `DRIVE_TOKEN_METHOD_VERSIONS_V2`). /// `record_token_shielded_pool_anchors` diff --git a/packages/wasm-dpp2/src/shielded/mod.rs b/packages/wasm-dpp2/src/shielded/mod.rs index b730e244fdd..dc85db286f2 100644 --- a/packages/wasm-dpp2/src/shielded/mod.rs +++ b/packages/wasm-dpp2/src/shielded/mod.rs @@ -44,10 +44,12 @@ use wasm_bindgen::prelude::wasm_bindgen; /// - Shielded withdrawal: `outputScript || amount (u64) || coreFeePerByte (u32) || /// pooling (1 byte)`. /// - Token pool transitions that only create notes — token shield, and mint, claim or direct -/// purchase into the pool: `kind tag (1 byte) || tokenId (32 bytes)`, with the tag `0x80`, -/// `0x81`, `0x82` and `0x83` in that order. Empty is rejected: such a bundle carries no -/// anchor, every token pool shares the empty-tree anchor it proves against, and nothing -/// else in the bundle says which pool or which kind it was built for. +/// purchase into the pool: `kind tag (1 byte) || tokenId (32 bytes) || ownerId (32 bytes)`, +/// with the tag `0x80`, `0x81`, `0x82` and `0x83` in that order. `ownerId` is the batch +/// owner; for a group action mint it is the proposer, and every other signer submits the +/// proposer's bundle unchanged. Empty is rejected: such a bundle carries no anchor, every +/// token pool shares the empty-tree anchor it proves against, and nothing else in the bundle +/// says which pool, which kind or which owner it was built for. /// - Other token pool transitions bind their own fields; see the `extra_sighash_data` /// builders in `dpp::shielded`. #[wasm_bindgen(js_name = computePlatformSighash)] From f1d007e13adc71cf01d046aaca88f12272d5a7ad Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 25 Sep 2026 02:02:26 +0700 Subject: [PATCH 32/94] docs(drive): say at the pool writers that they rely on the caller's duplicate check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both writers record a bundle's dummy nullifiers without looking for one repeated across the bundle's own actions. Validation does that, before the action is applied, and the write depends on it — but nothing here said so, and the dependency is not the kind a reader would guess. Two actions sharing a dummy nullifier produce byte-identical insert operations in a single batch. A node running the shipped batching default folds them in silence; a node verifying batch consistency refuses the batch. The two then disagree about one block, and neither side has anything to show for it. Wording only. The check this text points at already runs on every path that reaches these writers. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/drive/tokens/shielded_pool/mint_to_pool/v0/mod.rs | 6 ++++++ .../src/drive/tokens/shielded_pool/shield/v0/mod.rs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/mint_to_pool/v0/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/mint_to_pool/v0/mod.rs index 216c1057279..61d99c71ee3 100644 --- a/packages/rs-drive/src/drive/tokens/shielded_pool/mint_to_pool/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/mint_to_pool/v0/mod.rs @@ -40,6 +40,12 @@ impl Drive { // same. Each note takes its `rho` from its action's dummy nullifier, so the same bundle // entering this pool again would land notes with the same commitments and nullifiers; // the record is what lets validation refuse it. + // + // This writer does not check the bundle for a nullifier repeated across its own actions; + // validation does, before the action is applied, and this write relies on it. Two such + // actions would produce byte-identical insert operations in one batch, which a node + // running the shipped batching default folds in silence while a node verifying batch + // consistency refuses the batch — a disagreement on one block that neither side can see. let dummy_nullifiers: Vec<[u8; 32]> = notes.iter().map(|note| note.nullifier).collect(); drive_operations.extend(self.token_shielded_pool_update_operations( token_id, diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/shield/v0/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/shield/v0/mod.rs index e0109e45ab4..74797a7fbf4 100644 --- a/packages/rs-drive/src/drive/tokens/shielded_pool/shield/v0/mod.rs +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/shield/v0/mod.rs @@ -39,6 +39,12 @@ impl Drive { // same. Each note takes its `rho` from its action's dummy nullifier, so the same bundle // entering this pool again would land notes with the same commitments and nullifiers; // the record is what lets validation refuse it. + // + // This writer does not check the bundle for a nullifier repeated across its own actions; + // validation does, before the action is applied, and this write relies on it. Two such + // actions would produce byte-identical insert operations in one batch, which a node + // running the shipped batching default folds in silence while a node verifying batch + // consistency refuses the batch — a disagreement on one block that neither side can see. let dummy_nullifiers: Vec<[u8; 32]> = notes.iter().map(|note| note.nullifier).collect(); drive_operations.extend(self.token_shielded_pool_update_operations( token_id, From b73f92f5e3aa083d76caf3af993da281d3db3090 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 25 Sep 2026 00:38:46 +0700 Subject: [PATCH 33/94] feat(platform)!: give the credit pool's outputs-only bundles a sighash layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An Orchard bundle that only creates notes has no spends, so no nullifiers, and its anchor is never checked against a pool — the client builds it against the empty tree. The three credit-pool paths of that shape spelled out no sighash layout of their own, so nothing in the bundle named the transition carrying it. This brings them to the same footing as the token pools in this release, which bind a kind tag and the token id for the same reason. Each now binds a kind tag and what actually funds it, which is what a copier cannot reproduce. `ShieldFromIdentity` binds the identity. `ShieldFromAssetLock` binds the asset lock identifier derived from the full outpoint — the txid alone would not do, because multi-output asset locks exist and a sibling credit output of the same transaction would lift the bundle. `Shield` binds a hash over its funding addresses in canonical order, with nonces and amounts excluded, so it names who funds the bundle rather than pinning one exact transition. The three paths needed three different mechanisms, because they are reached differently. `Shield` is verified only in `validate_shielded_proof`, whose generation 1 no shipped table selects. `ShieldFromIdentity` is verified twice — there and in its own transformer, whose generation protocol versions 12 and 13 also select; that edit is in place and inert, because the transition is refused below 14 and because the preimage method is `None` in every table those versions use, rebuilding a byte-identical empty preimage. `ShieldFromAssetLock` could not be edited in place: it is admitted from protocol version 12 and its proof lives only in its transformer. It gets a new generation and a new transition version. At 14 only version 1 is accepted, and a version 0 is refused on its version byte during decode — before any proof work, with no penalty and the asset lock left unspent, so a client that has not updated resubmits from the same lock rather than losing it. Without that split a stale bundle would reach the new generation's verification failure, which burns the proof-verification penalty from the lock. Not closed here, and not claimed to be: a layout says which transition a bundle belongs to, not how many times it may be used. The credit pool records no dummy nullifiers, unlike the token pools in this release. That is a separate change. Every negative was shown red by mutating consensus code or version tables alone: the verifier half reverted; the protocol-version-14 activation removed; the binding wired into the version 8 and 9 tables, which turns the unchanged 12 and 13 tests red; the kind tag dropped; and the asset lock bound by txid instead of outpoint. Protocol versions 12 and 13 are pinned unchanged by their own tests, which still verify an empty preimage. Green: dpp shielded 324, drive-abci shield 316, active-version-range and decode tests 48, formatting clean, workspace clippy clean. Co-Authored-By: Claude Opus 5 (1M context) --- book/src/addresses/platform-addresses.md | 21 +- .../rs-dpp/src/shielded/builder/shield.rs | 16 +- .../builder/shield_from_asset_lock.rs | 86 ++- .../shielded/builder/shield_from_identity.rs | 17 +- packages/rs-dpp/src/shielded/mod.rs | 19 +- packages/rs-dpp/src/shielded/sighash.rs | 465 +++++++++++++- packages/rs-dpp/src/state_transition/mod.rs | 82 ++- .../accessors/mod.rs | 12 + .../methods/mod.rs | 33 +- .../shield_from_asset_lock_transition/mod.rs | 57 +- .../proved.rs | 2 + .../signing_tests.rs | 57 +- .../state_transition_like.rs | 9 + .../state_transition_validation.rs | 1 + .../v0/mod.rs | 2 +- .../v0/state_transition_validation.rs | 109 ++-- .../v1/mod.rs | 152 +++++ .../v1/proved.rs | 18 + .../v1/state_transition_like.rs | 60 ++ .../v1/state_transition_validation.rs | 20 + .../v1/types.rs | 17 + .../v1/v1_methods.rs | 88 +++ .../v1/version.rs | 9 + .../version.rs | 1 + .../processor/traits/shielded_proof.rs | 159 ++++- .../state_transitions/shield/tests.rs | 321 +++++++++- .../shield_from_asset_lock/mod.rs | 11 +- .../shield_from_asset_lock/tests.rs | 586 ++++++++++++++++-- .../transform_into_action/mod.rs | 1 + .../transform_into_action/v0/mod.rs | 19 + .../transform_into_action/v1/mod.rs | 418 +++++++++++++ .../shield_from_identity/tests.rs | 289 ++++++++- .../transform_into_action/v0/mod.rs | 18 +- .../state_transitions/shielded_common/mod.rs | 4 +- .../state_transitions/test_helpers.rs | 62 +- .../prove/prove_state_transition/v0/mod.rs | 6 +- .../shield_from_asset_lock/transformer.rs | 24 +- .../shield_from_asset_lock/v0/transformer.rs | 17 +- .../v0/mod.rs | 6 +- .../dpp_versions/dpp_method_versions/mod.rs | 14 +- .../dpp_versions/dpp_method_versions/v1.rs | 1 + .../dpp_versions/dpp_method_versions/v2.rs | 1 + .../dpp_versions/dpp_method_versions/v3.rs | 5 +- .../v3.rs | 9 +- .../drive_abci_validation_versions/v10.rs | 2 +- .../rs-platform-version/src/version/v14.rs | 27 +- .../wallet/shielded/fund_from_asset_lock.rs | 8 +- .../src/wallet/shielded/operations.rs | 11 +- .../transition/shield_from_asset_lock.rs | 8 + packages/wasm-dpp2/src/shielded/mod.rs | 12 +- .../shield_from_asset_lock_transition.rs | 107 ++-- 51 files changed, 3171 insertions(+), 328 deletions(-) create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/mod.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/proved.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/state_transition_like.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/state_transition_validation.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/types.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/v1_methods.rs create mode 100644 packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/version.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/transform_into_action/v1/mod.rs diff --git a/book/src/addresses/platform-addresses.md b/book/src/addresses/platform-addresses.md index 904fc107739..62425654fac 100644 --- a/book/src/addresses/platform-addresses.md +++ b/book/src/addresses/platform-addresses.md @@ -223,7 +223,7 @@ pub struct ShieldedWithdrawalTransitionV0 { An asset lock proof from the Core chain funds the shielded pool directly. The recipient is an `OrchardAddress` inside the Orchard bundle. No `PlatformAddress` inputs are needed -- the asset lock proof substitutes for them. -The transition also carries an optional `surplus_output: Option`. When the consumed asset lock exceeds `shield_amount + pool_fee`, the leftover *surplus* is credited to this platform address; if it is unset, the surplus folds into the fee pools (bounded by `shielded_implicit_fee_cap`). See [Entry-Transition Fees](../fees/shielded-fees.md#entry-transition-fees-shield-and-shieldfromassetlock). Unlike the transparent recipients of `Unshield`/`ShieldedWithdrawal` (which are bound through the Orchard sighash `extra_data` above), `surplus_output` is bound through the state transition's **own** `platform_signable` signature -- it sits before the `signature` field, so it is part of the signed payload and cannot be substituted or truncated after signing. The Orchard `extra_data` therefore remains empty for this transition. +The transition also carries an optional `surplus_output: Option`. When the consumed asset lock exceeds `shield_amount + pool_fee`, the leftover *surplus* is credited to this platform address; if it is unset, the surplus folds into the fee pools (bounded by `shielded_implicit_fee_cap`). See [Entry-Transition Fees](../fees/shielded-fees.md#entry-transition-fees-shield-and-shieldfromassetlock). Unlike the transparent recipients of `Unshield`/`ShieldedWithdrawal` (which are bound through the Orchard sighash `extra_data` above), `surplus_output` is bound through the state transition's **own** `platform_signable` signature -- it sits before the `signature` field, so it is part of the signed payload and cannot be substituted or truncated after signing. The Orchard `extra_data` therefore does not carry it; what it binds instead is the asset lock itself (see [below](#the-platform-sighash)). ## The Platform Sighash @@ -248,14 +248,29 @@ The `bundle_commitment` is a BLAKE2b-256 hash of the Orchard bundle (per ZIP-244 | Transition | extra_data | |-----------|------------| -| Shield | empty | +| Shield | `0x84 \|\| SHA-256(input addresses)` | +| Shield From Identity | `0x85 \|\| identity_id` | | Shielded Transfer | empty | | Unshield | `output_address.to_bytes() \|\| amount.to_le_bytes()` | | Shielded Withdrawal | `output_script.as_bytes()` | -| Shield From Asset Lock | empty | +| Shield From Asset Lock | `0x86 \|\| asset lock identifier` | This binding is critical for security. Without it, an attacker who intercepts an unshield transition could substitute the `output_address` while reusing the valid Orchard proof and signatures. The sighash ensures the Orchard bundle's spend authorization signatures commit to the specific transparent recipient. +The three transitions that only create notes bind something for a different reason. Their bundles have no spends, so they carry no anchor of their own: the proof and the binding signature verify wherever the bundle is submitted. Left unbound, the authorized bundle is a self-contained object anybody could lift out of the mempool and wrap in a transition of their own, paid with their own credits, and any bundle ever published could be replayed. The copier gains nothing — they pay the full amount to the original recipient — but the copy lands a second note with the same commitment and the same nullifier in the credit pool, so only one of the two can ever be spent. + +Each of these bundles therefore binds a kind tag and its owner, the thing that funds it and that a third party cannot authorize: + +- **Shield**: the SHA-256 of its input addresses, each in its 21-byte encoding (`PlatformAddress::to_bytes`), in the order the transition serializes its inputs. The nonces and contributed amounts are not included: the owner says who funds the bundle, not which transition carries it. +- **Shield From Identity**: the identity id. +- **Shield From Asset Lock**: the asset lock identifier, the double SHA-256 of the locked 36-byte outpoint (the same value an identity created from that lock would get as its id; the tag keeps the two apart). Because a successful shield consumes the whole lock, a bundle bound to it can land at most once, even when its own sender resubmits it. Binding only the transaction id would not do this, and would let the holder of another credit output of the same lock transaction lift the bundle. + +The tags `0x84`, `0x85` and `0x86` sit above the state transition type range, next to the token pools' outputs-only tags `0x80`–`0x83`. The binding starts at protocol version 14; at earlier protocol versions `Shield` and `Shield From Asset Lock` bind nothing, and a client building for one of those versions must bind nothing too. Both sides read the choice from the same version field (`dpp.methods.credit_pool_bundle_binding`), so a client that builds with the network's protocol version produces what that network verifies. + +`Shield From Asset Lock` also changes its transition version at protocol version 14: version 1 carries the bound bundle and is the only version 14 admits, while version 0, whose bundle binds nothing, is admitted only up to 13. A version 0 presented at 14, such as one still waiting in the mempool when 14 activates, is refused on its version byte when the transition is decoded, before any proof is verified: nothing is charged and its asset lock stays unspent, so its sender can resubmit it as version 1 from the same lock. Checking its unbound bundle against the bound preimage instead would fail the proof and burn the proof-failure penalty from an honest lock. + +The binding does not prevent Faerie Gold. The `rho` of an outputs-only note is the nullifier of its bundle's dummy spend, so a sender who builds and signs a fresh bundle reusing the same dummy spend note and `rseed` gets the same commitment and the same nullifier whatever the preimage binds, and a recipient that counts deposits by commitment credits two payments where only one can be spent. Nor does it stop a `Shield` or `Shield From Identity` funder landing their own bundle twice through a new transition, as a client retrying with a fresh nonce does. Neither is closed: that would need the bundles' dummy nullifiers recorded and checked, which the platform does not do. + The same `compute_platform_sighash` function is used on both sides: the client uses it when signing the bundle, and the platform uses it when verifying. ## Trial Decryption and Address Privacy diff --git a/packages/rs-dpp/src/shielded/builder/shield.rs b/packages/rs-dpp/src/shielded/builder/shield.rs index e8d9a5cc558..b78ac3dd77a 100644 --- a/packages/rs-dpp/src/shielded/builder/shield.rs +++ b/packages/rs-dpp/src/shielded/builder/shield.rs @@ -5,6 +5,7 @@ use crate::address_funds::{OrchardAddress, PlatformAddress}; use crate::fee::Credits; use crate::identity::signer::Signer; use crate::prelude::{AddressNonce, UserFeeIncrease}; +use crate::shielded::shield_extra_sighash_data; use crate::state_transition::shield_transition::methods::ShieldTransitionMethodsV0; use crate::state_transition::shield_transition::ShieldTransition; use crate::state_transition::StateTransition; @@ -52,10 +53,21 @@ pub async fn build_shield_transition, P: OrchardProve )); } + // Bound to the funding addresses, so nobody else can re-wrap the proved bundle; empty at + // protocol versions whose verifier predates the binding. + let extra_sighash_data = shield_extra_sighash_data(&inputs, platform_version)?; + // Shield (Type 15) never pads with anonymity-set fillers — only the // Type 18 ShieldFromAssetLock pool-seeding path does (`dummy_outputs`). - let bundle = - build_output_only_bundle(recipient, shield_amount, memo, sender_ovk, 0, &[], prover)?; + let bundle = build_output_only_bundle( + recipient, + shield_amount, + memo, + sender_ovk, + 0, + &extra_sighash_data, + prover, + )?; let sb = serialize_authorized_bundle(&bundle); ShieldTransition::try_from_bundle_with_signer( diff --git a/packages/rs-dpp/src/shielded/builder/shield_from_asset_lock.rs b/packages/rs-dpp/src/shielded/builder/shield_from_asset_lock.rs index 2b46bead121..bb5dbe3190c 100644 --- a/packages/rs-dpp/src/shielded/builder/shield_from_asset_lock.rs +++ b/packages/rs-dpp/src/shielded/builder/shield_from_asset_lock.rs @@ -1,12 +1,15 @@ use crate::address_funds::{OrchardAddress, PlatformAddress}; use crate::prelude::AssetLockProof; +use crate::shielded::shield_from_asset_lock_extra_sighash_data; use crate::state_transition::shield_from_asset_lock_transition::methods::ShieldFromAssetLockTransitionMethodsV0; use crate::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; use crate::state_transition::StateTransition; use crate::ProtocolError; use platform_version::version::PlatformVersion; -use super::{build_output_only_bundle, serialize_authorized_bundle, OrchardProver}; +use super::{ + build_output_only_bundle, serialize_authorized_bundle, OrchardProver, SerializedBundle, +}; /// Builds a ShieldFromAssetLock state transition (core asset lock -> shielded pool). /// @@ -46,28 +49,16 @@ pub fn build_shield_from_asset_lock_transition( dummy_outputs: usize, platform_version: &PlatformVersion, ) -> Result { - let bundle = build_output_only_bundle( + let (sb, value_balance) = build_bound_bundle( recipient, shield_amount, + &asset_lock_proof, + prover, memo, sender_ovk, dummy_outputs, - &[], - prover, + platform_version, )?; - let sb = serialize_authorized_bundle(&bundle); - - // For output-only bundles, Orchard value_balance is negative (value flowing in). - // Convert to u64 (absolute amount entering the pool). - let value_balance = sb - .value_balance - .checked_neg() - .and_then(|v| u64::try_from(v).ok()) - .ok_or_else(|| { - ProtocolError::ShieldedBuildError( - "shield_from_asset_lock: bundle value_balance is not negative".to_string(), - ) - })?; ShieldFromAssetLockTransition::try_from_asset_lock_with_bundle( asset_lock_proof, @@ -128,13 +119,57 @@ where P: OrchardProver, AS: ::key_wallet::signer::Signer, { + let (sb, value_balance) = build_bound_bundle( + recipient, + shield_amount, + &asset_lock_proof, + prover, + memo, + sender_ovk, + dummy_outputs, + platform_version, + )?; + + ShieldFromAssetLockTransition::try_from_asset_lock_with_bundle_and_signer( + asset_lock_proof, + asset_lock_proof_path, + asset_lock_signer, + sb.actions, + value_balance, + sb.anchor, + sb.proof, + sb.binding_signature, + surplus_output, + platform_version, + ) + .await +} + +/// Proves the outputs-only bundle of a `ShieldFromAssetLock` funded by `asset_lock_proof` and +/// returns it with the amount it moves into the pool. Both builders go through here, so a client +/// signing with a raw key and one signing through an external signer bind the same preimage. +#[allow(clippy::too_many_arguments)] +fn build_bound_bundle( + recipient: &OrchardAddress, + shield_amount: u64, + asset_lock_proof: &AssetLockProof, + prover: &P, + memo: [u8; 36], + sender_ovk: Option, + dummy_outputs: usize, + platform_version: &PlatformVersion, +) -> Result<(SerializedBundle, u64), ProtocolError> { + // Bound to the funding asset lock, so nobody can re-wrap the proved bundle around a lock + // of their own; empty at protocol versions whose verifier predates the binding. + let extra_sighash_data = + shield_from_asset_lock_extra_sighash_data(asset_lock_proof, platform_version)?; let bundle = build_output_only_bundle( recipient, shield_amount, memo, sender_ovk, dummy_outputs, - &[], + &extra_sighash_data, prover, )?; let sb = serialize_authorized_bundle(&bundle); @@ -150,20 +185,7 @@ where "shield_from_asset_lock: bundle value_balance is not negative".to_string(), ) })?; - - ShieldFromAssetLockTransition::try_from_asset_lock_with_bundle_and_signer( - asset_lock_proof, - asset_lock_proof_path, - asset_lock_signer, - sb.actions, - value_balance, - sb.anchor, - sb.proof, - sb.binding_signature, - surplus_output, - platform_version, - ) - .await + Ok((sb, value_balance)) } #[cfg(test)] diff --git a/packages/rs-dpp/src/shielded/builder/shield_from_identity.rs b/packages/rs-dpp/src/shielded/builder/shield_from_identity.rs index dc1cef250b4..37e2345bd3e 100644 --- a/packages/rs-dpp/src/shielded/builder/shield_from_identity.rs +++ b/packages/rs-dpp/src/shielded/builder/shield_from_identity.rs @@ -1,7 +1,9 @@ use crate::address_funds::OrchardAddress; +use crate::identity::accessors::IdentityGettersV0; use crate::identity::signer::Signer; use crate::identity::{Identity, IdentityPublicKey}; use crate::prelude::{IdentityNonce, UserFeeIncrease}; +use crate::shielded::shield_from_identity_extra_sighash_data; use crate::state_transition::shield_from_identity_transition::methods::ShieldFromIdentityTransitionMethodsV0; use crate::state_transition::shield_from_identity_transition::ShieldFromIdentityTransition; use crate::state_transition::StateTransition; @@ -38,8 +40,19 @@ pub async fn build_shield_from_identity_transition< )); } - let bundle = - build_output_only_bundle(recipient, shield_amount, memo, sender_ovk, 0, &[], prover)?; + // Bound to the funding identity, so no other identity can sign the proved bundle into a + // transition of its own. + let extra_sighash_data = + shield_from_identity_extra_sighash_data(&identity.id().to_buffer(), platform_version)?; + let bundle = build_output_only_bundle( + recipient, + shield_amount, + memo, + sender_ovk, + 0, + &extra_sighash_data, + prover, + )?; let sb = serialize_authorized_bundle(&bundle); ShieldFromIdentityTransition::try_from_bundle_with_identity_signer( diff --git a/packages/rs-dpp/src/shielded/mod.rs b/packages/rs-dpp/src/shielded/mod.rs index e33c30106a2..47f810148f0 100644 --- a/packages/rs-dpp/src/shielded/mod.rs +++ b/packages/rs-dpp/src/shielded/mod.rs @@ -44,15 +44,17 @@ pub fn serialized_actions_digest(actions: &[SerializedAction]) -> [u8; 32] { } pub use sighash::{ - compute_platform_sighash, document_token_payment_extra_sighash_data, - document_token_payment_extra_sighash_data_v0, identity_create_from_shielded_extra_sighash_data, + compute_platform_sighash, credit_pool_output_only_extra_sighash_data_v0, + document_token_payment_extra_sighash_data, document_token_payment_extra_sighash_data_v0, + identity_create_from_shielded_extra_sighash_data, identity_create_from_shielded_extra_sighash_data_v0, identity_top_up_from_shielded_extra_sighash_data, - identity_top_up_from_shielded_extra_sighash_data_v0, shielded_withdrawal_extra_sighash_data, - shielded_withdrawal_extra_sighash_data_v0, token_burn_from_pool_extra_sighash_data, - token_burn_from_pool_extra_sighash_data_v0, token_pool_fee_bundle_extra_sighash_data, - token_pool_fee_bundle_extra_sighash_data_v0, token_pool_output_only_extra_sighash_data, - token_pool_output_only_extra_sighash_data_v0, + identity_top_up_from_shielded_extra_sighash_data_v0, shield_extra_sighash_data, + shield_from_asset_lock_extra_sighash_data, shield_from_identity_extra_sighash_data, + shielded_withdrawal_extra_sighash_data, shielded_withdrawal_extra_sighash_data_v0, + token_burn_from_pool_extra_sighash_data, token_burn_from_pool_extra_sighash_data_v0, + token_pool_fee_bundle_extra_sighash_data, token_pool_fee_bundle_extra_sighash_data_v0, + token_pool_output_only_extra_sighash_data, token_pool_output_only_extra_sighash_data_v0, token_purchase_from_shielded_pool_extra_sighash_data, token_purchase_from_shielded_pool_extra_sighash_data_v0, token_shielded_transfer_extra_sighash_data, token_shielded_transfer_extra_sighash_data_v0, @@ -61,7 +63,8 @@ pub use sighash::{ token_unshield_extra_sighash_data, token_unshield_extra_sighash_data_v0, token_unshield_with_shielded_fee_extra_sighash_data, token_unshield_with_shielded_fee_extra_sighash_data_v0, unshield_extra_sighash_data, - unshield_extra_sighash_data_v0, TOKEN_CLAIM_TO_POOL_BUNDLE_TAG, + unshield_extra_sighash_data_v0, SHIELD_BUNDLE_TAG, SHIELD_FROM_ASSET_LOCK_BUNDLE_TAG, + SHIELD_FROM_IDENTITY_BUNDLE_TAG, TOKEN_CLAIM_TO_POOL_BUNDLE_TAG, TOKEN_DIRECT_PURCHASE_TO_POOL_BUNDLE_TAG, TOKEN_MINT_TO_POOL_BUNDLE_TAG, TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE, TOKEN_SHIELD_BUNDLE_TAG, TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE, diff --git a/packages/rs-dpp/src/shielded/sighash.rs b/packages/rs-dpp/src/shielded/sighash.rs index c354a7af9a8..4bf46574a32 100644 --- a/packages/rs-dpp/src/shielded/sighash.rs +++ b/packages/rs-dpp/src/shielded/sighash.rs @@ -4,10 +4,14 @@ //! per-action spend-auth signatures + the RedPallas binding signature over the platform sighash. //! These helpers build the transparent `extra_data` each transition binds into that sighash so the //! signing (client/builder) and verifying (consensus) sides commit to identical bytes. The byte -//! layouts are consensus-critical and versioned via `dpp.methods.shielded_extra_sighash_data`. +//! layouts are consensus-critical and versioned via `dpp.methods.shielded_extra_sighash_data`; the +//! credit pool's outputs-only bundles via `dpp.methods.credit_pool_bundle_binding`. use crate::address_funds::PlatformAddress; +use crate::fee::Credits; use crate::identity::identity_public_key::contract_bounds::ContractBounds; +use crate::identity::state_transition::asset_lock_proof::AssetLockProof; +use crate::prelude::AddressNonce; use crate::shielded::{serialized_actions_digest, SerializedAction}; use crate::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType; use crate::state_transition::public_key_in_creation::accessors::IdentityPublicKeyInCreationV0Getters; @@ -16,6 +20,7 @@ use crate::withdrawal::Pooling; use crate::ProtocolError; use platform_version::version::PlatformVersion; use sha2::{Digest, Sha256}; +use std::collections::BTreeMap; /// Domain separator for Platform sighash computation. const SIGHASH_DOMAIN: &[u8] = b"DashPlatformSighash"; @@ -43,6 +48,17 @@ pub const TOKEN_CLAIM_TO_POOL_BUNDLE_TAG: u8 = 0x82; /// Domain tag of a `TokenDirectPurchaseToPool` bundle. See [`TOKEN_SHIELD_BUNDLE_TAG`]. pub const TOKEN_DIRECT_PURCHASE_TO_POOL_BUNDLE_TAG: u8 = 0x83; +/// Domain tag of a credit pool `Shield` bundle. The credit pool's outputs-only tags continue the +/// token pool range above: they share the same preimage slot at the same length, so every tag +/// in both sets must stay distinct from each other and from every `StateTransitionType` byte. +/// `credit_pool_outputs_only_tags_cannot_collide_with_state_transition_types` and +/// `outputs_only_bundle_tags_are_pairwise_distinct` hold both reservations. +pub const SHIELD_BUNDLE_TAG: u8 = 0x84; +/// Domain tag of a `ShieldFromIdentity` bundle. See [`SHIELD_BUNDLE_TAG`]. +pub const SHIELD_FROM_IDENTITY_BUNDLE_TAG: u8 = 0x85; +/// Domain tag of a `ShieldFromAssetLock` bundle. See [`SHIELD_BUNDLE_TAG`]. +pub const SHIELD_FROM_ASSET_LOCK_BUNDLE_TAG: u8 = 0x86; + /// Computes the platform sighash from an Orchard bundle commitment and optional /// transparent field data. /// @@ -60,11 +76,11 @@ pub const TOKEN_DIRECT_PURCHASE_TO_POOL_BUNDLE_TAG: u8 = 0x83; /// against every pool. /// /// The same computation must be used on both the signing (client) and verification (platform) -/// sides. `extra_data` is empty for the credit pool's `Shield`, `ShieldFromIdentity`, -/// `ShieldFromAssetLock` and `ShieldedTransfer`; every other transition spells out a layout. -/// Of those four only `ShieldedTransfer` spends, so it carries an anchor and nullifiers that -/// pin it to one pool and one set of notes. The three credit shields are outputs-only and so -/// are pinned by nothing. +/// sides. `extra_data` is empty only for the credit pool's `ShieldedTransfer`, and for the credit +/// pool's outputs-only bundles at protocol versions that predate their binding (see +/// [`shield_extra_sighash_data`]); each other transition has a builder in this module that +/// spells out its layout. `ShieldedTransfer` is the one that needs no layout of its own: it +/// spends, so it carries an anchor and nullifiers that pin it to one pool and one set of notes. pub fn compute_platform_sighash(bundle_commitment: &[u8; 32], extra_data: &[u8]) -> [u8; 32] { let mut hasher = Sha256::new(); hasher.update(SIGHASH_DOMAIN); @@ -750,6 +766,153 @@ pub fn token_pool_output_only_extra_sighash_data_v0( data } +/// Extra sighash data of a credit pool `Shield` bundle: its kind tag and a digest of the platform +/// addresses that fund it. See [`credit_pool_output_only_extra_sighash_data_v0`] for why the +/// credit pool's outputs-only bundles bind anything at all. +/// +/// A `Shield` has no identity, so its owner is its funding: the SHA-256 of its input addresses, +/// each in its 21-byte encoding ([`PlatformAddress::to_bytes`]), in the order the `inputs` map +/// holds them. That order is part of the wire format. It is the map's key order, which is also +/// the order the transition serializes its inputs in, so a client that assembles the same set in +/// any other order still binds the same bytes. +/// +/// The nonces and the contributed amounts are deliberately left out. The owner answers "who +/// funds this", not "which transition carries it", exactly as `ShieldFromIdentity` binds its +/// identity and not its nonce: the preimage is there to stop somebody else from re-wrapping the +/// bundle. Binding the nonce would not stop the funder's own duplicates either, because a sender +/// can rebuild the same notes under any preimage (see +/// [`credit_pool_output_only_extra_sighash_data_v0`]); closing that would need the bundles' dummy +/// nullifiers recorded and checked, which consensus does not do. Adding the nonce later would +/// change the layout under every bundle already built for this one. +/// +/// Dispatches on `dpp.methods.credit_pool_bundle_binding`, which the client builder and the +/// consensus verifier both read: `None` binds nothing (the empty preimage every shipped verifier +/// expects), `Some(0)` binds `tag (1) || funding digest (32)`. +pub fn shield_extra_sighash_data( + inputs: &BTreeMap, + platform_version: &PlatformVersion, +) -> Result, ProtocolError> { + match platform_version.dpp.methods.credit_pool_bundle_binding { + None => Ok(Vec::new()), + Some(0) => Ok(credit_pool_output_only_extra_sighash_data_v0( + SHIELD_BUNDLE_TAG, + &shield_funding_digest_v0(inputs), + )), + Some(version) => Err(ProtocolError::UnknownVersionMismatch { + method: "shield_extra_sighash_data".to_string(), + known_versions: vec![0], + received: version, + }), + } +} + +/// The version 0 owner of a `Shield` bundle: SHA-256 over the 21-byte encoding of each input +/// address, in the map's key order. Frozen: never mutate; see [`shield_extra_sighash_data`]. +fn shield_funding_digest_v0( + inputs: &BTreeMap, +) -> [u8; 32] { + let mut hasher = Sha256::new(); + for address in inputs.keys() { + hasher.update(address.to_bytes()); + } + hasher.finalize().into() +} + +/// Extra sighash data of a `ShieldFromIdentity` bundle: its kind tag and the identity whose +/// balance funds it. See [`credit_pool_output_only_extra_sighash_data_v0`]. +/// +/// The identity signature already covers the bundle, but only this transition's: nothing in the +/// bundle itself says which identity it was proved for, so without this preimage any other +/// identity could sign a transition of its own around the same proved bytes. The funding identity +/// itself can still land the same bundle again under a new nonce, which is what a client retrying +/// with a fresh nonce does; stopping that would need the bundles' dummy nullifiers recorded and +/// checked, which consensus does not do. +/// +/// Dispatches on `dpp.methods.credit_pool_bundle_binding` like [`shield_extra_sighash_data`]. +pub fn shield_from_identity_extra_sighash_data( + identity_id: &[u8; 32], + platform_version: &PlatformVersion, +) -> Result, ProtocolError> { + match platform_version.dpp.methods.credit_pool_bundle_binding { + None => Ok(Vec::new()), + Some(0) => Ok(credit_pool_output_only_extra_sighash_data_v0( + SHIELD_FROM_IDENTITY_BUNDLE_TAG, + identity_id, + )), + Some(version) => Err(ProtocolError::UnknownVersionMismatch { + method: "shield_from_identity_extra_sighash_data".to_string(), + known_versions: vec![0], + received: version, + }), + } +} + +/// Extra sighash data of a `ShieldFromAssetLock` bundle: its kind tag and the identifier of the +/// asset lock that funds it ([`AssetLockProof::create_identifier`], the double SHA-256 of the +/// locked outpoint). See [`credit_pool_output_only_extra_sighash_data_v0`]. +/// +/// Binding the full outpoint, rather than the transaction id, makes this binding single-use. A +/// successful shield consumes the whole lock, so a given bundle can land at most once, whoever +/// wraps it and however often it is resubmitted: a copier would have to fund it from this very +/// lock, and so would the sender's own retry. Binding only the transaction id would let anybody +/// holding another credit output of the same asset-lock transaction lift the bundle. A sender who +/// deliberately rebuilds the same notes under another lock is still not stopped; see +/// [`credit_pool_output_only_extra_sighash_data_v0`]. +/// +/// The identifier is the same one an identity created from that lock would get; the kind tag keeps +/// this preimage apart from a `ShieldFromIdentity` of that identity. +/// +/// Dispatches on `dpp.methods.credit_pool_bundle_binding` like [`shield_extra_sighash_data`]. +pub fn shield_from_asset_lock_extra_sighash_data( + asset_lock_proof: &AssetLockProof, + platform_version: &PlatformVersion, +) -> Result, ProtocolError> { + match platform_version.dpp.methods.credit_pool_bundle_binding { + None => Ok(Vec::new()), + Some(0) => Ok(credit_pool_output_only_extra_sighash_data_v0( + SHIELD_FROM_ASSET_LOCK_BUNDLE_TAG, + &asset_lock_proof.create_identifier()?.to_buffer(), + )), + Some(version) => Err(ProtocolError::UnknownVersionMismatch { + method: "shield_from_asset_lock_extra_sighash_data".to_string(), + known_versions: vec![0], + received: version, + }), + } +} + +/// Version 0 layout of the credit pool's outputs-only bundles — `Shield`, `ShieldFromIdentity` and +/// `ShieldFromAssetLock`: `bundle tag (1) || owner (32)`. Frozen: never mutate; a layout change +/// requires a new `credit_pool_bundle_binding` version. +/// +/// These bundles have no spends, so they carry no anchor to pin them to anything: the proof and +/// the binding signature verify wherever they are submitted. Unbound, the authorized bundle bytes +/// are a free-standing, self-verifying object: anybody can lift them out of the mempool into a +/// transition of their own, funded by their own credits, and any bundle ever published can be +/// replayed. The copier pays the full amount to the original recipient and gains nothing, but the +/// copy lands a second note with the same commitment and the same nullifier in the credit pool, +/// whose notes share one nullifier set, so only one of the two can ever be spent. +/// +/// The owner is what a third party cannot authorize: the addresses, identity or asset lock whose +/// signatures fund the original. The tag stops a bundle proved for one kind from being submitted as another, +/// which the owner alone would not: the three kinds are otherwise indistinguishable to the proof, +/// and an identity created from an asset lock has that lock's identifier as its id. +/// +/// This does not reach Faerie Gold. The `rho` of an outputs-only note is the nullifier of its +/// bundle's dummy spend, so a sender who builds and signs a fresh bundle reusing the same dummy +/// spend note and `rseed` gets the same commitment and the same nullifier under any preimage, and +/// a recipient counting deposits by commitment credits two where only one can be spent. Nor does +/// it stop the funder landing their own bundle twice through a new transition, as a client that +/// retries with a fresh nonce does — except for `ShieldFromAssetLock`, whose lock can fund one +/// successful shield only (see [`shield_from_asset_lock_extra_sighash_data`]). Closing either +/// would need the bundles' dummy nullifiers recorded and checked, which consensus does not do. +pub fn credit_pool_output_only_extra_sighash_data_v0(bundle_tag: u8, owner: &[u8; 32]) -> Vec { + let mut data = Vec::with_capacity(1 + 32); + data.push(bundle_tag); + data.extend_from_slice(owner); + data +} + #[cfg(test)] mod tests { use super::*; @@ -1252,4 +1415,294 @@ mod tests { ); } } + + mod credit_pool_outputs_only { + use super::*; + use crate::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; + use crate::identity::state_transition::asset_lock_proof::InstantAssetLockProof; + use crate::util::hash::hash_double; + use dashcore::transaction::special_transaction::asset_lock::AssetLockPayload; + use dashcore::transaction::special_transaction::TransactionPayload; + use dashcore::{InstantLock, OutPoint, ScriptBuf, Transaction, TxOut}; + + fn protocol_version(version: u32) -> &'static PlatformVersion { + PlatformVersion::get(version).expect("known protocol version") + } + + fn chain_asset_lock_proof(outpoint: [u8; 36]) -> AssetLockProof { + AssetLockProof::Chain(ChainAssetLockProof { + core_chain_locked_height: 100, + out_point: OutPoint::from(outpoint), + }) + } + + fn inputs( + entries: &[(PlatformAddress, AddressNonce, Credits)], + ) -> BTreeMap { + entries + .iter() + .map(|(address, nonce, amount)| (*address, (*nonce, *amount))) + .collect() + } + + #[test] + fn should_pin_the_v0_layout_of_all_three_kinds() { + // Every byte here is consensus: a kind whose tag or owner changes invalidates every + // bundle already proved for it, so the tags are written literally, not through the + // constants, and the owners are derived independently of the code under test. + let version = PlatformVersion::latest(); + + // The map orders P2pkh before P2sh whatever their hash bytes, and each address is its + // 21-byte encoding: variant index, then the 20-byte hash. + let shield_inputs = inputs(&[ + (PlatformAddress::P2sh([0x00; 20]), 1, 10), + (PlatformAddress::P2pkh([0x11; 20]), 2, 20), + ]); + let mut funding = Vec::new(); + funding.push(0x00); + funding.extend_from_slice(&[0x11; 20]); + funding.push(0x01); + funding.extend_from_slice(&[0x00; 20]); + let mut expected = vec![0x84]; + expected.extend_from_slice(&Sha256::digest(&funding)); + assert_eq!( + shield_extra_sighash_data(&shield_inputs, version).expect("shield"), + expected, + "Shield: 0x84 || SHA-256 of the input addresses in map order" + ); + + let identity_id = [0x22u8; 32]; + let mut expected = vec![0x85]; + expected.extend_from_slice(&identity_id); + assert_eq!( + shield_from_identity_extra_sighash_data(&identity_id, version) + .expect("shield from identity"), + expected, + "ShieldFromIdentity: 0x85 || identity id" + ); + + let outpoint = [0x33u8; 36]; + let mut expected = vec![0x86]; + expected.extend_from_slice(&hash_double(outpoint)); + assert_eq!( + shield_from_asset_lock_extra_sighash_data( + &chain_asset_lock_proof(outpoint), + version + ) + .expect("shield from asset lock"), + expected, + "ShieldFromAssetLock: 0x86 || double SHA-256 of the locked outpoint" + ); + } + + #[test] + fn should_bind_the_same_shield_funding_whatever_order_the_inputs_were_assembled_in() { + // The funding digest hashes the addresses in the map's key order, and that order is + // wire format: a client that collected the same addresses in another order must + // still produce the bytes the verifier rebuilds from the transition. + let version = PlatformVersion::latest(); + let a = (PlatformAddress::P2pkh([0x01; 20]), 1, 100); + let b = (PlatformAddress::P2sh([0x02; 20]), 2, 200); + let c = (PlatformAddress::P2pkh([0x03; 20]), 3, 300); + + let forward = shield_extra_sighash_data(&inputs(&[a, b, c]), version).expect("shield"); + let backward = shield_extra_sighash_data(&inputs(&[c, b, a]), version).expect("shield"); + let shuffled = shield_extra_sighash_data(&inputs(&[b, c, a]), version).expect("shield"); + + assert_eq!(forward, backward); + assert_eq!(forward, shuffled); + } + + #[test] + fn should_bind_who_funds_a_shield_but_not_its_nonces_or_amounts() { + // The owner is the funding, not the transition: the same addresses with other nonces + // or other contributions bind the same bytes, so a later "tightening" that adds the + // nonce shows up here as a layout change rather than slipping in. + let version = PlatformVersion::latest(); + let first = PlatformAddress::P2pkh([0x01; 20]); + let second = PlatformAddress::P2pkh([0x02; 20]); + let base = + shield_extra_sighash_data(&inputs(&[(first, 1, 100)]), version).expect("shield"); + + assert_eq!( + base, + shield_extra_sighash_data(&inputs(&[(first, 9, 100)]), version).expect("shield"), + "the nonce must not be bound" + ); + assert_eq!( + base, + shield_extra_sighash_data(&inputs(&[(first, 1, 999)]), version).expect("shield"), + "the contributed amount must not be bound" + ); + assert_ne!( + base, + shield_extra_sighash_data(&inputs(&[(second, 1, 100)]), version).expect("shield"), + "another funding address is another owner" + ); + assert_ne!( + base, + shield_extra_sighash_data(&inputs(&[(first, 1, 100), (second, 1, 100)]), version) + .expect("shield"), + "an added funding address is another owner" + ); + assert_ne!( + base, + shield_extra_sighash_data( + &inputs(&[(PlatformAddress::P2sh([0x01; 20]), 1, 100)]), + version + ) + .expect("shield"), + "the address type is part of the owner, not only its hash" + ); + } + + #[test] + fn should_keep_kinds_apart_when_their_owners_coincide() { + // An identity created from an asset lock has that lock's identifier as its id, so a + // `ShieldFromAssetLock` and a `ShieldFromIdentity` can bind the very same owner bytes. + // Only the tag keeps a bundle proved for one from being accepted as the other. + let version = PlatformVersion::latest(); + let proof = chain_asset_lock_proof([0x44; 36]); + let identity_id = proof.create_identifier().expect("identifier").to_buffer(); + + let from_lock = + shield_from_asset_lock_extra_sighash_data(&proof, version).expect("asset lock"); + let from_identity = + shield_from_identity_extra_sighash_data(&identity_id, version).expect("identity"); + + assert_eq!(&from_lock[1..], &from_identity[1..], "the owners coincide"); + assert_ne!(from_lock, from_identity, "the kinds must not"); + } + + #[test] + fn should_bind_the_whole_outpoint_of_the_asset_lock_not_only_its_transaction() { + // A lock transaction can carry several credit outputs, each its own asset lock. Bound + // to the transaction id alone, a bundle could be lifted by whoever holds another + // output of the same transaction. The owner is the outpoint, and it is the same owner + // whichever kind of proof presents it. + let version = PlatformVersion::latest(); + let credit_output = |value| TxOut { + value, + script_pubkey: ScriptBuf::new(), + }; + let transaction = Transaction { + version: 3, + lock_time: 0, + input: vec![], + output: vec![], + special_transaction_payload: Some(TransactionPayload::AssetLockPayloadType( + AssetLockPayload { + version: 0, + credit_outputs: vec![credit_output(100_000), credit_output(200_000)], + }, + )), + }; + let txid = transaction.txid(); + let instant = |output_index| { + AssetLockProof::Instant(InstantAssetLockProof::new( + InstantLock::default(), + transaction.clone(), + output_index, + )) + }; + let bound = |proof: &AssetLockProof| { + shield_from_asset_lock_extra_sighash_data(proof, version).expect("asset lock") + }; + + assert_ne!( + bound(&instant(0)), + bound(&instant(1)), + "another output of the same lock transaction is another owner" + ); + assert_eq!( + bound(&instant(1)), + bound(&AssetLockProof::Chain(ChainAssetLockProof { + core_chain_locked_height: 100, + out_point: OutPoint::new(txid, 1), + })), + "the same outpoint is the same owner whichever proof presents it" + ); + } + + #[test] + fn should_bind_nothing_at_protocol_versions_before_the_binding() { + // Protocol versions 12 and 13 run the credit pool with verifiers that rebuild an empty + // preimage. A client building for one of them reads the same field, so it must get the + // empty preimage back, or every shield it makes there is rejected. + let lock = chain_asset_lock_proof([0x55; 36]); + let funding = inputs(&[(PlatformAddress::P2pkh([0x01; 20]), 1, 100)]); + for version in [12, 13] { + let version = protocol_version(version); + assert_eq!(version.dpp.methods.credit_pool_bundle_binding, None); + assert!(shield_extra_sighash_data(&funding, version) + .expect("shield") + .is_empty()); + assert!( + shield_from_identity_extra_sighash_data(&[0x66; 32], version) + .expect("shield from identity") + .is_empty() + ); + assert!(shield_from_asset_lock_extra_sighash_data(&lock, version) + .expect("shield from asset lock") + .is_empty()); + } + } + + #[test] + fn should_refuse_an_unknown_binding_version() { + // Neither side may guess: a builder that fell back to one layout while the verifier + // chose another would reject every honest shield. + let mut version = PlatformVersion::latest().clone(); + version.dpp.methods.credit_pool_bundle_binding = Some(1); + let lock = chain_asset_lock_proof([0x77; 36]); + + for result in [ + shield_extra_sighash_data(&BTreeMap::new(), &version), + shield_from_identity_extra_sighash_data(&[0x88; 32], &version), + shield_from_asset_lock_extra_sighash_data(&lock, &version), + ] { + assert_matches::assert_matches!( + result, + Err(ProtocolError::UnknownVersionMismatch { received: 1, .. }) + ); + } + } + + #[test] + fn credit_pool_outputs_only_tags_cannot_collide_with_state_transition_types() { + // The tags share a preimage slot with the `StateTransitionType` byte the identity-less + // token bundles commit to, at the same 1 + 32 length. Asking the enum itself is what + // makes this break on the day a transition type is assigned inside the tag range. + for tag in [ + SHIELD_BUNDLE_TAG, + SHIELD_FROM_IDENTITY_BUNDLE_TAG, + SHIELD_FROM_ASSET_LOCK_BUNDLE_TAG, + ] { + assert!( + StateTransitionType::try_from(tag).is_err(), + "state transition type {tag:#04x} now collides with a credit pool bundle tag" + ); + } + } + + #[test] + fn outputs_only_bundle_tags_are_pairwise_distinct() { + // All seven outputs-only layouts are `tag (1) || 32 bytes`, so two kinds sharing a + // tag would share a preimage whenever their 32 bytes coincide. + let tags = [ + TOKEN_SHIELD_BUNDLE_TAG, + TOKEN_MINT_TO_POOL_BUNDLE_TAG, + TOKEN_CLAIM_TO_POOL_BUNDLE_TAG, + TOKEN_DIRECT_PURCHASE_TO_POOL_BUNDLE_TAG, + SHIELD_BUNDLE_TAG, + SHIELD_FROM_IDENTITY_BUNDLE_TAG, + SHIELD_FROM_ASSET_LOCK_BUNDLE_TAG, + ]; + for (i, a) in tags.iter().enumerate() { + for b in tags.iter().skip(i + 1) { + assert_ne!(a, b, "outputs-only bundle tag {a:#04x} is used twice"); + } + } + } + } } diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index 0b4854368f5..b5b95dde270 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -1186,8 +1186,16 @@ impl StateTransition { StateTransition::Shield(_) | StateTransition::ShieldedTransfer(_) | StateTransition::Unshield(_) - | StateTransition::ShieldFromAssetLock(_) | StateTransition::ShieldedWithdrawal(_) => 12..=LATEST_VERSION, + // From protocol version 14 the bundle must bind its kind and its asset lock, which + // only version 1 does. Version 0 is refused there on its version byte, before any + // proof is verified: uncharged, with its asset lock left unspent, so a version 0 still + // waiting when 14 activates costs its sender nothing. There is no window in which 14 + // accepts version 0. + StateTransition::ShieldFromAssetLock(st) => match st { + ShieldFromAssetLockTransition::V0(_) => 12..=13, + ShieldFromAssetLockTransition::V1(_) => 14..=LATEST_VERSION, + }, StateTransition::ShieldFromIdentity(_) | StateTransition::IdentityTopUpFromShieldedPool(_) | StateTransition::IdentityKeyLimitsUpdate(_) @@ -3940,6 +3948,72 @@ mod tests { } } + // A `ShieldFromAssetLock` is refused on its version byte at decode, before any proof work: + // version 0 (whose bundle binds nothing) up to protocol version 13 only, version 1 (whose + // bundle binds its kind and asset lock) from 14 only. + #[cfg(all(feature = "state-transitions", feature = "validation"))] + #[test] + fn should_decode_each_shield_from_asset_lock_version_only_where_it_is_active() { + use crate::serialization::PlatformSerializable; + use crate::state_transition::shield_from_asset_lock_transition::v1::ShieldFromAssetLockTransitionV1; + + let v0 = sample_shield_from_asset_lock_st(); + let StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V0(body)) = &v0 + else { + panic!("expected a version 0 shield from asset lock"); + }; + let v1 = StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V1( + ShieldFromAssetLockTransitionV1 { + asset_lock_proof: body.asset_lock_proof.clone(), + actions: body.actions.clone(), + value_balance: body.value_balance, + anchor: body.anchor, + proof: body.proof.clone(), + binding_signature: body.binding_signature, + surplus_output: body.surplus_output, + signature: body.signature.clone(), + }, + )); + assert_eq!(v0.active_version_range(), 12..=13); + assert_eq!(v1.active_version_range(), 14..=LATEST_VERSION); + + let decodes_at = |transition: &StateTransition, protocol_version: u32| { + let bytes = + PlatformSerializable::serialize_to_bytes(transition).expect("serialize succeeds"); + let platform_version = + PlatformVersion::get(protocol_version).expect("known protocol version"); + match StateTransition::deserialize_from_bytes_untrusted_in_version( + &bytes, + platform_version, + ) { + Ok(decoded) => { + assert_eq!(&decoded, transition); + true + } + Err(ProtocolError::StateTransitionError( + crate::state_transition::errors::StateTransitionError::StateTransitionIsNotActiveError { + current_protocol_version, + .. + }, + )) => { + assert_eq!(current_protocol_version, protocol_version); + false + } + Err(other) => panic!("unexpected decode error: {other:?}"), + } + }; + + for protocol_version in [12, 13] { + assert!(decodes_at(&v0, protocol_version)); + assert!(!decodes_at(&v1, protocol_version)); + } + assert!( + !decodes_at(&v0, 14), + "protocol version 14 must refuse version 0" + ); + assert!(decodes_at(&v1, 14)); + } + // A version 1 data contract create carries contract groups, which only exist from // protocol version 14. Below that a node must reject it rather than create the // contract and drop the group data. @@ -4361,10 +4435,10 @@ mod tests { } #[test] - fn test_shield_from_asset_lock_active_range_12_latest() { + fn test_shield_from_asset_lock_version_0_active_range_12_to_13() { + // Version 0's bundle binds nothing; protocol version 14 admits only version 1. let range = sample_shield_from_asset_lock_st().active_version_range(); - assert_eq!(*range.start(), 12); - assert_eq!(*range.end(), LATEST_VERSION); + assert_eq!(range, 12..=13); } // ---------- Batch with Token transition exercises TokenTransfer arm diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/accessors/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/accessors/mod.rs index d6fee584ad1..8626313a040 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/accessors/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/accessors/mod.rs @@ -10,72 +10,84 @@ impl ShieldFromAssetLockTransitionAccessorsV0 for ShieldFromAssetLockTransition fn actions(&self) -> &[SerializedAction] { match self { ShieldFromAssetLockTransition::V0(v0) => &v0.actions, + ShieldFromAssetLockTransition::V1(v1) => &v1.actions, } } fn set_actions(&mut self, actions: Vec) { match self { ShieldFromAssetLockTransition::V0(v0) => v0.actions = actions, + ShieldFromAssetLockTransition::V1(v1) => v1.actions = actions, } } fn value_balance(&self) -> u64 { match self { ShieldFromAssetLockTransition::V0(v0) => v0.value_balance, + ShieldFromAssetLockTransition::V1(v1) => v1.value_balance, } } fn set_value_balance(&mut self, value_balance: u64) { match self { ShieldFromAssetLockTransition::V0(v0) => v0.value_balance = value_balance, + ShieldFromAssetLockTransition::V1(v1) => v1.value_balance = value_balance, } } fn anchor(&self) -> [u8; 32] { match self { ShieldFromAssetLockTransition::V0(v0) => v0.anchor, + ShieldFromAssetLockTransition::V1(v1) => v1.anchor, } } fn set_anchor(&mut self, anchor: [u8; 32]) { match self { ShieldFromAssetLockTransition::V0(v0) => v0.anchor = anchor, + ShieldFromAssetLockTransition::V1(v1) => v1.anchor = anchor, } } fn proof(&self) -> &[u8] { match self { ShieldFromAssetLockTransition::V0(v0) => &v0.proof, + ShieldFromAssetLockTransition::V1(v1) => &v1.proof, } } fn set_proof(&mut self, proof: Vec) { match self { ShieldFromAssetLockTransition::V0(v0) => v0.proof = proof, + ShieldFromAssetLockTransition::V1(v1) => v1.proof = proof, } } fn binding_signature(&self) -> [u8; 64] { match self { ShieldFromAssetLockTransition::V0(v0) => v0.binding_signature, + ShieldFromAssetLockTransition::V1(v1) => v1.binding_signature, } } fn set_binding_signature(&mut self, binding_signature: [u8; 64]) { match self { ShieldFromAssetLockTransition::V0(v0) => v0.binding_signature = binding_signature, + ShieldFromAssetLockTransition::V1(v1) => v1.binding_signature = binding_signature, } } fn surplus_output(&self) -> Option<&PlatformAddress> { match self { ShieldFromAssetLockTransition::V0(v0) => v0.surplus_output.as_ref(), + ShieldFromAssetLockTransition::V1(v1) => v1.surplus_output.as_ref(), } } fn set_surplus_output(&mut self, surplus_output: Option) { match self { ShieldFromAssetLockTransition::V0(v0) => v0.surplus_output = surplus_output, + ShieldFromAssetLockTransition::V1(v1) => v1.surplus_output = surplus_output, } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/methods/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/methods/mod.rs index 41a50003a4e..c1e421d27b9 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/methods/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/methods/mod.rs @@ -12,7 +12,8 @@ use crate::state_transition::shield_from_asset_lock_transition::ShieldFromAssetL #[cfg(feature = "state-transition-signing")] use crate::{ state_transition::{ - shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0, StateTransition, + shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0, + shield_from_asset_lock_transition::v1::ShieldFromAssetLockTransitionV1, StateTransition, }, ProtocolError, }; @@ -49,10 +50,21 @@ impl ShieldFromAssetLockTransitionMethodsV0 for ShieldFromAssetLockTransition { surplus_output, platform_version, ), + 1 => ShieldFromAssetLockTransitionV1::try_from_asset_lock_with_bundle( + asset_lock_proof, + asset_lock_proof_private_key, + actions, + value_balance, + anchor, + proof, + binding_signature, + surplus_output, + platform_version, + ), version => Err(ProtocolError::UnknownVersionMismatch { method: "ShieldFromAssetLockTransition::try_from_asset_lock_with_bundle" .to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, }), } @@ -95,10 +107,25 @@ impl ShieldFromAssetLockTransitionMethodsV0 for ShieldFromAssetLockTransition { ) .await } + 1 => { + ShieldFromAssetLockTransitionV1::try_from_asset_lock_with_bundle_and_signer( + asset_lock_proof, + asset_lock_proof_path, + asset_lock_signer, + actions, + value_balance, + anchor, + proof, + binding_signature, + surplus_output, + platform_version, + ) + .await + } version => Err(ProtocolError::UnknownVersionMismatch { method: "ShieldFromAssetLockTransition::try_from_asset_lock_with_bundle_and_signer" .to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, }), } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/mod.rs index 67fff81eaad..d8fffb0ef3a 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/mod.rs @@ -13,14 +13,17 @@ mod state_transition_estimated_fee_validation; mod state_transition_like; mod state_transition_validation; pub mod v0; +pub mod v1; mod version; use crate::state_transition::shield_from_asset_lock_transition::fields::{PROOF, SIGNATURE}; use crate::state_transition::shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0; use crate::state_transition::shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0Signable; +use crate::state_transition::shield_from_asset_lock_transition::v1::ShieldFromAssetLockTransitionV1; +use crate::state_transition::shield_from_asset_lock_transition::v1::ShieldFromAssetLockTransitionV1Signable; use crate::state_transition::StateTransitionFieldTypes; -pub type ShieldFromAssetLockTransitionLatest = ShieldFromAssetLockTransitionV0; +pub type ShieldFromAssetLockTransitionLatest = ShieldFromAssetLockTransitionV1; #[cfg(feature = "json-conversion")] use crate::serialization::JsonConvertible; @@ -67,6 +70,10 @@ use serde::{Deserialize, Serialize}; pub enum ShieldFromAssetLockTransition { #[cfg_attr(feature = "serde-conversion", serde(rename = "0"))] V0(ShieldFromAssetLockTransitionV0), + /// The bundle binds its kind and its asset lock. The only version protocol version 14 + /// admits; see [`ShieldFromAssetLockTransitionV1`]. + #[cfg_attr(feature = "serde-conversion", serde(rename = "1"))] + V1(ShieldFromAssetLockTransitionV1), } impl StateTransitionFieldTypes for ShieldFromAssetLockTransition { @@ -225,4 +232,52 @@ pub(crate) mod json_convertible_tests { let recovered = ShieldFromAssetLockTransition::from_object(value).expect("from_object"); assert_eq!(original, recovered); } + + /// Version 1 has the same fields as version 0; only the `$formatVersion` tag tells them + /// apart in JSON and Value form, and it must survive the round trip. + #[test] + fn version_1_round_trips_as_format_version_1() { + use crate::serialization::ValueConvertible; + use crate::state_transition::shield_from_asset_lock_transition::v1::ShieldFromAssetLockTransitionV1; + let ShieldFromAssetLockTransition::V0(v0) = fixture() else { + panic!("the fixture is version 0"); + }; + let original = ShieldFromAssetLockTransition::V1(ShieldFromAssetLockTransitionV1 { + asset_lock_proof: v0.asset_lock_proof, + actions: v0.actions, + value_balance: v0.value_balance, + anchor: v0.anchor, + proof: v0.proof, + binding_signature: v0.binding_signature, + surplus_output: v0.surplus_output, + signature: v0.signature, + }); + + let json = original.to_json().expect("to_json"); + assert_eq!( + json.as_object() + .expect("json is an object") + .get("$formatVersion"), + Some(&serde_json::json!("1")) + ); + assert_eq!( + ShieldFromAssetLockTransition::from_json(json).expect("from_json"), + original + ); + + let value = original.to_object().expect("to_object"); + assert_eq!( + value + .as_map() + .expect("value is a map") + .iter() + .find(|(k, _)| k.as_text() == Some("$formatVersion")) + .map(|(_, v)| v), + Some(&platform_value::Value::Text("1".to_string())) + ); + assert_eq!( + ShieldFromAssetLockTransition::from_object(value).expect("from_object"), + original + ); + } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/proved.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/proved.rs index 4c06ae7ea68..de1f4276fc9 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/proved.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/proved.rs @@ -16,12 +16,14 @@ impl AssetLockProved for ShieldFromAssetLockTransition { ) -> Result<(), ProtocolError> { match self { ShieldFromAssetLockTransition::V0(v0) => v0.set_asset_lock_proof(asset_lock_proof), + ShieldFromAssetLockTransition::V1(v1) => v1.set_asset_lock_proof(asset_lock_proof), } } fn asset_lock_proof(&self) -> &AssetLockProof { match self { ShieldFromAssetLockTransition::V0(v0) => v0.asset_lock_proof(), + ShieldFromAssetLockTransition::V1(v1) => v1.asset_lock_proof(), } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/signing_tests.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/signing_tests.rs index 719074e382e..678d9cd8b8e 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/signing_tests.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/signing_tests.rs @@ -26,6 +26,7 @@ use crate::prelude::AssetLockProof; use crate::shielded::builder::build_shield_from_asset_lock_transition_with_signer; use crate::state_transition::shield_from_asset_lock_transition::methods::ShieldFromAssetLockTransitionMethodsV0; use crate::state_transition::shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0; +use crate::state_transition::shield_from_asset_lock_transition::v1::ShieldFromAssetLockTransitionV1; use crate::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; use crate::state_transition::StateTransition; use dashcore::OutPoint; @@ -105,6 +106,15 @@ fn extract_v0(state_transition: StateTransition) -> ShieldFromAssetLockTransitio v0 } +fn extract_v1(state_transition: StateTransition) -> ShieldFromAssetLockTransitionV1 { + let StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V1(v1)) = + state_transition + else { + panic!("expected ShieldFromAssetLock V1 variant"); + }; + v1 +} + #[tokio::test] async fn try_from_asset_lock_with_bundle_and_signer_produces_recoverable_compact_sig_v0() { // Exercises `ShieldFromAssetLockTransitionV0::try_from_asset_lock_with_bundle_and_signer` @@ -153,10 +163,26 @@ async fn try_from_asset_lock_with_bundle_and_signer_produces_recoverable_compact async fn try_from_asset_lock_with_bundle_and_signer_via_outer_dispatcher() { // Same call but routed through the outer-enum dispatcher in // `methods/mod.rs` — pins that the version-routing path lands in - // the V0 impl and returns an equivalent transition. + // the version the protocol version builds: 1 at the latest, 0 at 13. let signer = FixedKeySigner::new([7u8; 32]); let path = DerivationPath::default(); + let st_13 = ShieldFromAssetLockTransition::try_from_asset_lock_with_bundle_and_signer( + make_chain_asset_lock_proof(), + &path, + &signer, + vec![], + 500_000, + [0u8; 32], + vec![], + [0u8; 64], + None, + PlatformVersion::get(13).expect("protocol version 13"), + ) + .await + .expect("outer dispatch should succeed"); + assert_eq!(extract_v0(st_13).value_balance, 500_000); + let st = ShieldFromAssetLockTransition::try_from_asset_lock_with_bundle_and_signer( make_chain_asset_lock_proof(), &path, @@ -172,20 +198,19 @@ async fn try_from_asset_lock_with_bundle_and_signer_via_outer_dispatcher() { .await .expect("outer dispatch should succeed"); - let v0 = extract_v0(st); - assert_eq!(v0.value_balance, 500_000); - assert_eq!(v0.signature.len(), 65); + let v1 = extract_v1(st); + assert_eq!(v1.value_balance, 500_000); + assert_eq!(v1.signature.len(), 65); } #[tokio::test] async fn outer_dispatcher_rejects_unknown_serialization_version() { // Synthesise a platform-version whose // `shield_from_asset_lock_state_transition.default_current_version` - // is non-zero, and confirm the dispatcher surfaces - // `UnknownVersionMismatch` instead of silently coercing to V0. - // This guards the V0-only assumption baked into the dispatcher - // so a future V1 introduction can't accidentally route through - // the wrong impl without an explicit code change. + // is an unknown version (99), and confirm the dispatcher surfaces + // `UnknownVersionMismatch` instead of silently coercing to a known + // version, so a future version can't route through the wrong impl + // without an explicit code change. let signer = FixedKeySigner::new([7u8; 32]); let path = DerivationPath::default(); @@ -255,18 +280,18 @@ async fn build_shield_from_asset_lock_transition_with_signer_end_to_end() { .await .expect("builder should succeed"); - let v0 = extract_v0(st); + let v1 = extract_v1(st); assert_eq!( - v0.value_balance, shield_amount, + v1.value_balance, shield_amount, "builder must thread the shield amount into the transition's value_balance", ); assert_eq!( - v0.signature.len(), + v1.signature.len(), 65, "asset-lock signature must be 65-byte recoverable compact", ); assert!( - !v0.actions.is_empty(), + !v1.actions.is_empty(), "output-only bundle must produce at least one Orchard action", ); } @@ -315,14 +340,14 @@ async fn seed_pool_batch_fits_max_state_transition_size() { max, ); - let v0 = extract_v0(st); + let v1 = extract_v1(st); assert_eq!( - v0.actions.len(), + v1.actions.len(), 6, "1 real + 5 dummy outputs must serialize to 6 Orchard actions", ); assert_eq!( - v0.value_balance, 50_000, + v1.value_balance, 50_000, "dummy outputs are zero-value: value_balance must equal the real amount", ); } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/state_transition_like.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/state_transition_like.rs index be94599a6ad..55f109f4fdb 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/state_transition_like.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/state_transition_like.rs @@ -10,12 +10,14 @@ impl StateTransitionLike for ShieldFromAssetLockTransition { fn modified_data_ids(&self) -> Vec { match self { ShieldFromAssetLockTransition::V0(transition) => transition.modified_data_ids(), + ShieldFromAssetLockTransition::V1(transition) => transition.modified_data_ids(), } } fn state_transition_protocol_version(&self) -> FeatureVersion { match self { ShieldFromAssetLockTransition::V0(_) => 0, + ShieldFromAssetLockTransition::V1(_) => 1, } } @@ -23,12 +25,14 @@ impl StateTransitionLike for ShieldFromAssetLockTransition { fn state_transition_type(&self) -> StateTransitionType { match self { ShieldFromAssetLockTransition::V0(transition) => transition.state_transition_type(), + ShieldFromAssetLockTransition::V1(transition) => transition.state_transition_type(), } } fn unique_identifiers(&self) -> Vec { match self { ShieldFromAssetLockTransition::V0(transition) => transition.unique_identifiers(), + ShieldFromAssetLockTransition::V1(transition) => transition.unique_identifiers(), } } } @@ -38,6 +42,7 @@ impl StateTransitionSingleSigned for ShieldFromAssetLockTransition { fn signature(&self) -> &BinaryData { match self { ShieldFromAssetLockTransition::V0(transition) => transition.signature(), + ShieldFromAssetLockTransition::V1(transition) => transition.signature(), } } @@ -45,6 +50,7 @@ impl StateTransitionSingleSigned for ShieldFromAssetLockTransition { fn set_signature(&mut self, signature: BinaryData) { match self { ShieldFromAssetLockTransition::V0(transition) => transition.set_signature(signature), + ShieldFromAssetLockTransition::V1(transition) => transition.set_signature(signature), } } @@ -53,6 +59,9 @@ impl StateTransitionSingleSigned for ShieldFromAssetLockTransition { ShieldFromAssetLockTransition::V0(transition) => { transition.set_signature_bytes(signature) } + ShieldFromAssetLockTransition::V1(transition) => { + transition.set_signature_bytes(signature) + } } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/state_transition_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/state_transition_validation.rs index bf6bf059c24..9cfed1d400d 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/state_transition_validation.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/state_transition_validation.rs @@ -10,6 +10,7 @@ impl StateTransitionStructureValidation for ShieldFromAssetLockTransition { ) -> SimpleConsensusValidationResult { match self { ShieldFromAssetLockTransition::V0(v0) => v0.validate_structure(platform_version), + ShieldFromAssetLockTransition::V1(v1) => v1.validate_structure(platform_version), } } } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/mod.rs index 9bfc5045fef..9c606c39cee 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/mod.rs @@ -1,6 +1,6 @@ mod proved; mod state_transition_like; -mod state_transition_validation; +pub(super) mod state_transition_validation; mod types; pub(super) mod v0_methods; mod version; diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/state_transition_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/state_transition_validation.rs index 2a27aca1599..b14402858da 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/state_transition_validation.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/state_transition_validation.rs @@ -1,5 +1,6 @@ use crate::consensus::basic::state_transition::ShieldedInvalidValueBalanceError; use crate::consensus::basic::BasicError; +use crate::shielded::SerializedAction; use crate::state_transition::shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0; use crate::state_transition::state_transitions::shielded::common_validation::{ validate_actions_count, validate_anchor_not_zero, validate_encrypted_note_sizes, @@ -14,61 +15,75 @@ impl StateTransitionStructureValidation for ShieldFromAssetLockTransitionV0 { &self, platform_version: &PlatformVersion, ) -> SimpleConsensusValidationResult { - // Actions count must be in [1, max] - let result = validate_actions_count( + validate_shield_from_asset_lock_structure( &self.actions, - platform_version - .system_limits - .max_shielded_transition_actions, - ); - if !result.is_valid() { - return result; - } + self.value_balance, + &self.proof, + &self.anchor, + platform_version, + ) + } +} - // Each action's encrypted_note must be exactly ENCRYPTED_NOTE_SIZE bytes - let result = validate_encrypted_note_sizes(&self.actions); - if !result.is_valid() { - return result; - } +/// The structure rules of a `ShieldFromAssetLock`, shared by every transition version: the +/// versions differ only in the preimage their bundle is proved against, not in their fields. +pub(in crate::state_transition::state_transitions::shielded::shield_from_asset_lock_transition) fn validate_shield_from_asset_lock_structure( + actions: &[SerializedAction], + value_balance: u64, + proof: &[u8], + anchor: &[u8; 32], + platform_version: &PlatformVersion, +) -> SimpleConsensusValidationResult { + // Actions count must be in [1, max] + let result = validate_actions_count( + actions, + platform_version + .system_limits + .max_shielded_transition_actions, + ); + if !result.is_valid() { + return result; + } - // value_balance must be > 0 (credits flowing into pool) - if self.value_balance == 0 { - return SimpleConsensusValidationResult::new_with_error( - BasicError::ShieldedInvalidValueBalanceError( - ShieldedInvalidValueBalanceError::new( - "shield_from_asset_lock value_balance must be greater than 0".to_string(), - ), - ) - .into(), - ); - } + // Each action's encrypted_note must be exactly ENCRYPTED_NOTE_SIZE bytes + let result = validate_encrypted_note_sizes(actions); + if !result.is_valid() { + return result; + } - // value_balance must fit in i64 (Orchard protocol uses i64 internally) - if self.value_balance > i64::MAX as u64 { - return SimpleConsensusValidationResult::new_with_error( - BasicError::ShieldedInvalidValueBalanceError( - ShieldedInvalidValueBalanceError::new( - "shield_from_asset_lock value_balance exceeds i64::MAX".to_string(), - ), - ) - .into(), - ); - } + // value_balance must be > 0 (credits flowing into pool) + if value_balance == 0 { + return SimpleConsensusValidationResult::new_with_error( + BasicError::ShieldedInvalidValueBalanceError(ShieldedInvalidValueBalanceError::new( + "shield_from_asset_lock value_balance must be greater than 0".to_string(), + )) + .into(), + ); + } - // Proof must not be empty - let result = validate_proof_not_empty(&self.proof); - if !result.is_valid() { - return result; - } + // value_balance must fit in i64 (Orchard protocol uses i64 internally) + if value_balance > i64::MAX as u64 { + return SimpleConsensusValidationResult::new_with_error( + BasicError::ShieldedInvalidValueBalanceError(ShieldedInvalidValueBalanceError::new( + "shield_from_asset_lock value_balance exceeds i64::MAX".to_string(), + )) + .into(), + ); + } - // Anchor must not be all zeros - let result = validate_anchor_not_zero(&self.anchor); - if !result.is_valid() { - return result; - } + // Proof must not be empty + let result = validate_proof_not_empty(proof); + if !result.is_valid() { + return result; + } - SimpleConsensusValidationResult::new() + // Anchor must not be all zeros + let result = validate_anchor_not_zero(anchor); + if !result.is_valid() { + return result; } + + SimpleConsensusValidationResult::new() } #[cfg(test)] diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/mod.rs new file mode 100644 index 00000000000..a108ddea7ce --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/mod.rs @@ -0,0 +1,152 @@ +mod proved; +mod state_transition_like; +mod state_transition_validation; +mod types; +pub(super) mod v1_methods; +mod version; + +use crate::address_funds::PlatformAddress; +use crate::identity::state_transition::asset_lock_proof::AssetLockProof; +use crate::shielded::SerializedAction; +use crate::ProtocolError; +use bincode::{Decode, DecodeUntrusted, Encode}; +use platform_serialization_derive::{ + PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, PlatformSignable, +}; +use platform_value::BinaryData; +#[cfg(feature = "serde-conversion")] +use serde::{Deserialize, Serialize}; + +/// Version 1 of `ShieldFromAssetLock`: the fields of version 0, with an Orchard bundle proved +/// against the preimage that binds its kind tag and the asset lock funding it +/// ([`crate::shielded::shield_from_asset_lock_extra_sighash_data`]). +/// +/// It is the only version protocol version 14 admits, and it is not admitted before. Version 0, +/// whose bundle binds nothing, is refused from protocol version 14 on its version byte when the +/// transition is decoded, before any proof is verified: it is not charged and its asset lock is +/// left unspent, so a version 0 transition still waiting when version 14 activates costs its +/// sender nothing. Accepting it would verify its unbound bundle against the bound preimage and +/// burn the proof-failure penalty from an honest lock. +#[cfg_attr(feature = "json-conversion", crate::serialization::json_safe_fields)] +#[derive( + Debug, + Clone, + Encode, + Decode, + PlatformSerialize, + PlatformDeserializeTrusted, + PlatformDeserializeUntrusted, + PlatformSignable, + PartialEq, + DecodeUntrusted, +)] +#[cfg_attr( + feature = "serde-conversion", + derive(Serialize, Deserialize), + serde(rename_all = "camelCase") +)] +#[platform_serialize(unversioned)] +pub struct ShieldFromAssetLockTransitionV1 { + /// Asset lock proof from L1 + pub asset_lock_proof: AssetLockProof, + /// Orchard actions (spend-output pairs) + pub actions: Vec, + /// Amount of credits flowing into the shielded pool from the asset lock. + /// Must be > 0 and <= i64::MAX. + pub value_balance: u64, + /// Sinsemilla root of the note commitment tree (Orchard Anchor) + pub anchor: [u8; 32], + /// Halo2 proof bytes + pub proof: Vec, + /// RedPallas binding signature + pub binding_signature: [u8; 64], + /// Optional platform-address output that receives the asset-lock surplus; see + /// [`ShieldFromAssetLockTransitionV0::surplus_output`](super::v0::ShieldFromAssetLockTransitionV0::surplus_output). + /// Covered by the signable bytes like every field before `signature`. + pub surplus_output: Option, + /// ECDSA signature over the signable bytes (excluded from sig hash) + #[platform_signable(exclude_from_sig_hash)] + pub signature: BinaryData, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::identity::state_transition::asset_lock_proof::chain::ChainAssetLockProof; + use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable, Signable}; + use crate::state_transition::shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0; + use crate::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; + use crate::state_transition::{FeatureVersioned, StateTransition, StateTransitionLike}; + use dashcore::OutPoint; + + fn make_v1() -> ShieldFromAssetLockTransitionV1 { + ShieldFromAssetLockTransitionV1 { + asset_lock_proof: AssetLockProof::Chain(ChainAssetLockProof { + core_chain_locked_height: 100, + out_point: OutPoint::from([11u8; 36]), + }), + actions: vec![SerializedAction { + nullifier: [1u8; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + }], + value_balance: 1000u64, + anchor: [7u8; 32], + proof: vec![8u8; 100], + binding_signature: [9u8; 64], + surplus_output: Some(PlatformAddress::P2pkh([0x33; 20])), + signature: BinaryData::new(vec![10u8; 65]), + } + } + + fn as_v0(v1: &ShieldFromAssetLockTransitionV1) -> ShieldFromAssetLockTransitionV0 { + ShieldFromAssetLockTransitionV0 { + asset_lock_proof: v1.asset_lock_proof.clone(), + actions: v1.actions.clone(), + value_balance: v1.value_balance, + anchor: v1.anchor, + proof: v1.proof.clone(), + binding_signature: v1.binding_signature, + surplus_output: v1.surplus_output, + signature: v1.signature.clone(), + } + } + + #[test] + fn should_round_trip_through_the_state_transition_bytes_as_version_1() { + let transition: StateTransition = make_v1().into(); + let bytes = transition.serialize_to_bytes().expect("serialize"); + let decoded = + StateTransition::deserialize_from_bytes_untrusted(&bytes).expect("deserialize"); + assert_eq!(decoded, transition); + assert!(matches!( + decoded, + StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V1(_)) + )); + } + + #[test] + fn should_report_version_1() { + let v1 = make_v1(); + assert_eq!(v1.feature_version(), 1); + assert_eq!(v1.state_transition_protocol_version(), 1); + let transition: ShieldFromAssetLockTransition = v1.into(); + assert_eq!(transition.feature_version(), 1); + } + + #[test] + fn should_commit_the_signature_to_the_transition_version() { + // The same fields signed as version 0 and as version 1 give different signable bytes, so + // nobody can turn a signed version 1 into a version 0 (or back) without the lock's key. + let v1 = make_v1(); + let as_v1: StateTransition = v1.clone().into(); + let as_v0: StateTransition = as_v0(&v1).into(); + assert_ne!( + as_v1.signable_bytes().expect("signable"), + as_v0.signable_bytes().expect("signable") + ); + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/proved.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/proved.rs new file mode 100644 index 00000000000..9e3d92f6bce --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/proved.rs @@ -0,0 +1,18 @@ +use crate::identity::state_transition::AssetLockProved; +use crate::prelude::AssetLockProof; +use crate::state_transition::shield_from_asset_lock_transition::v1::ShieldFromAssetLockTransitionV1; +use crate::ProtocolError; + +impl AssetLockProved for ShieldFromAssetLockTransitionV1 { + fn set_asset_lock_proof( + &mut self, + asset_lock_proof: AssetLockProof, + ) -> Result<(), ProtocolError> { + self.asset_lock_proof = asset_lock_proof; + Ok(()) + } + + fn asset_lock_proof(&self) -> &AssetLockProof { + &self.asset_lock_proof + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/state_transition_like.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/state_transition_like.rs new file mode 100644 index 00000000000..bf366417462 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/state_transition_like.rs @@ -0,0 +1,60 @@ +use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use platform_value::BinaryData; + +use crate::state_transition::shield_from_asset_lock_transition::v1::ShieldFromAssetLockTransitionV1; +use crate::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; +use crate::state_transition::{StateTransition, StateTransitionSingleSigned}; +use crate::version::FeatureVersion; +use crate::{ + prelude::Identifier, + state_transition::{StateTransitionLike, StateTransitionType}, +}; + +impl From for StateTransition { + fn from(value: ShieldFromAssetLockTransitionV1) -> Self { + let transition: ShieldFromAssetLockTransition = value.into(); + transition.into() + } +} + +impl StateTransitionLike for ShieldFromAssetLockTransitionV1 { + fn state_transition_protocol_version(&self) -> FeatureVersion { + 1 + } + + /// returns the type of State Transition + fn state_transition_type(&self) -> StateTransitionType { + StateTransitionType::ShieldFromAssetLock + } + + /// Returns IDs of modified data (none for shielded transitions) + fn modified_data_ids(&self) -> Vec { + vec![] + } + + /// Returns unique identifier based on the asset lock proof. + /// The asset lock can only be consumed once, making it the natural deduplication key. + fn unique_identifiers(&self) -> Vec { + match self.asset_lock_proof.create_identifier() { + Ok(identifier) => vec![BASE64_STANDARD.encode(identifier)], + Err(_) => vec![String::default()], + } + } +} + +impl StateTransitionSingleSigned for ShieldFromAssetLockTransitionV1 { + /// returns the signature as a byte-array + fn signature(&self) -> &BinaryData { + &self.signature + } + + /// set a new signature + fn set_signature(&mut self, signature: BinaryData) { + self.signature = signature + } + + fn set_signature_bytes(&mut self, signature: Vec) { + self.signature = BinaryData::new(signature) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/state_transition_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/state_transition_validation.rs new file mode 100644 index 00000000000..2eabe573bff --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/state_transition_validation.rs @@ -0,0 +1,20 @@ +use crate::state_transition::shield_from_asset_lock_transition::v0::state_transition_validation::validate_shield_from_asset_lock_structure; +use crate::state_transition::shield_from_asset_lock_transition::v1::ShieldFromAssetLockTransitionV1; +use crate::state_transition::StateTransitionStructureValidation; +use crate::validation::SimpleConsensusValidationResult; +use platform_version::version::PlatformVersion; + +impl StateTransitionStructureValidation for ShieldFromAssetLockTransitionV1 { + fn validate_structure( + &self, + platform_version: &PlatformVersion, + ) -> SimpleConsensusValidationResult { + validate_shield_from_asset_lock_structure( + &self.actions, + self.value_balance, + &self.proof, + &self.anchor, + platform_version, + ) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/types.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/types.rs new file mode 100644 index 00000000000..17ad8e7eb23 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/types.rs @@ -0,0 +1,17 @@ +use crate::state_transition::shield_from_asset_lock_transition::fields::{PROOF, SIGNATURE}; +use crate::state_transition::shield_from_asset_lock_transition::v1::ShieldFromAssetLockTransitionV1; +use crate::state_transition::StateTransitionFieldTypes; + +impl StateTransitionFieldTypes for ShieldFromAssetLockTransitionV1 { + fn signature_property_paths() -> Vec<&'static str> { + vec![SIGNATURE] + } + + fn identifiers_property_paths() -> Vec<&'static str> { + vec![] + } + + fn binary_property_paths() -> Vec<&'static str> { + vec![SIGNATURE, PROOF] + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/v1_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/v1_methods.rs new file mode 100644 index 00000000000..8eed62174fb --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/v1_methods.rs @@ -0,0 +1,88 @@ +#[cfg(feature = "state-transition-signing")] +use crate::address_funds::PlatformAddress; +#[cfg(feature = "state-transition-signing")] +use crate::prelude::AssetLockProof; +#[cfg(feature = "state-transition-signing")] +use crate::serialization::Signable; +#[cfg(feature = "state-transition-signing")] +use crate::shielded::SerializedAction; +use crate::state_transition::shield_from_asset_lock_transition::methods::ShieldFromAssetLockTransitionMethodsV0; +use crate::state_transition::shield_from_asset_lock_transition::v1::ShieldFromAssetLockTransitionV1; +#[cfg(feature = "state-transition-signing")] +use crate::{state_transition::StateTransition, ProtocolError}; +#[cfg(feature = "state-transition-signing")] +use dashcore::signer; +#[cfg(feature = "state-transition-signing")] +use platform_version::version::PlatformVersion; + +impl ShieldFromAssetLockTransitionMethodsV0 for ShieldFromAssetLockTransitionV1 { + #[cfg(feature = "state-transition-signing")] + fn try_from_asset_lock_with_bundle( + asset_lock_proof: AssetLockProof, + asset_lock_proof_private_key: &[u8], + actions: Vec, + value_balance: u64, + anchor: [u8; 32], + proof: Vec, + binding_signature: [u8; 64], + surplus_output: Option, + _platform_version: &PlatformVersion, + ) -> Result { + let mut transition = ShieldFromAssetLockTransitionV1 { + asset_lock_proof, + actions, + value_balance, + anchor, + proof, + binding_signature, + surplus_output, + signature: Default::default(), + }; + + // The signable bytes carry the transition version and exclude the signature field. + let state_transition: StateTransition = transition.clone().into(); + let signable_bytes = state_transition.signable_bytes()?; + + let signature = signer::sign(&signable_bytes, asset_lock_proof_private_key)?; + transition.signature = signature.to_vec().into(); + + Ok(transition.into()) + } + + #[cfg(all(feature = "state-transition-signing", feature = "core_key_wallet"))] + async fn try_from_asset_lock_with_bundle_and_signer( + asset_lock_proof: AssetLockProof, + asset_lock_proof_path: &::key_wallet::bip32::DerivationPath, + asset_lock_signer: &AS, + actions: Vec, + value_balance: u64, + anchor: [u8; 32], + proof: Vec, + binding_signature: [u8; 64], + surplus_output: Option, + _platform_version: &PlatformVersion, + ) -> Result + where + AS: ::key_wallet::signer::Signer, + { + let transition = ShieldFromAssetLockTransitionV1 { + asset_lock_proof, + actions, + value_balance, + anchor, + proof, + binding_signature, + surplus_output, + signature: Default::default(), + }; + + // `sign_with_core_signer` derives, signs and zeroises inside the external signer; the + // host never sees the raw key. + let mut state_transition: StateTransition = transition.into(); + state_transition + .sign_with_core_signer(asset_lock_proof_path, asset_lock_signer) + .await?; + + Ok(state_transition) + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/version.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/version.rs new file mode 100644 index 00000000000..fd45f24b0f5 --- /dev/null +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v1/version.rs @@ -0,0 +1,9 @@ +use crate::state_transition::shield_from_asset_lock_transition::v1::ShieldFromAssetLockTransitionV1; +use crate::state_transition::FeatureVersioned; +use crate::version::FeatureVersion; + +impl FeatureVersioned for ShieldFromAssetLockTransitionV1 { + fn feature_version(&self) -> FeatureVersion { + 1 + } +} diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/version.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/version.rs index 949a31fb04a..6bf7e552b77 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/version.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/version.rs @@ -6,6 +6,7 @@ impl FeatureVersioned for ShieldFromAssetLockTransition { fn feature_version(&self) -> FeatureVersion { match self { ShieldFromAssetLockTransition::V0(v0) => v0.feature_version(), + ShieldFromAssetLockTransition::V1(v1) => v1.feature_version(), } } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs index ce95fbea8fa..f5ee67639d5 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs @@ -29,7 +29,7 @@ use dpp::state_transition::batch_transition::batched_transition::token_transitio TokenTransition, TokenTransitionV0Methods, }; use dpp::shielded::{ - TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE, TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE, compute_token_purchase_from_shielded_pool_fee, compute_token_shielded_transfer_with_shielded_fee_fee, compute_token_unshield_with_shielded_fee_fee, document_token_payment_extra_sighash_data, token_burn_from_pool_extra_sighash_data, token_pool_fee_bundle_extra_sighash_data, token_pool_output_only_extra_sighash_data, token_purchase_from_shielded_pool_extra_sighash_data, token_shielded_transfer_extra_sighash_data, token_shielded_transfer_with_shielded_fee_extra_sighash_data, token_unshield_extra_sighash_data, token_unshield_with_shielded_fee_extra_sighash_data, + shield_extra_sighash_data, shield_from_identity_extra_sighash_data, TOKEN_PURCHASE_FROM_SHIELDED_POOL_TYPE, TOKEN_SHIELDED_TRANSFER_WITH_SHIELDED_FEE_TYPE, TOKEN_UNSHIELD_WITH_SHIELDED_FEE_TYPE, compute_token_purchase_from_shielded_pool_fee, compute_token_shielded_transfer_with_shielded_fee_fee, compute_token_unshield_with_shielded_fee_fee, document_token_payment_extra_sighash_data, token_burn_from_pool_extra_sighash_data, token_pool_fee_bundle_extra_sighash_data, token_pool_output_only_extra_sighash_data, token_purchase_from_shielded_pool_extra_sighash_data, token_shielded_transfer_extra_sighash_data, token_shielded_transfer_with_shielded_fee_extra_sighash_data, token_unshield_extra_sighash_data, token_unshield_with_shielded_fee_extra_sighash_data, }; use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRef; use dpp::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType; @@ -210,6 +210,9 @@ impl StateTransitionHasShieldedProofValidationV0 for StateTransition { dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition::V0(v0) => { v0.actions.len() } + dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition::V1(v1) => { + v1.actions.len() + } }, StateTransition::Batch(batch) => batch .transitions_iter() @@ -1115,6 +1118,11 @@ fn validate_batch_token_shielded_proofs( /// Generation 1 (protocol version 14): the credit pool rules of v0, the token shielded pools, /// and the identity key restrictions v0 cannot express. /// +/// `Shield` and `ShieldFromIdentity`, the credit pool outputs-only bundles checked here +/// (`ShieldFromAssetLock` is checked in its transform), are checked against the preimage +/// `dpp.methods.credit_pool_bundle_binding` selects, which binds their kind and what funds them; +/// v0 checks them against an empty one. +/// /// In `IdentityCreateFromShieldedPool` it refuses the keys the v0 Orchard sighash preimage cannot /// bind, before that preimage is built: a key bound to a contract group (the layout predates group /// bounds, and an error out of the preimage builder would be an internal error rather than a @@ -1171,8 +1179,13 @@ fn validate_shielded_proof_v1( } let result = match state_transition { + // The credit pool's outputs-only bundles carry no anchor, so the proved + // bytes verify wherever they land. Their preimage binds the kind and the + // funder, which a copy re-wrapped under someone else's funding cannot match. StateTransition::Shield(st) => match st { dpp::state_transition::shield_transition::ShieldTransition::V0(v0) => { + let extra_sighash_data = + shield_extra_sighash_data(&v0.inputs, platform_version)?; reconstruct_and_verify_bundle( &v0.actions, FLAGS_OUTPUTS_ONLY, @@ -1180,20 +1193,28 @@ fn validate_shielded_proof_v1( &v0.anchor, v0.proof.as_slice(), &v0.binding_signature, - &[], // No transparent fields for shield + &extra_sighash_data, ) } }, + // CheckTx's admission check. Block processing verifies the same bundle in + // `ShieldFromIdentity`'s transform, which must rebuild the same preimage. StateTransition::ShieldFromIdentity(st) => match st { - ShieldFromIdentityTransition::V0(v0) => reconstruct_and_verify_bundle( - &v0.actions, - FLAGS_OUTPUTS_ONLY, - -(v0.amount as i64), - &v0.anchor, - v0.proof.as_slice(), - &v0.binding_signature, - &[], - ), + ShieldFromIdentityTransition::V0(v0) => { + let extra_sighash_data = shield_from_identity_extra_sighash_data( + &v0.identity_id.to_buffer(), + platform_version, + )?; + reconstruct_and_verify_bundle( + &v0.actions, + FLAGS_OUTPUTS_ONLY, + -(v0.amount as i64), + &v0.anchor, + v0.proof.as_slice(), + &v0.binding_signature, + &extra_sighash_data, + ) + } }, StateTransition::ShieldedTransfer(st) => match st { dpp::state_transition::shielded_transfer_transition::ShieldedTransferTransition::V0(v0) => { @@ -1998,4 +2019,120 @@ mod tests { assert!(result.is_valid()); } } + + /// The credit pool's outputs-only bundles are bound by the builder according to + /// `dpp.methods.credit_pool_bundle_binding`, and verified by drive-abci generations chosen in + /// another table. The generations that predate the binding hardcode an empty preimage; the + /// ones after it rebuild whatever the field says. Were a protocol version to say "bound" + /// while selecting an old generation, every client would bind and every node would expect + /// empty; were it to select a new generation while saying "unbound", the bundles would + /// silently go unprotected. Nothing ties the two tables together except this test. + #[test] + fn credit_pool_bundle_binding_should_agree_with_the_selected_shield_verifiers_at_every_protocol_version( + ) { + use dpp::state_transition::shield_from_asset_lock_transition::v1::ShieldFromAssetLockTransitionV1; + use dpp::version::feature_initial_protocol_versions::{ + SHIELDED_POOL_INITIAL_PROTOCOL_VERSION, SHIELD_FROM_IDENTITY_INITIAL_PROTOCOL_VERSION, + }; + use platform_version::version::PLATFORM_VERSIONS; + + let asset_lock_v0 = StateTransition::ShieldFromAssetLock( + ShieldFromAssetLockTransition::V0(ShieldFromAssetLockTransitionV0 { + asset_lock_proof: Default::default(), + actions: vec![], + value_balance: 1, + anchor: [0; 32], + proof: vec![], + binding_signature: [0; 64], + surplus_output: None, + signature: Default::default(), + }), + ); + let asset_lock_v1 = StateTransition::ShieldFromAssetLock( + ShieldFromAssetLockTransition::V1(ShieldFromAssetLockTransitionV1 { + asset_lock_proof: Default::default(), + actions: vec![], + value_balance: 1, + anchor: [0; 32], + proof: vec![], + binding_signature: [0; 64], + surplus_output: None, + signature: Default::default(), + }), + ); + + for platform_version in PLATFORM_VERSIONS { + let protocol_version = platform_version.protocol_version; + let binds = match platform_version.dpp.methods.credit_pool_bundle_binding { + None => false, + Some(0) => true, + Some(other) => panic!( + "protocol version {protocol_version}: unknown credit_pool_bundle_binding {other}" + ), + }; + let validation = &platform_version.drive_abci.validation_and_processing; + + // `Shield`, and `ShieldFromIdentity` at admission: `validate_shielded_proof` v1 reads + // the field, v0 binds nothing. + assert_eq!( + binds, + validation.validate_shielded_proof >= 1, + "protocol version {protocol_version}: credit_pool_bundle_binding disagrees with \ + validate_shielded_proof {}", + validation.validate_shielded_proof + ); + // `ShieldFromAssetLock`: its transform v1 reads the field, v0 binds nothing. + let asset_lock_transform = validation + .state_transitions + .shield_from_asset_lock_state_transition + .transform_into_action; + assert_eq!( + binds, + asset_lock_transform >= 1, + "protocol version {protocol_version}: credit_pool_bundle_binding disagrees with \ + the ShieldFromAssetLock transform {asset_lock_transform}" + ); + // `ShieldFromAssetLock`'s transition version 1 carries the bound bundle and version 0 + // the unbound one. Wherever the transition exists, the version clients build and the + // only version that decodes must be the one the selected transform expects; + // otherwise a waiting version 0 would reach the bound check and burn its lock's + // penalty, or a bound bundle would meet an unbound check. + if protocol_version >= SHIELDED_POOL_INITIAL_PROTOCOL_VERSION { + let built = platform_version + .dpp + .state_transition_serialization_versions + .shield_from_asset_lock_state_transition + .default_current_version; + assert_eq!( + binds, + built == 1, + "protocol version {protocol_version}: credit_pool_bundle_binding disagrees \ + with the ShieldFromAssetLock version clients build, {built}" + ); + assert_eq!( + binds, + !asset_lock_v0 + .active_version_range() + .contains(&protocol_version), + "protocol version {protocol_version}: ShieldFromAssetLock version 0 must \ + decode exactly where the binding is off" + ); + assert_eq!( + binds, + asset_lock_v1 + .active_version_range() + .contains(&protocol_version), + "protocol version {protocol_version}: ShieldFromAssetLock version 1 must \ + decode exactly where the binding is on" + ); + } + // `ShieldFromIdentity` has never existed unbound: wherever it is allowed, it binds. + if protocol_version >= SHIELD_FROM_IDENTITY_INITIAL_PROTOCOL_VERSION { + assert!( + binds, + "protocol version {protocol_version} allows ShieldFromIdentity unbound" + ); + } + } + } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs index c4ac3e78ce4..ac65f78fb2d 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs @@ -22,7 +22,7 @@ mod tests { use dpp::identity::signer::Signer; use dpp::prelude::AddressNonce; use dpp::serialization::{PlatformSerializable, Signable}; - use dpp::shielded::SerializedAction; + use dpp::shielded::{shield_extra_sighash_data, SerializedAction}; use dpp::state_transition::shield_transition::v0::ShieldTransitionV0; use dpp::state_transition::shield_transition::ShieldTransition; use dpp::state_transition::StateTransition; @@ -132,6 +132,20 @@ mod tests { .await } + /// The Orchard sighash consensus checks a `Shield` funded from `funding` against. Only the + /// funding addresses are bound, not their nonces or contributions, so a fixture can prove its + /// bundle before it settles what each address contributes. + fn shield_sighash( + bundle_commitment: &[u8; 32], + funding: &[PlatformAddress], + platform_version: &PlatformVersion, + ) -> [u8; 32] { + let inputs = funding.iter().map(|address| (*address, (0, 0))).collect(); + let extra_sighash_data = + shield_extra_sighash_data(&inputs, platform_version).expect("shield sighash data"); + compute_platform_sighash(bundle_commitment, &extra_sighash_data) + } + // (Orchard ProvingKey and serialize_authorized_bundle are now shared // via test_helpers::get_proving_key / serialize_authorized_bundle_with_flags) @@ -810,7 +824,7 @@ mod tests { let (unauthorized, _) = builder.build::(&mut rng).unwrap().unwrap(); let bundle_commitment: [u8; 32] = unauthorized.commitment().into(); - let sighash = compute_platform_sighash(&bundle_commitment, &[]); + let sighash = shield_sighash(&bundle_commitment, &[input_address], platform_version); let proven = unauthorized.create_proof(pk, &mut rng).unwrap(); let bundle = proven.apply_signatures(rng, sighash, &[]).unwrap(); @@ -1007,7 +1021,7 @@ mod tests { let (unauthorized, _) = builder.build::(&mut rng).unwrap().unwrap(); let bundle_commitment: [u8; 32] = unauthorized.commitment().into(); - let sighash = compute_platform_sighash(&bundle_commitment, &[]); + let sighash = shield_sighash(&bundle_commitment, &[input_address], platform_version); let proven = unauthorized.create_proof(pk, &mut rng).unwrap(); let bundle = proven.apply_signatures(rng, sighash, &[]).unwrap(); @@ -1123,7 +1137,7 @@ mod tests { let (unauthorized, _) = builder.build::(&mut rng).unwrap().unwrap(); let bundle_commitment: [u8; 32] = unauthorized.commitment().into(); - let sighash = compute_platform_sighash(&bundle_commitment, &[]); + let sighash = shield_sighash(&bundle_commitment, &[input_address], platform_version); let proven = unauthorized.create_proof(pk, &mut rng).unwrap(); let bundle = proven.apply_signatures(rng, sighash, &[]).unwrap(); @@ -1229,10 +1243,13 @@ mod tests { ) .unwrap(); + // --- The address that funds the shield, which the bundle's sighash binds --- + let mut signer = TestAddressSigner::new(); + let input_address = signer.add_p2pkh([1u8; 32]); + let (unauthorized, _) = builder.build::(&mut rng).unwrap().unwrap(); let bundle_commitment: [u8; 32] = unauthorized.commitment().into(); - // Shield sighash extra_data is empty (no transparent output fields) - let sighash = compute_platform_sighash(&bundle_commitment, &[]); + let sighash = shield_sighash(&bundle_commitment, &[input_address], platform_version); let proven = unauthorized.create_proof(pk, &mut rng).unwrap(); let bundle = proven.apply_signatures(rng, sighash, &[]).unwrap(); @@ -1245,8 +1262,6 @@ mod tests { let shield_amount = (-value_balance) as u64; // --- Set up input address with enough balance --- - let mut signer = TestAddressSigner::new(); - let input_address = signer.add_p2pkh([1u8; 32]); let input_amount = shield_amount + dash_to_credits!(0.01); setup_address_with_balance(&mut platform, input_address, 0, dash_to_credits!(1.0)); @@ -1483,7 +1498,7 @@ mod tests { let (unauthorized, _) = builder.build::(&mut rng).unwrap().unwrap(); let bundle_commitment: [u8; 32] = unauthorized.commitment().into(); - let sighash = compute_platform_sighash(&bundle_commitment, &[]); + let sighash = shield_sighash(&bundle_commitment, &[input_address], platform_version); let proven = unauthorized.create_proof(pk, &mut rng).unwrap(); let bundle = proven.apply_signatures(rng, sighash, &[]).unwrap(); @@ -1666,7 +1681,7 @@ mod tests { .unwrap(); let (unauthorized, _) = builder.build::(&mut rng).unwrap().unwrap(); let bundle_commitment: [u8; 32] = unauthorized.commitment().into(); - let sighash = compute_platform_sighash(&bundle_commitment, &[]); + let sighash = shield_sighash(&bundle_commitment, &[addr_a, addr_b], platform_version); let proven = unauthorized.create_proof(pk, &mut rng).unwrap(); let bundle = proven.apply_signatures(rng, sighash, &[]).unwrap(); let (actions, _flags, value_balance, anchor_bytes, proof_bytes, binding_sig) = @@ -1782,7 +1797,13 @@ mod tests { binding_sig: [u8; 64], } - fn build_bundle() -> Bundle { + /// The address every run in this module funds its shield from, derived from the seed the + /// runs use. + fn funding_address() -> PlatformAddress { + TestAddressSigner::new().add_p2pkh([1u8; 32]) + } + + fn build_bundle(pv: &PlatformVersion) -> Bundle { let mut rng = OsRng; let pk = get_proving_key(); let sk = SpendingKey::from_bytes([0u8; 32]).unwrap(); @@ -1801,7 +1822,7 @@ mod tests { .unwrap(); let (unauthorized, _) = builder.build::(&mut rng).unwrap().unwrap(); let commitment: [u8; 32] = unauthorized.commitment().into(); - let sighash = compute_platform_sighash(&commitment, &[]); + let sighash = shield_sighash(&commitment, &[funding_address()], pv); let proven = unauthorized.create_proof(pk, &mut rng).unwrap(); let bundle = proven.apply_signatures(rng, sighash, &[]).unwrap(); @@ -1867,6 +1888,7 @@ mod tests { let mut signer = TestAddressSigner::new(); let addr = signer.add_p2pkh([1u8; 32]); + assert_eq!(addr, funding_address(), "the bundle binds this address"); let declared_input = b.shield_amount + headroom; setup_address_with_balance_and_system_credits(&mut platform, addr, 0, declared_input); @@ -1914,7 +1936,7 @@ mod tests { #[tokio::test] async fn shield_fee_estimate_and_actual_must_not_leave_a_halting_band() { let pv = PlatformVersion::latest(); - let b = build_bundle(); + let b = build_bundle(pv); const CEILING: u64 = 5_000_000_000; // 0.05 DASH, far above any plausible shield fee let (top, top_msg) = run_at(CEILING, &b, pv).await; @@ -1989,7 +2011,10 @@ mod tests { #[tokio::test] async fn dropped_shield_must_not_mutate_state() { let pv = PlatformVersion::get(13).expect("protocol version 13 should exist"); - let b = build_bundle(); + // `setup_platform` starts at the latest protocol version, and + // `process_state_transition` validates at the platform state's version, not at `pv`, + // so the bundle is bound the way the latest version's proof check rebuilds it. + let b = build_bundle(PlatformVersion::latest()); // Least headroom validation lets through to execution. const CEILING: u64 = 5_000_000_000; @@ -2042,6 +2067,7 @@ mod tests { insert_dummy_encrypted_notes(&platform, MAINNET_NOTES); let mut signer = TestAddressSigner::new(); let addr = signer.add_p2pkh([1u8; 32]); + assert_eq!(addr, funding_address(), "the bundle binds this address"); let declared_input = b.shield_amount + headroom; setup_address_with_balance_and_system_credits(&mut platform, addr, 0, declared_input); @@ -2141,7 +2167,7 @@ mod tests { #[tokio::test] async fn savepoint_rollback_must_undo_an_applied_shield() { let pv = PlatformVersion::latest(); - let b = build_bundle(); + let b = build_bundle(pv); // Generous headroom: this shield must SUCCEED so its writes all land. let headroom = 5_000_000_000u64; @@ -2149,6 +2175,7 @@ mod tests { insert_dummy_encrypted_notes(&platform, MAINNET_NOTES); let mut signer = TestAddressSigner::new(); let addr = signer.add_p2pkh([1u8; 32]); + assert_eq!(addr, funding_address(), "the bundle binds this address"); let declared_input = b.shield_amount + headroom; setup_address_with_balance_and_system_credits(&mut platform, addr, 0, declared_input); @@ -2315,7 +2342,7 @@ mod tests { use crate::execution::platform_events::state_transition_processing::test_fault_injection::FAIL_NEXT_SUCCESSFUL_EXECUTION; let pv = PlatformVersion::latest(); - let b = build_bundle(); + let b = build_bundle(pv); // Fully funded: without the injected failure this shield would execute and land. let headroom = 5_000_000_000u64; @@ -2323,6 +2350,7 @@ mod tests { insert_dummy_encrypted_notes(&platform, MAINNET_NOTES); let mut signer = TestAddressSigner::new(); let addr = signer.add_p2pkh([1u8; 32]); + assert_eq!(addr, funding_address(), "the bundle binds this address"); let declared_input = b.shield_amount + headroom; setup_address_with_balance_and_system_credits(&mut platform, addr, 0, declared_input); @@ -2455,4 +2483,265 @@ mod tests { ); } } + + // ========================================== + // Bundle binding + // ========================================== + + /// The bundle's sighash binds its kind and the addresses that fund it. Every negative test here + /// starts from a transition the public builder made and shows it admitted by CheckTx and + /// executed first, so a refusal that follows is about where the bundle was moved, not about + /// the bundle. + mod bundle_binding { + use super::*; + use crate::config::{PlatformConfig, PlatformTestConfig}; + use crate::execution::validation::state_transition::state_transitions::test_helpers::{ + check_tx_errors, test_orchard_recipient, TestOrchardProver, + }; + use crate::rpc::core::MockCoreRPCLike; + use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; + use dpp::dashcore::{Network, PrivateKey}; + use dpp::identity::KeyType; + use dpp::shielded::builder::{ + build_shield_from_asset_lock_transition, build_shield_transition, + }; + use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; + use dpp::tests::fixtures::instant_asset_lock_proof_fixture; + use rand::rngs::StdRng; + use rand::SeedableRng; + + const SHIELD_AMOUNT: u64 = 5_000; + + /// A funded address and the inputs that spend from it. + fn funding( + platform: &mut TempPlatform, + signer: &mut TestAddressSigner, + seed: u8, + ) -> BTreeMap { + let address = signer.add_p2pkh([seed; 32]); + setup_address_with_balance(platform, address, 0, dash_to_credits!(1.0)); + let mut inputs = BTreeMap::new(); + inputs.insert(address, (1, SHIELD_AMOUNT + dash_to_credits!(0.01))); + inputs + } + + async fn builder_made_shield( + inputs: BTreeMap, + signer: &TestAddressSigner, + platform_version: &PlatformVersion, + ) -> StateTransition { + build_shield_transition( + &test_orchard_recipient(), + SHIELD_AMOUNT, + inputs, + AddressFundsFeeStrategy::from(vec![AddressFundsFeeStrategyStep::DeductFromInput( + 0, + )]), + signer, + 0, + &TestOrchardProver, + [0u8; 36], + None, + platform_version, + ) + .await + .expect("client-built shield") + } + + /// Wraps `bundle` pieces in a `Shield` funded and signed from `inputs`. + async fn wrap( + inputs: BTreeMap, + signer: &TestAddressSigner, + actions: Vec, + amount: u64, + anchor: [u8; 32], + proof: Vec, + binding_signature: [u8; 64], + ) -> StateTransition { + let mut transition = + StateTransition::Shield(ShieldTransition::V0(ShieldTransitionV0 { + inputs: inputs.clone(), + actions, + amount, + anchor, + proof, + binding_signature, + fee_strategy: AddressFundsFeeStrategy::from(vec![ + AddressFundsFeeStrategyStep::DeductFromInput(0), + ]), + user_fee_increase: 0, + input_witnesses: vec![], + })); + let signable_bytes = transition.signable_bytes().expect("signable bytes"); + let mut witnesses = Vec::with_capacity(inputs.len()); + for address in inputs.keys() { + witnesses.push( + signer + .sign_create_witness(address, &signable_bytes) + .await + .expect("should sign"), + ); + } + if let StateTransition::Shield(ShieldTransition::V0(ref mut v0)) = transition { + v0.input_witnesses = witnesses; + } + transition + } + + fn assert_admitted_and_executed( + platform: &TempPlatform, + transition: &StateTransition, + platform_version: &PlatformVersion, + ) { + let admission = check_tx_errors(platform, transition); + assert!(admission.is_empty(), "CheckTx must admit it: {admission:?}"); + assert_matches!( + process_transition(platform, transition.clone(), platform_version) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + } + + fn assert_refused_for_its_proof( + platform: &TempPlatform, + transition: &StateTransition, + platform_version: &PlatformVersion, + ) { + let admission = check_tx_errors(platform, transition); + assert_matches!( + admission.as_slice(), + [ConsensusError::StateError( + StateError::InvalidShieldedProofError(_) + )], + "CheckTx must refuse it for its proof" + ); + assert_matches!( + process_transition(platform, transition.clone(), platform_version) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::StateError(StateError::InvalidShieldedProofError(_)) + )] + ); + } + + /// Somebody funds a `Shield` from their own addresses and wraps it around a bundle proved + /// for somebody else's. Without the funding in the sighash its proof verifies and a second + /// note with the same nullifier lands in the pool. + #[tokio::test] + async fn should_refuse_a_bundle_resubmitted_by_another_funder() { + let platform_version = PlatformVersion::latest(); + let mut platform = setup_platform(); + let mut victim_signer = TestAddressSigner::new(); + let victim_inputs = funding(&mut platform, &mut victim_signer, 50); + let mut copier_signer = TestAddressSigner::new(); + let copier_inputs = funding(&mut platform, &mut copier_signer, 51); + + let shield = builder_made_shield(victim_inputs, &victim_signer, platform_version).await; + // Positive control: the builder's bundle is what both CheckTx and the block accept. + assert_admitted_and_executed(&platform, &shield, platform_version); + + let StateTransition::Shield(ShieldTransition::V0(proven)) = &shield else { + panic!("expected a shield transition"); + }; + let copy = wrap( + copier_inputs, + &copier_signer, + proven.actions.clone(), + proven.amount, + proven.anchor, + proven.proof.clone(), + proven.binding_signature, + ) + .await; + + assert_refused_for_its_proof(&platform, ©, platform_version); + } + + /// A `ShieldFromAssetLock` bundle and a `Shield` bundle are indistinguishable to the proof: + /// same flags, same empty-tree anchor, same value balance. Moving one into the other kind + /// must fail on the kind and the owner it binds. + #[tokio::test] + async fn should_refuse_a_shield_from_asset_lock_bundle_resubmitted_as_a_shield() { + let platform_version = PlatformVersion::latest(); + let mut platform = setup_platform(); + + let mut rng = StdRng::seed_from_u64(52); + let (_, lock_key) = KeyType::ECDSA_SECP256K1 + .random_public_and_private_key_data(&mut rng, platform_version) + .expect("asset lock key"); + let from_lock = build_shield_from_asset_lock_transition( + &test_orchard_recipient(), + SHIELD_AMOUNT, + instant_asset_lock_proof_fixture( + Some(PrivateKey::from_byte_array(&lock_key, Network::Testnet).expect("key")), + None, + ), + &lock_key, + &TestOrchardProver, + [0u8; 36], + None, + Some(PlatformAddress::P2pkh([0x33; 20])), + 0, + platform_version, + ) + .expect("client-built shield from asset lock"); + // Positive control: the bundle is valid for the kind it was proved for. + assert_admitted_and_executed(&platform, &from_lock, platform_version); + + let StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V1(proven)) = + &from_lock + else { + panic!("expected a shield from asset lock transition"); + }; + let mut copier_signer = TestAddressSigner::new(); + let copier_inputs = funding(&mut platform, &mut copier_signer, 53); + let copy = wrap( + copier_inputs, + &copier_signer, + proven.actions.clone(), + proven.value_balance, + proven.anchor, + proven.proof.clone(), + proven.binding_signature, + ) + .await; + + assert_refused_for_its_proof(&platform, ©, platform_version); + } + + /// Protocol versions 12 and 13 shipped `Shield` verified by `validate_shielded_proof` v0, + /// which binds nothing, and they still select it through validation versions V8 and V9: + /// the binding lives in v1, which only V10, protocol version 14, selects. A client + /// building for 12 or 13 reads the same `credit_pool_bundle_binding` field consensus + /// does, so what it builds is accepted there, while a bundle bound the way 14 binds it is + /// refused, exactly as those versions always have. + #[tokio::test] + async fn should_keep_the_unbound_bundle_at_protocol_versions_12_and_13() { + for protocol_version in [12u32, 13] { + let platform_version = + PlatformVersion::get(protocol_version).expect("known protocol version"); + let mut platform = TestPlatformBuilder::new() + .with_config(PlatformConfig { + testing_configs: PlatformTestConfig { + disable_instant_lock_signature_verification: true, + ..Default::default() + }, + ..Default::default() + }) + .with_initial_protocol_version(protocol_version) + .build_with_mock_rpc() + .set_genesis_state(); + + let mut signer = TestAddressSigner::new(); + let inputs = funding(&mut platform, &mut signer, 54); + let shield = builder_made_shield(inputs.clone(), &signer, platform_version).await; + assert_admitted_and_executed(&platform, &shield, platform_version); + + let bound = builder_made_shield(inputs, &signer, PlatformVersion::latest()).await; + assert_refused_for_its_proof(&platform, &bound, platform_version); + } + } + } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/mod.rs index d847d011652..9b852bf9d7e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/mod.rs @@ -10,6 +10,7 @@ use crate::error::execution::ExecutionError; use crate::error::Error; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; use crate::execution::validation::state_transition::shield_from_asset_lock::transform_into_action::v0::ShieldFromAssetLockStateTransitionTransformIntoActionValidationV0; +use crate::execution::validation::state_transition::shield_from_asset_lock::transform_into_action::v1::ShieldFromAssetLockStateTransitionTransformIntoActionValidationV1; use crate::execution::validation::state_transition::ValidationMode; use crate::platform_types::platform::PlatformRef; use crate::platform_types::check_tx_proof_verifier::CheckTxProofVerifier; @@ -66,9 +67,17 @@ impl StateTransitionShieldFromAssetLockTransitionActionTransformer check_tx_proof_verifier, tx, ), + 1 => self.transform_into_action_v1( + platform, + signable_bytes, + validation_mode, + execution_context, + check_tx_proof_verifier, + tx, + ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "shield from asset lock transition: transform_into_action".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/tests.rs index a4337de4fb7..8b6754de9bb 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/tests.rs @@ -18,11 +18,13 @@ mod tests { use dpp::consensus::ConsensusError; use dpp::dash_to_credits; use dpp::dashcore::{Network, PrivateKey}; + use dpp::identity::state_transition::asset_lock_proof::AssetLockProof; use dpp::identity::KeyType::ECDSA_SECP256K1; use dpp::platform_value::BinaryData; use dpp::serialization::{PlatformSerializable, Signable}; - use dpp::shielded::SerializedAction; + use dpp::shielded::{shield_from_asset_lock_extra_sighash_data, SerializedAction}; use dpp::state_transition::shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0; + use dpp::state_transition::shield_from_asset_lock_transition::v1::ShieldFromAssetLockTransitionV1; use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; use dpp::state_transition::StateTransition; use dpp::tests::fixtures::instant_asset_lock_proof_fixture; @@ -136,7 +138,7 @@ mod tests { binding_signature: [u8; 64], ) -> StateTransition { // Create unsigned transition to compute signable bytes - let unsigned = ShieldFromAssetLockTransitionV0 { + let unsigned = ShieldFromAssetLockTransitionV1 { asset_lock_proof: asset_lock_proof.clone(), actions: actions.clone(), value_balance, @@ -161,8 +163,8 @@ mod tests { let signature = dpp::dashcore::signer::sign(&signable_bytes, asset_lock_private_key).unwrap(); - StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V0( - ShieldFromAssetLockTransitionV0 { + StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V1( + ShieldFromAssetLockTransitionV1 { asset_lock_proof, actions, value_balance, @@ -189,7 +191,7 @@ mod tests { binding_signature: [u8; 64], ) -> StateTransition { // Create unsigned transition to compute signable bytes - let unsigned = ShieldFromAssetLockTransitionV0 { + let unsigned = ShieldFromAssetLockTransitionV1 { asset_lock_proof: asset_lock_proof.clone(), actions: actions.clone(), value_balance, @@ -208,8 +210,8 @@ mod tests { let signature = dpp::dashcore::signer::sign(&signable_bytes, asset_lock_private_key).unwrap(); - StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V0( - ShieldFromAssetLockTransitionV0 { + StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V1( + ShieldFromAssetLockTransitionV1 { asset_lock_proof, actions, value_balance, @@ -232,8 +234,8 @@ mod tests { proof: Vec, binding_signature: [u8; 64], ) -> StateTransition { - StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V0( - ShieldFromAssetLockTransitionV0 { + StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V1( + ShieldFromAssetLockTransitionV1 { asset_lock_proof, actions, value_balance, @@ -246,6 +248,20 @@ mod tests { )) } + /// The Orchard sighash consensus checks a `ShieldFromAssetLock` funded by `asset_lock_proof` + /// against. The bundle binds the asset lock, so a fixture has to create the lock before it + /// proves the bundle. + fn shield_from_asset_lock_sighash( + bundle_commitment: &[u8; 32], + asset_lock_proof: &AssetLockProof, + platform_version: &PlatformVersion, + ) -> [u8; 32] { + let extra_sighash_data = + shield_from_asset_lock_extra_sighash_data(asset_lock_proof, platform_version) + .expect("shield from asset lock sighash data"); + compute_platform_sighash(bundle_commitment, &extra_sighash_data) + } + // (Orchard ProvingKey and serialize_authorized_bundle are now shared // via test_helpers::get_proving_key / serialize_authorized_bundle_with_flags) @@ -567,7 +583,7 @@ mod tests { // Build transition with a completely zeroed (invalid) signature let transition = StateTransition::ShieldFromAssetLock( - ShieldFromAssetLockTransition::V0(ShieldFromAssetLockTransitionV0 { + ShieldFromAssetLockTransition::V1(ShieldFromAssetLockTransitionV1 { asset_lock_proof, actions: vec![create_dummy_serialized_action()], value_balance: 5000, @@ -676,8 +692,11 @@ mod tests { let (unauthorized, _) = builder.build::(&mut orchard_rng).unwrap().unwrap(); let bundle_commitment: [u8; 32] = unauthorized.commitment().into(); - // No extra_sighash_data for shield_from_asset_lock (empty, like shield) - let sighash = compute_platform_sighash(&bundle_commitment, &[]); + let sighash = shield_from_asset_lock_sighash( + &bundle_commitment, + &asset_lock_proof, + platform_version, + ); let proven = unauthorized.create_proof(pk, &mut orchard_rng).unwrap(); let bundle = proven.apply_signatures(orchard_rng, sighash, &[]).unwrap(); @@ -825,7 +844,11 @@ mod tests { let (unauthorized, _) = builder.build::(&mut orchard_rng).unwrap().unwrap(); let bundle_commitment: [u8; 32] = unauthorized.commitment().into(); - let sighash = compute_platform_sighash(&bundle_commitment, &[]); + let sighash = shield_from_asset_lock_sighash( + &bundle_commitment, + &asset_lock_proof, + platform_version, + ); let proven = unauthorized.create_proof(pk, &mut orchard_rng).unwrap(); let bundle = proven.apply_signatures(orchard_rng, sighash, &[]).unwrap(); @@ -894,7 +917,11 @@ mod tests { let (unauthorized, _) = builder.build::(&mut orchard_rng).unwrap().unwrap(); let bundle_commitment: [u8; 32] = unauthorized.commitment().into(); - let sighash = compute_platform_sighash(&bundle_commitment, &[]); + let sighash = shield_from_asset_lock_sighash( + &bundle_commitment, + &asset_lock_proof, + platform_version, + ); let proven = unauthorized.create_proof(pk, &mut orchard_rng).unwrap(); let bundle = proven.apply_signatures(orchard_rng, sighash, &[]).unwrap(); @@ -986,7 +1013,11 @@ mod tests { let (unauthorized, _) = builder.build::(&mut orchard_rng).unwrap().unwrap(); let bundle_commitment: [u8; 32] = unauthorized.commitment().into(); - let sighash = compute_platform_sighash(&bundle_commitment, &[]); + let sighash = shield_from_asset_lock_sighash( + &bundle_commitment, + &asset_lock_proof, + platform_version, + ); let proven = unauthorized.create_proof(pk, &mut orchard_rng).unwrap(); let bundle = proven.apply_signatures(orchard_rng, sighash, &[]).unwrap(); @@ -1167,7 +1198,11 @@ mod tests { let (unauthorized, _) = builder.build::(&mut orchard_rng).unwrap().unwrap(); let bundle_commitment: [u8; 32] = unauthorized.commitment().into(); - let sighash = compute_platform_sighash(&bundle_commitment, &[]); + let sighash = shield_from_asset_lock_sighash( + &bundle_commitment, + &asset_lock_proof, + platform_version, + ); let proven = unauthorized.create_proof(pk, &mut orchard_rng).unwrap(); let bundle = proven.apply_signatures(orchard_rng, sighash, &[]).unwrap(); @@ -1359,7 +1394,7 @@ mod tests { // number of duffs. const SHIELD_VALUE_BASE: u64 = 1_000_000; - let mut build_bundle = |shield_value: u64| { + let mut build_bundle = |shield_value: u64, asset_lock_proof: &AssetLockProof| { let mut builder = Builder::::new( BundleType::Transactional { flags: OrchardFlags::SPENDS_DISABLED, @@ -1377,14 +1412,22 @@ mod tests { .unwrap(); let (unauthorized, _) = builder.build::(&mut orchard_rng).unwrap().unwrap(); let bundle_commitment: [u8; 32] = unauthorized.commitment().into(); - let sighash = compute_platform_sighash(&bundle_commitment, &[]); + let sighash = shield_from_asset_lock_sighash( + &bundle_commitment, + asset_lock_proof, + platform_version, + ); let proven = unauthorized.create_proof(pk, &mut orchard_rng).unwrap(); let bundle = proven.apply_signatures(orchard_rng, sighash, &[]).unwrap(); serialize_authorized_bundle_with_flags(&bundle) }; - // Probe to learn the (value-independent) action count, then the pool fee. - let (probe_actions, _, _, _, _, _) = build_bundle(SHIELD_VALUE_BASE); + // Probe to learn the (value-independent) action count, then the pool fee. The probe + // is never submitted, so any lock will do for its binding. + let (probe_actions, _, _, _, _, _) = build_bundle( + SHIELD_VALUE_BASE, + &instant_asset_lock_proof_fixture(None, None), + ); let num_actions = probe_actions.len(); let shielded_fee = compute_minimum_shielded_fee(num_actions, platform_version) .expect("should compute minimum shielded fee"); @@ -1408,19 +1451,21 @@ mod tests { % CREDITS_PER_DUFF; let shield_value = SHIELD_VALUE_BASE + correction; - let (actions, _flags, value_balance, anchor_bytes, proof_bytes, binding_sig) = - build_bundle(shield_value); - assert!(value_balance < 0); - let shield_amount = (-value_balance) as u64; - - let lock_credits = cap + shield_amount + pool_fee; + let lock_credits = cap + shield_value + pool_fee; assert_eq!(lock_credits % CREDITS_PER_DUFF, 0); let lock_amount_duffs = lock_credits / CREDITS_PER_DUFF; + // The bundle binds the asset lock that funds it, so the lock comes first. let mut rng = StdRng::seed_from_u64(567); let (asset_lock_proof, asset_lock_pk) = create_asset_lock_proof_with_key_and_amount(&mut rng, lock_amount_duffs); + let (actions, _flags, value_balance, anchor_bytes, proof_bytes, binding_sig) = + build_bundle(shield_value, &asset_lock_proof); + assert!(value_balance < 0); + let shield_amount = (-value_balance) as u64; + assert_eq!(shield_amount, shield_value); + let transition = create_signed_shield_from_asset_lock_transition_no_surplus( asset_lock_proof, &asset_lock_pk, @@ -1564,6 +1609,8 @@ mod tests { /// (Real proving — uses the cached `get_proving_key`, so each build is a few seconds.) fn build_valid_shield_bundle( shield_value: u64, + asset_lock_proof: &AssetLockProof, + platform_version: &PlatformVersion, ) -> ( Vec, u64, @@ -1599,8 +1646,11 @@ mod tests { let (unauthorized, _) = builder.build::(&mut orchard_rng).unwrap().unwrap(); let bundle_commitment: [u8; 32] = unauthorized.commitment().into(); - // No extra_sighash_data for shield_from_asset_lock (empty, like shield). - let sighash = compute_platform_sighash(&bundle_commitment, &[]); + let sighash = shield_from_asset_lock_sighash( + &bundle_commitment, + asset_lock_proof, + platform_version, + ); let proven = unauthorized.create_proof(pk, &mut orchard_rng).unwrap(); let bundle = proven.apply_signatures(orchard_rng, sighash, &[]).unwrap(); @@ -1626,14 +1676,15 @@ mod tests { } /// Pieces needed to drive a boundary case end-to-end: a valid bundle whose surplus lands - /// exactly on `target_surplus`, plus the asset-lock funding (in duffs) that achieves it. + /// exactly on `target_surplus`, plus the asset lock (and its key) funded to achieve it. struct BoundaryCase { actions: Vec, shield_amount: u64, anchor: [u8; 32], proof: Vec, binding_signature: [u8; 64], - lock_amount_duffs: u64, + asset_lock_proof: AssetLockProof, + asset_lock_pk: Vec, } /// Build a valid `ShieldFromAssetLock` bundle and matching asset-lock funding such that the @@ -1646,8 +1697,9 @@ mod tests { /// 1. Build a probe bundle to learn the (value-independent) action count → `pool_fee`. /// 2. Pick `shield_value ≡ −(target_surplus + pool_fee) (mod CREDITS_PER_DUFF)` so that /// `target_surplus + shield_value + pool_fee` is an exact number of duffs. - /// 3. Rebuild the bundle with that `shield_value` (the action count is independent of the - /// value, so `pool_fee` is unchanged) and derive the funding `amount_duffs`. + /// 3. Derive the funding `amount_duffs`, create the asset lock, and rebuild the bundle + /// with that `shield_value`, bound to that lock (the action count is independent of + /// the value, so `pool_fee` is unchanged). fn build_boundary_case( target_surplus: u64, platform_version: &PlatformVersion, @@ -1655,8 +1707,13 @@ mod tests { // A round base shield value, comfortably positive after the modular correction below. const SHIELD_VALUE_BASE: u64 = 1_000_000; - // Step 1: probe to learn the action count and thus the pool fee. - let (_, _, probe_num_actions, _, _, _) = build_valid_shield_bundle(SHIELD_VALUE_BASE); + // Step 1: probe to learn the action count and thus the pool fee. The probe is never + // submitted, so any lock will do for its binding. + let (_, _, probe_num_actions, _, _, _) = build_valid_shield_bundle( + SHIELD_VALUE_BASE, + &instant_asset_lock_proof_fixture(None, None), + platform_version, + ); let pool_fee = pool_fee_for_actions(probe_num_actions, platform_version); // Step 2: choose `shield_value` so the required lock value is a whole number of duffs. @@ -1665,21 +1722,25 @@ mod tests { % CREDITS_PER_DUFF; let shield_value = SHIELD_VALUE_BASE + correction; - // Step 3: build the real bundle and derive the funding. - let (actions, shield_amount, num_actions, anchor, proof, binding_signature) = - build_valid_shield_bundle(shield_value); - assert_eq!( - num_actions, probe_num_actions, - "action count must not depend on the shielded value" - ); - - let lock_credits = target_surplus + shield_amount + pool_fee; + // Step 3: derive the funding, create the lock the bundle binds, then build the real + // bundle. + let lock_credits = target_surplus + shield_value + pool_fee; assert_eq!( lock_credits % CREDITS_PER_DUFF, 0, "lock value must be a whole number of duffs" ); let lock_amount_duffs = lock_credits / CREDITS_PER_DUFF; + let mut rng = StdRng::seed_from_u64(567); + let (asset_lock_proof, asset_lock_pk) = + create_asset_lock_proof_with_key_and_amount(&mut rng, lock_amount_duffs); + + let (actions, shield_amount, num_actions, anchor, proof, binding_signature) = + build_valid_shield_bundle(shield_value, &asset_lock_proof, platform_version); + assert_eq!( + num_actions, probe_num_actions, + "action count must not depend on the shielded value" + ); // The surplus the transform will compute (`lock_credits − shield_amount − pool_fee`) // lands exactly on the target by construction. @@ -1695,7 +1756,8 @@ mod tests { anchor, proof, binding_signature, - lock_amount_duffs, + asset_lock_proof, + asset_lock_pk, } } @@ -1717,13 +1779,9 @@ mod tests { let cap = implicit_fee_cap(platform_version); let case = build_boundary_case(cap, platform_version); - let mut rng = StdRng::seed_from_u64(567); - let (asset_lock_proof, asset_lock_pk) = - create_asset_lock_proof_with_key_and_amount(&mut rng, case.lock_amount_duffs); - let transition = create_signed_shield_from_asset_lock_transition_no_surplus( - asset_lock_proof, - &asset_lock_pk, + case.asset_lock_proof, + &case.asset_lock_pk, case.actions, case.shield_amount, case.anchor, @@ -1749,13 +1807,9 @@ mod tests { let cap = implicit_fee_cap(platform_version); let case = build_boundary_case(cap + 1, platform_version); - let mut rng = StdRng::seed_from_u64(567); - let (asset_lock_proof, asset_lock_pk) = - create_asset_lock_proof_with_key_and_amount(&mut rng, case.lock_amount_duffs); - let transition = create_signed_shield_from_asset_lock_transition_no_surplus( - asset_lock_proof, - &asset_lock_pk, + case.asset_lock_proof, + &case.asset_lock_pk, case.actions, case.shield_amount, case.anchor, @@ -1832,9 +1886,12 @@ mod tests { shielded_fee.checked_add(albc).expect("pool fee overflow") } - /// Build a real, valid single-output Orchard shield bundle of `shield_value` credits. + /// Build a real, valid single-output Orchard shield bundle of `shield_value` credits, bound + /// to the asset lock that will fund it. fn build_valid_shield_bundle( shield_value: u64, + asset_lock_proof: &AssetLockProof, + platform_version: &PlatformVersion, ) -> (Vec, u64, [u8; 32], Vec, [u8; 64]) { let mut orchard_rng = OsRng; let pk = get_proving_key(); @@ -1861,7 +1918,11 @@ mod tests { let (unauthorized, _) = builder.build::(&mut orchard_rng).unwrap().unwrap(); let bundle_commitment: [u8; 32] = unauthorized.commitment().into(); - let sighash = compute_platform_sighash(&bundle_commitment, &[]); + let sighash = shield_from_asset_lock_sighash( + &bundle_commitment, + asset_lock_proof, + platform_version, + ); let proven = unauthorized.create_proof(pk, &mut orchard_rng).unwrap(); let bundle = proven.apply_signatures(orchard_rng, sighash, &[]).unwrap(); @@ -1912,7 +1973,7 @@ mod tests { let shield_value = 5_000u64; let (actions, shield_amount, anchor_bytes, proof_bytes, binding_sig) = - build_valid_shield_bundle(shield_value); + build_valid_shield_bundle(shield_value, &asset_lock_proof, platform_version); // Production fee + the consumed lock value, so we can pin the surplus exactly. let pool_fee = pool_fee_for_actions(actions.len(), platform_version); @@ -2025,30 +2086,36 @@ mod tests { // Probe to learn the action count → pool_fee, then choose `shield_value` so the required // lock value is a whole number of duffs (the fixture quantises funding to duffs). const SHIELD_VALUE_BASE: u64 = 1_000_000; - let (probe_actions, _, _, _, _) = build_valid_shield_bundle(SHIELD_VALUE_BASE); + // The probe is never submitted, so any lock will do for its binding. + let (probe_actions, _, _, _, _) = build_valid_shield_bundle( + SHIELD_VALUE_BASE, + &instant_asset_lock_proof_fixture(None, None), + platform_version, + ); let pool_fee = pool_fee_for_actions(probe_actions.len(), platform_version); let correction = (CREDITS_PER_DUFF - ((cap + SHIELD_VALUE_BASE + pool_fee) % CREDITS_PER_DUFF)) % CREDITS_PER_DUFF; let shield_value = SHIELD_VALUE_BASE + correction; - let (actions, shield_amount, anchor_bytes, proof_bytes, binding_sig) = - build_valid_shield_bundle(shield_value); - - let consumed = cap + shield_amount + pool_fee; + let consumed = cap + shield_value + pool_fee; assert_eq!( consumed % CREDITS_PER_DUFF, 0, "lock value must be a whole number of duffs" ); let lock_amount_duffs = consumed / CREDITS_PER_DUFF; - // The surplus the transform computes lands exactly on the cap by construction. - assert_eq!(consumed - shield_amount - pool_fee, cap); + // The bundle binds the asset lock that funds it, so the lock comes first. let mut rng = StdRng::seed_from_u64(567); let (asset_lock_proof, asset_lock_pk) = create_asset_lock_proof_with_key_and_amount(&mut rng, lock_amount_duffs); + let (actions, shield_amount, anchor_bytes, proof_bytes, binding_sig) = + build_valid_shield_bundle(shield_value, &asset_lock_proof, platform_version); + // The surplus the transform computes lands exactly on the cap by construction. + assert_eq!(consumed - shield_amount - pool_fee, cap); + let transition = create_signed_shield_from_asset_lock_transition_no_surplus( asset_lock_proof, &asset_lock_pk, @@ -2104,4 +2171,391 @@ mod tests { ); } } + + // ========================================== + // Bundle binding + // ========================================== + + /// The bundle's sighash binds its kind and the asset lock that funds it, from protocol + /// version 14 on, through `transform_into_action` v1. Every negative test here starts from a + /// transition the public builder made and shows it admitted by CheckTx and executed first, so + /// a refusal that follows is about where the bundle was moved, not about the bundle. + mod bundle_binding { + use super::*; + use crate::config::{PlatformConfig, PlatformTestConfig}; + use crate::execution::check_tx::CheckTxLevel; + use crate::execution::validation::state_transition::state_transitions::test_helpers::{ + check_tx_errors, setup_address_with_balance, test_orchard_recipient, TestAddressSigner, + TestOrchardProver, + }; + use crate::platform_types::platform::PlatformRef; + use crate::rpc::core::MockCoreRPCLike; + use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; + use dpp::address_funds::{AddressFundsFeeStrategyStep, PlatformAddress}; + use dpp::asset_lock::StoredAssetLockInfo; + use dpp::block::block_info::BlockInfo; + use dpp::fee::fee_result::FeeResult; + use dpp::platform_value::Bytes36; + use dpp::shielded::builder::{ + build_shield_from_asset_lock_transition, build_shield_transition, + }; + use dpp::state_transition::errors::StateTransitionError; + use dpp::state_transition::shield_transition::ShieldTransition; + use dpp::ProtocolError; + use std::collections::BTreeMap; + + const SHIELD_AMOUNT: u64 = 5_000; + + fn builder_made_shield_from_asset_lock( + asset_lock_proof: AssetLockProof, + asset_lock_key: &[u8], + platform_version: &PlatformVersion, + ) -> StateTransition { + build_shield_from_asset_lock_transition( + &test_orchard_recipient(), + SHIELD_AMOUNT, + asset_lock_proof, + asset_lock_key, + &TestOrchardProver, + [0u8; 36], + None, + // The fixture lock holds 1 Dash, far past the implicit fee cap, so the surplus + // needs somewhere to go. + Some(PlatformAddress::P2pkh([0x33; 20])), + 0, + platform_version, + ) + .expect("client-built shield from asset lock") + } + + fn assert_admitted_and_executed( + platform: &TempPlatform, + transition: &StateTransition, + platform_version: &PlatformVersion, + ) { + let admission = check_tx_errors(platform, transition); + assert!(admission.is_empty(), "CheckTx must admit it: {admission:?}"); + assert_matches!( + process_transition(platform, transition.clone(), platform_version) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + } + + /// Refused for its proof at admission, and in the block as a paid failure that burns + /// part of the lock. + fn assert_refused_for_its_proof( + platform: &TempPlatform, + transition: &StateTransition, + platform_version: &PlatformVersion, + ) { + let admission = check_tx_errors(platform, transition); + assert!( + admission.iter().any(|error| matches!( + error, + ConsensusError::StateError(StateError::InvalidShieldedProofError(_)) + )), + "CheckTx must refuse it for its proof: {admission:?}" + ); + assert_matches!( + process_transition(platform, transition.clone(), platform_version) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::InvalidShieldedProofError(_)), + .. + }] + ); + } + + /// Somebody funds a transition of their own with their own asset lock and wraps it around + /// a bundle proved for somebody else's lock. Without the lock in the sighash its proof + /// verifies and a second note with the same nullifier lands in the pool. + #[test] + fn should_refuse_a_bundle_resubmitted_under_another_asset_lock() { + let platform_version = PlatformVersion::latest(); + let platform = setup_platform(); + let mut rng = StdRng::seed_from_u64(40); + let (victim_lock, victim_key) = create_asset_lock_proof_with_key(&mut rng); + let (copier_lock, copier_key) = create_asset_lock_proof_with_key(&mut rng); + assert_ne!( + victim_lock.create_identifier().expect("identifier"), + copier_lock.create_identifier().expect("identifier"), + "the two locks must be different owners" + ); + + let shield = + builder_made_shield_from_asset_lock(victim_lock, &victim_key, platform_version); + // Positive control: the builder's bundle is what both CheckTx and the block accept. + assert_admitted_and_executed(&platform, &shield, platform_version); + + let StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V1(proven)) = + &shield + else { + panic!("expected a shield from asset lock transition"); + }; + let copy = create_signed_shield_from_asset_lock_transition( + copier_lock, + &copier_key, + proven.actions.clone(), + proven.value_balance, + proven.anchor, + proven.proof.clone(), + proven.binding_signature, + ); + + assert_refused_for_its_proof(&platform, ©, platform_version); + } + + /// A `Shield` bundle and a `ShieldFromAssetLock` bundle are indistinguishable to the proof: + /// same flags, same empty-tree anchor, same value balance. Moving one into the other kind + /// must fail on the kind and the owner it binds. + #[tokio::test] + async fn should_refuse_a_shield_bundle_resubmitted_as_a_shield_from_asset_lock() { + let platform_version = PlatformVersion::latest(); + let mut platform = setup_platform(); + + let mut signer = TestAddressSigner::new(); + let funding = signer.add_p2pkh([41u8; 32]); + setup_address_with_balance(&mut platform, funding, 0, dash_to_credits!(1.0)); + let mut inputs = BTreeMap::new(); + inputs.insert(funding, (1, SHIELD_AMOUNT + dash_to_credits!(0.01))); + let shield = build_shield_transition( + &test_orchard_recipient(), + SHIELD_AMOUNT, + inputs, + vec![AddressFundsFeeStrategyStep::DeductFromInput(0)], + &signer, + 0, + &TestOrchardProver, + [0u8; 36], + None, + platform_version, + ) + .await + .expect("client-built shield"); + // Positive control: the bundle is valid for the kind it was proved for. + assert!(check_tx_errors(&platform, &shield).is_empty()); + assert_matches!( + process_transition(&platform, shield.clone(), platform_version) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + let StateTransition::Shield(ShieldTransition::V0(proven)) = &shield else { + panic!("expected a shield transition"); + }; + let mut rng = StdRng::seed_from_u64(42); + let (copier_lock, copier_key) = create_asset_lock_proof_with_key(&mut rng); + let copy = create_signed_shield_from_asset_lock_transition( + copier_lock, + &copier_key, + proven.actions.clone(), + proven.amount, + proven.anchor, + proven.proof.clone(), + proven.binding_signature, + ); + + assert_refused_for_its_proof(&platform, ©, platform_version); + } + + fn platform_at(protocol_version: u32) -> TempPlatform { + TestPlatformBuilder::new() + .with_config(PlatformConfig { + testing_configs: PlatformTestConfig { + disable_instant_lock_signature_verification: true, + ..Default::default() + }, + ..Default::default() + }) + .with_initial_protocol_version(protocol_version) + .build_with_mock_rpc() + .set_genesis_state() + } + + /// Protocol versions 12 and 13 shipped `ShieldFromAssetLock` with a transform that binds + /// nothing, and they still select it through validation versions V8 and V9: the binding + /// lives in `transform_into_action` v1, which only V10, protocol version 14, selects. So + /// at 12 and 13 a client building for that version gets an unbound bundle that is + /// accepted, and a bundle bound the way 14 binds it is refused, exactly as those versions + /// always have. + #[test] + fn should_keep_the_unbound_bundle_at_protocol_versions_12_and_13() { + for protocol_version in [12u32, 13] { + let platform_version = + PlatformVersion::get(protocol_version).expect("known protocol version"); + let platform = platform_at(protocol_version); + let mut rng = StdRng::seed_from_u64(43 + protocol_version as u64); + + let (lock, key) = create_asset_lock_proof_with_key(&mut rng); + let shield = builder_made_shield_from_asset_lock(lock, &key, platform_version); + assert_admitted_and_executed(&platform, &shield, platform_version); + + // 12 and 13 admit only transition version 0, so the bundle the latest builder + // proves is re-signed as one: what is left to show is that their transform + // checks it against an empty preimage and refuses it for its proof. + let (lock, key) = create_asset_lock_proof_with_key(&mut rng); + let bound = as_version_0( + &builder_made_shield_from_asset_lock(lock, &key, PlatformVersion::latest()), + &key, + ); + assert_refused_for_its_proof(&platform, &bound, platform_version); + } + } + + /// Re-signs a version 1 transition's fields, bundle included, as transition version 0. + fn as_version_0(transition: &StateTransition, asset_lock_key: &[u8]) -> StateTransition { + let StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V1(v1)) = + transition + else { + panic!("expected a version 1 shield from asset lock transition"); + }; + let mut v0 = ShieldFromAssetLockTransitionV0 { + asset_lock_proof: v1.asset_lock_proof.clone(), + actions: v1.actions.clone(), + value_balance: v1.value_balance, + anchor: v1.anchor, + proof: v1.proof.clone(), + binding_signature: v1.binding_signature, + surplus_output: v1.surplus_output, + signature: Default::default(), + }; + let unsigned: StateTransition = v0.clone().into(); + let signable_bytes = unsigned.signable_bytes().expect("signable bytes"); + let signature = + dpp::dashcore::signer::sign(&signable_bytes, asset_lock_key).expect("sign"); + v0.signature = BinaryData::new(signature.to_vec()); + v0.into() + } + + /// A version 0 transition built for protocol version 13 (so its bundle binds nothing) + /// and still waiting when 14 activates must be refused at 14 on its version byte: before + /// any proof work, uncharged, with its asset lock left unspent. Verifying its unbound + /// bundle against the bound preimage instead would burn the proof-failure penalty from + /// an honest lock. The sender then resubmits as version 1 from the same lock and loses + /// nothing. + #[test] + fn should_refuse_a_version_0_at_protocol_version_14_without_touching_its_asset_lock() { + let platform_version = PlatformVersion::latest(); + assert_eq!(platform_version.protocol_version, 14); + let platform = setup_platform(); + let mut rng = StdRng::seed_from_u64(44); + let (lock, key) = create_asset_lock_proof_with_key(&mut rng); + let outpoint = Bytes36::new(lock.out_point().expect("outpoint").into()); + + let waiting = builder_made_shield_from_asset_lock( + lock.clone(), + &key, + PlatformVersion::get(13).expect("protocol version 13"), + ); + assert!(matches!( + waiting, + StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V0(_)) + )); + let raw = waiting.serialize_to_bytes().expect("serialize"); + + // A block carrying it: refused, charged nothing, the lock left unspent. + let state = platform.state.load(); + let transaction = platform.drive.grove.start_transaction(); + let pool_before = platform + .drive + .read_shielded_pool_total_balance(Some(&transaction), &mut vec![], platform_version) + .expect("pool balance"); + let in_block = platform + .platform + .process_raw_state_transitions( + &vec![raw.clone()], + &state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("processing must not be a block-level error"); + let penalties = in_block.invalid_paid_count(); + let fees = in_block.aggregated_fees().clone(); + let lock_info = platform + .drive + .fetch_asset_lock_outpoint_info( + &outpoint, + Some(&transaction), + &platform_version.drive, + ) + .expect("asset lock info"); + let pool_after = platform + .drive + .read_shielded_pool_total_balance(Some(&transaction), &mut vec![], platform_version) + .expect("pool balance"); + let observed = format!( + "penalties charged {penalties}, fees {fees:?}, asset lock {lock_info:?}, results \ + {:?}", + in_block.execution_results() + ); + assert_eq!(penalties, 0, "no penalty may be charged: {observed}"); + assert_eq!( + lock_info, + StoredAssetLockInfo::NotPresent, + "the asset lock must be left unspent: {observed}" + ); + assert_eq!( + fees, + FeeResult::default(), + "nothing may be paid for it: {observed}" + ); + assert_eq!(pool_after, pool_before, "nothing may enter the pool"); + assert_matches!( + in_block.execution_results().as_slice(), + [StateTransitionExecutionResult::InternalError(_)], + "a version 0 is dropped from the block, not executed" + ); + + // The same lock still funds the resubmission as version 1, in the same block. + let resubmitted = builder_made_shield_from_asset_lock(lock, &key, platform_version); + let in_block = platform + .platform + .process_raw_state_transitions( + &vec![resubmitted.serialize_to_bytes().expect("serialize")], + &state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("processing must not be a block-level error"); + assert_matches!( + in_block.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }], + "the lock the refused version 0 named must still be spendable" + ); + + // Admission refuses it on the version byte while decoding, before any validation: + let platform_ref = PlatformRef { + drive: &platform.drive, + state: &state, + config: &platform.config, + core_rpc: &platform.core_rpc, + }; + // Both the first check and the recheck that evicts a version 0 admitted at 13 from + // the mempool once 14 is active. + for level in [CheckTxLevel::FirstTimeCheck, CheckTxLevel::Recheck] { + let admission = platform.check_tx(&raw, level, &platform_ref, platform_version); + assert!( + matches!( + &admission, + Err(crate::error::Error::Protocol( + ProtocolError::StateTransitionError( + StateTransitionError::StateTransitionIsNotActiveError { .. } + ) + )) + ), + "CheckTx ({level:?}) must refuse version 0 as not active, got {admission:?}" + ); + } + } + } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/transform_into_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/transform_into_action/mod.rs index e88e88c052f..0f74e3c39fc 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/transform_into_action/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/transform_into_action/mod.rs @@ -1 +1,2 @@ pub(in crate::execution::validation::state_transition::state_transitions::shield_from_asset_lock) mod v0; +pub(in crate::execution::validation::state_transition::state_transitions::shield_from_asset_lock) mod v1; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/transform_into_action/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/transform_into_action/v0/mod.rs index 9a4661bb1df..5caff640825 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/transform_into_action/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/transform_into_action/v0/mod.rs @@ -68,6 +68,12 @@ impl ShieldFromAssetLockStateTransitionTransformIntoActionValidationV0 // Step 1: Get the shield amount (value_balance is u64, the amount entering the pool) let shield_amount: Credits = match self { ShieldFromAssetLockTransition::V0(v0) => v0.value_balance, + // Protocol versions 12 and 13 are the only ones that select this generation, and a + // version 1 transition cannot exist there: `StateTransition::active_version_range` + // gives it `14..=LATEST_VERSION`, refused while decoding by the one funnel both block + // processing and CheckTx go through. The version 1 arms here and below exist only + // because the enum has the variant; none of them is reachable at 12 or 13. + ShieldFromAssetLockTransition::V1(v1) => v1.value_balance, }; // Step 3: Calculate minimum required fee from platform_version. @@ -87,6 +93,7 @@ impl ShieldFromAssetLockStateTransitionTransformIntoActionValidationV0 let albc = required_balance; let num_actions = match self { ShieldFromAssetLockTransition::V0(v0) => v0.actions.len(), + ShieldFromAssetLockTransition::V1(v1) => v1.actions.len(), }; let shielded_fee = compute_minimum_shielded_fee(num_actions, platform_version)?; let pool_fee = @@ -260,6 +267,7 @@ impl ShieldFromAssetLockStateTransitionTransformIntoActionValidationV0 let surplus_output = match self { ShieldFromAssetLockTransition::V0(v0) => &v0.surplus_output, + ShieldFromAssetLockTransition::V1(v1) => &v1.surplus_output, }; // When no surplus_output is set, the surplus is donated to the fee pools — but only up to @@ -307,6 +315,17 @@ impl ShieldFromAssetLockStateTransitionTransformIntoActionValidationV0 v0.proof.as_slice(), &v0.binding_signature, ), + // This verifies a version 1 body against the empty preimage, which at protocol + // version 14 would be exactly the unbound check version 1 was introduced to close. + // It is unreachable: 14 selects transform_into_action 1, and 12 and 13 cannot decode + // a version 1 at all. Do not treat this arm as the verification path for version 1 — + // that is transform_into_action/v1. + ShieldFromAssetLockTransition::V1(v1) => ( + &v1.actions, + &v1.anchor, + v1.proof.as_slice(), + &v1.binding_signature, + ), }; if let Err(e) = reconstruct_and_verify_bundle( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/transform_into_action/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/transform_into_action/v1/mod.rs new file mode 100644 index 00000000000..f7604cbd1c7 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/transform_into_action/v1/mod.rs @@ -0,0 +1,418 @@ +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::execution_operation::signature_verification_operation::SignatureVerificationOperation; +use crate::execution::types::execution_operation::{ValidationOperation, SHA256_BLOCK_SIZE}; +use crate::execution::types::state_transition_execution_context::{ + StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0, +}; +use crate::execution::validation::state_transition::common::asset_lock::proof::validate::AssetLockProofValidation; +use crate::execution::validation::state_transition::common::asset_lock::transaction::fetch_asset_lock_transaction_output_sync::fetch_asset_lock_transaction_output_sync; +use crate::execution::validation::state_transition::state_transitions::shielded_common::{ + read_pool_total_balance, reconstruct_and_verify_bundle, FLAGS_OUTPUTS_ONLY, +}; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformRef; +use crate::platform_types::check_tx_proof_verifier::CheckTxProofVerifier; +use crate::platform_types::platform_state::PlatformStateV0Methods; +use crate::rpc::core::CoreRPCLike; +use dpp::asset_lock::reduced_asset_lock_value::{AssetLockValue, AssetLockValueGettersV0}; +use dpp::balances::credits::CREDITS_PER_DUFF; +use dpp::consensus::basic::identity::IdentityAssetLockTransactionOutPointNotEnoughBalanceError; +use dpp::consensus::basic::state_transition::ShieldedImplicitFeeCapExceededError; +use dpp::consensus::signature::{BasicECDSAError, SignatureError}; +use dpp::consensus::state::state_error::StateError; +use dpp::dashcore::hashes::Hash; +use dpp::dashcore::{signer, ScriptBuf, Txid}; +use dpp::fee::Credits; +use dpp::shielded::{compute_minimum_shielded_fee, shield_from_asset_lock_extra_sighash_data}; +use dpp::identity::state_transition::AssetLockProved; +use dpp::identity::KeyType; +use dpp::platform_value::{Bytes32, Bytes36}; +use dpp::prelude::ConsensusValidationResult; +use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; +use dpp::state_transition::signable_bytes_hasher::SignableBytesHasher; +use dpp::state_transition::{StateTransitionEstimatedFeeValidation, StateTransitionSingleSigned}; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::shielded::shield_from_asset_lock::ShieldFromAssetLockTransitionAction; +use drive::state_transition_action::system::partially_use_asset_lock_action::PartiallyUseAssetLockActionV0; +use drive::state_transition_action::system::partially_use_asset_lock_action::PartiallyUseAssetLockAction; +use drive::state_transition_action::StateTransitionAction; + +pub(in crate::execution::validation::state_transition::state_transitions::shield_from_asset_lock) trait ShieldFromAssetLockStateTransitionTransformIntoActionValidationV1 +{ + fn transform_into_action_v1( + &self, + platform: &PlatformRef, + signable_bytes: Vec, + validation_mode: ValidationMode, + execution_context: &mut StateTransitionExecutionContext, + check_tx_proof_verifier: Option<&CheckTxProofVerifier>, + tx: TransactionArg, + ) -> Result, Error>; +} + +impl ShieldFromAssetLockStateTransitionTransformIntoActionValidationV1 + for ShieldFromAssetLockTransition +{ + fn transform_into_action_v1( + &self, + platform: &PlatformRef, + signable_bytes: Vec, + validation_mode: ValidationMode, + execution_context: &mut StateTransitionExecutionContext, + check_tx_proof_verifier: Option<&CheckTxProofVerifier>, + tx: TransactionArg, + ) -> Result, Error> { + let platform_version = platform.state.current_platform_version()?; + + // Only transition version 1 reaches this generation: protocol version 14 refuses version 0 + // when it decodes the transition (`StateTransition::active_version_range`), uncharged and + // with its asset lock unspent. The version 0 arms below exist because the enum has them. + + // Step 1: Get the shield amount (value_balance is u64, the amount entering the pool) + let shield_amount: Credits = match self { + ShieldFromAssetLockTransition::V0(v0) => v0.value_balance, + ShieldFromAssetLockTransition::V1(v1) => v1.value_balance, + }; + + // Step 3: Calculate minimum required fee from platform_version. + // `required_balance` (= asset_lock_base_cost, `albc`) is the L1 asset-lock-proof viability + // floor, reused unchanged in Step 4's `validate(...)` call below. + let required_balance = self.calculate_min_required_fee(platform_version)?; + + // Step 3b: Compute the flat fee charged to the fee pools. ShieldFromAssetLock does strictly + // more work than a transparent shield, so it pays BOTH the shielded operation cost and the + // L1 asset-lock processing cost, mirroring the established asset-lock fee composition + // (operation base cost + asset_lock_base_cost): + // + // pool_fee = compute_minimum_shielded_fee(num_actions) [Halo2 proof + per-action] + // + albc [asset-lock processing] + // + // Set outside GroveDB; booked at the execution-event layer (PaidFromAssetLockToPool). + let albc = required_balance; + let num_actions = match self { + ShieldFromAssetLockTransition::V0(v0) => v0.actions.len(), + ShieldFromAssetLockTransition::V1(v1) => v1.actions.len(), + }; + let shielded_fee = compute_minimum_shielded_fee(num_actions, platform_version)?; + let pool_fee = + shielded_fee + .checked_add(albc) + .ok_or(Error::Execution(ExecutionError::Overflow( + "shielded fee + asset_lock_base_cost overflow in shield_from_asset_lock", + )))?; + // The asset lock must cover `shield_amount + pool_fee`, so the surplus is always >= 0. + let required_lock_value = shield_amount.checked_add(pool_fee).ok_or(Error::Execution( + ExecutionError::Overflow("shield_amount + pool_fee overflow in shield_from_asset_lock"), + ))?; + + let signable_bytes_len = signable_bytes.len(); + + let mut signable_bytes_hasher = SignableBytesHasher::Bytes(signable_bytes); + + // Step 4: Validate asset lock proof + let asset_lock_proof_validation = if validation_mode != ValidationMode::NoValidation { + AssetLockProved::asset_lock_proof(self).validate( + platform, + &mut signable_bytes_hasher, + required_balance, + validation_mode, + tx, + platform_version, + )? + } else { + ConsensusValidationResult::new() + }; + + if !asset_lock_proof_validation.is_valid() { + return Ok(ConsensusValidationResult::new_with_errors( + asset_lock_proof_validation.errors, + )); + } + + // Step 5: Fetch/validate asset lock transaction output + let mut needs_signature_verification = true; + + let asset_lock_value_to_be_consumed = if asset_lock_proof_validation.has_data() { + let asset_lock_value = asset_lock_proof_validation.into_data()?; + // There is no need to recheck signatures on recheck tx + if validation_mode == ValidationMode::RecheckTx { + needs_signature_verification = false; + } + asset_lock_value + } else { + let tx_out_validation = fetch_asset_lock_transaction_output_sync( + platform.core_rpc, + AssetLockProved::asset_lock_proof(self), + platform_version, + )?; + + if !tx_out_validation.is_valid() { + return Ok(ConsensusValidationResult::new_with_errors( + tx_out_validation.errors, + )); + } + + let tx_out = tx_out_validation.into_data()?; + + let tx_out_credit_value = tx_out.value.saturating_mul(CREDITS_PER_DUFF); + + // Verify locked amount >= shield_amount + pool_fee (so the surplus is >= 0). This is an + // early reject; Step 7 re-applies the same floor authoritatively on every path. + let required_total = required_lock_value; + if tx_out_credit_value < required_total { + let asset_lock_proof = AssetLockProved::asset_lock_proof(self); + return Ok(ConsensusValidationResult::new_with_error( + IdentityAssetLockTransactionOutPointNotEnoughBalanceError::new( + asset_lock_proof + .out_point() + .map(|outpoint| outpoint.txid) + .unwrap_or(Txid::all_zeros()), + asset_lock_proof.output_index() as usize, + tx_out_credit_value, + tx_out_credit_value, + required_total, + ) + .into(), + )); + } + + if validation_mode == ValidationMode::RecheckTx { + needs_signature_verification = false; + } + + let initial_balance_amount = tx_out.value.saturating_mul(CREDITS_PER_DUFF); + AssetLockValue::new( + initial_balance_amount, + tx_out.script_pubkey.0, + initial_balance_amount, + vec![], + platform_version, + )? + }; + + // Step 6: Verify ECDSA signature over signable_bytes (P2PKH from asset lock output) + if needs_signature_verification { + let tx_out_script_pubkey = + ScriptBuf(asset_lock_value_to_be_consumed.tx_out_script().clone()); + + let public_key_hash = tx_out_script_pubkey + .p2pkh_public_key_hash_bytes() + .ok_or_else(|| { + Error::Execution(ExecutionError::CorruptedCachedState( + "the script inside the state must be a p2pkh".to_string(), + )) + })?; + + let block_count = + (signable_bytes_len / SHA256_BLOCK_SIZE as usize).min(u16::MAX as usize) as u16; + + execution_context.add_operation(ValidationOperation::DoubleSha256(block_count)); + execution_context.add_operation(ValidationOperation::SignatureVerification( + SignatureVerificationOperation::new(KeyType::ECDSA_HASH160), + )); + + if let Err(e) = signer::verify_hash_signature( + signable_bytes_hasher.hash_bytes().as_slice(), + self.signature().as_slice(), + public_key_hash, + ) { + return Ok(ConsensusValidationResult::new_with_error( + SignatureError::BasicECDSAError(BasicECDSAError::new(e.to_string())).into(), + )); + } + } + + // Step 7: The remaining asset-lock balance must cover `shield_amount + pool_fee`. + // This is the AUTHORITATIVE funding floor: the fresh-fetch, cached `AssetLockValue`, and + // recheck paths all converge here, so a lock that under-funds the fee can never reach the + // action (which would mint credits at booking). It also guarantees `surplus >= 0` below. + let remaining_credit_value = asset_lock_value_to_be_consumed.remaining_credit_value(); + if remaining_credit_value < required_lock_value { + let asset_lock_proof = AssetLockProved::asset_lock_proof(self); + return Ok(ConsensusValidationResult::new_with_error( + IdentityAssetLockTransactionOutPointNotEnoughBalanceError::new( + asset_lock_proof + .out_point() + .map(|outpoint| outpoint.txid) + .unwrap_or(Txid::all_zeros()), + asset_lock_proof.output_index() as usize, + remaining_credit_value, + remaining_credit_value, + required_lock_value, + ) + .into(), + )); + } + + // Step 7b: Compute the surplus and reject an over-cap implicit donation BEFORE the expensive + // Orchard proof verification (Step 9). The surplus is fully derivable here — it depends only + // on the (now-validated) `remaining_credit_value`, the `shield_amount`, the flat `pool_fee`, + // and whether a `surplus_output` is set — none of which depend on the ZK proof or the pool + // read. Rejecting an over-cap transition here avoids ~100ms of Halo2 verification for a + // transition that can never succeed. + // + // Distribute the fully-consumed lock: + // shield_amount -> shielded pool + // pool_fee -> fee pools + // surplus -> surplus_output address (when set), else folded into the fee pools. + // `surplus >= 0` is guaranteed by the Step 7 floor; `checked_sub` is a defensive guard. + let surplus = remaining_credit_value + .checked_sub(shield_amount) + .and_then(|v| v.checked_sub(pool_fee)) + .ok_or(Error::Execution(ExecutionError::Overflow( + "asset lock value underflow computing shield_from_asset_lock surplus (should be guarded by the Step 7 funding floor)", + )))?; + + let surplus_output = match self { + ShieldFromAssetLockTransition::V0(v0) => &v0.surplus_output, + ShieldFromAssetLockTransition::V1(v1) => &v1.surplus_output, + }; + + // When no surplus_output is set, the surplus is donated to the fee pools — but only up to + // `shielded_implicit_fee_cap`, so a client cannot accidentally forfeit a large remainder. + if surplus_output.is_none() { + let implicit_fee_cap = platform_version + .drive_abci + .validation_and_processing + .event_constants + .shielded_implicit_fee_cap; + if surplus > implicit_fee_cap { + return Ok(ConsensusValidationResult::new_with_error( + ShieldedImplicitFeeCapExceededError::new(surplus, implicit_fee_cap).into(), + )); + } + } + + // Step 8: Read current shielded pool total balance from GroveDB. + // + // ShieldFromAssetLock pays the flat `pool_fee` (computed in Step 3b) at the execution-event + // layer (PaidFromAssetLockToPool). We do NOT derive a GroveDB fee from these read operations + // — the flat fee subsumes them. + let mut drive_operations = vec![]; + let current_total_balance = + read_pool_total_balance(platform.drive, tx, &mut drive_operations, platform_version)?; + + // CheckTx admits the expensive proof only after the asset lock, its + // signature, funding, fee cap, and current pool state have all passed. + // Proposal and block processing pass `None` and retain the existing + // penalty action when proof verification fails. + let _check_tx_permit = match check_tx_proof_verifier { + Some(verifier) => Some(verifier.try_acquire(num_actions).ok_or(Error::Execution( + ExecutionError::CheckTxProofVerificationBusy, + ))?), + None => None, + }; + + // Step 9: Verify Orchard ZK proof via reconstruct_and_verify_bundle() + // The asset lock signature authenticates this transition, but the bundle itself carries + // no anchor and says nothing about who proved it, so anybody could re-wrap the proved + // bytes around an asset lock of their own. The sighash therefore binds the kind and the + // funding asset lock, whose outpoint structure validation has already established. + let extra_sighash_data = shield_from_asset_lock_extra_sighash_data( + AssetLockProved::asset_lock_proof(self), + platform_version, + )?; + let (actions, anchor, proof, binding_signature) = match self { + ShieldFromAssetLockTransition::V0(v0) => ( + &v0.actions, + &v0.anchor, + v0.proof.as_slice(), + &v0.binding_signature, + ), + ShieldFromAssetLockTransition::V1(v1) => ( + &v1.actions, + &v1.anchor, + v1.proof.as_slice(), + &v1.binding_signature, + ), + }; + + if let Err(e) = reconstruct_and_verify_bundle( + actions, + FLAGS_OUTPUTS_ONLY, + -(shield_amount as i64), + anchor, + proof, + binding_signature, + &extra_sighash_data, + ) { + // Step 10: ZK proof failed -- consume asset lock with penalty (PartiallyUseAssetLockAction) + let penalty = platform_version + .drive_abci + .validation_and_processing + .penalties + .shielded_proof_verification_failure; + + let desired_used_credits = penalty + .checked_add(execution_context.fee_cost(platform_version)?.processing_fee) + .ok_or(Error::Execution(ExecutionError::Overflow( + "processing fee overflow in shield_from_asset_lock penalty calculation", + )))?; + + let asset_lock_outpoint = AssetLockProved::asset_lock_proof(self) + .out_point() + .ok_or_else(|| { + Error::Execution(ExecutionError::CorruptedCachedState( + "asset lock proof must have an outpoint after validation".to_string(), + )) + })?; + + let signable_bytes_hash: Bytes32 = signable_bytes_hasher.into_hashed_bytes(); + let mut previous_transaction_hashes = + asset_lock_value_to_be_consumed.used_tags_ref().clone(); + previous_transaction_hashes.push(signable_bytes_hash); + + let remaining_after_penalty = + remaining_credit_value.saturating_sub(desired_used_credits); + let used_credits = std::cmp::min(remaining_credit_value, desired_used_credits); + + let partially_use_action = + PartiallyUseAssetLockAction::from(PartiallyUseAssetLockActionV0 { + asset_lock_outpoint: Bytes36::new(asset_lock_outpoint.into()), + initial_credit_value: asset_lock_value_to_be_consumed.initial_credit_value(), + previous_transaction_hashes, + asset_lock_script: asset_lock_value_to_be_consumed.tx_out_script().clone(), + remaining_credit_value: remaining_after_penalty, + used_credits, + user_fee_increase: 0, + inputs_with_remaining_balance: None, + fee_strategy: None, + }); + + return Ok(ConsensusValidationResult::new_with_data_and_errors( + StateTransitionAction::PartiallyUseAssetLockAction(partially_use_action), + vec![StateError::InvalidShieldedProofError(e).into()], + )); + } + + // Step 11: Build the successful action + let asset_lock_outpoint = AssetLockProved::asset_lock_proof(self) + .out_point() + .ok_or_else(|| { + Error::Execution(ExecutionError::CorruptedCachedState( + "asset lock proof must have an outpoint after validation".to_string(), + )) + })?; + + let asset_lock_value_credits = asset_lock_value_to_be_consumed.remaining_credit_value(); + let signable_bytes_hash: [u8; 32] = signable_bytes_hasher.into_hashed_bytes().0; + + // The action routes `surplus_amount` to `surplus_output` (when set); otherwise 0 and the + // surplus folds into the fee pools at the execution event. The surplus and the implicit-fee + // cap were already computed and enforced in Step 7b (before proof verification). + let surplus_amount = if surplus_output.is_some() { surplus } else { 0 }; + + let result = ShieldFromAssetLockTransitionAction::try_from_transition( + self, + asset_lock_outpoint.into(), + asset_lock_value_credits, + signable_bytes_hash, + shield_amount, + current_total_balance, + surplus_amount, + ); + + Ok(result.map(|action| action.into())) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/tests.rs index 68c0a7bbcd5..45bad7c65ff 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/tests.rs @@ -26,7 +26,7 @@ mod tests { use dpp::platform_value::BinaryData; use dpp::prelude::IdentityNonce; use dpp::serialization::PlatformSerializable; - use dpp::shielded::SerializedAction; + use dpp::shielded::{shield_from_identity_extra_sighash_data, SerializedAction}; use dpp::state_transition::proof_result::StateTransitionProofResult; use dpp::state_transition::shield_from_identity_transition::methods::ShieldFromIdentityTransitionMethodsV0; use dpp::state_transition::shield_from_identity_transition::v0::ShieldFromIdentityTransitionV0; @@ -125,8 +125,12 @@ mod tests { } /// Builds and proves a real outputs-only Orchard bundle paying `shield_value` - /// to a fresh recipient. Identical to the `Shield` fixture: no extra sighash data. - fn build_valid_bundle(shield_value: u64) -> ProvenBundle { + /// to a fresh recipient, bound to the identity that will fund it. + fn build_valid_bundle( + shield_value: u64, + identity: &Identity, + platform_version: &PlatformVersion, + ) -> ProvenBundle { let mut rng = OsRng; let pk = get_proving_key(); @@ -152,7 +156,10 @@ mod tests { let (unauthorized, _) = builder.build::(&mut rng).unwrap().unwrap(); let bundle_commitment: [u8; 32] = unauthorized.commitment().into(); - let sighash = compute_platform_sighash(&bundle_commitment, &[]); + let extra_sighash_data = + shield_from_identity_extra_sighash_data(&identity.id().to_buffer(), platform_version) + .expect("shield from identity sighash data"); + let sighash = compute_platform_sighash(&bundle_commitment, &extra_sighash_data); let proven = unauthorized.create_proof(pk, &mut rng).unwrap(); let bundle = proven.apply_signatures(rng, sighash, &[]).unwrap(); @@ -733,7 +740,7 @@ mod tests { ); add_identity_to_drive(&mut platform, &identity); - let bundle = build_valid_bundle(5_000); + let bundle = build_valid_bundle(5_000, &identity, platform_version); let mutated_amount = bundle.amount + 1; // The identity signature is over the mutated amount, so it verifies; the Orchard // binding signature commits to the real value balance and must reject it. @@ -867,7 +874,7 @@ mod tests { "fixture must start balanced" ); - let bundle = build_valid_bundle(5_000); + let bundle = build_valid_bundle(5_000, &identity, platform_version); let shield_amount = bundle.amount; let num_actions = bundle.actions.len(); let st = create_signed_transition( @@ -1005,7 +1012,7 @@ mod tests { platform_version, ); add_identity_to_drive(&mut platform, &identity); - let bundle = build_valid_bundle(5_000); + let bundle = build_valid_bundle(5_000, &identity, platform_version); let amount = bundle.amount; let st = create_signed_transition( &identity, @@ -1044,7 +1051,7 @@ mod tests { ); add_identity_to_drive(&mut platform, &identity); - let bundle = build_valid_bundle(5_000); + let bundle = build_valid_bundle(5_000, &identity, platform_version); let amount = bundle.amount; let st = create_signed_transition(&identity, &signer, bundle, amount, 1, 0, platform_version) @@ -1102,4 +1109,270 @@ mod tests { assert_eq!(proven_balance, identity_balance(&platform, &identity)); assert!(proven_balance < initial_balance - amount); } + + // ========================================== + // Bundle binding + // ========================================== + + /// The bundle's sighash binds its kind and the identity that funds it. That bundle is + /// verified in two places — CheckTx's admission check (`validate_shielded_proof`) and block + /// processing's transform — and a disagreement between them rejects every honest shield at + /// admission while block-level tests stay green. Every test here therefore puts its + /// transitions through both, and starts from a transition the public builder made. + mod bundle_binding { + use super::*; + use crate::execution::validation::state_transition::processor::traits::shielded_proof::StateTransitionShieldedProofValidationV0; + use crate::execution::validation::state_transition::state_transitions::test_helpers::{ + check_tx_errors, test_orchard_recipient, TestOrchardProver, + }; + use dpp::address_funds::PlatformAddress; + use dpp::dashcore::{Network, PrivateKey}; + use dpp::shielded::builder::{ + build_shield_from_asset_lock_transition, build_shield_from_identity_transition, + }; + use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; + use dpp::tests::fixtures::instant_asset_lock_proof_fixture; + + const SHIELD_AMOUNT: u64 = 5_000; + + fn funded_identity( + platform: &mut TempPlatform, + id: [u8; 32], + seed: u64, + platform_version: &PlatformVersion, + ) -> (Identity, SimpleSigner) { + let mut rng = StdRng::seed_from_u64(seed); + let (identity, signer) = create_identity_with_transfer_key( + id, + dash_to_credits!(1.0), + &mut rng, + platform_version, + ); + add_identity_to_drive(platform, &identity); + (identity, signer) + } + + async fn builder_made_shield( + identity: &Identity, + signer: &SimpleSigner, + platform_version: &PlatformVersion, + ) -> StateTransition { + build_shield_from_identity_transition( + identity, + &test_orchard_recipient(), + SHIELD_AMOUNT, + 1, + signer, + None, + 0, + &TestOrchardProver, + [0u8; 36], + None, + platform_version, + ) + .await + .expect("client-built shield from identity") + } + + fn lift(transition: &StateTransition) -> ProvenBundle { + let StateTransition::ShieldFromIdentity(ShieldFromIdentityTransition::V0(v0)) = + transition + else { + panic!("expected a shield from identity transition"); + }; + ProvenBundle { + actions: v0.actions.clone(), + amount: v0.amount, + anchor: v0.anchor, + proof: v0.proof.clone(), + binding_signature: v0.binding_signature, + } + } + + /// CheckTx's proof check, the whole admission path and block execution all accept it. + fn assert_accepted_at_admission_and_in_the_block( + platform: &TempPlatform, + transition: &StateTransition, + platform_version: &PlatformVersion, + ) { + let proof_check = transition + .validate_shielded_proof(platform_version) + .expect("proof check must not error"); + assert!( + proof_check.is_valid(), + "CheckTx's proof check must accept it: {:?}", + proof_check.errors + ); + let admission = check_tx_errors(platform, transition); + assert!(admission.is_empty(), "CheckTx must admit it: {admission:?}"); + assert_matches!( + process_transition(platform, transition.clone(), platform_version) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + } + + /// CheckTx's proof check, the whole admission path and block execution all refuse it + /// for its proof. + fn assert_refused_for_its_proof_at_admission_and_in_the_block( + platform: &TempPlatform, + transition: &StateTransition, + platform_version: &PlatformVersion, + ) { + let proof_check = transition + .validate_shielded_proof(platform_version) + .expect("proof check must not error"); + assert_matches!( + proof_check.errors.as_slice(), + [ConsensusError::StateError( + StateError::InvalidShieldedProofError(_) + )], + "CheckTx's proof check must refuse it" + ); + let admission = check_tx_errors(platform, transition); + assert_matches!( + admission.as_slice(), + [ConsensusError::StateError( + StateError::InvalidShieldedProofError(_) + )], + "CheckTx must refuse it" + ); + assert_matches!( + process_transition(platform, transition.clone(), platform_version) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::InvalidShieldedProofError(_)), + .. + }] + ); + } + + /// The bundle a client ships must be the one both verification sites accept. The builder + /// binds the preimage from `dpp`, and each site rebuilds it on its own; this is the test + /// that fails if either site rebuilds something else. + #[tokio::test] + async fn should_accept_a_builder_made_bundle_at_admission_and_in_the_block() { + let platform_version = PlatformVersion::latest(); + let mut platform = setup_platform(); + let (identity, signer) = + funded_identity(&mut platform, [30u8; 32], 30, platform_version); + + let shield = builder_made_shield(&identity, &signer, platform_version).await; + + assert_accepted_at_admission_and_in_the_block(&platform, &shield, platform_version); + } + + /// Another identity signs a transition of its own around a bundle proved for somebody + /// else: without the owner in the sighash its proof verifies and a second note with the + /// same nullifier lands in the pool. + #[tokio::test] + async fn should_refuse_a_bundle_resubmitted_by_another_identity() { + let platform_version = PlatformVersion::latest(); + let mut platform = setup_platform(); + let (victim, victim_signer) = + funded_identity(&mut platform, [31u8; 32], 31, platform_version); + let (copier, copier_signer) = + funded_identity(&mut platform, [32u8; 32], 32, platform_version); + + let shield = builder_made_shield(&victim, &victim_signer, platform_version).await; + // Positive control: the bundle is valid, so a refusal below is about who carries it. + assert_accepted_at_admission_and_in_the_block(&platform, &shield, platform_version); + + let bundle = lift(&shield); + let amount = bundle.amount; + let copy = create_signed_transition( + &copier, + &copier_signer, + bundle, + amount, + 1, + 0, + platform_version, + ) + .await; + + assert_refused_for_its_proof_at_admission_and_in_the_block( + &platform, + ©, + platform_version, + ); + } + + /// An identity created from an asset lock has that lock's identifier as its id, so a + /// `ShieldFromAssetLock` bundle and a `ShieldFromIdentity` of that identity bind the very + /// same owner bytes. Only the kind tag tells them apart, and this is the one place a + /// forgotten tag would show. + #[tokio::test] + async fn should_refuse_an_asset_lock_bundle_resubmitted_by_the_identity_sharing_its_owner() + { + let platform_version = PlatformVersion::latest(); + let mut platform = setup_platform(); + + let mut rng = StdRng::seed_from_u64(33); + let (_, lock_key) = KeyType::ECDSA_SECP256K1 + .random_public_and_private_key_data(&mut rng, platform_version) + .expect("asset lock key"); + let asset_lock_proof = instant_asset_lock_proof_fixture( + Some(PrivateKey::from_byte_array(&lock_key, Network::Testnet).expect("key")), + None, + ); + let owner = asset_lock_proof + .create_identifier() + .expect("asset lock identifier"); + + let from_lock = build_shield_from_asset_lock_transition( + &test_orchard_recipient(), + SHIELD_AMOUNT, + asset_lock_proof, + &lock_key, + &TestOrchardProver, + [0u8; 36], + None, + Some(PlatformAddress::P2pkh([0x33; 20])), + 0, + platform_version, + ) + .expect("client-built shield from asset lock"); + // Positive control: the bundle is valid for the kind it was proved for. + assert!(check_tx_errors(&platform, &from_lock).is_empty()); + assert_matches!( + process_transition(&platform, from_lock.clone(), platform_version) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + let (identity, signer) = + funded_identity(&mut platform, owner.to_buffer(), 34, platform_version); + let StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V1(proven)) = + &from_lock + else { + panic!("expected a shield from asset lock transition"); + }; + let copy = create_signed_transition( + &identity, + &signer, + ProvenBundle { + actions: proven.actions.clone(), + amount: proven.value_balance, + anchor: proven.anchor, + proof: proven.proof.clone(), + binding_signature: proven.binding_signature, + }, + proven.value_balance, + 1, + 0, + platform_version, + ) + .await; + + assert_refused_for_its_proof_at_admission_and_in_the_block( + &platform, + ©, + platform_version, + ); + } + } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/transform_into_action/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/transform_into_action/v0/mod.rs index 8697e52d3ab..dbe54045b81 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/transform_into_action/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/transform_into_action/v0/mod.rs @@ -11,6 +11,7 @@ use dpp::block::block_info::BlockInfo; use dpp::consensus::state::state_error::StateError; use dpp::fee::fee_result::FeeResult; use dpp::prelude::ConsensusValidationResult; +use dpp::shielded::shield_from_identity_extra_sighash_data; use dpp::state_transition::shield_from_identity_transition::ShieldFromIdentityTransition; use dpp::version::PlatformVersion; use drive::drive::Drive; @@ -89,9 +90,18 @@ impl ShieldFromIdentityStateTransitionTransformIntoActionValidationV0 .map(|action| action.into())); } - // Outputs-only bundle entering the pool, exactly like `Shield`. The identity ECDSA - // signature already binds every bundle field to the identity and nonce, so no extra - // sighash data is needed. + // Outputs-only bundle entering the pool, exactly like `Shield`. The identity signature + // binds the bundle to this transition, but the bundle itself carries no anchor and says + // nothing about who proved it, so any other identity could sign a transition of its own + // around the same proved bytes. The sighash therefore binds the kind and the funding + // identity, the same preimage CheckTx rebuilds in `validate_shielded_proof`. + // + // This generation is also selected by every protocol version before 14, where the + // binding cannot change anything: `is_allowed` refuses `ShieldFromIdentity` below + // version 14, so no such transition reaches this transform there, and those versions' + // `credit_pool_bundle_binding` is `None`, which rebuilds an empty preimage. + let extra_sighash_data = + shield_from_identity_extra_sighash_data(&v0.identity_id.to_buffer(), platform_version)?; if let Err(e) = reconstruct_and_verify_bundle( &v0.actions, FLAGS_OUTPUTS_ONLY, @@ -99,7 +109,7 @@ impl ShieldFromIdentityStateTransitionTransformIntoActionValidationV0 &v0.anchor, v0.proof.as_slice(), &v0.binding_signature, - &[], + &extra_sighash_data, ) { // Paid penalty: the same versioned amount `ShieldFromAssetLock` burns from its // asset lock, charged here as a processing operation of the identity-paid diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_common/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_common/mod.rs index 4c90d2dcfe3..1af80837b79 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_common/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shielded_common/mod.rs @@ -72,7 +72,9 @@ const _: () = assert!( /// /// `extra_sighash_data` binds transparent fields to the Orchard signatures (built by the /// shared `dpp::shielded::*_extra_sighash_data` helpers so the signer and verifier agree): -/// - Shield: empty (no transparent outputs) +/// - Shield, ShieldFromIdentity, ShieldFromAssetLock: `kind tag (1) || owner (32)`, where the +/// owner is what funds the bundle (see `dpp::shielded::shield_extra_sighash_data`); empty at +/// protocol versions before that binding /// - Shielded transfer: empty (no transparent fields) /// - Unshield: `output_address || unshielding_amount (u64 LE)` /// - Shielded withdrawal: `output_script || unshielding_amount (u64 LE) || core_fee_per_byte diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/test_helpers.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/test_helpers.rs index 1e19a410e66..b2d1d0968df 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/test_helpers.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/test_helpers.rs @@ -5,11 +5,15 @@ //! balance setup utilities, and shielded pool state helpers. use crate::config::{PlatformConfig, PlatformTestConfig}; +use crate::execution::check_tx::CheckTxLevel; +use crate::platform_types::platform::PlatformRef; +use crate::platform_types::platform_state::PlatformStateV0Methods; use crate::platform_types::state_transitions_processing_result::StateTransitionsProcessingResult; use crate::rpc::core::MockCoreRPCLike; use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; -use dpp::address_funds::{AddressWitness, PlatformAddress}; +use dpp::address_funds::{AddressWitness, OrchardAddress, PlatformAddress}; use dpp::block::block_info::BlockInfo; +use dpp::consensus::ConsensusError; use dpp::dashcore::blockdata::script::ScriptBuf; use dpp::dashcore::hashes::{sha256, Hash}; use dpp::dashcore::secp256k1::{PublicKey as RawPublicKey, Secp256k1, SecretKey as RawSecretKey}; @@ -18,6 +22,7 @@ use dpp::identity::signer::Signer; use dpp::platform_value::BinaryData; use dpp::prelude::AddressNonce; use dpp::serialization::PlatformSerializable; +use dpp::shielded::builder::OrchardProver; use dpp::shielded::SerializedAction; use dpp::state_transition::StateTransition; use dpp::ProtocolError; @@ -612,7 +617,10 @@ pub fn insert_dummy_encrypted_notes(platform: &TempPlatform, co // Shared Orchard Proving Key & Serialization // ========================================== -use grovedb_commitment_tree::{Authorized as OrchardAuthorized, Bundle, DashMemo, ProvingKey}; +use grovedb_commitment_tree::{ + Authorized as OrchardAuthorized, Bundle, DashMemo, FullViewingKey, ProvingKey, Scope, + SpendingKey, +}; use std::sync::OnceLock; /// Single process-wide proving key shared by ALL shielded tests. @@ -628,6 +636,56 @@ pub fn get_proving_key() -> &'static ProvingKey { SHARED_PROVING_KEY.get_or_init(ProvingKey::build) } +/// The prover the public `dpp::shielded::builder` functions take, backed by the shared key. +pub struct TestOrchardProver; + +impl OrchardProver for TestOrchardProver { + fn proving_key(&self) -> &ProvingKey { + get_proving_key() + } +} + +/// A fixed Orchard recipient for bundles built through the public builders. +pub fn test_orchard_recipient() -> OrchardAddress { + let full_viewing_key = FullViewingKey::from(&SpendingKey::from_bytes([0u8; 32]).unwrap()); + OrchardAddress::from_raw_bytes( + &full_viewing_key + .address_at(0u32, Scope::External) + .to_raw_address_bytes(), + ) + .expect("valid orchard address bytes") +} + +/// Runs `transition` through a first-time CheckTx against the platform's current state and +/// returns what admission refused it for; an empty list means it was admitted. +pub fn check_tx_errors( + platform: &TempPlatform, + transition: &StateTransition, +) -> Vec { + let raw = transition + .serialize_to_bytes() + .expect("should serialize transition"); + let state = platform.state.load(); + let platform_version = state + .current_platform_version() + .expect("expected the current platform version"); + let platform_ref = PlatformRef { + drive: &platform.drive, + state: &state, + config: &platform.config, + core_rpc: &platform.core_rpc, + }; + platform + .check_tx( + &raw, + CheckTxLevel::FirstTimeCheck, + &platform_ref, + platform_version, + ) + .expect("check_tx must not error") + .errors +} + /// Serialize an authorized Orchard bundle into the platform-compatible /// format used by **spend** transitions (unshield, shielded-withdrawal). /// diff --git a/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs b/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs index 920f03ada2b..57cd1201142 100644 --- a/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs +++ b/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs @@ -613,7 +613,7 @@ impl Drive { } StateTransition::ShieldFromAssetLock(st) => { use dpp::identity::state_transition::AssetLockProved; - use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; + use dpp::state_transition::shield_from_asset_lock_transition::accessors::ShieldFromAssetLockTransitionAccessorsV0; let outpoint = st.asset_lock_proof().out_point().ok_or_else(|| { Error::Proof(ProofError::InvalidTransition( @@ -630,9 +630,7 @@ impl Drive { grovedb::SizedQuery::new(query, Some(1), None), ); - // No accessor trait exposes `surplus_output`, so read it directly off the V0 body. - let ShieldFromAssetLockTransition::V0(v0) = st; - match &v0.surplus_output { + match st.surplus_output() { Some(surplus_address) => { // Mirror the Unshield arm: also prove the balance of the signed // surplus-output address so a light client can confirm the surplus diff --git a/packages/rs-drive/src/state_transition_action/shielded/shield_from_asset_lock/transformer.rs b/packages/rs-drive/src/state_transition_action/shielded/shield_from_asset_lock/transformer.rs index f90ccf14c5d..3ac9bf36054 100644 --- a/packages/rs-drive/src/state_transition_action/shielded/shield_from_asset_lock/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/shielded/shield_from_asset_lock/transformer.rs @@ -16,19 +16,15 @@ impl ShieldFromAssetLockTransitionAction { current_total_balance: Credits, surplus_amount: Credits, ) -> ConsensusValidationResult { - match value { - ShieldFromAssetLockTransition::V0(v0) => { - let result = ShieldFromAssetLockTransitionActionV0::try_from_transition( - v0, - asset_lock_outpoint, - asset_lock_value_to_be_consumed, - signable_bytes_hasher, - shield_amount, - current_total_balance, - surplus_amount, - ); - result.map(|action| action.into()) - } - } + ShieldFromAssetLockTransitionActionV0::try_from_transition( + value, + asset_lock_outpoint, + asset_lock_value_to_be_consumed, + signable_bytes_hasher, + shield_amount, + current_total_balance, + surplus_amount, + ) + .map(|action| action.into()) } } diff --git a/packages/rs-drive/src/state_transition_action/shielded/shield_from_asset_lock/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/shielded/shield_from_asset_lock/v0/transformer.rs index 2c0f92877ff..66a98d065ea 100644 --- a/packages/rs-drive/src/state_transition_action/shielded/shield_from_asset_lock/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/shielded/shield_from_asset_lock/v0/transformer.rs @@ -2,13 +2,15 @@ use crate::state_transition_action::shielded::shield_from_asset_lock::v0::Shield use crate::state_transition_action::shielded::ShieldedActionNote; use dpp::fee::Credits; use dpp::prelude::ConsensusValidationResult; -use dpp::state_transition::state_transitions::shielded::shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0; +use dpp::state_transition::state_transitions::shielded::shield_from_asset_lock_transition::accessors::ShieldFromAssetLockTransitionAccessorsV0; +use dpp::state_transition::state_transitions::shielded::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; impl ShieldFromAssetLockTransitionActionV0 { - /// Transforms the shield from asset lock transition into an action + /// Transforms the shield from asset lock transition into an action. Every transition version + /// carries the same fields, so the action does not depend on which one it came from. #[allow(clippy::too_many_arguments)] pub fn try_from_transition( - value: &ShieldFromAssetLockTransitionV0, + value: &ShieldFromAssetLockTransition, asset_lock_outpoint: [u8; 36], asset_lock_value_to_be_consumed: Credits, signable_bytes_hasher: [u8; 32], @@ -16,8 +18,11 @@ impl ShieldFromAssetLockTransitionActionV0 { current_total_balance: Credits, surplus_amount: Credits, ) -> ConsensusValidationResult { - let notes: Vec = - value.actions.iter().map(ShieldedActionNote::from).collect(); + let notes: Vec = value + .actions() + .iter() + .map(ShieldedActionNote::from) + .collect(); ConsensusValidationResult::new_with_data(ShieldFromAssetLockTransitionActionV0 { asset_lock_outpoint, @@ -26,7 +31,7 @@ impl ShieldFromAssetLockTransitionActionV0 { shield_amount, notes, current_total_balance, - surplus_output: value.surplus_output, + surplus_output: value.surplus_output().copied(), surplus_amount, }) } diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs index dacf06817bd..353d8f360fb 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs @@ -2151,7 +2151,7 @@ impl Drive { use dpp::state_transition::proof_result::StateTransitionProofResult::{ VerifiedAssetLockConsumed, VerifiedAssetLockConsumedWithAddressInfos, }; - use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; + use dpp::state_transition::shield_from_asset_lock_transition::accessors::ShieldFromAssetLockTransitionAccessorsV0; use grovedb::Element; let outpoint = st.asset_lock_proof().out_point().ok_or_else(|| { @@ -2161,9 +2161,7 @@ impl Drive { })?; let outpoint_bytes: [u8; 36] = outpoint.into(); - // No accessor trait exposes `surplus_output`, so read it directly off the V0 body. - let ShieldFromAssetLockTransition::V0(v0) = st; - let surplus_output = &v0.surplus_output; + let surplus_output = st.surplus_output(); // Build the outpoint sub-query exactly as the prove side does (same path, same key). let mut outpoint_query = grovedb::Query::new(); diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_method_versions/mod.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_method_versions/mod.rs index 3c654283a53..0e39efc722d 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_method_versions/mod.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_method_versions/mod.rs @@ -1,4 +1,4 @@ -use versioned_feature_core::FeatureVersion; +use versioned_feature_core::{FeatureVersion, OptionalFeatureVersion}; pub mod v1; pub mod v2; @@ -11,4 +11,16 @@ pub struct DPPMethodVersions { pub deduct_fee_from_outputs_or_remaining_balance_of_inputs: FeatureVersion, pub compute_minimum_shielded_fee: FeatureVersion, pub shielded_extra_sighash_data: FeatureVersion, + /// The preimage the outputs-only bundles of the credit pool (`Shield`, `ShieldFromIdentity`, + /// `ShieldFromAssetLock`) bind into their Orchard sighash. `None` on versions that predate the + /// binding: they bind nothing, which is what every shipped verifier expects. `Some(0)`: they + /// bind `kind tag (1) || owner (32)`. + /// + /// The client builders and the consensus sites protocol version 14 selects read this one field + /// through the same helpers, so a client building for a protocol version produces the preimage + /// that version's verifier rebuilds. `validate_shielded_proof` v0 and the + /// `ShieldFromAssetLock` transform v0 rebuild an empty preimage without reading it; + /// `credit_pool_bundle_binding_should_agree_with_the_selected_shield_verifiers_at_every_protocol_version` + /// holds them and this field together. + pub credit_pool_bundle_binding: OptionalFeatureVersion, } diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_method_versions/v1.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_method_versions/v1.rs index e73b30c47f8..f2ddc819b9c 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_method_versions/v1.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_method_versions/v1.rs @@ -5,4 +5,5 @@ pub const DPP_METHOD_VERSIONS_V1: DPPMethodVersions = DPPMethodVersions { deduct_fee_from_outputs_or_remaining_balance_of_inputs: 0, compute_minimum_shielded_fee: 0, shielded_extra_sighash_data: 0, + credit_pool_bundle_binding: None, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_method_versions/v2.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_method_versions/v2.rs index 12b88028ab5..ba78347f6f9 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_method_versions/v2.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_method_versions/v2.rs @@ -5,4 +5,5 @@ pub const DPP_METHOD_VERSIONS_V2: DPPMethodVersions = DPPMethodVersions { deduct_fee_from_outputs_or_remaining_balance_of_inputs: 0, compute_minimum_shielded_fee: 0, shielded_extra_sighash_data: 0, + credit_pool_bundle_binding: None, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_method_versions/v3.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_method_versions/v3.rs index e2cc9994d40..8d1e13f480e 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_method_versions/v3.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_method_versions/v3.rs @@ -2,11 +2,14 @@ use crate::version::dpp_versions::dpp_method_versions::DPPMethodVersions; /// DPP method versions 3. Introduced in protocol v14: `daily_withdrawal_limit` 1 → 2 replaces the /// flat daily withdrawal limit with a percentage of the total credits Platform held a day ago -/// (`SystemLimits::daily_withdrawal_limit_percent`). Everything else matches V2. +/// (`SystemLimits::daily_withdrawal_limit_percent`), and `credit_pool_bundle_binding` +/// `None` → `Some(0)` binds a kind tag and an owner into the credit pool's outputs-only bundles. +/// Everything else matches V2. pub const DPP_METHOD_VERSIONS_V3: DPPMethodVersions = DPPMethodVersions { epoch_core_reward_credits_for_distribution: 0, daily_withdrawal_limit: 2, deduct_fee_from_outputs_or_remaining_balance_of_inputs: 0, compute_minimum_shielded_fee: 0, shielded_extra_sighash_data: 0, + credit_pool_bundle_binding: Some(0), }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs index 0022e6c7c67..4a437149a7c 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v3.rs @@ -4,6 +4,9 @@ //! (`DocumentIndexOnlyDeleteTransition`). It is its own kind, not a //! version of the delete transition: `document_delete_state_transition` //! keeps V2's bounds and keeps evolving independently for stored types. +//! `shield_from_asset_lock_state_transition` moves to version 1 alone: its +//! bundle binds its kind and its asset lock, and version 0 is not admitted +//! (`StateTransition::active_version_range` refuses it at decode). use crate::version::dpp_versions::dpp_state_transition_serialization_versions::{ DPPStateTransitionSerializationVersions, DocumentFeatureVersionBounds, @@ -181,9 +184,9 @@ pub const STATE_TRANSITION_SERIALIZATION_VERSIONS_V3: DPPStateTransitionSerializ default_current_version: 0, }, shield_from_asset_lock_state_transition: FeatureVersionBounds { - min_version: 0, - max_version: 0, - default_current_version: 0, + min_version: 1, // changed: version 0, whose bundle binds nothing, is refused from here + max_version: 1, // changed: version 1's bundle binds its kind and its asset lock + default_current_version: 1, }, shielded_withdrawal_state_transition: FeatureVersionBounds { min_version: 0, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs index a8ba07b8221..aeba3c3b02f 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs @@ -363,7 +363,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = identity_signatures: None, nonce: None, state: 0, - transform_into_action: 0, + transform_into_action: 1, // changed: the bundle's sighash binds its kind tag and the asset lock it is funded from }, shielded_withdrawal_state_transition: DriveAbciStateTransitionValidationVersion { basic_structure: Some(0), diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index 0d680b5adbb..c2752b2379e 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -1155,8 +1155,25 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// structure, identity signature, and nonce validation. It moves credits /// from an identity balance straight into the shielded pool: the funding /// side is identity-signed like `IdentityCreditTransferToAddresses`, the -/// pool side is an outputs-only Orchard bundle like `Shield`, and the fee -/// is metered plus the shielded compute fee, paid from the identity. +/// pool side is an outputs-only Orchard bundle like `Shield`, bound to the +/// funding identity (next item), and the fee is metered plus the shielded +/// compute fee, paid from the identity. +/// +/// * The credit pool's outputs-only bundles bind `kind tag || owner` into their +/// Orchard sighash (`DPP_METHOD_VERSIONS_V3` sets `credit_pool_bundle_binding` +/// to `Some(0)`): `Shield` (`0x84`) the SHA-256 of its input addresses, +/// checked by `validate_shielded_proof` v1; `ShieldFromIdentity` (`0x85`) its +/// identity id; `ShieldFromAssetLock` (`0x86`) its asset lock identifier, +/// checked by the `transform_into_action` v1 that +/// `DRIVE_ABCI_VALIDATION_VERSIONS_V10` selects. A third party can no longer +/// wrap a proved bundle in a transition of their own. v13 keeps both checks +/// unbound. A sender rebuilding the same notes (Faerie Gold) is not stopped: +/// that needs the bundles' dummy nullifiers recorded and checked. +/// `ShieldFromAssetLock` also gains transition version 1 +/// (`STATE_TRANSITION_SERIALIZATION_VERSIONS_V3`), the only version 14 +/// admits: version 0 is refused at decode by `active_version_range`, before +/// any proof work, uncharged and with its asset lock left unspent, so one +/// still waiting when 14 activates is not burned by the bound check. /// /// * `IdentityTopUpFromShieldedPool` (state transition type 22) activates at the /// same gate (`IDENTITY_TOP_UP_FROM_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION = 14`, @@ -1208,7 +1225,7 @@ pub const PLATFORM_V14: PlatformVersion = PlatformVersion { drive_abci: DriveAbciVersion { structs: DRIVE_ABCI_STRUCTURE_VERSIONS_V2, // changed: saved platform state structure 1 keeps masternodes and validator sets as one aux entry each methods: DRIVE_ABCI_METHOD_VERSIONS_V10, // changed: records the per-block total credits history for the daily withdrawal limit; record_token_shielded_pool_anchors records and prunes the anchors of the token pools a block touched - validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V10, // changed: contested-index cross-check + refersTo document reference validation; the ContractUserModeration gates and the batch transformer's contract_moderation_gate; the three shielded-fee token pool transitions gain basic structure validation and document_base_transition_state_validation 1 admits a document token cost paid from a token pool + validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V10, // changed: contested-index cross-check + refersTo document reference validation; the ContractUserModeration gates and the batch transformer's contract_moderation_gate; the three shielded-fee token pool transitions gain basic structure validation and document_base_transition_state_validation 1 admits a document token cost paid from a token pool; the ShieldFromAssetLock transform_into_action 1 checks its bundle against the bound preimage withdrawal_constants: DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V3, // changed: prune bound for the total credits history query: DRIVE_ABCI_QUERY_VERSIONS_V3, // changed: ranked + boolean-HAVING routing gate; the v1 handler also resolves IN_TIME_RANGE from committed block time checkpoints: DRIVE_ABCI_CHECKPOINT_PARAMETERS_V1, @@ -1216,7 +1233,7 @@ pub const PLATFORM_V14: PlatformVersion = PlatformVersion { dpp: DPPVersion { costs: DPP_COSTS_VERSIONS_V1, validation: DPP_VALIDATION_VERSIONS_V5, // changed: validate_config_update 2 admits the contract moderation declaration of config V2; validate_token_config_update 1 keeps the shielded pool opt-in immutable after creation - state_transition_serialization_versions: STATE_TRANSITION_SERIALIZATION_VERSIONS_V3, // changed: the indexOnly delete-by-values kind (documentIndexOnlyDelete) joins the wire; the ContractUserModeration transition + state_transition_serialization_versions: STATE_TRANSITION_SERIALIZATION_VERSIONS_V3, // changed: the indexOnly delete-by-values kind (documentIndexOnlyDelete) joins the wire; ShieldFromAssetLock moves to version 1 alone; the ContractUserModeration transition state_transition_conversion_versions: STATE_TRANSITION_CONVERSION_VERSIONS_V2, state_transition_method_versions: STATE_TRANSITION_METHOD_VERSIONS_V2, // changed: public keys in creation may carry a budget or an expiry state_transitions: STATE_TRANSITION_VERSIONS_V4, @@ -1226,7 +1243,7 @@ pub const PLATFORM_V14: PlatformVersion = PlatformVersion { voting_versions: VOTING_VERSION_V2, token_versions: TOKEN_VERSIONS_V3, // changed: distribution_function_evaluate v1 — deterministic libm for token reward math; reward_distribution_max_cycle_moment v1: the epoch claim cap no longer wraps; distribution_function_cycle_epochs v1: evonode cycles weighted by the epochs they span asset_lock_versions: DPP_ASSET_LOCK_VERSIONS_V1, - methods: DPP_METHOD_VERSIONS_V3, // changed: daily_withdrawal_limit v2 — a percentage of the total credits a day ago + methods: DPP_METHOD_VERSIONS_V3, // changed: daily_withdrawal_limit v2 — a percentage of the total credits a day ago; credit_pool_bundle_binding Some(0) — the credit pool's outputs-only bundles bind a kind tag and their owner factory_versions: DPP_FACTORY_VERSIONS_V1, }, system_data_contracts: SYSTEM_DATA_CONTRACT_VERSIONS_V3, // changed: DashPay v2 adds profile payment address fields (DIP-33); withdrawals v2 admits the terminal FAILED status diff --git a/packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs b/packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs index 097fd169f45..fb7e036a786 100644 --- a/packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs +++ b/packages/rs-platform-wallet/src/wallet/shielded/fund_from_asset_lock.rs @@ -721,7 +721,7 @@ where AS: ::key_wallet::signer::Signer, P: OrchardProver, { - use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; + use dpp::state_transition::shield_from_asset_lock_transition::accessors::ShieldFromAssetLockTransitionAccessorsV0; use dpp::state_transition::StateTransition; let st = build_shield_from_asset_lock_transition_with_signer( @@ -739,12 +739,8 @@ where ) .await?; - // `actions` is a public field on the V0 struct (no accessor trait for - // Shield / ShieldFromAssetLock). let actions = match &st { - StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V0(v0)) => { - v0.actions.clone() - } + StateTransition::ShieldFromAssetLock(transition) => transition.actions().to_vec(), _ => Vec::new(), }; diff --git a/packages/rs-platform-wallet/src/wallet/shielded/operations.rs b/packages/rs-platform-wallet/src/wallet/shielded/operations.rs index 399d5160f1f..e7509b85389 100644 --- a/packages/rs-platform-wallet/src/wallet/shielded/operations.rs +++ b/packages/rs-platform-wallet/src/wallet/shielded/operations.rs @@ -265,11 +265,10 @@ pub(super) async fn queue_shielded_activity( /// Each shielded variant's `actions()` comes from its own accessor / /// methods trait, so the relevant traits are imported locally. fn shielded_actions(st: &StateTransition) -> &[dpp::shielded::SerializedAction] { - // `actions()` is on the per-type accessor trait for the spend-based - // transitions; Shield / ShieldFromAssetLock expose `actions` as a - // public field on their V0 struct (no accessor trait), so match down - // to the V0 variant for those two. - use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; + // `actions()` is on each type's accessor trait; Shield exposes `actions` + // only as a public field on its V0 struct, so match down to that + // variant for it. + use dpp::state_transition::shield_from_asset_lock_transition::accessors::ShieldFromAssetLockTransitionAccessorsV0; use dpp::state_transition::shield_transition::ShieldTransition; use dpp::state_transition::shielded_transfer_transition::accessors::ShieldedTransferTransitionAccessorsV0; use dpp::state_transition::shielded_withdrawal_transition::accessors::ShieldedWithdrawalTransitionAccessorsV0; @@ -282,7 +281,7 @@ fn shielded_actions(st: &StateTransition) -> &[dpp::shielded::SerializedAction] StateTransition::Shield(ShieldTransition::V0(v0)) => &v0.actions, StateTransition::ShieldedTransfer(t) => t.actions(), StateTransition::Unshield(t) => t.actions(), - StateTransition::ShieldFromAssetLock(ShieldFromAssetLockTransition::V0(v0)) => &v0.actions, + StateTransition::ShieldFromAssetLock(t) => t.actions(), StateTransition::ShieldedWithdrawal(t) => t.actions(), StateTransition::IdentityCreateFromShieldedPool(t) => t.actions(), StateTransition::ShieldFromIdentity(t) => t.actions(), diff --git a/packages/rs-sdk/src/platform/transition/shield_from_asset_lock.rs b/packages/rs-sdk/src/platform/transition/shield_from_asset_lock.rs index 7448bc3dc73..c3c27cb1ddf 100644 --- a/packages/rs-sdk/src/platform/transition/shield_from_asset_lock.rs +++ b/packages/rs-sdk/src/platform/transition/shield_from_asset_lock.rs @@ -18,6 +18,14 @@ pub trait ShieldFromAssetLock { /// the shielded amount and the pool fee) to a platform address. When `None`, the /// surplus is implicitly donated to the fee pools, which consensus only permits up /// to `shielded_implicit_fee_cap` — supply an address to receive a larger remainder. + /// + /// `bundle` must be proved and signed against the sighash preimage of the protocol + /// version this SDK reports (`self.version()`), which also picks the transition version: + /// `dpp::shielded::shield_from_asset_lock_extra_sighash_data(&asset_lock_proof, version)`. + /// From protocol version 14 that preimage binds the bundle's kind and this asset lock, + /// and a bundle proved against another preimage fails its proof in consensus, which takes + /// the proof-failure penalty from the asset lock. `dpp::shielded::builder` builds the whole + /// transition from one version and cannot mismatch. #[allow(clippy::too_many_arguments)] async fn shield_from_asset_lock( &self, diff --git a/packages/wasm-dpp2/src/shielded/mod.rs b/packages/wasm-dpp2/src/shielded/mod.rs index dc85db286f2..bae01e5ebfb 100644 --- a/packages/wasm-dpp2/src/shielded/mod.rs +++ b/packages/wasm-dpp2/src/shielded/mod.rs @@ -38,8 +38,16 @@ use wasm_bindgen::prelude::wasm_bindgen; /// it and compares, so a byte out of place rejects an otherwise valid bundle. Integers are /// little endian. /// -/// - Credit pool `Shield`, `ShieldFromIdentity`, `ShieldFromAssetLock` and `ShieldedTransfer`: -/// empty. +/// - Credit pool `ShieldedTransfer`: empty. +/// - Credit pool transitions that only create notes — shield, shield from identity and shield +/// from asset lock: `kind tag (1 byte) || owner (32 bytes)`, with the tag `0x84`, `0x85` and +/// `0x86` in that order. The owner is what funds the bundle: for shield, the SHA-256 of its +/// input addresses, each as `type byte (0x00 P2PKH, 0x01 P2SH) || 20-byte hash` (not the +/// bech32m type byte), sorted by type byte then hash, nonces and amounts excluded; for shield +/// from identity, the identity id; for shield from asset lock, the double SHA-256 of the +/// locked 36-byte outpoint. Before protocol version 14 shield and shield from asset lock bind +/// nothing, so build for the network's protocol version. Such a bundle carries no anchor of +/// its own, so this data is what ties it to the transition that carries it. /// - `Unshield`: `outputAddress || amount (u64)`. /// - Shielded withdrawal: `outputScript || amount (u64) || coreFeePerByte (u32) || /// pooling (1 byte)`. diff --git a/packages/wasm-dpp2/src/shielded/shield_from_asset_lock_transition.rs b/packages/wasm-dpp2/src/shielded/shield_from_asset_lock_transition.rs index 93f971f89d7..ad9b9b196e7 100644 --- a/packages/wasm-dpp2/src/shielded/shield_from_asset_lock_transition.rs +++ b/packages/wasm-dpp2/src/shielded/shield_from_asset_lock_transition.rs @@ -4,12 +4,17 @@ use crate::identifier::IdentifierWasm; use crate::platform_address::PlatformAddressWasm; use crate::shielded::orchard_action::{SerializedOrchardActionWasm, actions_from_js_options}; use crate::utils::{try_from_options, try_from_options_optional, try_vec_to_fixed_bytes}; +use crate::version::PlatformVersionWasm; use crate::{impl_wasm_conversions_inner, impl_wasm_type_info}; +use dpp::identity::state_transition::AssetLockProved; use dpp::platform_value::BinaryData; use dpp::serialization::{PlatformDeserializableUntrusted, PlatformSerializable}; use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; +use dpp::state_transition::shield_from_asset_lock_transition::accessors::ShieldFromAssetLockTransitionAccessorsV0; use dpp::state_transition::shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0; -use dpp::state_transition::{StateTransition, StateTransitionLike}; +use dpp::state_transition::shield_from_asset_lock_transition::v1::ShieldFromAssetLockTransitionV1; +use dpp::state_transition::{StateTransition, StateTransitionLike, StateTransitionSingleSigned}; +use dpp::version::PlatformVersion; use serde::{Deserialize, Serialize}; use wasm_bindgen::prelude::*; @@ -49,6 +54,13 @@ export interface ShieldFromAssetLockTransitionOptions { * Accepts a PlatformAddress, a 21-byte Uint8Array, or a bech32m string. */ surplusOutput?: PlatformAddressLike; + /** + * The protocol version to build for; defaults to the version this library + * targets. Protocol version 14 admits only transition version 1, whose + * bundle binds its kind and its asset lock; earlier versions admit only + * version 0. Build the bundle's sighash for the same protocol version. + */ + platformVersion?: PlatformVersionLike; } /** @@ -125,6 +137,10 @@ impl ShieldFromAssetLockTransitionWasm { // absent / null / undefined → `None`. let surplus_output: Option = try_from_options_optional(options.as_ref(), "surplusOutput")?; + let platform_version: PlatformVersion = + try_from_options_optional::(options.as_ref(), "platformVersion")? + .map(Into::into) + .unwrap_or_else(|| PlatformVersionWasm::default().into()); // Extract remaining simple fields via serde (consumes options) let fields: ShieldFromAssetLockTransitionSimpleFields = @@ -135,92 +151,101 @@ impl ShieldFromAssetLockTransitionWasm { let binding_signature: [u8; 64] = try_vec_to_fixed_bytes(fields.binding_signature, "bindingSignature")?; - Ok(ShieldFromAssetLockTransitionWasm( - ShieldFromAssetLockTransition::V0(ShieldFromAssetLockTransitionV0 { - asset_lock_proof: asset_lock.into(), - actions: actions.into_iter().map(Into::into).collect(), + let asset_lock_proof = asset_lock.into(); + let actions = actions.into_iter().map(Into::into).collect(); + let surplus_output = surplus_output.map(Into::into); + let signature = BinaryData::from(fields.signature); + let transition = match platform_version + .dpp + .state_transition_serialization_versions + .shield_from_asset_lock_state_transition + .default_current_version + { + 0 => ShieldFromAssetLockTransition::V0(ShieldFromAssetLockTransitionV0 { + asset_lock_proof, + actions, value_balance: fields.value_balance, anchor, proof: fields.proof, binding_signature, - surplus_output: surplus_output.map(Into::into), - signature: BinaryData::from(fields.signature), + surplus_output, + signature, }), - )) + 1 => ShieldFromAssetLockTransition::V1(ShieldFromAssetLockTransitionV1 { + asset_lock_proof, + actions, + value_balance: fields.value_balance, + anchor, + proof: fields.proof, + binding_signature, + surplus_output, + signature, + }), + version => { + return Err(WasmDppError::invalid_argument(format!( + "unknown ShieldFromAssetLockTransition version {version}" + ))); + } + }; + + Ok(ShieldFromAssetLockTransitionWasm(transition)) } /// Returns the asset lock proof. #[wasm_bindgen(getter = "assetLockProof")] pub fn asset_lock_proof(&self) -> AssetLockProofWasm { - match &self.0 { - ShieldFromAssetLockTransition::V0(v0) => { - AssetLockProofWasm::from(v0.asset_lock_proof.clone()) - } - } + AssetLockProofWasm::from(self.0.asset_lock_proof().clone()) } /// Returns the serialized Orchard actions. #[wasm_bindgen(getter = "actions")] pub fn actions(&self) -> Vec { - match &self.0 { - ShieldFromAssetLockTransition::V0(v0) => v0 - .actions - .iter() - .cloned() - .map(SerializedOrchardActionWasm::from) - .collect(), - } + self.0 + .actions() + .iter() + .cloned() + .map(SerializedOrchardActionWasm::from) + .collect() } /// Returns the net value balance. #[wasm_bindgen(getter = "valueBalance")] pub fn value_balance(&self) -> u64 { - match &self.0 { - ShieldFromAssetLockTransition::V0(v0) => v0.value_balance, - } + self.0.value_balance() } /// Returns the optional surplus-output platform address, or /// `undefined` when the surplus folds into the fee pools. #[wasm_bindgen(getter = "surplusOutput")] pub fn surplus_output(&self) -> Option { - match &self.0 { - ShieldFromAssetLockTransition::V0(v0) => { - v0.surplus_output.map(PlatformAddressWasm::from) - } - } + self.0 + .surplus_output() + .copied() + .map(PlatformAddressWasm::from) } /// Returns the anchor (32-byte Merkle root). #[wasm_bindgen(getter = "anchor")] pub fn anchor(&self) -> Vec { - match &self.0 { - ShieldFromAssetLockTransition::V0(v0) => v0.anchor.to_vec(), - } + self.0.anchor().to_vec() } /// Returns the Halo2 proof bytes. #[wasm_bindgen(getter = "proof")] pub fn proof(&self) -> Vec { - match &self.0 { - ShieldFromAssetLockTransition::V0(v0) => v0.proof.clone(), - } + self.0.proof().to_vec() } /// Returns the RedPallas binding signature (64 bytes). #[wasm_bindgen(getter = "bindingSignature")] pub fn binding_signature(&self) -> Vec { - match &self.0 { - ShieldFromAssetLockTransition::V0(v0) => v0.binding_signature.to_vec(), - } + self.0.binding_signature().to_vec() } /// Returns the ECDSA signature. #[wasm_bindgen(getter = "signature")] pub fn signature(&self) -> Vec { - match &self.0 { - ShieldFromAssetLockTransition::V0(v0) => v0.signature.to_vec(), - } + self.0.signature().to_vec() } #[wasm_bindgen(js_name = getModifiedDataIds)] From 6aaeeb088db1072942ae1923a27a8647d8bd51a9 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Sat, 26 Sep 2026 10:16:43 +0700 Subject: [PATCH 34/94] fix(drive-abci)!: record and check the nullifiers of shielding transitions (PV14) Every Orchard action reveals a nullifier; in an outputs-only bundle it is the nullifier of a dummy spend and becomes the new note's rho. The spends record theirs and refuse one already recorded. From protocol version 14, Shield, ShieldFromAssetLock and ShieldFromIdentity do the same, so every revealed nullifier is recorded once. - Shield and ShieldFromAssetLock transform_into_action 1 run the spends' validate_nullifiers (a nullifier repeated inside the bundle or already recorded is refused with NullifierAlreadySpentError, unpaid). ShieldFromAssetLock checks after the asset lock and before the proof. - ShieldFromIdentity transform_into_action 0 runs the same check in place (the transition is refused before protocol version 14), metered with the pool read, as a paid nonce bump before the proof. - The high-level operations insert the nullifiers: generation 1 for Shield and ShieldFromAssetLock, ShieldFromIdentity 0 in place. - The ShieldFromIdentity admission floor gains 120 effective bytes per action and its flat identity component grows from 20 to 60, so it still covers the metered fee with the nullifier writes. ShieldFromAssetLock's flat pool fee already covers them (tested up to 16 actions). - Protocol versions 12 and 13 are unchanged (tested). Co-Authored-By: Claude Opus 5.5 --- book/src/fees/shielded-fees.md | 36 +- .../compute_minimum_shielded_fee/mod.rs | 10 +- .../compute_minimum_shielded_fee/v0/mod.rs | 48 +- packages/rs-dpp/src/shielded/mod.rs | 40 +- ...ate_transition_estimated_fee_validation.rs | 6 +- .../state_transitions/shield/mod.rs | 11 +- .../state_transitions/shield/tests.rs | 263 +++++++++++ .../shield/transform_into_action/mod.rs | 1 + .../shield/transform_into_action/v0/mod.rs | 2 +- .../shield/transform_into_action/v1/mod.rs | 102 +++++ .../shield_from_asset_lock/mod.rs | 11 +- .../shield_from_asset_lock/tests.rs | 310 +++++++++++++ .../transform_into_action/mod.rs | 1 + .../transform_into_action/v1/mod.rs | 427 ++++++++++++++++++ .../shield_from_identity/tests.rs | 265 +++++++++++ .../transform_into_action/v0/mod.rs | 41 +- .../state_transitions/test_helpers.rs | 176 ++++++++ .../shielded/mod.rs | 6 +- .../shield_from_asset_lock_transition.rs | 113 ++++- .../shield_from_identity_transition.rs | 20 +- .../shielded/shield_transition.rs | 75 ++- .../drive_abci_validation_versions/v10.rs | 7 +- .../v4.rs | 7 +- .../rs-platform-version/src/version/v14.rs | 21 + .../src/shielded_send.rs | 7 +- 25 files changed, 1932 insertions(+), 74 deletions(-) create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/transform_into_action/v1/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/transform_into_action/v1/mod.rs diff --git a/book/src/fees/shielded-fees.md b/book/src/fees/shielded-fees.md index b9df1216525..d823229c1b5 100644 --- a/book/src/fees/shielded-fees.md +++ b/book/src/fees/shielded-fees.md @@ -68,6 +68,14 @@ is funded by a consumed asset lock with no metering anchor, so it pays the flat is the on-wire action count of the bundle (a single-output, spends-disabled Orchard bundle pads to 2 actions, so the minimum is the 2-action fee). +Every action of an outputs-only bundle still reveals a nullifier, that of a dummy spend, +which becomes the new note's `rho`. From protocol version 14 the entry transitions record +those nullifiers and refuse one repeated inside the bundle or already recorded +(`NullifierAlreadySpentError`), as the spends do, so every revealed nullifier is recorded +once. `Shield` and `ShieldFromIdentity` meter the nullifier writes like the rest of their +storage; `ShieldFromAssetLock`'s flat fee already prices a note and a nullifier write per +action (see [Per-Action Storage Fee](#3-per-action-storage-fee)). + ### Shield `Shield` is charged like any other address-funded transition: GroveDB **meters** the @@ -99,8 +107,8 @@ compute` funding gate is `validate_fees_of_event`. `ShieldFromIdentity` (protocol version 14) is `Shield` with the identity balance as the funding side. It is identity-signed (TRANSFER key, identity nonce) like `IdentityCreditTransferToAddresses`, and carries the same outputs-only Orchard bundle -as `Shield`. The fee model is identical to `Shield`'s: GroveDB meters the note -inserts and the identity balance and nonce writes, and the shielded compute fee is +as `Shield`. The fee model is identical to `Shield`'s: GroveDB meters the note and +nullifier inserts and the identity balance and nonce writes, and the shielded compute fee is added as `additional_fixed_fee_cost`: ``` @@ -110,13 +118,16 @@ identity_balance_after = identity_balance_before - amount - fee `user_fee_increase` applies to the metered processing portion. The stateless floor requires `identity_balance >= amount + compute_shielded_identity_balance_write_fee`, -the conservative complete fee (`compute_minimum_shielded_fee` plus the flat -`SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES` identity-write component at the -storage rate: 20 effective bytes covering the nonce and balance rewrites, which add no -storage but replace 883 bytes of Merk path for a measured 466,760 credits of -processing, folded into one flat figure with headroom like the other shielded -components), so an identity that could not pay the complete fee is refused before the -Orchard proof is verified. The authoritative gate is the identity-paid fee +the conservative complete fee: `compute_minimum_shielded_fee`, plus +`SHIELDED_IDENTITY_ACTION_WRITE_STORAGE_BYTES` (120 effective bytes) per action for the +metered processing of the note and nullifier writes, which the per-action allowance +(sized for the storage a spend books) does not price, plus the flat +`SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES` identity-side component (60 effective +bytes: the nonce and balance rewrites, which add no storage but replace 883 bytes of +Merk path for a measured 466,760 credits of processing, and the reads and pool-total +update around them, 1,076,280 credits in all), each at the storage rate and with +headroom like the other shielded components. An identity that could not pay the +complete fee is therefore refused before the Orchard proof is verified. The authoritative gate is the identity-paid fee validation of the execution event (`Paid`), which rejects with `IdentityInsufficientBalanceError`. The identity balance and the pool total are both terms of the block conservation equation, so the converter emits no @@ -205,10 +216,11 @@ batch verification. For a spend-bearing action that marginal work includes: - Nullifier duplicate check (hash + tree lookup) - Note commitment insertion into the Sinsemilla-based Merkle tree -Output-only entry transitions (Shield / ShieldFromAssetLock) do no spends or -nullifier checks, but each output action still enlarges the proof and so carries +Output-only entry transitions (Shield / ShieldFromAssetLock / ShieldFromIdentity) do +no spends, but each output action still enlarges the proof and so carries the same per-action processing charge — this fee tracks the marginal verification -work, not a fixed per-action checklist. +work, not a fixed per-action checklist. From protocol version 14 it also prices the +check of the nullifier each of their actions reveals. The fee is calibrated at roughly a 4.5:1 ratio against the fixed proof-verification fee (100M : 22M) rather than the looser ratio used before the diff --git a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/mod.rs b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/mod.rs index 614dd9e5c40..617af13eb83 100644 --- a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/mod.rs +++ b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/mod.rs @@ -137,10 +137,12 @@ pub fn compute_shielded_unshield_fee( } /// Computes the conservative **admission floor** (in credits) of a shielded transition that also -/// writes an identity balance (`ShieldFromIdentity`): [`compute_minimum_shielded_fee`] plus the -/// flat identity-write component (`SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES` effective bytes -/// at the per-byte storage rate: the nonce and balance rewrites' replace-only tree work, folded -/// into one flat figure like the other shielded components). +/// writes an identity balance (`ShieldFromIdentity`): [`compute_minimum_shielded_fee`] plus a +/// per-action write component (`SHIELDED_IDENTITY_ACTION_WRITE_STORAGE_BYTES` effective bytes per +/// action: the metered processing of the note and nullifier writes the per-action allowance does +/// not price) and the flat identity-side component (`SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES` +/// effective bytes: the nonce and balance rewrites and the reads around them), both at the +/// per-byte storage rate and folded into flat figures like the other shielded components. /// /// The transition's authoritative fee is metered at execution; this floor stands in for it where /// state is not yet available, so that an identity that could not pay the complete fee is refused diff --git a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs index 3f23ef06605..91fa261c363 100644 --- a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs +++ b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs @@ -1,7 +1,8 @@ use crate::fee::Credits; use crate::shielded::{ - SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES, SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES, - SHIELDED_UNSHIELD_ADDRESS_STORAGE_BYTES, SHIELDED_WITHDRAWAL_DOCUMENT_STORAGE_BYTES, + SHIELDED_IDENTITY_ACTION_WRITE_STORAGE_BYTES, SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES, + SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES, SHIELDED_UNSHIELD_ADDRESS_STORAGE_BYTES, + SHIELDED_WITHDRAWAL_DOCUMENT_STORAGE_BYTES, }; use crate::ProtocolError; use platform_version::version::PlatformVersion; @@ -15,9 +16,10 @@ use platform_version::version::PlatformVersion; /// `processing_fee` that prices the MARGINAL verification work each additional action adds to the /// bundle (a larger bundle is a larger circuit and a longer batch verification). For spend-bearing /// transitions that marginal work includes the per-action RedPallas spend-auth signature -/// verification and nullifier check; output-only entry transitions (Shield / ShieldFromAssetLock) -/// do no spends or nullifier checks, but each output action still enlarges the proof and so carries -/// the same per-action processing charge. +/// verification and nullifier check; output-only entry transitions (Shield / ShieldFromAssetLock / +/// ShieldFromIdentity) do no spends, but each output action still enlarges the proof and so carries +/// the same per-action processing charge, which from protocol version 14 also prices the check of +/// the nullifier the action reveals. /// /// It carries **no storage term**: storage is the real cost of the note/nullifier writes and is /// metered separately by GroveDB. @@ -195,13 +197,17 @@ pub fn compute_shielded_unshield_fee_v0( /// v0 of the `ShieldFromIdentity` **admission floor**: /// /// `floor = compute_minimum_shielded_fee_v0(num_actions) -/// + SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES × (disk + processing) credits/byte` +/// + (num_actions × SHIELDED_IDENTITY_ACTION_WRITE_STORAGE_BYTES +/// + SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES) × (disk + processing) credits/byte` /// -/// [`compute_minimum_shielded_fee_v0`] plus one flat component for the identity-side writes -/// (the nonce and balance rewrites), built the same way as -/// [`compute_shielded_unshield_fee_v0`]'s address-write component: the writes' replace-only tree -/// work (they add no storage) folded into a flat effective-byte figure with headroom, see -/// `SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES`. The transition's real fee is metered at +/// [`compute_minimum_shielded_fee_v0`] plus a per-action component for the metered processing of +/// the note and nullifier writes that the per-action allowance (sized for a spend's booked +/// storage) does not price, see `SHIELDED_IDENTITY_ACTION_WRITE_STORAGE_BYTES`, plus one flat +/// component for the identity-side work (the nonce and balance rewrites and the reads around +/// them), built the same way as [`compute_shielded_unshield_fee_v0`]'s address-write component, +/// see `SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES`. `ShieldFromIdentity` is refused by +/// `is_allowed` before protocol version 14, so this formula has never been applied at an earlier +/// version, and its components were sized in place. The transition's real fee is metered at /// execution; this floor is the conservative stand-in the stateless balance pre-check uses so /// that a short identity is refused before the Orchard proof is verified, and the client-side /// estimate of the total fee. @@ -222,8 +228,14 @@ pub fn compute_shielded_identity_balance_write_fee_v0( .ok_or(ProtocolError::Overflow( "shielded storage per-byte rate overflow", ))?; - let identity_write_fee = SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES - .checked_mul(per_byte_rate) + let action_write_bytes = (num_actions as u64) + .checked_mul(SHIELDED_IDENTITY_ACTION_WRITE_STORAGE_BYTES) + .ok_or(ProtocolError::Overflow( + "shielded identity action write bytes overflow", + ))?; + let identity_write_fee = action_write_bytes + .checked_add(SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES) + .and_then(|bytes| bytes.checked_mul(per_byte_rate)) .ok_or(ProtocolError::Overflow( "shielded identity balance write fee overflow", ))?; @@ -333,8 +345,9 @@ mod tests { /// version's own constant tables, and must decompose as /// `compute_fee + num_actions × storage_allowance` — the component split the pool-paid /// booking and the fee-floor tests rely on. - /// The `ShieldFromIdentity` admission floor is the minimum fee plus the flat - /// identity-write allowance at the storage rate, and strictly above the compute fee. + /// The `ShieldFromIdentity` admission floor is the minimum fee plus the per-action write + /// allowance and the flat identity-write allowance at the storage rate, and strictly above + /// the compute fee. #[test] fn compute_shielded_identity_balance_write_fee_v0_adds_identity_write_allowance() { let platform_version = PlatformVersion::latest(); @@ -351,7 +364,10 @@ mod tests { .expect("compute"); assert_eq!( floor, - minimum + SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES * per_byte_rate + minimum + + (num_actions as u64 * SHIELDED_IDENTITY_ACTION_WRITE_STORAGE_BYTES + + SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES) + * per_byte_rate ); assert!(floor > compute); } diff --git a/packages/rs-dpp/src/shielded/mod.rs b/packages/rs-dpp/src/shielded/mod.rs index 2298376f250..c2ca79bb957 100644 --- a/packages/rs-dpp/src/shielded/mod.rs +++ b/packages/rs-dpp/src/shielded/mod.rs @@ -97,15 +97,17 @@ pub const SHIELDED_UNSHIELD_ADDRESS_STORAGE_BYTES: u64 = 222; /// [`compute_minimum_shielded_fee::compute_shielded_identity_top_up_fee`]. pub const SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES: u64 = 8; -/// Flat component (in effective bytes at the per-byte storage rate) for the identity-side writes a -/// `ShieldFromIdentity` performs on top of its per-action note inserts: the `UpdateIdentityNonce` -/// and `RemoveFromIdentityBalance` operations. +/// Flat component (in effective bytes at the per-byte storage rate) for the identity-side work a +/// `ShieldFromIdentity` performs on top of its per-action note and nullifier writes: the +/// `UpdateIdentityNonce` and `RemoveFromIdentityBalance` operations, and the reads and updates +/// around them (the identity fetched for its signature, the pool total read and rewritten). /// /// The transition's real fee is metered and only known at execution, so its stateless admission /// floor needs a conservative stand-in for the metered part: [`compute_minimum_shielded_fee`] -/// (compute plus the per-action note allowance, which covers the note inserts with headroom) -/// plus this component. Admission below `amount + floor` is refused BEFORE the Orchard proof is -/// verified, so a short identity never occupies a proof-verification slot. +/// (compute plus the per-action allowance), plus +/// [`SHIELDED_IDENTITY_ACTION_WRITE_STORAGE_BYTES`] per action, plus this component. Admission +/// below `amount + floor` is refused BEFORE the Orchard proof is verified, so a short identity +/// never occupies a proof-verification slot. /// /// What the two writes do: the identity already exists, so neither adds storage. Each rewrites /// its element and every Merk node on the path to the root (replaced bytes, charged at the @@ -116,11 +118,29 @@ pub const SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES: u64 = 8; /// `SHIELDED_UNSHIELD_ADDRESS_STORAGE_BYTES`), that variable tree work is folded into ONE flat /// effective-byte figure priced at the full storage rate so it tracks the rate as it evolves, /// rather than modelled per replaced byte: 466,760 credits is 17.0 effective bytes at 27,400 -/// credits/byte, and 20 leaves headroom for the path growing by about a node (roughly 0.7 -/// effective bytes) each time the identity count doubles. The pool-total update is not priced -/// separately, exactly as for the other pool-paid transitions. See +/// credits/byte. With the reads and the pool-total update, the whole flat part of a +/// `ShieldFromIdentity` measured 1,076,280 credits at protocol version 14 (39.3 effective bytes; +/// the transition's metered fee is exactly linear in the action count on a fresh pool), and 60 +/// leaves headroom for the paths growing by about a node (roughly 0.7 effective bytes per write) +/// each time the identity count doubles. See /// [`compute_minimum_shielded_fee::compute_shielded_identity_balance_write_fee`]. -pub const SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES: u64 = 20; +pub const SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES: u64 = 60; + +/// Per-action component (in effective bytes at the per-byte storage rate) the `ShieldFromIdentity` +/// admission floor adds on top of the per-action allowance of [`compute_minimum_shielded_fee`] +/// (`shielded_storage_bytes_per_action`). +/// +/// That allowance is sized for the storage a pool-paid spend books for its note and nullifier; the +/// spends' flat fee absorbs the writes' processing in its compute part. `ShieldFromIdentity` is +/// metered, and from protocol version 14 records the nullifier of each action as the spends do, so +/// its floor has to stand in for that processing as well. Measured at protocol version 14 on an +/// empty pool, each action costs 16,474,000 credits: 8,451,000 of note storage, 5,886,000 of +/// nullifier storage and 2,137,000 of processing, 601 effective bytes against the allowance's 550. +/// The nullifier write's processing grows with the nullifier tree, by about 46,000 credits (1.7 +/// effective bytes) each time the recorded nullifiers double (459,900 credits for the first write, +/// 1,371,280 at 131,072), so 120 covers each action up to about 2^30 recorded nullifiers. See +/// [`compute_minimum_shielded_fee::compute_shielded_identity_balance_write_fee`]. +pub const SHIELDED_IDENTITY_ACTION_WRITE_STORAGE_BYTES: u64 = 120; /// Common Orchard bundle parameters shared across all shielded transition types. /// diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_identity_transition/state_transition_estimated_fee_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_identity_transition/state_transition_estimated_fee_validation.rs index 8ebba0e2676..84068d7d96c 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_identity_transition/state_transition_estimated_fee_validation.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_identity_transition/state_transition_estimated_fee_validation.rs @@ -12,9 +12,9 @@ use platform_version::version::PlatformVersion; impl StateTransitionEstimatedFeeValidation for ShieldFromIdentityTransition { /// The stateless floor is the conservative complete fee: the shielded compute fee, - /// the per-action note storage allowance, and the flat identity balance write - /// allowance (`compute_shielded_identity_balance_write_fee`). Storage and processing - /// of the note and identity writes are metered by GroveDB, so the authoritative + /// the per-action note and nullifier write allowance, and the flat identity balance + /// write allowance (`compute_shielded_identity_balance_write_fee`). Storage and processing + /// of the note, nullifier and identity writes are metered by GroveDB, so the authoritative /// funding gate is still the identity-paid fee validation of the execution event; /// this floor exists so an identity that could not pay the complete fee is refused /// before the expensive Orchard proof verification runs. diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/mod.rs index 9c840487a4a..ec20133669c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/mod.rs @@ -16,6 +16,7 @@ use crate::error::execution::ExecutionError; use crate::error::Error; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; use crate::execution::validation::state_transition::shield::transform_into_action::v0::ShieldStateTransitionTransformIntoActionValidationV0; +use crate::execution::validation::state_transition::shield::transform_into_action::v1::ShieldStateTransitionTransformIntoActionValidationV1; use crate::platform_types::platform::PlatformRef; use crate::rpc::core::CoreRPCLike; @@ -60,9 +61,17 @@ impl StateTransitionShieldTransitionActionTransformer for ShieldTransition { execution_context, platform_version, ), + 1 => self.transform_into_action_v1( + platform.drive, + tx, + inputs_with_remaining_balance, + block_info, + execution_context, + platform_version, + ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "shield transition: transform_into_action".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs index c4ac3e78ce4..77a5d7dcb2e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs @@ -1386,6 +1386,269 @@ mod tests { } } + // ========================================== + // NULLIFIER TESTS (every revealed nullifier is recorded once) + // ========================================== + + mod nullifiers { + use super::*; + use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; + use crate::execution::validation::state_transition::shield::StateTransitionShieldTransitionActionTransformer; + use crate::execution::validation::state_transition::state_transitions::test_helpers::{ + build_outputs_only_bundle, has_recorded_nullifier, process_transition_and_commit, + setup_platform_at_protocol_version, shielded_transfer_errors_revealing, + OutputsOnlyBundle, + }; + use crate::platform_types::platform::PlatformRef; + use crate::rpc::core::MockCoreRPCLike; + use crate::test::helpers::setup::TempPlatform; + use dpp::block::block_info::BlockInfo; + use dpp::validation::ConsensusValidationResult; + use dpp::version::DefaultForPlatformVersion; + use drive::state_transition_action::StateTransitionAction; + use std::sync::OnceLock; + + /// One proven bundle shared by the tests of this module; each runs on a fresh platform. + fn bundle() -> &'static OutputsOnlyBundle { + static BUNDLE: OnceLock = OnceLock::new(); + BUNDLE.get_or_init(|| build_outputs_only_bundle(5_000)) + } + + /// A shield of `bundle` from `address` at `nonce`, signed by `signer`. + async fn signed_shield( + signer: &TestAddressSigner, + address: PlatformAddress, + nonce: AddressNonce, + bundle: &OutputsOnlyBundle, + ) -> StateTransition { + let mut inputs = BTreeMap::new(); + inputs.insert(address, (nonce, bundle.amount + dash_to_credits!(0.01))); + + let mut st = StateTransition::Shield(ShieldTransition::V0(ShieldTransitionV0 { + inputs: inputs.clone(), + actions: bundle.actions.clone(), + amount: bundle.amount, + anchor: bundle.anchor, + proof: bundle.proof.clone(), + binding_signature: bundle.binding_signature, + fee_strategy: AddressFundsFeeStrategy::from(vec![ + AddressFundsFeeStrategyStep::DeductFromInput(0), + ]), + user_fee_increase: 0, + input_witnesses: vec![], + })); + let signable_bytes = st.signable_bytes().expect("should compute signable bytes"); + let mut witnesses: Vec = Vec::with_capacity(inputs.len()); + for input in inputs.keys() { + witnesses.push( + signer + .sign_create_witness(input, &signable_bytes) + .await + .expect("should sign"), + ); + } + if let StateTransition::Shield(ShieldTransition::V0(ref mut v0)) = st { + v0.input_witnesses = witnesses; + } + st + } + + /// A funded address on `platform` and the signer for it. + fn funded_address( + platform: &mut TempPlatform, + ) -> (TestAddressSigner, PlatformAddress) { + let mut signer = TestAddressSigner::new(); + let address = signer.add_p2pkh([1u8; 32]); + setup_address_with_balance(platform, address, 0, dash_to_credits!(1.0)); + (signer, address) + } + + /// Runs the shield transform directly on a shield whose two actions reveal the same + /// nullifier. Block processing verifies the proof in the stateless step before this + /// transform, and no Orchard builder emits a bundle that repeats a nullifier, so the + /// check is reached here directly, as block processing reaches it after the proof. + fn transform_shield_repeating_a_nullifier_inside_the_bundle( + platform: &TempPlatform, + platform_version: &PlatformVersion, + ) -> ConsensusValidationResult { + let address = create_platform_address(1); + let requested = dash_to_credits!(0.1); + let mut inputs = BTreeMap::new(); + inputs.insert(address, (1 as AddressNonce, requested)); + let transition = ShieldTransition::V0(ShieldTransitionV0 { + inputs, + actions: vec![ + create_dummy_serialized_action(), + create_dummy_serialized_action(), + ], + amount: 1_000, + anchor: [0u8; 32], + proof: vec![0u8; 100], + binding_signature: [0u8; 64], + fee_strategy: AddressFundsFeeStrategy::from(vec![ + AddressFundsFeeStrategyStep::DeductFromInput(0), + ]), + user_fee_increase: 0, + input_witnesses: vec![], + }); + // The address held exactly `requested`, all of it debited by the balance check. + let mut remaining = BTreeMap::new(); + remaining.insert(address, (1 as AddressNonce, 0)); + + let platform_state = platform.state.load(); + let platform_ref = PlatformRef { + drive: &platform.drive, + state: &platform_state, + config: &platform.config, + core_rpc: &platform.core_rpc, + }; + let mut execution_context = + StateTransitionExecutionContext::default_for_platform_version(platform_version) + .expect("execution context"); + transition + .transform_into_action_for_shield_transition( + &platform_ref, + remaining, + &BlockInfo::default(), + &mut execution_context, + None, + ) + .expect("expected the shield transform to run") + } + + #[tokio::test] + async fn should_record_the_nullifiers_a_shield_reveals() { + let platform_version = PlatformVersion::latest(); + let mut platform = setup_platform(); + let (signer, address) = funded_address(&mut platform); + let bundle = bundle(); + + let st = signed_shield(&signer, address, 1, bundle).await; + let result = process_transition_and_commit(&platform, st, platform_version); + + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + for nullifier in bundle.nullifiers() { + assert!( + has_recorded_nullifier(&platform, &nullifier), + "every nullifier the shield reveals must be recorded" + ); + } + } + + #[tokio::test] + async fn should_refuse_a_shield_repeating_a_recorded_nullifier() { + let platform_version = PlatformVersion::latest(); + let mut platform = setup_platform(); + let (signer, address) = funded_address(&mut platform); + let bundle = bundle(); + + let first = signed_shield(&signer, address, 1, bundle).await; + let result = process_transition_and_commit(&platform, first, platform_version); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + // The same bundle again, under the next address nonce: the same note, the same + // nullifiers. + let repeat = signed_shield(&signer, address, 2, bundle).await; + let result = process_transition_and_commit(&platform, repeat, platform_version); + + let first_nullifier = bundle.nullifiers()[0]; + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::StateError(StateError::NullifierAlreadySpentError(e)) + )] if e.nullifier() == first_nullifier + ); + } + + #[test] + fn should_refuse_a_nullifier_repeated_inside_the_bundle() { + let platform_version = PlatformVersion::latest(); + let platform = setup_platform(); + + let result = transform_shield_repeating_a_nullifier_inside_the_bundle( + &platform, + platform_version, + ); + + assert!(!result.has_data(), "no action for a refused shield"); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::StateError(StateError::NullifierAlreadySpentError(e))] + if e.nullifier() == create_dummy_serialized_action().nullifier + ); + } + + #[tokio::test] + async fn should_refuse_a_spend_revealing_a_nullifier_a_shield_recorded() { + let platform_version = PlatformVersion::latest(); + let mut platform = setup_platform(); + let (signer, address) = funded_address(&mut platform); + let bundle = bundle(); + + let st = signed_shield(&signer, address, 1, bundle).await; + let result = process_transition_and_commit(&platform, st, platform_version); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + assert!( + shielded_transfer_errors_revealing(&platform, [0xEE; 32]).is_empty(), + "a spend revealing an unrecorded nullifier passes the spend-side check" + ); + let recorded = bundle.nullifiers()[1]; + assert_matches!( + shielded_transfer_errors_revealing(&platform, recorded).as_slice(), + [ConsensusError::StateError(StateError::NullifierAlreadySpentError(e))] + if e.nullifier() == recorded + ); + } + + #[tokio::test] + async fn should_neither_record_nor_check_nullifiers_before_protocol_version_14() { + let platform_version = PlatformVersion::get(13).expect("protocol version 13"); + let mut platform = setup_platform_at_protocol_version(13); + let (signer, address) = funded_address(&mut platform); + let bundle = bundle(); + + let first = signed_shield(&signer, address, 1, bundle).await; + let result = process_transition_and_commit(&platform, first, platform_version); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + for nullifier in bundle.nullifiers() { + assert!( + !has_recorded_nullifier(&platform, &nullifier), + "protocol version 13 records no shield nullifier" + ); + } + + let repeat = signed_shield(&signer, address, 2, bundle).await; + let result = process_transition_and_commit(&platform, repeat, platform_version); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }], + "protocol version 13 accepts a repeated shield" + ); + + let result = transform_shield_repeating_a_nullifier_inside_the_bundle( + &platform, + platform_version, + ); + assert!( + result.is_valid_with_data(), + "protocol version 13 does not check a nullifier repeated inside the bundle" + ); + } + } + // ========================================== // CREDIT CONSERVATION TESTS (sum-tree balance) // ========================================== diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/transform_into_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/transform_into_action/mod.rs index 9a1925de7fc..008be12cc67 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/transform_into_action/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/transform_into_action/mod.rs @@ -1 +1,2 @@ pub(crate) mod v0; +pub(crate) mod v1; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/transform_into_action/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/transform_into_action/v0/mod.rs index 02b2e79c9f0..15322829922 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/transform_into_action/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/transform_into_action/v0/mod.rs @@ -74,7 +74,7 @@ use std::collections::{BTreeMap, BTreeSet}; /// below is the load-bearing defense if a future path ever reaches this transform without that /// invariant — it would otherwise mint credits (pool credited `shield_amount`, addresses debited /// less). -fn reallocate_inputs_for_shield_amount( +pub(super) fn reallocate_inputs_for_shield_amount( transition: &ShieldTransitionV0, inputs_with_remaining_balance: BTreeMap, shield_amount: Credits, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/transform_into_action/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/transform_into_action/v1/mod.rs new file mode 100644 index 00000000000..596eedfa96a --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/transform_into_action/v1/mod.rs @@ -0,0 +1,102 @@ +use super::v0::reallocate_inputs_for_shield_amount; +use crate::error::Error; +use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; +use crate::execution::validation::state_transition::state_transitions::shielded_common::{ + read_pool_total_balance, validate_nullifiers, +}; +use dpp::address_funds::PlatformAddress; +use dpp::block::block_info::BlockInfo; +use dpp::fee::Credits; +use dpp::prelude::{AddressNonce, ConsensusValidationResult}; +use dpp::state_transition::shield_transition::ShieldTransition; +use dpp::version::PlatformVersion; +use drive::drive::Drive; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::shielded::shield::ShieldTransitionAction; +use drive::state_transition_action::StateTransitionAction; +use std::collections::BTreeMap; + +pub(in crate::execution::validation::state_transition::state_transitions::shield) trait ShieldStateTransitionTransformIntoActionValidationV1 +{ + fn transform_into_action_v1( + &self, + drive: &Drive, + transaction: TransactionArg, + inputs_with_remaining_balance: BTreeMap, + block_info: &BlockInfo, + execution_context: &mut StateTransitionExecutionContext, + platform_version: &PlatformVersion, + ) -> Result, Error>; +} + +impl ShieldStateTransitionTransformIntoActionValidationV1 for ShieldTransition { + /// Version 0, plus the nullifier checks every spend runs: each action of an outputs-only + /// bundle still reveals a nullifier (that of a dummy spend, which becomes the new note's + /// `rho`), and the action's operations record it, so a nullifier repeated inside the bundle + /// or already recorded by an earlier spend or shield is refused with + /// `NullifierAlreadySpentError`. + /// + /// The refusal is unpaid, like the spends' and like a failed proof of this transition: + /// the proof was verified by the stateless step before this transform, and no action is + /// returned, so proposers leave the transition out of the block and CheckTx refuses it. + fn transform_into_action_v1( + &self, + drive: &Drive, + transaction: TransactionArg, + inputs_with_remaining_balance: BTreeMap, + _block_info: &BlockInfo, + _execution_context: &mut StateTransitionExecutionContext, + platform_version: &PlatformVersion, + ) -> Result, Error> { + let ShieldTransition::V0(transition_v0) = self; + let shield_amount: Credits = transition_v0.amount; + + // The shared address-balance validation debits the FULL per-input `requested` + // (a max contribution), but the shielded pool only receives `shield_amount`. + // Reallocate so the total debited across inputs equals exactly `shield_amount`, + // leaving the excess in the source addresses. This keeps credits conserved + // (addresses lose `shield_amount` + fee, pool gains `shield_amount`). + let inputs_with_remaining_balance = reallocate_inputs_for_shield_amount( + transition_v0, + inputs_with_remaining_balance, + shield_amount, + )?; + + // Read current shielded pool state from GroveDB. + // + // Shield is metered + compute: GroveDB meters the real storage and processing of the + // note/nullifier writes, and the execution-event layer adds the shielded COMPUTE fee + // `compute_shielded_verification_fee(num_actions)` (proof verification + per-action + // processing, which prices the per-action nullifier check) on top as + // `additional_fixed_fee_cost`. These validation reads are not charged separately, as in + // the spends. + let mut drive_operations = vec![]; + let current_total_balance = + read_pool_total_balance(drive, transaction, &mut drive_operations, platform_version)?; + + // Validate nullifiers: intra-bundle duplicates + already recorded in state + let nullifiers: Vec<[u8; 32]> = transition_v0 + .actions + .iter() + .map(|action| action.nullifier) + .collect(); + if let Some(consensus_error) = validate_nullifiers( + drive, + &nullifiers, + transaction, + &mut drive_operations, + platform_version, + )? { + return Ok(consensus_error); + } + + let result = ShieldTransitionAction::try_from_transition( + self, + inputs_with_remaining_balance, + shield_amount, + current_total_balance, + ); + + Ok(result.map(|action| action.into())) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/mod.rs index d847d011652..9b852bf9d7e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/mod.rs @@ -10,6 +10,7 @@ use crate::error::execution::ExecutionError; use crate::error::Error; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; use crate::execution::validation::state_transition::shield_from_asset_lock::transform_into_action::v0::ShieldFromAssetLockStateTransitionTransformIntoActionValidationV0; +use crate::execution::validation::state_transition::shield_from_asset_lock::transform_into_action::v1::ShieldFromAssetLockStateTransitionTransformIntoActionValidationV1; use crate::execution::validation::state_transition::ValidationMode; use crate::platform_types::platform::PlatformRef; use crate::platform_types::check_tx_proof_verifier::CheckTxProofVerifier; @@ -66,9 +67,17 @@ impl StateTransitionShieldFromAssetLockTransitionActionTransformer check_tx_proof_verifier, tx, ), + 1 => self.transform_into_action_v1( + platform, + signable_bytes, + validation_mode, + execution_context, + check_tx_proof_verifier, + tx, + ), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "shield from asset lock transition: transform_into_action".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/tests.rs index a4337de4fb7..aad77568f2e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/tests.rs @@ -755,6 +755,316 @@ mod tests { } } + // ========================================== + // NULLIFIER TESTS (every revealed nullifier is recorded once) + // ========================================== + + mod nullifiers { + use super::*; + use crate::execution::validation::state_transition::state_transitions::test_helpers::{ + build_outputs_only_bundle, has_recorded_nullifier, process_transition_and_commit, + setup_platform_at_protocol_version, shielded_transfer_errors_revealing, + OutputsOnlyBundle, + }; + use std::sync::OnceLock; + + /// One proven bundle shared by the tests of this module; each runs on a fresh platform. + fn bundle() -> &'static OutputsOnlyBundle { + static BUNDLE: OnceLock = OnceLock::new(); + BUNDLE.get_or_init(|| build_outputs_only_bundle(5_000)) + } + + /// A shield of `actions` (with the fields of `bundle`) from a fresh asset lock drawn + /// from `seed`; the surplus goes to a platform address. + fn signed_shield( + seed: u64, + actions: Vec, + bundle: &OutputsOnlyBundle, + ) -> StateTransition { + let mut rng = StdRng::seed_from_u64(seed); + let (asset_lock_proof, asset_lock_pk) = create_asset_lock_proof_with_key(&mut rng); + create_signed_shield_from_asset_lock_transition( + asset_lock_proof, + &asset_lock_pk, + actions, + bundle.amount, + bundle.anchor, + bundle.proof.clone(), + bundle.binding_signature, + ) + } + + /// Two dummy actions revealing the same nullifier, with unprovable proof bytes. + fn shield_repeating_a_nullifier_inside_the_bundle() -> StateTransition { + let mut rng = StdRng::seed_from_u64(9); + let (asset_lock_proof, asset_lock_pk) = create_asset_lock_proof_with_key(&mut rng); + create_signed_shield_from_asset_lock_transition( + asset_lock_proof, + &asset_lock_pk, + vec![ + create_dummy_serialized_action(), + create_dummy_serialized_action(), + ], + 5000, + [42u8; 32], + vec![0u8; 100], + [0u8; 64], + ) + } + + #[test] + fn should_record_the_nullifiers_a_shield_reveals() { + let platform_version = PlatformVersion::latest(); + let platform = setup_platform(); + let bundle = bundle(); + + let st = signed_shield(1, bundle.actions.clone(), bundle); + let result = process_transition_and_commit(&platform, st, platform_version); + + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + for nullifier in bundle.nullifiers() { + assert!( + has_recorded_nullifier(&platform, &nullifier), + "every nullifier the shield reveals must be recorded" + ); + } + } + + #[test] + fn should_refuse_a_shield_repeating_a_recorded_nullifier() { + let platform_version = PlatformVersion::latest(); + let platform = setup_platform(); + let bundle = bundle(); + + let first = signed_shield(1, bundle.actions.clone(), bundle); + let result = process_transition_and_commit(&platform, first, platform_version); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + // The same bundle from a second asset lock: the same note, the same nullifiers. + let repeat = signed_shield(2, bundle.actions.clone(), bundle); + let result = process_transition_and_commit(&platform, repeat, platform_version); + + let first_nullifier = bundle.nullifiers()[0]; + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::StateError(StateError::NullifierAlreadySpentError(e)) + )] if e.nullifier() == first_nullifier + ); + } + + /// The check runs before the proof, so the unprovable bundle is refused for its nullifier, + /// unpaid, instead of burning the asset lock for its proof. + #[test] + fn should_refuse_a_nullifier_repeated_inside_the_bundle() { + let platform_version = PlatformVersion::latest(); + let platform = setup_platform(); + + let result = process_transition( + &platform, + shield_repeating_a_nullifier_inside_the_bundle(), + platform_version, + ); + + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::StateError(StateError::NullifierAlreadySpentError(e)) + )] if e.nullifier() == create_dummy_serialized_action().nullifier + ); + } + + #[test] + fn should_refuse_a_spend_revealing_a_nullifier_a_shield_recorded() { + let platform_version = PlatformVersion::latest(); + let platform = setup_platform(); + let bundle = bundle(); + + let st = signed_shield(1, bundle.actions.clone(), bundle); + let result = process_transition_and_commit(&platform, st, platform_version); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + assert!( + shielded_transfer_errors_revealing(&platform, [0xEE; 32]).is_empty(), + "a spend revealing an unrecorded nullifier passes the spend-side check" + ); + let recorded = bundle.nullifiers()[1]; + assert_matches!( + shielded_transfer_errors_revealing(&platform, recorded).as_slice(), + [ConsensusError::StateError(StateError::NullifierAlreadySpentError(e))] + if e.nullifier() == recorded + ); + } + + /// The flat `pool_fee` (`compute_minimum_shielded_fee` + the asset-lock base cost) must + /// still cover the estimated cost `validate_fees_of_event` checks it against, now that the + /// operations also record one nullifier per action. Checked at every action count up to + /// `max_shielded_transition_actions` (the 20 KiB size limit stops real transitions near 6), + /// with the signature verification the transform meters over a maximum-size transition. + #[test] + fn should_cover_the_recorded_nullifiers_with_the_flat_pool_fee() { + use crate::execution::types::execution_event::ExecutionEvent; + use crate::execution::types::execution_operation::signature_verification_operation::SignatureVerificationOperation; + use crate::execution::types::execution_operation::{ + ValidationOperation, SHA256_BLOCK_SIZE, + }; + use crate::execution::types::state_transition_execution_context::{ + StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0, + }; + use dpp::block::block_info::BlockInfo; + use dpp::block::epoch::Epoch; + use dpp::fee::default_costs::CachedEpochIndexFeeVersions; + use dpp::identity::KeyType; + use dpp::shielded::compute_minimum_shielded_fee; + use dpp::state_transition::StateTransitionEstimatedFeeValidation; + use dpp::version::DefaultForPlatformVersion; + use drive::state_transition_action::shielded::shield_from_asset_lock::v0::ShieldFromAssetLockTransitionActionV0; + use drive::state_transition_action::shielded::shield_from_asset_lock::ShieldFromAssetLockTransitionAction; + use drive::state_transition_action::shielded::ShieldedActionNote; + use drive::state_transition_action::StateTransitionAction; + use drive::util::batch::drive_op_batch::ShieldedPoolOperationType; + use drive::util::batch::DriveOperation; + + let platform_version = PlatformVersion::latest(); + let platform = setup_platform(); + let StateTransition::ShieldFromAssetLock(transition) = + shield_repeating_a_nullifier_inside_the_bundle() + else { + panic!("expected a shield from asset lock"); + }; + let asset_lock_base_cost = transition + .calculate_min_required_fee(platform_version) + .expect("asset lock base cost"); + let max_actions = platform_version + .system_limits + .max_shielded_transition_actions as usize; + let signable_blocks = (platform_version.system_limits.max_state_transition_size + / SHA256_BLOCK_SIZE as u64) as u16; + + for num_actions in 1..=max_actions { + let notes: Vec = (0..num_actions as u8) + .map(|i| ShieldedActionNote { + nullifier: [i; 32], + cmx: [i.wrapping_add(100); 32], + cv_net: [i.wrapping_add(200); 32], + encrypted_note: vec![0x77; 216], + }) + .collect(); + let pool_fee = compute_minimum_shielded_fee(num_actions, platform_version) + .expect("shielded fee") + + asset_lock_base_cost; + let shield_amount = 5_000; + let action = ShieldFromAssetLockTransitionAction::V0( + ShieldFromAssetLockTransitionActionV0 { + asset_lock_outpoint: [num_actions as u8; 36], + asset_lock_value_to_be_consumed: shield_amount + pool_fee, + signable_bytes_hasher: [0; 32], + shield_amount, + notes, + current_total_balance: 0, + surplus_output: None, + surplus_amount: 0, + }, + ); + + let mut execution_context = + StateTransitionExecutionContext::default_for_platform_version(platform_version) + .expect("execution context"); + execution_context.add_operation(ValidationOperation::DoubleSha256(signable_blocks)); + execution_context.add_operation(ValidationOperation::SignatureVerification( + SignatureVerificationOperation::new(KeyType::ECDSA_HASH160), + )); + let event = ExecutionEvent::create_from_state_transition_action( + StateTransitionAction::ShieldFromAssetLockAction(action), + None, + &Epoch::new(0).expect("epoch"), + execution_context, + platform_version, + ) + .expect("execution event"); + let ExecutionEvent::PaidFromAssetLockToPool { ref operations, .. } = event else { + panic!("expected a PaidFromAssetLockToPool event"); + }; + assert!( + operations.iter().any(|op| matches!( + op, + DriveOperation::ShieldedPoolOperation( + ShieldedPoolOperationType::InsertNullifiers { .. } + ) + )), + "the event records the nullifiers" + ); + + let result = platform + .platform + .validate_fees_of_event( + &event, + &BlockInfo::default(), + None, + platform_version, + &CachedEpochIndexFeeVersions::new(), + ) + .expect("fee validation runs"); + assert!( + result.is_valid(), + "{num_actions} actions: the pool fee {pool_fee} must cover the estimate: {:?}", + result.errors + ); + } + } + + #[test] + fn should_neither_record_nor_check_nullifiers_before_protocol_version_14() { + let platform_version = PlatformVersion::get(13).expect("protocol version 13"); + let platform = setup_platform_at_protocol_version(13); + let bundle = bundle(); + + let first = signed_shield(1, bundle.actions.clone(), bundle); + let result = process_transition_and_commit(&platform, first, platform_version); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + for nullifier in bundle.nullifiers() { + assert!( + !has_recorded_nullifier(&platform, &nullifier), + "protocol version 13 records no shield nullifier" + ); + } + + let repeat = signed_shield(2, bundle.actions.clone(), bundle); + let result = process_transition_and_commit(&platform, repeat, platform_version); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }], + "protocol version 13 accepts a repeated shield" + ); + + // Without the check, the unprovable bundle reaches its proof and pays the penalty. + let result = process_transition( + &platform, + shield_repeating_a_nullifier_inside_the_bundle(), + platform_version, + ); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::InvalidShieldedProofError(_)), + .. + }] + ); + } + } + // ========================================== // SECURITY AUDIT TESTS // ========================================== diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/transform_into_action/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/transform_into_action/mod.rs index e88e88c052f..0f74e3c39fc 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/transform_into_action/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/transform_into_action/mod.rs @@ -1 +1,2 @@ pub(in crate::execution::validation::state_transition::state_transitions::shield_from_asset_lock) mod v0; +pub(in crate::execution::validation::state_transition::state_transitions::shield_from_asset_lock) mod v1; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/transform_into_action/v1/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/transform_into_action/v1/mod.rs new file mode 100644 index 00000000000..36e48b6c327 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/transform_into_action/v1/mod.rs @@ -0,0 +1,427 @@ +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::execution_operation::signature_verification_operation::SignatureVerificationOperation; +use crate::execution::types::execution_operation::{ValidationOperation, SHA256_BLOCK_SIZE}; +use crate::execution::types::state_transition_execution_context::{ + StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0, +}; +use crate::execution::validation::state_transition::common::asset_lock::proof::validate::AssetLockProofValidation; +use crate::execution::validation::state_transition::common::asset_lock::transaction::fetch_asset_lock_transaction_output_sync::fetch_asset_lock_transaction_output_sync; +use crate::execution::validation::state_transition::state_transitions::shielded_common::{ + read_pool_total_balance, reconstruct_and_verify_bundle, validate_nullifiers, + FLAGS_OUTPUTS_ONLY, +}; +use crate::execution::validation::state_transition::ValidationMode; +use crate::platform_types::platform::PlatformRef; +use crate::platform_types::check_tx_proof_verifier::CheckTxProofVerifier; +use crate::platform_types::platform_state::PlatformStateV0Methods; +use crate::rpc::core::CoreRPCLike; +use dpp::asset_lock::reduced_asset_lock_value::{AssetLockValue, AssetLockValueGettersV0}; +use dpp::balances::credits::CREDITS_PER_DUFF; +use dpp::consensus::basic::identity::IdentityAssetLockTransactionOutPointNotEnoughBalanceError; +use dpp::consensus::basic::state_transition::ShieldedImplicitFeeCapExceededError; +use dpp::consensus::signature::{BasicECDSAError, SignatureError}; +use dpp::consensus::state::state_error::StateError; +use dpp::dashcore::hashes::Hash; +use dpp::dashcore::{signer, ScriptBuf, Txid}; +use dpp::fee::Credits; +use dpp::shielded::compute_minimum_shielded_fee; +use dpp::identity::state_transition::AssetLockProved; +use dpp::identity::KeyType; +use dpp::platform_value::{Bytes32, Bytes36}; +use dpp::prelude::ConsensusValidationResult; +use dpp::state_transition::shield_from_asset_lock_transition::ShieldFromAssetLockTransition; +use dpp::state_transition::signable_bytes_hasher::SignableBytesHasher; +use dpp::state_transition::{StateTransitionEstimatedFeeValidation, StateTransitionSingleSigned}; +use drive::grovedb::TransactionArg; +use drive::state_transition_action::shielded::shield_from_asset_lock::ShieldFromAssetLockTransitionAction; +use drive::state_transition_action::system::partially_use_asset_lock_action::PartiallyUseAssetLockActionV0; +use drive::state_transition_action::system::partially_use_asset_lock_action::PartiallyUseAssetLockAction; +use drive::state_transition_action::StateTransitionAction; + +pub(in crate::execution::validation::state_transition::state_transitions::shield_from_asset_lock) trait ShieldFromAssetLockStateTransitionTransformIntoActionValidationV1 +{ + fn transform_into_action_v1( + &self, + platform: &PlatformRef, + signable_bytes: Vec, + validation_mode: ValidationMode, + execution_context: &mut StateTransitionExecutionContext, + check_tx_proof_verifier: Option<&CheckTxProofVerifier>, + tx: TransactionArg, + ) -> Result, Error>; +} + +impl ShieldFromAssetLockStateTransitionTransformIntoActionValidationV1 + for ShieldFromAssetLockTransition +{ + /// Version 0, plus the nullifier checks every spend runs (Step 8b): each action of an + /// outputs-only bundle still reveals a nullifier (that of a dummy spend, which becomes the + /// new note's `rho`), and the action's operations record it, so a nullifier repeated inside + /// the bundle or already recorded by an earlier spend or shield is refused with + /// `NullifierAlreadySpentError`. + /// + /// The check runs after the asset lock, its signature, the funding floor and the fee cap + /// have passed, so a re-broadcast of an executed transition still reports its consumed + /// asset lock, and before the Orchard proof, so the refusal costs no proof verification. + /// Like the fee cap refusal it is unpaid: the asset lock stays unconsumed. + fn transform_into_action_v1( + &self, + platform: &PlatformRef, + signable_bytes: Vec, + validation_mode: ValidationMode, + execution_context: &mut StateTransitionExecutionContext, + check_tx_proof_verifier: Option<&CheckTxProofVerifier>, + tx: TransactionArg, + ) -> Result, Error> { + let platform_version = platform.state.current_platform_version()?; + + // Step 1: Get the shield amount (value_balance is u64, the amount entering the pool) + let shield_amount: Credits = match self { + ShieldFromAssetLockTransition::V0(v0) => v0.value_balance, + }; + + // Step 3: Calculate minimum required fee from platform_version. + // `required_balance` (= asset_lock_base_cost, `albc`) is the L1 asset-lock-proof viability + // floor, reused unchanged in Step 4's `validate(...)` call below. + let required_balance = self.calculate_min_required_fee(platform_version)?; + + // Step 3b: Compute the flat fee charged to the fee pools. ShieldFromAssetLock does strictly + // more work than a transparent shield, so it pays BOTH the shielded operation cost and the + // L1 asset-lock processing cost, mirroring the established asset-lock fee composition + // (operation base cost + asset_lock_base_cost): + // + // pool_fee = compute_minimum_shielded_fee(num_actions) [Halo2 proof + per-action] + // + albc [asset-lock processing] + // + // Set outside GroveDB; booked at the execution-event layer (PaidFromAssetLockToPool). + let albc = required_balance; + let num_actions = match self { + ShieldFromAssetLockTransition::V0(v0) => v0.actions.len(), + }; + let shielded_fee = compute_minimum_shielded_fee(num_actions, platform_version)?; + let pool_fee = + shielded_fee + .checked_add(albc) + .ok_or(Error::Execution(ExecutionError::Overflow( + "shielded fee + asset_lock_base_cost overflow in shield_from_asset_lock", + )))?; + // The asset lock must cover `shield_amount + pool_fee`, so the surplus is always >= 0. + let required_lock_value = shield_amount.checked_add(pool_fee).ok_or(Error::Execution( + ExecutionError::Overflow("shield_amount + pool_fee overflow in shield_from_asset_lock"), + ))?; + + let signable_bytes_len = signable_bytes.len(); + + let mut signable_bytes_hasher = SignableBytesHasher::Bytes(signable_bytes); + + // Step 4: Validate asset lock proof + let asset_lock_proof_validation = if validation_mode != ValidationMode::NoValidation { + AssetLockProved::asset_lock_proof(self).validate( + platform, + &mut signable_bytes_hasher, + required_balance, + validation_mode, + tx, + platform_version, + )? + } else { + ConsensusValidationResult::new() + }; + + if !asset_lock_proof_validation.is_valid() { + return Ok(ConsensusValidationResult::new_with_errors( + asset_lock_proof_validation.errors, + )); + } + + // Step 5: Fetch/validate asset lock transaction output + let mut needs_signature_verification = true; + + let asset_lock_value_to_be_consumed = if asset_lock_proof_validation.has_data() { + let asset_lock_value = asset_lock_proof_validation.into_data()?; + // There is no need to recheck signatures on recheck tx + if validation_mode == ValidationMode::RecheckTx { + needs_signature_verification = false; + } + asset_lock_value + } else { + let tx_out_validation = fetch_asset_lock_transaction_output_sync( + platform.core_rpc, + AssetLockProved::asset_lock_proof(self), + platform_version, + )?; + + if !tx_out_validation.is_valid() { + return Ok(ConsensusValidationResult::new_with_errors( + tx_out_validation.errors, + )); + } + + let tx_out = tx_out_validation.into_data()?; + + let tx_out_credit_value = tx_out.value.saturating_mul(CREDITS_PER_DUFF); + + // Verify locked amount >= shield_amount + pool_fee (so the surplus is >= 0). This is an + // early reject; Step 7 re-applies the same floor authoritatively on every path. + let required_total = required_lock_value; + if tx_out_credit_value < required_total { + let asset_lock_proof = AssetLockProved::asset_lock_proof(self); + return Ok(ConsensusValidationResult::new_with_error( + IdentityAssetLockTransactionOutPointNotEnoughBalanceError::new( + asset_lock_proof + .out_point() + .map(|outpoint| outpoint.txid) + .unwrap_or(Txid::all_zeros()), + asset_lock_proof.output_index() as usize, + tx_out_credit_value, + tx_out_credit_value, + required_total, + ) + .into(), + )); + } + + if validation_mode == ValidationMode::RecheckTx { + needs_signature_verification = false; + } + + let initial_balance_amount = tx_out.value.saturating_mul(CREDITS_PER_DUFF); + AssetLockValue::new( + initial_balance_amount, + tx_out.script_pubkey.0, + initial_balance_amount, + vec![], + platform_version, + )? + }; + + // Step 6: Verify ECDSA signature over signable_bytes (P2PKH from asset lock output) + if needs_signature_verification { + let tx_out_script_pubkey = + ScriptBuf(asset_lock_value_to_be_consumed.tx_out_script().clone()); + + let public_key_hash = tx_out_script_pubkey + .p2pkh_public_key_hash_bytes() + .ok_or_else(|| { + Error::Execution(ExecutionError::CorruptedCachedState( + "the script inside the state must be a p2pkh".to_string(), + )) + })?; + + let block_count = + (signable_bytes_len / SHA256_BLOCK_SIZE as usize).min(u16::MAX as usize) as u16; + + execution_context.add_operation(ValidationOperation::DoubleSha256(block_count)); + execution_context.add_operation(ValidationOperation::SignatureVerification( + SignatureVerificationOperation::new(KeyType::ECDSA_HASH160), + )); + + if let Err(e) = signer::verify_hash_signature( + signable_bytes_hasher.hash_bytes().as_slice(), + self.signature().as_slice(), + public_key_hash, + ) { + return Ok(ConsensusValidationResult::new_with_error( + SignatureError::BasicECDSAError(BasicECDSAError::new(e.to_string())).into(), + )); + } + } + + // Step 7: The remaining asset-lock balance must cover `shield_amount + pool_fee`. + // This is the AUTHORITATIVE funding floor: the fresh-fetch, cached `AssetLockValue`, and + // recheck paths all converge here, so a lock that under-funds the fee can never reach the + // action (which would mint credits at booking). It also guarantees `surplus >= 0` below. + let remaining_credit_value = asset_lock_value_to_be_consumed.remaining_credit_value(); + if remaining_credit_value < required_lock_value { + let asset_lock_proof = AssetLockProved::asset_lock_proof(self); + return Ok(ConsensusValidationResult::new_with_error( + IdentityAssetLockTransactionOutPointNotEnoughBalanceError::new( + asset_lock_proof + .out_point() + .map(|outpoint| outpoint.txid) + .unwrap_or(Txid::all_zeros()), + asset_lock_proof.output_index() as usize, + remaining_credit_value, + remaining_credit_value, + required_lock_value, + ) + .into(), + )); + } + + // Step 7b: Compute the surplus and reject an over-cap implicit donation BEFORE the expensive + // Orchard proof verification (Step 9). The surplus is fully derivable here — it depends only + // on the (now-validated) `remaining_credit_value`, the `shield_amount`, the flat `pool_fee`, + // and whether a `surplus_output` is set — none of which depend on the ZK proof or the pool + // read. Rejecting an over-cap transition here avoids ~100ms of Halo2 verification for a + // transition that can never succeed. + // + // Distribute the fully-consumed lock: + // shield_amount -> shielded pool + // pool_fee -> fee pools + // surplus -> surplus_output address (when set), else folded into the fee pools. + // `surplus >= 0` is guaranteed by the Step 7 floor; `checked_sub` is a defensive guard. + let surplus = remaining_credit_value + .checked_sub(shield_amount) + .and_then(|v| v.checked_sub(pool_fee)) + .ok_or(Error::Execution(ExecutionError::Overflow( + "asset lock value underflow computing shield_from_asset_lock surplus (should be guarded by the Step 7 funding floor)", + )))?; + + let surplus_output = match self { + ShieldFromAssetLockTransition::V0(v0) => &v0.surplus_output, + }; + + // When no surplus_output is set, the surplus is donated to the fee pools — but only up to + // `shielded_implicit_fee_cap`, so a client cannot accidentally forfeit a large remainder. + if surplus_output.is_none() { + let implicit_fee_cap = platform_version + .drive_abci + .validation_and_processing + .event_constants + .shielded_implicit_fee_cap; + if surplus > implicit_fee_cap { + return Ok(ConsensusValidationResult::new_with_error( + ShieldedImplicitFeeCapExceededError::new(surplus, implicit_fee_cap).into(), + )); + } + } + + // Step 8: Read current shielded pool total balance from GroveDB. + // + // ShieldFromAssetLock pays the flat `pool_fee` (computed in Step 3b) at the execution-event + // layer (PaidFromAssetLockToPool). We do NOT derive a GroveDB fee from these read operations + // — the flat fee subsumes them. + let mut drive_operations = vec![]; + let current_total_balance = + read_pool_total_balance(platform.drive, tx, &mut drive_operations, platform_version)?; + + // Step 8b: Validate nullifiers: intra-bundle duplicates + already recorded in state. + // The flat `pool_fee` subsumes these reads too, as it does the pool read above. + let nullifiers: Vec<[u8; 32]> = match self { + ShieldFromAssetLockTransition::V0(v0) => { + v0.actions.iter().map(|action| action.nullifier).collect() + } + }; + if let Some(consensus_error) = validate_nullifiers( + platform.drive, + &nullifiers, + tx, + &mut drive_operations, + platform_version, + )? { + return Ok(consensus_error); + } + + // CheckTx admits the expensive proof only after the asset lock, its + // signature, funding, fee cap, and current pool state have all passed. + // Proposal and block processing pass `None` and retain the existing + // penalty action when proof verification fails. + let _check_tx_permit = match check_tx_proof_verifier { + Some(verifier) => Some(verifier.try_acquire(num_actions).ok_or(Error::Execution( + ExecutionError::CheckTxProofVerificationBusy, + ))?), + None => None, + }; + + // Step 9: Verify Orchard ZK proof via reconstruct_and_verify_bundle() + // Use EMPTY extra_sighash_data -- no transparent binding needed since + // the asset lock proof authenticates the source of funds. + let (actions, anchor, proof, binding_signature) = match self { + ShieldFromAssetLockTransition::V0(v0) => ( + &v0.actions, + &v0.anchor, + v0.proof.as_slice(), + &v0.binding_signature, + ), + }; + + if let Err(e) = reconstruct_and_verify_bundle( + actions, + FLAGS_OUTPUTS_ONLY, + -(shield_amount as i64), + anchor, + proof, + binding_signature, + &[], // No transparent fields to bind for shield_from_asset_lock + ) { + // Step 10: ZK proof failed -- consume asset lock with penalty (PartiallyUseAssetLockAction) + let penalty = platform_version + .drive_abci + .validation_and_processing + .penalties + .shielded_proof_verification_failure; + + let desired_used_credits = penalty + .checked_add(execution_context.fee_cost(platform_version)?.processing_fee) + .ok_or(Error::Execution(ExecutionError::Overflow( + "processing fee overflow in shield_from_asset_lock penalty calculation", + )))?; + + let asset_lock_outpoint = AssetLockProved::asset_lock_proof(self) + .out_point() + .ok_or_else(|| { + Error::Execution(ExecutionError::CorruptedCachedState( + "asset lock proof must have an outpoint after validation".to_string(), + )) + })?; + + let signable_bytes_hash: Bytes32 = signable_bytes_hasher.into_hashed_bytes(); + let mut previous_transaction_hashes = + asset_lock_value_to_be_consumed.used_tags_ref().clone(); + previous_transaction_hashes.push(signable_bytes_hash); + + let remaining_after_penalty = + remaining_credit_value.saturating_sub(desired_used_credits); + let used_credits = std::cmp::min(remaining_credit_value, desired_used_credits); + + let partially_use_action = + PartiallyUseAssetLockAction::from(PartiallyUseAssetLockActionV0 { + asset_lock_outpoint: Bytes36::new(asset_lock_outpoint.into()), + initial_credit_value: asset_lock_value_to_be_consumed.initial_credit_value(), + previous_transaction_hashes, + asset_lock_script: asset_lock_value_to_be_consumed.tx_out_script().clone(), + remaining_credit_value: remaining_after_penalty, + used_credits, + user_fee_increase: 0, + inputs_with_remaining_balance: None, + fee_strategy: None, + }); + + return Ok(ConsensusValidationResult::new_with_data_and_errors( + StateTransitionAction::PartiallyUseAssetLockAction(partially_use_action), + vec![StateError::InvalidShieldedProofError(e).into()], + )); + } + + // Step 11: Build the successful action + let asset_lock_outpoint = AssetLockProved::asset_lock_proof(self) + .out_point() + .ok_or_else(|| { + Error::Execution(ExecutionError::CorruptedCachedState( + "asset lock proof must have an outpoint after validation".to_string(), + )) + })?; + + let asset_lock_value_credits = asset_lock_value_to_be_consumed.remaining_credit_value(); + let signable_bytes_hash: [u8; 32] = signable_bytes_hasher.into_hashed_bytes().0; + + // The action routes `surplus_amount` to `surplus_output` (when set); otherwise 0 and the + // surplus folds into the fee pools at the execution event. The surplus and the implicit-fee + // cap were already computed and enforced in Step 7b (before proof verification). + let surplus_amount = if surplus_output.is_some() { surplus } else { 0 }; + + let result = ShieldFromAssetLockTransitionAction::try_from_transition( + self, + asset_lock_outpoint.into(), + asset_lock_value_credits, + signable_bytes_hash, + shield_amount, + current_total_balance, + surplus_amount, + ); + + Ok(result.map(|action| action.into())) + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/tests.rs index 68c0a7bbcd5..72cdd580419 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/tests.rs @@ -1102,4 +1102,269 @@ mod tests { assert_eq!(proven_balance, identity_balance(&platform, &identity)); assert!(proven_balance < initial_balance - amount); } + + // ========================================== + // Nullifiers: every revealed nullifier is recorded once + // ========================================== + + mod nullifiers { + use super::*; + use crate::execution::validation::state_transition::state_transitions::test_helpers::{ + build_outputs_only_bundle, has_recorded_nullifier, shielded_transfer_errors_revealing, + OutputsOnlyBundle, + }; + use std::sync::OnceLock; + + /// One proven bundle shared by the tests of this module; each runs on a fresh platform. + fn bundle() -> &'static OutputsOnlyBundle { + static BUNDLE: OnceLock = OnceLock::new(); + BUNDLE.get_or_init(|| build_outputs_only_bundle(5_000)) + } + + fn proven(bundle: &OutputsOnlyBundle) -> ProvenBundle { + ProvenBundle { + actions: bundle.actions.clone(), + amount: bundle.amount, + anchor: bundle.anchor, + proof: bundle.proof.clone(), + binding_signature: bundle.binding_signature, + } + } + + /// A funded identity on `platform` and its signer. + fn funded_identity( + platform: &mut TempPlatform, + seed: u8, + platform_version: &PlatformVersion, + ) -> (Identity, SimpleSigner) { + let mut rng = StdRng::seed_from_u64(seed as u64); + let (identity, signer) = create_identity_with_transfer_key( + [seed; 32], + dash_to_credits!(1.0), + &mut rng, + platform_version, + ); + add_identity_to_drive(platform, &identity); + (identity, signer) + } + + fn pool_total(platform: &TempPlatform) -> u64 { + platform + .drive + .read_shielded_pool_total_balance(None, &mut vec![], PlatformVersion::latest()) + .expect("fetch pool total") + } + + #[tokio::test] + async fn should_record_the_nullifiers_a_shield_reveals() { + let platform_version = PlatformVersion::latest(); + let mut platform = setup_platform(); + let (identity, signer) = funded_identity(&mut platform, 71, platform_version); + let bundle = bundle(); + + let st = create_signed_transition( + &identity, + &signer, + proven(bundle), + bundle.amount, + 1, + 0, + platform_version, + ) + .await; + let result = process_transition_and_commit(&platform, st, platform_version); + + let charged = match result.execution_results().as_slice() { + [StateTransitionExecutionResult::SuccessfulExecution { fee_result, .. }] => { + fee_result.total_base_fee() + } + other => panic!("expected a successful shield, got {other:?}"), + }; + for nullifier in bundle.nullifiers() { + assert!( + has_recorded_nullifier(&platform, &nullifier), + "every nullifier the shield reveals must be recorded" + ); + } + // The admission floor is the client's estimate of the complete fee; the nullifier + // writes must stay inside it. + let floor = dpp::shielded::compute_shielded_identity_balance_write_fee( + bundle.actions.len(), + platform_version, + ) + .expect("floor"); + assert!( + charged <= floor, + "the charged fee ({charged}) must stay within the admission floor ({floor})" + ); + } + + /// The repeat is refused at CheckTx, and in a block it is a paid failure: the identity + /// nonce is consumed and the reads are charged, and nothing reaches the pool. + #[tokio::test] + async fn should_refuse_a_shield_repeating_a_recorded_nullifier() { + let platform_version = PlatformVersion::latest(); + let mut platform = setup_platform(); + let (identity, signer) = funded_identity(&mut platform, 72, platform_version); + let bundle = bundle(); + + let first = create_signed_transition( + &identity, + &signer, + proven(bundle), + bundle.amount, + 1, + 0, + platform_version, + ) + .await; + let result = process_transition_and_commit(&platform, first, platform_version); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + let balance_before = identity_balance(&platform, &identity); + let pool_before = pool_total(&platform); + + // The same bundle under the next nonce: the same note, the same nullifiers. + let repeat = create_signed_transition( + &identity, + &signer, + proven(bundle), + bundle.amount, + 2, + 0, + platform_version, + ) + .await; + let first_nullifier = bundle.nullifiers()[0]; + + let raw = repeat.serialize_to_bytes().expect("serialize"); + { + let state = platform.state.load(); + let platform_ref = PlatformRef { + drive: &platform.drive, + state: &state, + config: &platform.config, + core_rpc: &platform.core_rpc, + }; + let check = platform + .check_tx( + &raw, + CheckTxLevel::FirstTimeCheck, + &platform_ref, + platform_version, + ) + .expect("check_tx runs"); + assert_matches!( + check.errors.as_slice(), + [ConsensusError::StateError(StateError::NullifierAlreadySpentError(e))] + if e.nullifier() == first_nullifier + ); + } + + let result = process_transition_and_commit(&platform, repeat, platform_version); + let charged = match result.execution_results().as_slice() { + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::NullifierAlreadySpentError(e)), + actual_fees, + .. + }] if e.nullifier() == first_nullifier => actual_fees.total_base_fee(), + other => panic!("expected a paid nullifier refusal, got {other:?}"), + }; + assert!(charged > 0, "the refusal is paid"); + assert_eq!( + identity_balance(&platform, &identity), + balance_before - charged + ); + assert_eq!(identity_nonce(&platform, &identity), 2); + assert_eq!( + pool_total(&platform), + pool_before, + "nothing reaches the pool" + ); + } + + /// The check runs before the proof, so the unprovable bundle is refused for its nullifier, + /// without the proof-failure penalty. + #[tokio::test] + async fn should_refuse_a_nullifier_repeated_inside_the_bundle() { + let platform_version = PlatformVersion::latest(); + let mut platform = setup_platform(); + let (identity, signer) = funded_identity(&mut platform, 73, platform_version); + + let mut repeating = dummy_bundle(); + repeating.actions = vec![ + create_dummy_serialized_action(), + create_dummy_serialized_action(), + ]; + let st = create_signed_transition( + &identity, + &signer, + repeating, + 1_000, + 1, + 0, + platform_version, + ) + .await; + let result = process_transition_and_commit(&platform, st, platform_version); + + let penalty = platform_version + .drive_abci + .validation_and_processing + .penalties + .shielded_proof_verification_failure; + let charged = match result.execution_results().as_slice() { + [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::NullifierAlreadySpentError(e)), + actual_fees, + .. + }] if e.nullifier() == create_dummy_serialized_action().nullifier => { + actual_fees.total_base_fee() + } + other => panic!("expected a paid nullifier refusal, got {other:?}"), + }; + assert!( + charged < penalty, + "the refusal ({charged}) must not charge the proof-failure penalty ({penalty})" + ); + assert_eq!(identity_nonce(&platform, &identity), 1); + } + + #[tokio::test] + async fn should_refuse_a_spend_revealing_a_nullifier_a_shield_recorded() { + let platform_version = PlatformVersion::latest(); + let mut platform = setup_platform(); + let (identity, signer) = funded_identity(&mut platform, 74, platform_version); + let bundle = bundle(); + + let st = create_signed_transition( + &identity, + &signer, + proven(bundle), + bundle.amount, + 1, + 0, + platform_version, + ) + .await; + let result = process_transition_and_commit(&platform, st, platform_version); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + assert!( + shielded_transfer_errors_revealing(&platform, [0xEE; 32]).is_empty(), + "a spend revealing an unrecorded nullifier passes the spend-side check" + ); + let recorded = bundle.nullifiers()[1]; + assert_matches!( + shielded_transfer_errors_revealing(&platform, recorded).as_slice(), + [ConsensusError::StateError(StateError::NullifierAlreadySpentError(e))] + if e.nullifier() == recorded + ); + } + } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/transform_into_action/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/transform_into_action/v0/mod.rs index 8697e52d3ab..bb415fa98bd 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/transform_into_action/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/transform_into_action/v0/mod.rs @@ -4,7 +4,7 @@ use crate::execution::types::state_transition_execution_context::{ StateTransitionExecutionContext, StateTransitionExecutionContextMethodsV0, }; use crate::execution::validation::state_transition::state_transitions::shielded_common::{ - read_pool_total_balance, reconstruct_and_verify_bundle, FLAGS_OUTPUTS_ONLY, + read_pool_total_balance, reconstruct_and_verify_bundle, validate_nullifiers, FLAGS_OUTPUTS_ONLY, }; use crate::platform_types::check_tx_proof_verifier::CheckTxProofVerifier; use dpp::block::block_info::BlockInfo; @@ -41,6 +41,16 @@ impl ShieldFromIdentityStateTransitionTransformIntoActionValidationV0 /// so the action can bump it, and that read is metered into the execution context so /// the identity pays for it. /// + /// The nullifier each action reveals (that of a dummy spend, which becomes the new + /// note's `rho`) is checked as the spends check theirs: a nullifier repeated inside the + /// bundle or already recorded by an earlier spend or shield is refused with + /// `NullifierAlreadySpentError`, before proof verification. Those reads are metered with + /// the pool read, and the refusal returns a `BumpIdentityNonceAction`, a paid failure + /// like the failed proof below but without its penalty. The action's operations record + /// the nullifiers. This generation is selected only from protocol version 14, the first + /// version whose `is_allowed` admits the transition, so these checks were edited into it + /// in place. + /// /// The Orchard proof is verified HERE rather than in the shared stateless proof step /// (like `ShieldFromAssetLock`), because a failed proof must not be a free rejection: /// the transition is identity-signed, so a rejection that left the nonce and balance @@ -60,11 +70,23 @@ impl ShieldFromIdentityStateTransitionTransformIntoActionValidationV0 check_tx_proof_verifier: Option<&CheckTxProofVerifier>, platform_version: &PlatformVersion, ) -> Result, Error> { + let ShieldFromIdentityTransition::V0(v0) = self; + let mut drive_operations = vec![]; let current_total_balance = read_pool_total_balance(drive, transaction, &mut drive_operations, platform_version)?; - let pool_read_fee = Drive::calculate_fee( + // Validate nullifiers: intra-bundle duplicates + already recorded in state. + let nullifiers: Vec<[u8; 32]> = v0.actions.iter().map(|action| action.nullifier).collect(); + let nullifier_refusal = validate_nullifiers( + drive, + &nullifiers, + transaction, + &mut drive_operations, + platform_version, + )?; + + let state_read_fee = Drive::calculate_fee( None, Some(drive_operations), &block_info.epoch, @@ -72,9 +94,20 @@ impl ShieldFromIdentityStateTransitionTransformIntoActionValidationV0 platform_version, None, )?; - execution_context.add_operation(ValidationOperation::PrecalculatedOperation(pool_read_fee)); + execution_context + .add_operation(ValidationOperation::PrecalculatedOperation(state_read_fee)); - let ShieldFromIdentityTransition::V0(v0) = self; + // Refused before CheckTx returns and before the proof: CheckTx rejects the result + // (it carries an error), block processing charges the reads and bumps the nonce. + if let Some(refusal) = nullifier_refusal { + let bump_action = StateTransitionAction::BumpIdentityNonceAction( + BumpIdentityNonceAction::from_borrowed_shield_from_identity_transition(self), + ); + return Ok(ConsensusValidationResult::new_with_data_and_errors( + bump_action, + refusal.errors, + )); + } // CheckTx must first build and meter the successful action so its full // identity-balance fee gate runs before expensive proof work. Its caller diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/test_helpers.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/test_helpers.rs index 1e19a410e66..fa7383fdca2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/test_helpers.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/test_helpers.rs @@ -5,11 +5,13 @@ //! balance setup utilities, and shielded pool state helpers. use crate::config::{PlatformConfig, PlatformTestConfig}; +use crate::execution::validation::state_transition::state_transitions::shielded_common::compute_platform_sighash; use crate::platform_types::state_transitions_processing_result::StateTransitionsProcessingResult; use crate::rpc::core::MockCoreRPCLike; use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; use dpp::address_funds::{AddressWitness, PlatformAddress}; use dpp::block::block_info::BlockInfo; +use dpp::consensus::ConsensusError; use dpp::dashcore::blockdata::script::ScriptBuf; use dpp::dashcore::hashes::{sha256, Hash}; use dpp::dashcore::secp256k1::{PublicKey as RawPublicKey, Secp256k1, SecretKey as RawSecretKey}; @@ -20,6 +22,7 @@ use dpp::prelude::AddressNonce; use dpp::serialization::PlatformSerializable; use dpp::shielded::SerializedAction; use dpp::state_transition::StateTransition; +use dpp::version::ProtocolVersion; use dpp::ProtocolError; use drive::drive::shielded::paths::{ shielded_credit_pool_anchors_path, shielded_credit_pool_notes_path, @@ -435,6 +438,179 @@ pub fn setup_platform() -> TempPlatform { .set_genesis_state() } +/// [`setup_platform`] at `protocol_version` instead of the latest one, so that validation reads +/// the tables of that version from the platform state. +pub fn setup_platform_at_protocol_version( + protocol_version: ProtocolVersion, +) -> TempPlatform { + let platform_config = PlatformConfig { + testing_configs: PlatformTestConfig { + disable_instant_lock_signature_verification: true, + ..Default::default() + }, + ..Default::default() + }; + + TestPlatformBuilder::new() + .with_config(platform_config) + .with_initial_protocol_version(protocol_version) + .build_with_mock_rpc() + .set_genesis_state() +} + +/// [`process_transition`] with the block transaction COMMITTED, so that a later transition and +/// the reads of the test see what the transition wrote. +pub fn process_transition_and_commit( + platform: &TempPlatform, + transition: StateTransition, + platform_version: &PlatformVersion, +) -> StateTransitionsProcessingResult { + let transition_bytes = transition + .serialize_to_bytes() + .expect("should serialize transition"); + let platform_state = platform.state.load(); + let transaction = platform.drive.grove.start_transaction(); + + let result = platform + .platform + .process_raw_state_transitions( + &vec![transition_bytes], + &platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process state transition"); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit transaction"); + result +} + +/// Whether `nullifier` is recorded in the committed nullifier tree of the shielded pool. +pub fn has_recorded_nullifier( + platform: &TempPlatform, + nullifier: &[u8; 32], +) -> bool { + platform + .drive + .has_nullifier(nullifier, None, &mut vec![], PlatformVersion::latest()) + .expect("expected to read the nullifier tree") +} + +/// The fields of a proven outputs-only Orchard bundle, ready to put into a `Shield`, +/// `ShieldFromAssetLock` or `ShieldFromIdentity` transition. +#[derive(Clone)] +pub struct OutputsOnlyBundle { + /// The serialized actions; each reveals the nullifier of a dummy spend. + pub actions: Vec, + /// The amount entering the pool (the negated value balance). + pub amount: u64, + /// The anchor of the empty tree. + pub anchor: [u8; 32], + /// The Halo 2 proof. + pub proof: Vec, + /// The binding signature. + pub binding_signature: [u8; 64], +} + +impl OutputsOnlyBundle { + /// The nullifiers the actions reveal. + pub fn nullifiers(&self) -> Vec<[u8; 32]> { + self.actions.iter().map(|action| action.nullifier).collect() + } +} + +/// Builds and proves an outputs-only bundle paying `value` to a fixed recipient, with no extra +/// sighash data (what every shielding transition signs). Orchard pads it to two actions. +pub fn build_outputs_only_bundle(value: u64) -> OutputsOnlyBundle { + use grovedb_commitment_tree::{ + Anchor, Builder, BundleType, Flags as OrchardFlags, FullViewingKey, NoteValue, Scope, + SpendingKey, + }; + use rand::rngs::OsRng; + + let mut rng = OsRng; + let sk = SpendingKey::from_bytes([0u8; 32]).unwrap(); + let recipient = FullViewingKey::from(&sk).address_at(0u32, Scope::External); + + let mut builder = Builder::::new( + BundleType::Transactional { + flags: OrchardFlags::SPENDS_DISABLED, + bundle_required: false, + }, + Anchor::empty_tree(), + ); + builder + .add_output(None, recipient, NoteValue::from_raw(value), [0u8; 36]) + .unwrap(); + let (unauthorized, _) = builder.build::(&mut rng).unwrap().unwrap(); + let bundle_commitment: [u8; 32] = unauthorized.commitment().into(); + let sighash = compute_platform_sighash(&bundle_commitment, &[]); + let proven = unauthorized + .create_proof(get_proving_key(), &mut rng) + .unwrap(); + let bundle = proven.apply_signatures(rng, sighash, &[]).unwrap(); + + let (actions, _flags, value_balance, anchor, proof, binding_signature) = + serialize_authorized_bundle_with_flags(&bundle); + assert!(value_balance < 0, "value must enter the pool"); + OutputsOnlyBundle { + actions, + amount: value_balance.unsigned_abs(), + anchor, + proof, + binding_signature, + } +} + +/// The consensus errors the transform every shielded transfer runs returns for a transfer +/// revealing `nullifier`. +/// +/// The transfer carries dummy proof bytes: block processing verifies the proof in the stateless +/// step before this transform, and no one can prove a spend of a note whose nullifier is a +/// shield's dummy-spend nullifier, so the spend-side nullifier check is reached here directly. +/// Records an anchor for the transfer to name first; its fee is 0 so the pool covers it. +pub fn shielded_transfer_errors_revealing( + platform: &TempPlatform, + nullifier: [u8; 32], +) -> Vec { + use crate::execution::validation::state_transition::shielded_transfer::StateTransitionShieldedTransferTransitionActionTransformer; + use crate::platform_types::platform::PlatformRef; + use dpp::state_transition::shielded_transfer_transition::v0::ShieldedTransferTransitionV0; + use dpp::state_transition::shielded_transfer_transition::ShieldedTransferTransition; + + let anchor = [42u8; 32]; + insert_anchor_into_state(platform, &anchor); + + let mut action = create_dummy_serialized_action(); + action.nullifier = nullifier; + let transfer = ShieldedTransferTransition::V0(ShieldedTransferTransitionV0 { + actions: vec![action], + value_balance: 0, + anchor, + proof: vec![0u8; 100], + binding_signature: [0u8; 64], + }); + + let platform_state = platform.state.load(); + let platform_ref = PlatformRef { + drive: &platform.drive, + state: &platform_state, + config: &platform.config, + core_rpc: &platform.core_rpc, + }; + transfer + .transform_into_action_for_shielded_transfer_transition(&platform_ref, None) + .expect("expected the transfer transform to run") + .errors +} + /// Execute a state transition through the full processing pipeline and return the result. pub fn process_transition( platform: &TempPlatform, diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/mod.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/mod.rs index ed4eae5143a..e9ccaf6fbb6 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/mod.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/mod.rs @@ -11,8 +11,10 @@ use crate::state_transition_action::shielded::ShieldedActionNote; use crate::util::batch::drive_op_batch::ShieldedPoolOperationType; use crate::util::batch::DriveOperation; -/// Insert nullifiers into the permanent tree (double-spend prevention) and -/// per-block sync storage (catch-up RPCs). +/// Insert nullifiers into the permanent nullifier tree, which refuses a nullifier +/// revealed twice: every spend records the nullifiers its actions reveal, and from +/// protocol version 14 so does every shield (an outputs-only bundle's actions +/// reveal the nullifiers of dummy spends, each the `rho` of its new note). pub(super) fn insert_nullifiers<'a>( ops: &mut Vec>, notes: &[ShieldedActionNote], diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/shield_from_asset_lock_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/shield_from_asset_lock_transition.rs index 2ee2ac44946..9247eb1dc17 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/shield_from_asset_lock_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/shield_from_asset_lock_transition.rs @@ -1,4 +1,4 @@ -use super::{insert_notes, update_balance}; +use super::{insert_notes, insert_nullifiers, update_balance}; use crate::error::drive::DriveError; use crate::error::Error; use crate::state_transition_action::action_convert_to_operations::DriveHighLevelOperationConverter; @@ -86,10 +86,79 @@ impl DriveHighLevelOperationConverter for ShieldFromAssetLockTransitionAction { Ok(ops) } }, + // Version 1 also records the nullifier each action reveals, as the spends do, so a + // later shield or spend revealing it again is refused. + 1 => match self { + ShieldFromAssetLockTransitionAction::V0(v0) => { + let mut ops: Vec> = Vec::new(); + + // 1. Add the FULL consumed asset-lock value to system credits. It is + // distributed below: `shield_amount` -> shielded pool, `surplus_amount` -> + // `surplus_output` address (when set), and the remainder -> fee pools + // (computed by the execution event as consumed - shield_amount - surplus). + ops.push(DriveOperation::SystemOperation( + SystemOperationType::AddToSystemCredits { + amount: v0.asset_lock_value_to_be_consumed, + }, + )); + + // 2. Record asset lock as consumed (prevent replay) + let asset_lock_value = AssetLockValue::new( + v0.asset_lock_value_to_be_consumed, + vec![], // tx_out_script not needed for shielded + 0, // remaining_credit_value = 0 (fully consumed) + vec![], // no used tags for shielded + platform_version, + ) + .map_err(|e| Error::Protocol(Box::new(e)))?; + ops.push(DriveOperation::SystemOperation( + SystemOperationType::AddUsedAssetLock { + asset_lock_outpoint: v0.asset_lock_outpoint.into(), + asset_lock_value, + }, + )); + + // 3. Route the surplus to the optional platform-address output. When + // `surplus_output` is `None`, `surplus_amount` is 0 and the surplus is + // instead folded into the fee pools by the execution event. Conservation: + // AddToSystemCredits(consumed) == shield_amount (pool) + surplus_amount + // (address) + fee (pools). + if let Some(surplus_address) = v0.surplus_output { + if v0.surplus_amount > 0 { + ops.push(DriveOperation::AddressFundsOperation( + AddressFundsOperationType::AddBalanceToAddress { + address: surplus_address, + balance_to_add: v0.surplus_amount, + }, + )); + } + } + + // 4. Insert each nullifier (known to not exist after validation) + insert_nullifiers(&mut ops, &v0.notes); + + // 5. Insert notes into CommitmentTree + insert_notes(&mut ops, &v0.notes); + + // 6. Update total balance + let new_total_balance = + v0.current_total_balance + .checked_add(v0.shield_amount) + .ok_or_else(|| { + Error::Drive(DriveError::CorruptedDriveState( + "shielded pool total balance overflow when adding shield_from_asset_lock amount" + .to_string(), + )) + })?; + update_balance(&mut ops, new_total_balance); + + Ok(ops) + } + }, version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "ShieldFromAssetLockTransitionAction::into_high_level_drive_operations" .to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } @@ -128,17 +197,48 @@ mod tests { } #[test] - fn test_produces_system_credits_asset_lock_notes_and_balance_ops() { + fn should_record_the_action_nullifiers_before_the_notes() { let action = make_action(); let epoch = Epoch::new(0).unwrap(); let platform_version = PlatformVersion::latest(); + let ops = action + .into_high_level_drive_operations(&epoch, platform_version) + .expect("expected operations"); + + // AddToSystemCredits + AddUsedAssetLock + InsertNullifiers + InsertNote (1) + // + UpdateTotalBalance + assert_eq!(ops.len(), 5); + match &ops[2] { + DriveOperation::ShieldedPoolOperation( + ShieldedPoolOperationType::InsertNullifiers { nullifiers }, + ) => assert_eq!(nullifiers, &vec![[0x11; 32]]), + other => panic!("expected InsertNullifiers, got {:?}", other), + } + assert!(matches!( + &ops[3], + DriveOperation::ShieldedPoolOperation(ShieldedPoolOperationType::InsertNote { .. }) + )); + } + + #[test] + fn should_not_record_nullifiers_before_protocol_version_14() { + let action = make_action(); + let epoch = Epoch::new(0).unwrap(); + let platform_version = PlatformVersion::get(13).expect("protocol version 13"); + let ops = action .into_high_level_drive_operations(&epoch, platform_version) .expect("expected operations"); // AddToSystemCredits + AddUsedAssetLock + InsertNote (1) + UpdateTotalBalance assert_eq!(ops.len(), 4); + assert!(!ops.iter().any(|op| matches!( + op, + DriveOperation::ShieldedPoolOperation( + ShieldedPoolOperationType::InsertNullifiers { .. } + ) + ))); } #[test] @@ -237,8 +337,9 @@ mod tests { .into_high_level_drive_operations(&epoch, platform_version) .expect("expected operations"); - // AddToSystemCredits + AddUsedAssetLock + AddBalanceToAddress + InsertNote + UpdateTotalBalance - assert_eq!(ops.len(), 5); + // AddToSystemCredits + AddUsedAssetLock + AddBalanceToAddress + InsertNullifiers + // + InsertNote + UpdateTotalBalance + assert_eq!(ops.len(), 6); // The FULL consumed lock is added to system credits (not just the shield amount). match &ops[0] { @@ -278,7 +379,7 @@ mod tests { .into_high_level_drive_operations(&epoch, platform_version) .expect("expected operations"); - assert_eq!(ops.len(), 4); + assert_eq!(ops.len(), 5); assert!( !ops.iter() .any(|op| matches!(op, DriveOperation::AddressFundsOperation(_))), diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/shield_from_identity_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/shield_from_identity_transition.rs index 2746f560045..9d305889904 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/shield_from_identity_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/shield_from_identity_transition.rs @@ -1,4 +1,4 @@ -use super::{insert_notes, update_balance}; +use super::{insert_notes, insert_nullifiers, update_balance}; use crate::error::drive::DriveError; use crate::error::Error; use crate::state_transition_action::action_convert_to_operations::DriveHighLevelOperationConverter; @@ -39,6 +39,11 @@ impl DriveHighLevelOperationConverter for ShieldFromIdentityTransitionAction { }), ]; + // Record the nullifier each action reveals, as the spends do. Edited in + // place: `ShieldFromIdentity` is refused by `is_allowed` before protocol + // version 14, so no earlier version reaches this generation. + insert_nullifiers(&mut ops, &v0.notes); + insert_notes(&mut ops, &v0.notes); let new_total_balance = v0 @@ -102,10 +107,11 @@ mod tests { } #[test] - fn test_nonce_then_balance_removal_then_notes_then_pool_total() { + fn test_nonce_then_balance_removal_then_nullifiers_then_notes_then_pool_total() { let ops = ops(make_action(2)); - // UpdateIdentityNonce + RemoveFromIdentityBalance + 2 InsertNote + UpdateTotalBalance - assert_eq!(ops.len(), 5); + // UpdateIdentityNonce + RemoveFromIdentityBalance + InsertNullifiers + 2 InsertNote + // + UpdateTotalBalance + assert_eq!(ops.len(), 6); assert!(matches!( &ops[0], IdentityOperation(IdentityOperationType::UpdateIdentityNonce { nonce: 7, .. }) @@ -119,6 +125,12 @@ mod tests { )); assert!(matches!( &ops[2], + DriveOperation::ShieldedPoolOperation(ShieldedPoolOperationType::InsertNullifiers { + nullifiers + }) if nullifiers.len() == 2 + )); + assert!(matches!( + &ops[3], DriveOperation::ShieldedPoolOperation(ShieldedPoolOperationType::InsertNote { .. }) )); assert!(matches!( diff --git a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/shield_transition.rs b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/shield_transition.rs index cd5ab2ab649..c8533568e51 100644 --- a/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/shield_transition.rs +++ b/packages/rs-drive/src/state_transition_action/action_convert_to_operations/shielded/shield_transition.rs @@ -1,4 +1,4 @@ -use super::{insert_notes, update_balance}; +use super::{insert_notes, insert_nullifiers, update_balance}; use crate::error::drive::DriveError; use crate::error::Error; use crate::state_transition_action::action_convert_to_operations::DriveHighLevelOperationConverter; @@ -54,9 +54,47 @@ impl DriveHighLevelOperationConverter for ShieldTransitionAction { Ok(ops) } }, + // Version 1 also records the nullifier each action reveals, as the spends do, so a + // later shield or spend revealing it again is refused. + 1 => match self { + ShieldTransitionAction::V0(v0) => { + let mut ops: Vec> = Vec::new(); + + // 1. Debit each input address: set remaining balance + for (address, (nonce, remaining_balance)) in v0.inputs_with_remaining_balance { + ops.push(DriveOperation::AddressFundsOperation( + AddressFundsOperationType::SetBalanceToAddress { + address, + nonce, + balance: remaining_balance, + }, + )); + } + + // 2. Insert each nullifier (known to not exist after validation) + insert_nullifiers(&mut ops, &v0.notes); + + // 3. Insert notes into CommitmentTree + insert_notes(&mut ops, &v0.notes); + + // 4. Update total balance + let new_total_balance = v0 + .current_total_balance + .checked_add(v0.shield_amount) + .ok_or_else(|| { + Error::Drive(DriveError::CorruptedDriveState( + "shielded pool total balance overflow when adding shield amount" + .to_string(), + )) + })?; + update_balance(&mut ops, new_total_balance); + + Ok(ops) + } + }, version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "ShieldTransitionAction::into_high_level_drive_operations".to_string(), - known_versions: vec![0], + known_versions: vec![0, 1], received: version, })), } @@ -98,17 +136,48 @@ mod tests { } #[test] - fn test_produces_set_balance_insert_note_and_update_balance() { + fn should_record_the_action_nullifiers_before_the_notes() { let action = make_action(); let epoch = Epoch::new(0).unwrap(); let platform_version = PlatformVersion::latest(); + let ops = action + .into_high_level_drive_operations(&epoch, platform_version) + .expect("expected operations"); + + // SetBalanceToAddress (1 input) + InsertNullifiers + InsertNote (1 note) + // + UpdateTotalBalance + assert_eq!(ops.len(), 4); + match &ops[1] { + DriveOperation::ShieldedPoolOperation( + ShieldedPoolOperationType::InsertNullifiers { nullifiers }, + ) => assert_eq!(nullifiers, &vec![[0x11; 32]]), + other => panic!("expected InsertNullifiers, got {:?}", other), + } + assert!(matches!( + &ops[2], + DriveOperation::ShieldedPoolOperation(ShieldedPoolOperationType::InsertNote { .. }) + )); + } + + #[test] + fn should_not_record_nullifiers_before_protocol_version_14() { + let action = make_action(); + let epoch = Epoch::new(0).unwrap(); + let platform_version = PlatformVersion::get(13).expect("protocol version 13"); + let ops = action .into_high_level_drive_operations(&epoch, platform_version) .expect("expected operations"); // SetBalanceToAddress (1 input) + InsertNote (1 note) + UpdateTotalBalance assert_eq!(ops.len(), 3); + assert!(!ops.iter().any(|op| matches!( + op, + DriveOperation::ShieldedPoolOperation( + ShieldedPoolOperationType::InsertNullifiers { .. } + ) + ))); } #[test] diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs index 4ba0915044f..5b5b2299884 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs @@ -31,6 +31,9 @@ use crate::version::drive_abci_versions::drive_abci_validation_versions::{ // (DocumentPropertyConstraintViolatedError, 10422): the check runs inside dpp's // `DataContract::validate_document_properties` 0, which both call, and is inert // before this version through its own dpp gate. +// Shield and shield from asset lock transform_into_action 1 refuse an action +// nullifier repeated inside the bundle or already recorded in state +// (NullifierAlreadySpentError), the same check the spends run. // v9 remains unchanged for PROTOCOL_VERSION_13 chain replay. pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = DriveAbciValidationVersions { @@ -324,7 +327,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = identity_signatures: None, nonce: None, state: 0, - transform_into_action: 0, + transform_into_action: 1, // changed: nullifier checks }, shielded_transfer_state_transition: DriveAbciStateTransitionValidationVersion { basic_structure: Some(0), @@ -348,7 +351,7 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = identity_signatures: None, nonce: None, state: 0, - transform_into_action: 0, + transform_into_action: 1, // changed: nullifier checks }, shielded_withdrawal_state_transition: DriveAbciStateTransitionValidationVersion { basic_structure: Some(0), diff --git a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs index b4e6636725c..43d3e4897a4 100644 --- a/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_versions/drive_state_transition_method_versions/v4.rs @@ -59,8 +59,11 @@ pub const DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4: DriveStateTransitionMethodV address_funds_transfer_transition: 0, address_credit_withdrawal_transition: 0, address_funding_from_asset_lock_transition: 0, - shield_transition: 0, - shield_from_asset_lock_transition: 0, + // PROTOCOL_VERSION_14: v1 also records the nullifiers the + // shield's actions reveal, as the spends do. v0 stays for + // PROTOCOL_VERSION_12 and 13 chain replay. + shield_transition: 1, // changed + shield_from_asset_lock_transition: 1, // changed shielded_transfer_transition: 0, unshield_transition: 0, shielded_withdrawal_transition: 0, diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index fd6b4bd0dc1..837a032a0d6 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -1192,6 +1192,27 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// hash. Version 1 also reports a transition whose version is not active /// as `StateTransitionNotActiveError` (10603) instead of a decode failure. /// +/// 47. **Every revealed nullifier is recorded once**: each action of an +/// outputs-only Orchard bundle reveals a nullifier (that of a dummy spend, +/// which becomes the new note's `rho`). The spends already recorded and +/// checked theirs; now `Shield`, `ShieldFromAssetLock` and +/// `ShieldFromIdentity` do too. `transform_into_action` 1 of the shield and +/// the shield from asset lock (`DRIVE_ABCI_VALIDATION_VERSIONS_V10`), and +/// `transform_into_action` 0 of the shield from identity in place, refuse a +/// nullifier repeated inside the bundle or already recorded, with +/// `NullifierAlreadySpentError`: unpaid for the first two, as for the +/// spends, and a paid nonce bump for the identity-signed one. The +/// high-level operations of the shield and the shield from asset lock 1 +/// (`DRIVE_STATE_TRANSITION_METHOD_VERSIONS_V4`), and of the shield from +/// identity 0 in place, record the nullifiers. Recording them is metered +/// storage for the shield and the shield from identity; the shield from +/// asset lock's flat pool fee already prices a note and a nullifier write +/// per action. The shield from identity's admission floor +/// (`compute_shielded_identity_balance_write_fee` 0, the client's estimate +/// of its complete fee) gains 120 effective bytes per action and its flat +/// identity component grows from 20 to 60, so it still covers the metered +/// fee. Nullifiers revealed by shields before this version are not added. +/// /// The app-connect system contract (`SystemDataContract::AppConnect`, schema v1) /// carries only the wallet's `loginKeyResponse`: a flat indexOnly entry keyed by /// the app's ephemeral key hash and the responding identity, with the wallet's diff --git a/packages/rs-platform-wallet-ffi/src/shielded_send.rs b/packages/rs-platform-wallet-ffi/src/shielded_send.rs index f0570b466d4..1b7628baedb 100644 --- a/packages/rs-platform-wallet-ffi/src/shielded_send.rs +++ b/packages/rs-platform-wallet-ffi/src/shielded_send.rs @@ -195,9 +195,10 @@ fn shielded_fee_formula( /// - `2` → ShieldedWithdrawal (`compute_shielded_withdrawal_fee` — base + /// the flat Core withdrawal-document cost), /// - `3` → ShieldFromIdentity (`compute_shielded_identity_balance_write_fee`: -/// the conservative complete-fee floor, compute + note storage allowance + -/// identity write allowance, that consensus requires the identity to hold on -/// top of the amount; the exact fee is metered at execution), +/// the conservative complete-fee floor, compute + note and nullifier write +/// allowance + identity write allowance, that consensus requires the +/// identity to hold on top of the amount; the exact fee is metered at +/// execution), /// - `4` → IdentityTopUpFromShieldedPool (`compute_shielded_identity_top_up_fee`: /// base + the flat identity-balance write cost, carved from the value balance). /// From a07d5d2f55eb0ead05ac93f649ac2e29c28b4a8e Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sat, 26 Sep 2026 20:49:51 +0700 Subject: [PATCH 35/94] fix(drive-abci): report the boundary a transition version actually missed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `decode_raw_state_transitions` v1 reports a transition version outside its active range as a coded consensus refusal rather than a decode failure, and named the start of that range as `required_protocol_version`. That is correct when a version is not active yet. It is wrong once a version has been superseded: a `ShieldFromAssetLock` version 0 is active over protocol versions 12 to 13, so a node at 14 told the submitter "required protocol version is 12" — a version the chain is already past, when what the submitter must actually do is emit version 1. The range can be missed from either side, so the boundary reported is now its end when the current protocol version is above it, and its start otherwise. The shared error message drops its directional claim, which was only ever true on one side, and keeps what is useful in both: the transition type, the current protocol version and the boundary. That message is a thiserror Display over three unchanged fields, so no serialized bytes move. Operator-facing only — the transition is refused unpaid and removed from the block either way. `DRIVE_ABCI_METHOD_VERSIONS_V10` is selected by `v14.rs` alone, so the generation is unreleased and edited in place rather than superseded. Test would have caught this in CI: the assertion in `should_refuse_a_version_0_at_protocol_version_14_without_touching_its_asset_lock` pinning `required_protocol_version() == 13` fails against the unfixed decoder, which reports 12, and passes after the fix — ✖ before, ✔ after. The not-yet-active direction stays covered by `should_report_an_inactive_transition_as_not_active`. Co-Authored-By: Claude Opus 5 (1M context) --- .../state_transition_not_active_error.rs | 2 +- .../decode_raw_state_transitions/v1/mod.rs | 13 ++++++++++++- .../shield_from_asset_lock/tests.rs | 11 ++++++----- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/rs-dpp/src/errors/consensus/basic/state_transition/state_transition_not_active_error.rs b/packages/rs-dpp/src/errors/consensus/basic/state_transition/state_transition_not_active_error.rs index 3fe130463e6..dee0ede33e7 100644 --- a/packages/rs-dpp/src/errors/consensus/basic/state_transition/state_transition_not_active_error.rs +++ b/packages/rs-dpp/src/errors/consensus/basic/state_transition/state_transition_not_active_error.rs @@ -22,7 +22,7 @@ use thiserror::Error; DecodeUntrusted, )] #[error( - "State transition type {state_transition_type} is not yet active. Current protocol version is {current_protocol_version}, required protocol version is {required_protocol_version}" + "State transition type {state_transition_type} is not active at protocol version {current_protocol_version}. The boundary of its active range is protocol version {required_protocol_version}" )] #[platform_serialize(unversioned)] pub struct StateTransitionNotActiveError { diff --git a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/decode_raw_state_transitions/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/decode_raw_state_transitions/v1/mod.rs index deca72aa685..c9b12566b98 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/decode_raw_state_transitions/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/decode_raw_state_transitions/v1/mod.rs @@ -113,10 +113,21 @@ where current_protocol_version, }, ) => { + // The range can be missed from either side. Below its start, the + // start is the version to reach. Above its end, the end is the + // last protocol version that accepted these bytes; naming the + // start there would point at a version the chain is already past. + let boundary = + if current_protocol_version > *active_version_range.end() { + *active_version_range.end() + } else { + *active_version_range.start() + }; + let consensus_error = StateTransitionNotActiveError::new( state_transition_type, current_protocol_version, - *active_version_range.start(), + boundary, ) .into(); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/tests.rs index 60bfbd16ae3..6ebae6fa217 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/tests.rs @@ -2507,16 +2507,17 @@ mod tests { assert_eq!(pool_after, pool_before, "nothing may enter the pool"); // The bytes decode fine — only the version is outside its active range — so the // submitter is owed a coded answer and the transition is dropped from the block - // unpaid instead of being counted as an internal error. The error's - // `required_protocol_version` is the start of that active range, which names nothing - // to move to once a version has been superseded, so what is pinned here is the answer - // the submitter receives. + // unpaid instead of being counted as an internal error. Version 0 was accepted up + // to protocol version 13, and that is the boundary reported: the active range was + // missed from above, so naming its start would point at a version the chain is + // already past. assert_matches!( in_block.execution_results().as_slice(), [StateTransitionExecutionResult::UnpaidConsensusError( ConsensusError::BasicError(BasicError::StateTransitionNotActiveError(error)), )] if error.state_transition_type() == "ShieldFromAssetLock" - && error.current_protocol_version() == platform_version.protocol_version, + && error.current_protocol_version() == platform_version.protocol_version + && error.required_protocol_version() == 13, "a version 0 is refused unpaid and dropped from the block: {observed}" ); From 1caa49eef89e4e13c76d5f25a5e9bbe2dee3785a Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sat, 26 Sep 2026 21:17:37 +0700 Subject: [PATCH 36/94] test(drive-abci): a batch does need the is_allowed check on this branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v4.2-dev merge brought in `should_return_false_for_transitions_not_requiring_allowed_check`, whose fixture lists `Batch` among the transitions with no `is_allowed` phase. That is true upstream: the batch's own `is_allowed` refused contested documents before a target epoch and was deleted once those were allowed. It is not true here, because this branch gives the batch a different gate — the one that refuses the token shielded pool's transitions, the pool's configuration items and a document token cost paid from a pool before the protocol version that admits them. So `Batch` moves to the other fixture, with the reason stated next to it. Caught by CI rather than locally: `Rust workspace tests` was green on b73f92f5e3 and failed on the merge commit with `expected has_is_allowed_validation=false for Discriminant(2)`. The local runs covered the suites the feature touches — shield, token, decode, dpp, wallet — and a workspace clippy, none of which execute the tests of the file whose conflict was resolved. Co-Authored-By: Claude Opus 5 (1M context) --- .../state_transition/processor/traits/is_allowed.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs index 00d983ed71a..8328f1affd5 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs @@ -467,6 +467,10 @@ mod tests { /// Returns all state transitions grouped by expected `has_is_allowed_validation` result. fn transitions_requiring_allowed_validation() -> Vec { vec![ + // A batch carries the gate that refuses the token shielded pool's transitions, the + // pool's configuration items and a document token cost paid from a pool before the + // protocol version admitting them. + StateTransition::Batch(BatchTransition::V0(BatchTransitionV0::default())), StateTransition::IdentityTopUpFromAddresses(IdentityTopUpFromAddressesTransition::V0( IdentityTopUpFromAddressesTransitionV0::default(), )), @@ -502,7 +506,6 @@ mod tests { fn transitions_not_requiring_allowed_validation() -> Vec { vec![ - StateTransition::Batch(BatchTransition::V0(BatchTransitionV0::default())), make_data_contract_create_st(), make_data_contract_update_st(), StateTransition::IdentityCreate(IdentityCreateTransition::V0( From 8d136712016ede756e8c8528e9bf99978a508aaf Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sat, 26 Sep 2026 22:11:30 +0700 Subject: [PATCH 37/94] test(drive-abci): prove each shield's bundle against the funder that pays for it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The nullifier tests each held one proven bundle per module in a `OnceLock` and wrapped it around a fresh funder per test. That is reuse of one authorized bundle across funding sources, which is exactly what the sighash binding removes, so after the re-stack on #4760 those bundles verified nowhere. Each test now proves a bundle bound to its own funder, through `build_outputs_only_bundle_bound`; the old `build_outputs_only_bundle` stays for the protocol versions that bind nothing. The three kinds bind different things, so the repair differs per module. `ShieldFromIdentity` binds the identity alone, not the nonce, so one bound bundle still serves several shields by the same identity and the repeat-under-the-next-nonce test is unchanged apart from how its bundle is built. `ShieldFromAssetLock` binds the lock, so no bundle can be moved to a second lock. The repeat case uses a bundle with unprovable proof bytes instead: the nullifier check runs before the Orchard proof, so a transition meant to be refused for its nullifier never reaches verification and needs no binding to get there. The protocol-version-13 case keeps a shared unbound bundle, which is honest — 13 binds nothing, and the test exists to show one bundle travelling between locks there — but it now builds version 0, the version 13 admits. `Shield` verifies the proof in the stateless step before the transform, so an unprovable bundle cannot be driven through block processing. Its repeat case goes through the transform directly, the path its sibling intra-bundle test already used; that helper is now generic over the nullifiers revealed. The bound bundle is built from the inputs `signed_shield` derives, and an assertion ties the two together so a drift between them cannot pass silently. Co-Authored-By: Claude Opus 5 (1M context) --- .../state_transitions/shield/tests.rs | 104 +++++++++----- .../shield_from_asset_lock/tests.rs | 133 +++++++++++++----- .../shield_from_identity/tests.rs | 27 ++-- .../state_transitions/test_helpers.rs | 14 +- 4 files changed, 197 insertions(+), 81 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs index 83673d0eb50..74e9f429f91 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs @@ -1410,9 +1410,9 @@ mod tests { use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; use crate::execution::validation::state_transition::shield::StateTransitionShieldTransitionActionTransformer; use crate::execution::validation::state_transition::state_transitions::test_helpers::{ - build_outputs_only_bundle, has_recorded_nullifier, process_transition_and_commit, - setup_platform_at_protocol_version, shielded_transfer_errors_revealing, - OutputsOnlyBundle, + build_outputs_only_bundle, build_outputs_only_bundle_bound, has_recorded_nullifier, + process_transition_and_commit, setup_platform_at_protocol_version, + shielded_transfer_errors_revealing, OutputsOnlyBundle, }; use crate::platform_types::platform::PlatformRef; use crate::rpc::core::MockCoreRPCLike; @@ -1423,12 +1423,44 @@ mod tests { use drive::state_transition_action::StateTransitionAction; use std::sync::OnceLock; - /// One proven bundle shared by the tests of this module; each runs on a fresh platform. - fn bundle() -> &'static OutputsOnlyBundle { + /// One proven, unbound bundle, shared by the protocol-version-13 test: 13 binds nothing, + /// so there a single bundle still verifies under any input set, which is what that test + /// submits twice. + fn unbound_bundle() -> &'static OutputsOnlyBundle { static BUNDLE: OnceLock = OnceLock::new(); BUNDLE.get_or_init(|| build_outputs_only_bundle(5_000)) } + /// A shield from `address` at `nonce`, its bundle proved against those inputs. The sighash + /// binds the funding addresses, their nonces and the credits they pay, so the bundle + /// verifies under no other input set and each test proves its own. Returns the transition + /// and the nullifiers its actions reveal. + async fn bound_shield( + signer: &TestAddressSigner, + address: PlatformAddress, + nonce: AddressNonce, + platform_version: &PlatformVersion, + ) -> (StateTransition, Vec<[u8; 32]>) { + // `signed_shield` derives the inputs from the amount the bundle pays, and the binding + // covers those inputs, so the amount is fixed here rather than read back off a bundle + // that would have to exist first. + const SHIELDED: u64 = 5_000; + let mut inputs = BTreeMap::new(); + inputs.insert(address, (nonce, SHIELDED + dash_to_credits!(0.01))); + let extra_sighash_data = shield_extra_sighash_data(&inputs, platform_version) + .expect("the binding of the funding inputs"); + let bundle = build_outputs_only_bundle_bound(SHIELDED, &extra_sighash_data); + assert_eq!( + bundle.amount, SHIELDED, + "the inputs signed_shield derives must be the ones the bundle is bound to" + ); + let nullifiers = bundle.nullifiers(); + ( + signed_shield(signer, address, nonce, &bundle).await, + nullifiers, + ) + } + /// A shield of `bundle` from `address` at `nonce`, signed by `signer`. async fn signed_shield( signer: &TestAddressSigner, @@ -1482,9 +1514,10 @@ mod tests { /// nullifier. Block processing verifies the proof in the stateless step before this /// transform, and no Orchard builder emits a bundle that repeats a nullifier, so the /// check is reached here directly, as block processing reaches it after the proof. - fn transform_shield_repeating_a_nullifier_inside_the_bundle( + fn transform_shield_revealing( platform: &TempPlatform, platform_version: &PlatformVersion, + nullifiers: &[[u8; 32]], ) -> ConsensusValidationResult { let address = create_platform_address(1); let requested = dash_to_credits!(0.1); @@ -1492,10 +1525,13 @@ mod tests { inputs.insert(address, (1 as AddressNonce, requested)); let transition = ShieldTransition::V0(ShieldTransitionV0 { inputs, - actions: vec![ - create_dummy_serialized_action(), - create_dummy_serialized_action(), - ], + actions: nullifiers + .iter() + .map(|nullifier| SerializedAction { + nullifier: *nullifier, + ..create_dummy_serialized_action() + }) + .collect(), amount: 1_000, anchor: [0u8; 32], proof: vec![0u8; 100], @@ -1536,16 +1572,15 @@ mod tests { let platform_version = PlatformVersion::latest(); let mut platform = setup_platform(); let (signer, address) = funded_address(&mut platform); - let bundle = bundle(); - let st = signed_shield(&signer, address, 1, bundle).await; + let (st, nullifiers) = bound_shield(&signer, address, 1, platform_version).await; let result = process_transition_and_commit(&platform, st, platform_version); assert_matches!( result.execution_results().as_slice(), [StateTransitionExecutionResult::SuccessfulExecution { .. }] ); - for nullifier in bundle.nullifiers() { + for nullifier in nullifiers { assert!( has_recorded_nullifier(&platform, &nullifier), "every nullifier the shield reveals must be recorded" @@ -1558,26 +1593,26 @@ mod tests { let platform_version = PlatformVersion::latest(); let mut platform = setup_platform(); let (signer, address) = funded_address(&mut platform); - let bundle = bundle(); - let first = signed_shield(&signer, address, 1, bundle).await; + let (first, nullifiers) = bound_shield(&signer, address, 1, platform_version).await; let result = process_transition_and_commit(&platform, first, platform_version); assert_matches!( result.execution_results().as_slice(), [StateTransitionExecutionResult::SuccessfulExecution { .. }] ); - // The same bundle again, under the next address nonce: the same note, the same - // nullifiers. - let repeat = signed_shield(&signer, address, 2, bundle).await; - let result = process_transition_and_commit(&platform, repeat, platform_version); + // A second shield revealing what the first recorded. It cannot carry the first's + // bundle — the sighash binds the inputs that fund it — so the repeat is shown to the + // transform, which is where the state check lives and where block processing reaches + // it after the stateless proof step. + let first_nullifier = nullifiers[0]; + let result = transform_shield_revealing(&platform, platform_version, &nullifiers); - let first_nullifier = bundle.nullifiers()[0]; + assert!(!result.has_data(), "no action for a refused shield"); assert_matches!( - result.execution_results().as_slice(), - [StateTransitionExecutionResult::UnpaidConsensusError( - ConsensusError::StateError(StateError::NullifierAlreadySpentError(e)) - )] if e.nullifier() == first_nullifier + result.errors.as_slice(), + [ConsensusError::StateError(StateError::NullifierAlreadySpentError(e))] + if e.nullifier() == first_nullifier ); } @@ -1586,10 +1621,9 @@ mod tests { let platform_version = PlatformVersion::latest(); let platform = setup_platform(); - let result = transform_shield_repeating_a_nullifier_inside_the_bundle( - &platform, - platform_version, - ); + let repeated = create_dummy_serialized_action().nullifier; + let result = + transform_shield_revealing(&platform, platform_version, &[repeated, repeated]); assert!(!result.has_data(), "no action for a refused shield"); assert_matches!( @@ -1604,9 +1638,8 @@ mod tests { let platform_version = PlatformVersion::latest(); let mut platform = setup_platform(); let (signer, address) = funded_address(&mut platform); - let bundle = bundle(); - let st = signed_shield(&signer, address, 1, bundle).await; + let (st, nullifiers) = bound_shield(&signer, address, 1, platform_version).await; let result = process_transition_and_commit(&platform, st, platform_version); assert_matches!( result.execution_results().as_slice(), @@ -1617,7 +1650,7 @@ mod tests { shielded_transfer_errors_revealing(&platform, [0xEE; 32]).is_empty(), "a spend revealing an unrecorded nullifier passes the spend-side check" ); - let recorded = bundle.nullifiers()[1]; + let recorded = nullifiers[1]; assert_matches!( shielded_transfer_errors_revealing(&platform, recorded).as_slice(), [ConsensusError::StateError(StateError::NullifierAlreadySpentError(e))] @@ -1630,7 +1663,7 @@ mod tests { let platform_version = PlatformVersion::get(13).expect("protocol version 13"); let mut platform = setup_platform_at_protocol_version(13); let (signer, address) = funded_address(&mut platform); - let bundle = bundle(); + let bundle = unbound_bundle(); let first = signed_shield(&signer, address, 1, bundle).await; let result = process_transition_and_commit(&platform, first, platform_version); @@ -1653,10 +1686,9 @@ mod tests { "protocol version 13 accepts a repeated shield" ); - let result = transform_shield_repeating_a_nullifier_inside_the_bundle( - &platform, - platform_version, - ); + let repeated = create_dummy_serialized_action().nullifier; + let result = + transform_shield_revealing(&platform, platform_version, &[repeated, repeated]); assert!( result.is_valid_with_data(), "protocol version 13 does not check a nullifier repeated inside the bundle" diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/tests.rs index 2d7417d0f56..de039f3b6e3 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/tests.rs @@ -781,38 +781,96 @@ mod tests { mod nullifiers { use super::*; use crate::execution::validation::state_transition::state_transitions::test_helpers::{ - build_outputs_only_bundle, has_recorded_nullifier, process_transition_and_commit, - setup_platform_at_protocol_version, shielded_transfer_errors_revealing, - OutputsOnlyBundle, + build_outputs_only_bundle, build_outputs_only_bundle_bound, has_recorded_nullifier, + process_transition_and_commit, setup_platform_at_protocol_version, + shielded_transfer_errors_revealing, OutputsOnlyBundle, }; use std::sync::OnceLock; - /// One proven bundle shared by the tests of this module; each runs on a fresh platform. - fn bundle() -> &'static OutputsOnlyBundle { - static BUNDLE: OnceLock = OnceLock::new(); - BUNDLE.get_or_init(|| build_outputs_only_bundle(5_000)) - } - - /// A shield of `actions` (with the fields of `bundle`) from a fresh asset lock drawn - /// from `seed`; the surplus goes to a platform address. - fn signed_shield( + /// A shield funded by a fresh asset lock drawn from `seed`, its bundle proved against that + /// lock. The sighash binds the lock's identifier, so the proved bundle verifies under no + /// other lock and each test has to prove its own. Returns the transition together with the + /// nullifiers its actions reveal. + fn bound_shield( seed: u64, - actions: Vec, - bundle: &OutputsOnlyBundle, - ) -> StateTransition { + platform_version: &PlatformVersion, + ) -> (StateTransition, Vec<[u8; 32]>) { let mut rng = StdRng::seed_from_u64(seed); let (asset_lock_proof, asset_lock_pk) = create_asset_lock_proof_with_key(&mut rng); - create_signed_shield_from_asset_lock_transition( + let extra_sighash_data = + shield_from_asset_lock_extra_sighash_data(&asset_lock_proof, platform_version) + .expect("the binding of the funding asset lock"); + let bundle = build_outputs_only_bundle_bound(5_000, &extra_sighash_data); + let nullifiers = bundle.nullifiers(); + let transition = create_signed_shield_from_asset_lock_transition( asset_lock_proof, &asset_lock_pk, - actions, + bundle.actions, bundle.amount, bundle.anchor, - bundle.proof.clone(), + bundle.proof, bundle.binding_signature, + ); + (transition, nullifiers) + } + + /// A shield whose actions reveal `nullifiers`, with unprovable proof bytes. The nullifier + /// check runs before the proof, so a bundle meant to be refused for its nullifier never + /// reaches verification and needs no binding to reach the check. + fn unprovable_shield_revealing(seed: u64, nullifiers: &[[u8; 32]]) -> StateTransition { + let mut rng = StdRng::seed_from_u64(seed); + let (asset_lock_proof, asset_lock_pk) = create_asset_lock_proof_with_key(&mut rng); + let actions = nullifiers + .iter() + .map(|nullifier| SerializedAction { + nullifier: *nullifier, + ..create_dummy_serialized_action() + }) + .collect(); + create_signed_shield_from_asset_lock_transition( + asset_lock_proof, + &asset_lock_pk, + actions, + 5_000, + [42u8; 32], + vec![0u8; 100], + [0u8; 64], ) } + /// One proven, unbound bundle shared by the protocol-version-13 test: 13 binds nothing, so + /// there one bundle still verifies under any asset lock. + fn unbound_bundle() -> &'static OutputsOnlyBundle { + static BUNDLE: OnceLock = OnceLock::new(); + BUNDLE.get_or_init(|| build_outputs_only_bundle(5_000)) + } + + /// A version 0 shield of `bundle` from a fresh asset lock drawn from `seed`. Version 0 is + /// what protocol version 13 admits, and it binds nothing. + fn signed_shield_v0(seed: u64, bundle: &OutputsOnlyBundle) -> StateTransition { + use dpp::state_transition::shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0; + + let mut rng = StdRng::seed_from_u64(seed); + let (asset_lock_proof, asset_lock_pk) = create_asset_lock_proof_with_key(&mut rng); + let surplus_output = Some(dpp::address_funds::PlatformAddress::P2pkh([0x33; 20])); + let body = |signature: BinaryData| ShieldFromAssetLockTransitionV0 { + asset_lock_proof: asset_lock_proof.clone(), + actions: bundle.actions.clone(), + value_balance: bundle.amount, + anchor: bundle.anchor, + proof: bundle.proof.clone(), + binding_signature: bundle.binding_signature, + surplus_output, + signature, + }; + let unsigned: StateTransition = body(Default::default()).into(); + let signable_bytes = unsigned + .signable_bytes() + .expect("should compute signable bytes"); + let signature = dpp::dashcore::signer::sign(&signable_bytes, &asset_lock_pk).unwrap(); + body(BinaryData::new(signature.to_vec())).into() + } + /// Two dummy actions revealing the same nullifier, with unprovable proof bytes. fn shield_repeating_a_nullifier_inside_the_bundle() -> StateTransition { let mut rng = StdRng::seed_from_u64(9); @@ -835,16 +893,15 @@ mod tests { fn should_record_the_nullifiers_a_shield_reveals() { let platform_version = PlatformVersion::latest(); let platform = setup_platform(); - let bundle = bundle(); - let st = signed_shield(1, bundle.actions.clone(), bundle); + let (st, nullifiers) = bound_shield(1, platform_version); let result = process_transition_and_commit(&platform, st, platform_version); assert_matches!( result.execution_results().as_slice(), [StateTransitionExecutionResult::SuccessfulExecution { .. }] ); - for nullifier in bundle.nullifiers() { + for nullifier in nullifiers { assert!( has_recorded_nullifier(&platform, &nullifier), "every nullifier the shield reveals must be recorded" @@ -856,20 +913,21 @@ mod tests { fn should_refuse_a_shield_repeating_a_recorded_nullifier() { let platform_version = PlatformVersion::latest(); let platform = setup_platform(); - let bundle = bundle(); - let first = signed_shield(1, bundle.actions.clone(), bundle); + let (first, nullifiers) = bound_shield(1, platform_version); let result = process_transition_and_commit(&platform, first, platform_version); assert_matches!( result.execution_results().as_slice(), [StateTransitionExecutionResult::SuccessfulExecution { .. }] ); - // The same bundle from a second asset lock: the same note, the same nullifiers. - let repeat = signed_shield(2, bundle.actions.clone(), bundle); + // A second shield, from a second asset lock, revealing the nullifiers the first + // recorded. Its bundle cannot be the first's — the sighash binds the lock that funds + // it — but it does not have to be: the nullifier is refused before the proof is read. + let repeat = unprovable_shield_revealing(2, &nullifiers); let result = process_transition_and_commit(&platform, repeat, platform_version); - let first_nullifier = bundle.nullifiers()[0]; + let first_nullifier = nullifiers[0]; assert_matches!( result.execution_results().as_slice(), [StateTransitionExecutionResult::UnpaidConsensusError( @@ -903,9 +961,8 @@ mod tests { fn should_refuse_a_spend_revealing_a_nullifier_a_shield_recorded() { let platform_version = PlatformVersion::latest(); let platform = setup_platform(); - let bundle = bundle(); - let st = signed_shield(1, bundle.actions.clone(), bundle); + let (st, nullifiers) = bound_shield(1, platform_version); let result = process_transition_and_commit(&platform, st, platform_version); assert_matches!( result.execution_results().as_slice(), @@ -916,7 +973,7 @@ mod tests { shielded_transfer_errors_revealing(&platform, [0xEE; 32]).is_empty(), "a spend revealing an unrecorded nullifier passes the spend-side check" ); - let recorded = bundle.nullifiers()[1]; + let recorded = nullifiers[1]; assert_matches!( shielded_transfer_errors_revealing(&platform, recorded).as_slice(), [ConsensusError::StateError(StateError::NullifierAlreadySpentError(e))] @@ -1045,9 +1102,9 @@ mod tests { fn should_neither_record_nor_check_nullifiers_before_protocol_version_14() { let platform_version = PlatformVersion::get(13).expect("protocol version 13"); let platform = setup_platform_at_protocol_version(13); - let bundle = bundle(); + let bundle = unbound_bundle(); - let first = signed_shield(1, bundle.actions.clone(), bundle); + let first = signed_shield_v0(1, bundle); let result = process_transition_and_commit(&platform, first, platform_version); assert_matches!( result.execution_results().as_slice(), @@ -1060,7 +1117,7 @@ mod tests { ); } - let repeat = signed_shield(2, bundle.actions.clone(), bundle); + let repeat = signed_shield_v0(2, bundle); let result = process_transition_and_commit(&platform, repeat, platform_version); assert_matches!( result.execution_results().as_slice(), @@ -1069,9 +1126,19 @@ mod tests { ); // Without the check, the unprovable bundle reaches its proof and pays the penalty. + let repeated_inside = OutputsOnlyBundle { + actions: vec![ + create_dummy_serialized_action(), + create_dummy_serialized_action(), + ], + amount: 5_000, + anchor: [42u8; 32], + proof: vec![0u8; 100], + binding_signature: [0u8; 64], + }; let result = process_transition( &platform, - shield_repeating_a_nullifier_inside_the_bundle(), + signed_shield_v0(9, &repeated_inside), platform_version, ); assert_matches!( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/tests.rs index b55819fc5f9..aadf8777922 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/tests.rs @@ -1383,15 +1383,22 @@ mod tests { mod nullifiers { use super::*; use crate::execution::validation::state_transition::state_transitions::test_helpers::{ - build_outputs_only_bundle, has_recorded_nullifier, shielded_transfer_errors_revealing, - OutputsOnlyBundle, + build_outputs_only_bundle_bound, has_recorded_nullifier, + shielded_transfer_errors_revealing, OutputsOnlyBundle, }; - use std::sync::OnceLock; - /// One proven bundle shared by the tests of this module; each runs on a fresh platform. - fn bundle() -> &'static OutputsOnlyBundle { - static BUNDLE: OnceLock = OnceLock::new(); - BUNDLE.get_or_init(|| build_outputs_only_bundle(5_000)) + /// A bundle proved against `identity_id`. The sighash binds the identity funding the + /// shield, so the bundle verifies for that identity's transitions and for no other's, and + /// each test proves its own. The binding does not cover the nonce, so one bundle still + /// serves several shields by the same identity. + fn bound_bundle( + identity_id: &[u8; 32], + platform_version: &PlatformVersion, + ) -> OutputsOnlyBundle { + let extra_sighash_data = + shield_from_identity_extra_sighash_data(identity_id, platform_version) + .expect("the binding of the funding identity"); + build_outputs_only_bundle_bound(5_000, &extra_sighash_data) } fn proven(bundle: &OutputsOnlyBundle) -> ProvenBundle { @@ -1433,7 +1440,7 @@ mod tests { let platform_version = PlatformVersion::latest(); let mut platform = setup_platform(); let (identity, signer) = funded_identity(&mut platform, 71, platform_version); - let bundle = bundle(); + let bundle = &bound_bundle(&identity.id().to_buffer(), platform_version); let st = create_signed_transition( &identity, @@ -1479,7 +1486,7 @@ mod tests { let platform_version = PlatformVersion::latest(); let mut platform = setup_platform(); let (identity, signer) = funded_identity(&mut platform, 72, platform_version); - let bundle = bundle(); + let bundle = &bound_bundle(&identity.id().to_buffer(), platform_version); let first = create_signed_transition( &identity, @@ -1610,7 +1617,7 @@ mod tests { let platform_version = PlatformVersion::latest(); let mut platform = setup_platform(); let (identity, signer) = funded_identity(&mut platform, 74, platform_version); - let bundle = bundle(); + let bundle = &bound_bundle(&identity.id().to_buffer(), platform_version); let st = create_signed_transition( &identity, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/test_helpers.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/test_helpers.rs index 2029530f932..c33eef4a8c5 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/test_helpers.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/test_helpers.rs @@ -531,8 +531,18 @@ impl OutputsOnlyBundle { } /// Builds and proves an outputs-only bundle paying `value` to a fixed recipient, with no extra -/// sighash data (what every shielding transition signs). Orchard pads it to two actions. +/// sighash data. Orchard pads it to two actions. Only the protocol versions that do not bind these +/// bundles accept one: from the version that does, use `build_outputs_only_bundle_bound` with the +/// funder's binding, or the bundle verifies nowhere. pub fn build_outputs_only_bundle(value: u64) -> OutputsOnlyBundle { + build_outputs_only_bundle_bound(value, &[]) +} + +/// Builds and proves an outputs-only bundle paying `value` to a fixed recipient, committing +/// `extra_sighash_data` — the binding that ties the bundle to the addresses, identity or asset lock +/// funding it, so it cannot be lifted into a transition funded by anything else. Orchard pads it to +/// two actions. +pub fn build_outputs_only_bundle_bound(value: u64, extra_sighash_data: &[u8]) -> OutputsOnlyBundle { use grovedb_commitment_tree::{ Anchor, Builder, BundleType, Flags as OrchardFlags, FullViewingKey, NoteValue, Scope, SpendingKey, @@ -555,7 +565,7 @@ pub fn build_outputs_only_bundle(value: u64) -> OutputsOnlyBundle { .unwrap(); let (unauthorized, _) = builder.build::(&mut rng).unwrap().unwrap(); let bundle_commitment: [u8; 32] = unauthorized.commitment().into(); - let sighash = compute_platform_sighash(&bundle_commitment, &[]); + let sighash = compute_platform_sighash(&bundle_commitment, extra_sighash_data); let proven = unauthorized .create_proof(get_proving_key(), &mut rng) .unwrap(); From 39f4916fb594c4b91cc91f299bcd1f521e66f527 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 00:10:02 +0700 Subject: [PATCH 38/94] test(drive): drop four evonode reward-ratio tests that test only themselves They were written to mirror the closure the `EvonodesByParticipation` branch passed to `rewards_in_interval`, and that closure no longer lives in this file: the reward computation moved into `Drive::evonode_participation_rewards`. What the tests kept doing was rebuilding the arithmetic locally and asserting on their own expressions, so none of them could fail however the production code behaved. One of them also described a rule the live code does not follow. `evonodes_reward_ratio_empty_range_returns_none` asserted `None` for a range with no blocks, while `evonode_participation_rewards/v1/mod.rs:143` returns `Some` in that case and always has. A test that cannot fail is merely useless; one that cannot fail while its name states the opposite of the rule is read as documentation and believed. The `is_allowed` comment alongside narrated the merge rather than the code, and now says what a batch carries instead of what was removed from it. Co-Authored-By: Claude Opus 5 (1M context) --- .../processor/traits/is_allowed.rs | 5 +- .../v0/transformer.rs | 94 ------------------- 2 files changed, 2 insertions(+), 97 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs index 8328f1affd5..d1ef5c08802 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs @@ -164,9 +164,8 @@ impl StateTransitionIsAllowedValidationV0 for StateTransition { ])); } } - // The batch's own `is_allowed` covered contested documents before a target - // epoch and was removed upstream once those were allowed; the version gate above - // is all a batch needs here now. + // A batch needs no further `is_allowed` check: the token shielded pool version + // gate above is the only one it carries. Ok(ConsensusValidationResult::new()) } StateTransition::IdentityTopUpFromAddresses(_) diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs index 24ab2b68c6c..7a17b3b7f58 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs @@ -935,100 +935,6 @@ mod tests { assert_eq!(recipient, TokenDistributionRecipient::Identity(id)); } - /// Reimplements the closure passed to `rewards_in_interval` inside the - /// `EvonodesByParticipation` branch and checks its single-epoch output - /// matches the blocks proposed by the owner. - #[test] - fn evonodes_reward_ratio_single_epoch() { - let me = owner(); - let other = stranger(); - let mut block_proposers: BTreeMap = BTreeMap::new(); - block_proposers.insert(me, 7); - block_proposers.insert(other, 3); - let total_blocks_in_epoch = 10u64; - - let ratio = RewardRatio { - numerator: block_proposers.get(&me).copied().unwrap_or_default(), - denominator: total_blocks_in_epoch, - }; - assert_eq!(ratio.numerator, 7); - assert_eq!(ratio.denominator, 10); - } - - /// Multi-epoch branch should sum totals and only count blocks proposed by the owner. - #[test] - fn evonodes_reward_ratio_multi_epoch_sum() { - let me = owner(); - let mut epochs: BTreeMap)> = BTreeMap::new(); - - let mut bp1 = BTreeMap::new(); - bp1.insert(me, 5); - epochs.insert(0, (20, bp1)); - - let mut bp2 = BTreeMap::new(); - bp2.insert(me, 2); - epochs.insert(1, (30, bp2)); - - let mut total_blocks = 0u64; - let mut total_proposed_blocks = 0u64; - for idx in 0u16..=1u16 { - if let Some((blocks, proposers)) = epochs.get(&idx) { - total_blocks += *blocks; - total_proposed_blocks += proposers.get(&me).copied().unwrap_or_default(); - } - } - assert_eq!(total_blocks, 50); - assert_eq!(total_proposed_blocks, 7); - let ratio = if total_blocks > 0 { - Some(RewardRatio { - numerator: total_proposed_blocks, - denominator: total_blocks, - }) - } else { - None - }; - assert_eq!( - ratio, - Some(RewardRatio { - numerator: 7, - denominator: 50, - }) - ); - } - - /// With no epochs in range, total_blocks remains 0 so the ratio is None. - #[test] - fn evonodes_reward_ratio_empty_range_returns_none() { - let total_blocks = 0u64; - let total_proposed_blocks = 0u64; - let ratio = if total_blocks > 0 { - Some(RewardRatio { - numerator: total_proposed_blocks, - denominator: total_blocks, - }) - } else { - None - }; - assert!(ratio.is_none()); - } - - /// When an epoch index exists but the owner never proposed, numerator is 0. - #[test] - fn evonodes_reward_ratio_owner_has_no_proposed_blocks() { - let me = owner(); - let other = stranger(); - let mut bp: BTreeMap = BTreeMap::new(); - bp.insert(other, 4); - let total_blocks = 10u64; - - let ratio = RewardRatio { - numerator: bp.get(&me).copied().unwrap_or_default(), - denominator: total_blocks, - }; - assert_eq!(ratio.numerator, 0); - assert_eq!(ratio.denominator, 10); - } - /// The `last_paid_moment.unwrap_or(contract_creation_cycle_start)` fallback /// should pick the explicit last-paid if set, otherwise the cycle start. #[test] From 8216d2fd93791b34c6f56eea2ccfcfa371e85408 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 00:30:18 +0700 Subject: [PATCH 39/94] test(drive-abci): put the shield repeat back on the block path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `should_refuse_a_shield_repeating_a_recorded_nullifier` was rewritten to call `transform_into_action_for_shield_transition` directly, on the stated grounds that a second shield "cannot carry the first's bundle — the sighash binds the inputs that fund it". That is not what the binding does. `shield_funding_digest_v0` hashes `inputs.keys()` and nothing else, and the layout doc says so in as many words: "The nonces and the contributed amounts are deliberately left out." One bundle bound to an address is still valid under that address's next nonce, the same way `ShieldFromIdentity` binds the identity and not the nonce. So the repeat never needed to leave block processing, and taking it off that path cost the only coverage proving the pipeline reaches the nullifier check for a `Shield` at all — the check itself was still proven, its reachability was not. `ShieldFromIdentity` and `ShieldFromAssetLock` both kept a full-pipeline repeat; `Shield` now does again, submitting the same bound bundle under the next nonce. The helper returns the bundle instead of a transition, which is what made the bundle unreachable to a second call, and its doc now states the binding as the digest computes it. Co-Authored-By: Claude Opus 5 (1M context) --- .../state_transitions/shield/tests.rs | 66 ++++++++++--------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs index 74e9f429f91..d69cd5b74bb 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs @@ -1431,22 +1431,24 @@ mod tests { BUNDLE.get_or_init(|| build_outputs_only_bundle(5_000)) } - /// A shield from `address` at `nonce`, its bundle proved against those inputs. The sighash - /// binds the funding addresses, their nonces and the credits they pay, so the bundle - /// verifies under no other input set and each test proves its own. Returns the transition - /// and the nullifiers its actions reveal. - async fn bound_shield( - signer: &TestAddressSigner, + /// A bundle proved against the addresses that fund the shield. `shield_funding_digest_v0` + /// hashes the input addresses and nothing else — neither the nonce nor the credits — so the + /// bundle verifies under no other set of funders, while one bundle still serves several + /// shields from the same funder. + fn bound_bundle( address: PlatformAddress, - nonce: AddressNonce, platform_version: &PlatformVersion, - ) -> (StateTransition, Vec<[u8; 32]>) { - // `signed_shield` derives the inputs from the amount the bundle pays, and the binding - // covers those inputs, so the amount is fixed here rather than read back off a bundle - // that would have to exist first. + ) -> OutputsOnlyBundle { + // `signed_shield` derives the inputs from the amount the bundle pays, so the amount is + // fixed here rather than read back off a bundle that would have to exist first. The + // nonce below reaches no hash; it is here because the map the digest reads is keyed + // this way. const SHIELDED: u64 = 5_000; let mut inputs = BTreeMap::new(); - inputs.insert(address, (nonce, SHIELDED + dash_to_credits!(0.01))); + inputs.insert( + address, + (1 as AddressNonce, SHIELDED + dash_to_credits!(0.01)), + ); let extra_sighash_data = shield_extra_sighash_data(&inputs, platform_version) .expect("the binding of the funding inputs"); let bundle = build_outputs_only_bundle_bound(SHIELDED, &extra_sighash_data); @@ -1454,11 +1456,7 @@ mod tests { bundle.amount, SHIELDED, "the inputs signed_shield derives must be the ones the bundle is bound to" ); - let nullifiers = bundle.nullifiers(); - ( - signed_shield(signer, address, nonce, &bundle).await, - nullifiers, - ) + bundle } /// A shield of `bundle` from `address` at `nonce`, signed by `signer`. @@ -1573,14 +1571,15 @@ mod tests { let mut platform = setup_platform(); let (signer, address) = funded_address(&mut platform); - let (st, nullifiers) = bound_shield(&signer, address, 1, platform_version).await; + let bundle = bound_bundle(address, platform_version); + let st = signed_shield(&signer, address, 1, &bundle).await; let result = process_transition_and_commit(&platform, st, platform_version); assert_matches!( result.execution_results().as_slice(), [StateTransitionExecutionResult::SuccessfulExecution { .. }] ); - for nullifier in nullifiers { + for nullifier in bundle.nullifiers() { assert!( has_recorded_nullifier(&platform, &nullifier), "every nullifier the shield reveals must be recorded" @@ -1594,25 +1593,27 @@ mod tests { let mut platform = setup_platform(); let (signer, address) = funded_address(&mut platform); - let (first, nullifiers) = bound_shield(&signer, address, 1, platform_version).await; + let bundle = bound_bundle(address, platform_version); + let first = signed_shield(&signer, address, 1, &bundle).await; let result = process_transition_and_commit(&platform, first, platform_version); assert_matches!( result.execution_results().as_slice(), [StateTransitionExecutionResult::SuccessfulExecution { .. }] ); - // A second shield revealing what the first recorded. It cannot carry the first's - // bundle — the sighash binds the inputs that fund it — so the repeat is shown to the - // transform, which is where the state check lives and where block processing reaches - // it after the stateless proof step. - let first_nullifier = nullifiers[0]; - let result = transform_shield_revealing(&platform, platform_version, &nullifiers); + // The same bundle again under the next address nonce: the same note, the same + // nullifiers. The binding does not stop this — it covers the funding addresses, not + // the transition carrying them — so the repeat travels the whole block path and the + // recorded nullifier is what refuses it. + let repeat = signed_shield(&signer, address, 2, &bundle).await; + let result = process_transition_and_commit(&platform, repeat, platform_version); - assert!(!result.has_data(), "no action for a refused shield"); + let first_nullifier = bundle.nullifiers()[0]; assert_matches!( - result.errors.as_slice(), - [ConsensusError::StateError(StateError::NullifierAlreadySpentError(e))] - if e.nullifier() == first_nullifier + result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::StateError(StateError::NullifierAlreadySpentError(e)) + )] if e.nullifier() == first_nullifier ); } @@ -1639,7 +1640,8 @@ mod tests { let mut platform = setup_platform(); let (signer, address) = funded_address(&mut platform); - let (st, nullifiers) = bound_shield(&signer, address, 1, platform_version).await; + let bundle = bound_bundle(address, platform_version); + let st = signed_shield(&signer, address, 1, &bundle).await; let result = process_transition_and_commit(&platform, st, platform_version); assert_matches!( result.execution_results().as_slice(), @@ -1650,7 +1652,7 @@ mod tests { shielded_transfer_errors_revealing(&platform, [0xEE; 32]).is_empty(), "a spend revealing an unrecorded nullifier passes the spend-side check" ); - let recorded = nullifiers[1]; + let recorded = bundle.nullifiers()[1]; assert_matches!( shielded_transfer_errors_revealing(&platform, recorded).as_slice(), [ConsensusError::StateError(StateError::NullifierAlreadySpentError(e))] From 92e2d554c486252bd48b1c55ae6149b9ba954391 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 09:40:49 +0700 Subject: [PATCH 40/94] test(drive-abci): pin the boundary the below-range arm reports The decoder now picks which end of a transition's active range to report, and the whole point of that change is which number reaches the submitter. Only the above-range arm was asserted. `should_report_an_inactive_transition_as_not_active` exercised the other one but checked the variant alone, so swapping the two arms left it green. It now pins 9, the protocol version a batch version 1 becomes active at, and the current version alongside it. Verified by swapping the arms: the test fails with "a batch version 1 is active from protocol version 9" and passes again once they are put back. Co-Authored-By: Claude Opus 5 (1M context) --- .../decode_raw_state_transitions/v1/mod.rs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/decode_raw_state_transitions/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/decode_raw_state_transitions/v1/mod.rs index c9b12566b98..93b7eb7e961 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/decode_raw_state_transitions/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/decode_raw_state_transitions/v1/mod.rs @@ -305,15 +305,21 @@ mod tests { .into_iter() .collect(); + // The range is missed from below, so the boundary reported is its start: the version the + // submitter has to reach. Pinning the number is what tells this side of the choice apart + // from the other, which reports the range's end. match decoded.as_slice() { - [DecodedStateTransition::InvalidEncoding(invalid)] => assert!( - matches!( - &invalid.error, - ConsensusError::BasicError(BasicError::StateTransitionNotActiveError(_)) - ), - "expected StateTransitionNotActiveError, got {:?}", - invalid.error - ), + [DecodedStateTransition::InvalidEncoding(invalid)] => match &invalid.error { + ConsensusError::BasicError(BasicError::StateTransitionNotActiveError(error)) => { + assert_eq!( + error.required_protocol_version(), + 9, + "a batch version 1 is active from protocol version 9" + ); + assert_eq!(error.current_protocol_version(), 1); + } + other => panic!("expected StateTransitionNotActiveError, got {other:?}"), + }, other => panic!("expected one InvalidEncoding, got {other:?}"), } } From 93fa8d6a33bf5b4410aca799e7794deccb9a8094 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 09:41:04 +0700 Subject: [PATCH 41/94] test(drive-abci): keep platform_version last and drop a shadowing import `transform_shield_revealing` took its new `nullifiers` parameter after `platform_version`, which the conventions reserve for the last position; new parameters go up with the context items. Both call sites follow. The `ShieldFromAssetLockTransitionV0` import inside `signed_shield_v0` restates one the enclosing `mod tests` already makes, which `use super::*` carries down. No lint reports it, because a shadowing import counts as used. Co-Authored-By: Claude Opus 5 (1M context) --- .../state_transition/state_transitions/shield/tests.rs | 6 +++--- .../state_transitions/shield_from_asset_lock/tests.rs | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs index d69cd5b74bb..6c3375da708 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs @@ -1514,8 +1514,8 @@ mod tests { /// check is reached here directly, as block processing reaches it after the proof. fn transform_shield_revealing( platform: &TempPlatform, - platform_version: &PlatformVersion, nullifiers: &[[u8; 32]], + platform_version: &PlatformVersion, ) -> ConsensusValidationResult { let address = create_platform_address(1); let requested = dash_to_credits!(0.1); @@ -1624,7 +1624,7 @@ mod tests { let repeated = create_dummy_serialized_action().nullifier; let result = - transform_shield_revealing(&platform, platform_version, &[repeated, repeated]); + transform_shield_revealing(&platform, &[repeated, repeated], platform_version); assert!(!result.has_data(), "no action for a refused shield"); assert_matches!( @@ -1690,7 +1690,7 @@ mod tests { let repeated = create_dummy_serialized_action().nullifier; let result = - transform_shield_revealing(&platform, platform_version, &[repeated, repeated]); + transform_shield_revealing(&platform, &[repeated, repeated], platform_version); assert!( result.is_valid_with_data(), "protocol version 13 does not check a nullifier repeated inside the bundle" diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/tests.rs index de039f3b6e3..93e344e7d7a 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_asset_lock/tests.rs @@ -848,8 +848,6 @@ mod tests { /// A version 0 shield of `bundle` from a fresh asset lock drawn from `seed`. Version 0 is /// what protocol version 13 admits, and it binds nothing. fn signed_shield_v0(seed: u64, bundle: &OutputsOnlyBundle) -> StateTransition { - use dpp::state_transition::shield_from_asset_lock_transition::v0::ShieldFromAssetLockTransitionV0; - let mut rng = StdRng::seed_from_u64(seed); let (asset_lock_proof, asset_lock_pk) = create_asset_lock_proof_with_key(&mut rng); let surplus_output = Some(dpp::address_funds::PlatformAddress::P2pkh([0x33; 20])); From b55bba73c7ba4ae4a5f62d1cd93e108349c7fa41 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 09:48:56 +0700 Subject: [PATCH 42/94] docs(platform): say why each shipped generation this edits stays inert, and show it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four generations edited here predate this feature and ship in `v4.1.1`, which declares `LATEST_VERSION = PROTOCOL_VERSION_13`. Editing a shipped generation in place is allowed only when the edit cannot change consensus at any version that selects it, and the conventions ask for two things beyond the permission itself: a comment at the edited lines saying why they are inert there, and a test at the last shipped version showing the outcome unchanged. Neither was present. The comments now sit at the edits. For `batch/transformer/v0` the reason is the sharpest: protocol versions 1 through 11 select it, they refuse every pool transition kind at the batch's `is_allowed` gate, and the version that admits those kinds does not select that generation at all — the arms are there to keep the match exhaustive and nothing more. `batch/state/v0` and the `rs-dpp` batch `validate_basic_structure/v0` carry the same argument plus the one for a document cost paid out of a pool, which travels in `TokenPaymentInfo::V1` and is gated the same way. Each of the nine `drive/shielded/*/v0` primitives now says that its pool path became a parameter and that the credit pool passes the path the function used to build for itself. For the primitives that argument is testable, so it is tested rather than asserted: `credit_pool_primitives_give_the_same_outcomes_at_protocol_version_13` drives the whole set through the dispatchers at 13 — counts, balance, nullifier membership both ways, and the anchor — and pins the outcomes the credit pool gave before the path moved into a parameter. Two imports left over from the deleted reward-ratio tests go with them. Co-Authored-By: Claude Opus 5 (1M context) --- .../validate_basic_structure/v0/mod.rs | 4 + .../state_transitions/batch/state/v0/mod.rs | 5 + .../batch/transformer/v0/mod.rs | 4 + .../src/drive/shielded/has_anchor/v0/mod.rs | 3 + .../drive/shielded/has_nullifier/v0/mod.rs | 3 + .../src/drive/shielded/insert_note/v0/mod.rs | 3 + .../shielded/insert_nullifiers/v0/mod.rs | 3 + packages/rs-drive/src/drive/shielded/mod.rs | 93 +++++++++++++++++++ .../src/drive/shielded/notes_count/v0/mod.rs | 3 + .../drive/shielded/prune_anchors/v0/mod.rs | 3 + .../shielded/read_total_balance/v0/mod.rs | 3 + .../record_anchor_if_changed/v0/mod.rs | 3 + .../shielded/update_total_balance/v0/mod.rs | 3 + .../v0/transformer.rs | 2 - 14 files changed, 133 insertions(+), 2 deletions(-) diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs index b5bc68dbcff..1607d179b0d 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs @@ -251,6 +251,10 @@ impl BatchTransition { ) => { set_price_for_direct_purchase_transition.validate_structure(platform_version)? } + // Every token shielded pool arm below is unreachable on the protocol versions + // that select this generation: the batch's `is_allowed` gate refuses each pool + // transition kind until the version that admits them. The arms keep the match + // exhaustive and carry the same structure validation that version runs. TokenTransition::Shield(shield_transition) => { shield_transition.validate_structure(platform_version)? } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs index fbdb15acf61..f996b6db9b8 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs @@ -300,6 +300,11 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { transaction, platform_version, )?, + // Every token shielded pool arm below is unreachable on the protocol + // versions that select this generation. The pool transition kinds are refused + // at the batch's `is_allowed` gate until the version that admits them, and a + // document cost paid out of a pool travels in `TokenPaymentInfo::V1`, whose + // `active_version_range` opens at that same version. TokenTransitionAction::ShieldAction(shield_action) => shield_action .validate_state( platform, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs index 774f62bb90d..d8cc8eb46dd 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs @@ -753,6 +753,10 @@ impl BatchTransitionInternalTransformerV0 for BatchTransition { Ok(batched_action) } + // Every token shielded pool arm below is unreachable in this generation. The + // protocol versions that select it, 1 through 11, refuse each pool transition kind at + // the batch's `is_allowed` gate, and the version that admits those kinds does not + // select this generation at all. The arms exist so the match stays exhaustive. TokenTransition::Shield(token_shield) => { let (batched_action, fee_result) = TokenShieldTransitionAction::try_from_borrowed_token_shield_transition_with_contract_lookup(drive, owner_id, token_shield, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { Ok(data_contract_fetch_info.clone()) diff --git a/packages/rs-drive/src/drive/shielded/has_anchor/v0/mod.rs b/packages/rs-drive/src/drive/shielded/has_anchor/v0/mod.rs index b4d9a08e714..a16b80a4af2 100644 --- a/packages/rs-drive/src/drive/shielded/has_anchor/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/has_anchor/v0/mod.rs @@ -19,6 +19,9 @@ impl Drive { platform_version: &PlatformVersion, ) -> Result { let anchors_path = shielded_credit_pool_anchors_path(); + // The pool's path is a parameter now, and the credit pool passes the very path this + // function used to build for itself: every protocol version that selects this + // generation sees the same operations against the same tree. self.has_anchor_in_pool_v0( &anchors_path, anchor, diff --git a/packages/rs-drive/src/drive/shielded/has_nullifier/v0/mod.rs b/packages/rs-drive/src/drive/shielded/has_nullifier/v0/mod.rs index 09e1132baae..cf0cbdac0c5 100644 --- a/packages/rs-drive/src/drive/shielded/has_nullifier/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/has_nullifier/v0/mod.rs @@ -19,6 +19,9 @@ impl Drive { platform_version: &PlatformVersion, ) -> Result { let nullifiers_path = shielded_credit_pool_nullifiers_path(); + // The pool's path is a parameter now, and the credit pool passes the very path this + // function used to build for itself: every protocol version that selects this + // generation sees the same operations against the same tree. self.has_nullifier_in_pool_v0( &nullifiers_path, nullifier, diff --git a/packages/rs-drive/src/drive/shielded/insert_note/v0/mod.rs b/packages/rs-drive/src/drive/shielded/insert_note/v0/mod.rs index 59f152cfe9f..96fa695368c 100644 --- a/packages/rs-drive/src/drive/shielded/insert_note/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/insert_note/v0/mod.rs @@ -17,6 +17,9 @@ impl Drive { cv_net: [u8; 32], encrypted_note: Vec, ) -> Result, Error> { + // The pool's path is a parameter now, and the credit pool passes the very path this + // function used to build for itself: every protocol version that selects this + // generation sees the same operations against the same tree. Ok(Self::insert_note_op_in_pool_v0( shielded_credit_pool_notes_path_vec(), nullifier, diff --git a/packages/rs-drive/src/drive/shielded/insert_nullifiers/v0/mod.rs b/packages/rs-drive/src/drive/shielded/insert_nullifiers/v0/mod.rs index d4e7837144a..910957b504e 100644 --- a/packages/rs-drive/src/drive/shielded/insert_nullifiers/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/insert_nullifiers/v0/mod.rs @@ -15,6 +15,9 @@ impl Drive { &self, nullifiers: &[[u8; 32]], ) -> Result, Error> { + // The pool's path is a parameter now, and the credit pool passes the very path this + // function used to build for itself: every protocol version that selects this + // generation sees the same operations against the same tree. Ok(Self::insert_nullifiers_in_pool_v0( shielded_credit_pool_nullifiers_path_vec(), nullifiers, diff --git a/packages/rs-drive/src/drive/shielded/mod.rs b/packages/rs-drive/src/drive/shielded/mod.rs index c54d1e84463..c6d54fbe891 100644 --- a/packages/rs-drive/src/drive/shielded/mod.rs +++ b/packages/rs-drive/src/drive/shielded/mod.rs @@ -49,3 +49,96 @@ mod read_total_balance; /// Count the notes in the shielded pool commitment tree #[cfg(feature = "server")] mod notes_count; + +#[cfg(test)] +mod tests { + use crate::fees::op::LowLevelDriveOperation; + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use dpp::version::PlatformVersion; + + /// Every primitive here takes the pool's path as a parameter so a token pool can reuse it, and + /// the credit pool passes the path each one used to build for itself. Protocol version 13 is + /// the newest released version selecting these generations, so it is where that has to be shown + /// rather than argued: the whole set is driven through the dispatchers at 13 and each outcome + /// is the one the credit pool gave before the path became a parameter. + #[test] + fn credit_pool_primitives_give_the_same_outcomes_at_protocol_version_13() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::get(13).expect("protocol version 13 should exist"); + let transaction = drive.grove.start_transaction(); + let mut ops = vec![]; + + // An untouched pool holds nothing. + assert_eq!( + drive + .shielded_pool_notes_count(Some(&transaction), &mut ops, platform_version) + .expect("notes count on an untouched pool"), + 0 + ); + assert_eq!( + drive + .read_shielded_pool_total_balance(Some(&transaction), &mut ops, platform_version) + .expect("balance of an untouched pool"), + 0 + ); + let nullifier = [1u8; 32]; + assert!(!drive + .has_nullifier(&nullifier, Some(&transaction), &mut ops, platform_version) + .expect("has_nullifier on an untouched pool")); + + // One note, its nullifier, and a balance. + let mut batch = crate::drive::Drive::insert_note_op( + nullifier, + [2u8; 32], + [3u8; 32], + vec![0xEEu8; 216], + platform_version, + ) + .expect("the note insertion operation"); + batch.extend( + drive + .insert_nullifiers(&[nullifier], platform_version) + .expect("the nullifier insertion operations"), + ); + batch.extend( + crate::drive::Drive::update_total_balance_op(5_000, platform_version) + .expect("the balance write operation"), + ); + drive + .grove_apply_batch( + LowLevelDriveOperation::grovedb_operations_batch_consume(batch), + false, + Some(&transaction), + &platform_version.drive, + ) + .expect("applying the pool writes"); + + assert_eq!( + drive + .shielded_pool_notes_count(Some(&transaction), &mut ops, platform_version) + .expect("notes count after one note"), + 1 + ); + assert_eq!( + drive + .read_shielded_pool_total_balance(Some(&transaction), &mut ops, platform_version) + .expect("balance after the write"), + 5_000 + ); + assert!(drive + .has_nullifier(&nullifier, Some(&transaction), &mut ops, platform_version) + .expect("has_nullifier for a recorded one")); + assert!(!drive + .has_nullifier(&[9u8; 32], Some(&transaction), &mut ops, platform_version) + .expect("has_nullifier for an unrecorded one")); + + // The anchor the note produced is recorded once and found. + crate::drive::Drive::record_shielded_pool_anchor_if_changed( + &drive, + 1, + &transaction, + platform_version, + ) + .expect("recording the anchor"); + } +} diff --git a/packages/rs-drive/src/drive/shielded/notes_count/v0/mod.rs b/packages/rs-drive/src/drive/shielded/notes_count/v0/mod.rs index d78194ccfa6..2d98f6af74e 100644 --- a/packages/rs-drive/src/drive/shielded/notes_count/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/notes_count/v0/mod.rs @@ -17,6 +17,9 @@ impl Drive { platform_version: &PlatformVersion, ) -> Result { let pool_path = shielded_credit_pool_path(); + // The pool's path is a parameter now, and the credit pool passes the very path this + // function used to build for itself: every protocol version that selects this + // generation sees the same operations against the same tree. self.pool_notes_count_v0(&pool_path, transaction, drive_operations, platform_version) } diff --git a/packages/rs-drive/src/drive/shielded/prune_anchors/v0/mod.rs b/packages/rs-drive/src/drive/shielded/prune_anchors/v0/mod.rs index 7f269af1b4f..0628a735f0b 100644 --- a/packages/rs-drive/src/drive/shielded/prune_anchors/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/prune_anchors/v0/mod.rs @@ -37,6 +37,9 @@ impl Drive { ) -> Result<(), Error> { let anchors_path = shielded_credit_pool_anchors_path(); let by_height_path = shielded_credit_pool_anchors_by_height_path(); + // The pool's path is a parameter now, and the credit pool passes the very path this + // function used to build for itself: every protocol version that selects this + // generation sees the same operations against the same tree. self.prune_pool_anchors_v0( &anchors_path, &by_height_path, diff --git a/packages/rs-drive/src/drive/shielded/read_total_balance/v0/mod.rs b/packages/rs-drive/src/drive/shielded/read_total_balance/v0/mod.rs index 899e4eb0372..42d107b6cb2 100644 --- a/packages/rs-drive/src/drive/shielded/read_total_balance/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/read_total_balance/v0/mod.rs @@ -19,6 +19,9 @@ impl Drive { platform_version: &PlatformVersion, ) -> Result { let pool_path = shielded_credit_pool_path(); + // The pool's path is a parameter now, and the credit pool passes the very path this + // function used to build for itself: every protocol version that selects this + // generation sees the same operations against the same tree. self.read_pool_total_balance_v0(&pool_path, transaction, drive_operations, platform_version) } diff --git a/packages/rs-drive/src/drive/shielded/record_anchor_if_changed/v0/mod.rs b/packages/rs-drive/src/drive/shielded/record_anchor_if_changed/v0/mod.rs index 900ffd20131..04fa0064088 100644 --- a/packages/rs-drive/src/drive/shielded/record_anchor_if_changed/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/record_anchor_if_changed/v0/mod.rs @@ -34,6 +34,9 @@ impl Drive { let pool_path = shielded_credit_pool_path(); let anchors_path = shielded_credit_pool_anchors_path(); let anchors_by_height_path = shielded_credit_pool_anchors_by_height_path(); + // The pool's path is a parameter now, and the credit pool passes the very path this + // function used to build for itself: every protocol version that selects this + // generation sees the same operations against the same tree. self.record_pool_anchor_if_changed_v0( &pool_path, &anchors_path, diff --git a/packages/rs-drive/src/drive/shielded/update_total_balance/v0/mod.rs b/packages/rs-drive/src/drive/shielded/update_total_balance/v0/mod.rs index 6c18a57122a..6a92fa1ab5b 100644 --- a/packages/rs-drive/src/drive/shielded/update_total_balance/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/update_total_balance/v0/mod.rs @@ -15,6 +15,9 @@ impl Drive { pub(in crate::drive) fn update_total_balance_op_v0( new_total_balance: u64, ) -> Result, Error> { + // The pool's path is a parameter now, and the credit pool passes the very path this + // function used to build for itself: every protocol version that selects this + // generation sees the same operations against the same tree. Self::update_total_balance_op_in_pool_v0(shielded_credit_pool_path_vec(), new_total_balance) } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs index 7a17b3b7f58..ab3c0e2b5ab 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs @@ -623,9 +623,7 @@ mod tests { //! * the `From for TokenDistributionRecipient` roundtrip //! * `ClaimAction` variant dispatch / clone / enum wrapper accessors use dpp::balances::credits::TokenAmount; - use dpp::block::epoch::EpochIndex; use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionInfo; - use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::reward_ratio::RewardRatio; use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_recipient::{ TokenDistributionRecipient, TokenDistributionResolvedRecipient, }; From b16674299101c35abf97b0162f3f182be94048c4 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 11:51:46 +0700 Subject: [PATCH 43/94] fix(dpp): tell the two sides of an inactive version apart in the message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `required_protocol_version` carries two different facts since the decoder began choosing which end of an active range to report. The nine `is_allowed` producers pass a feature's initial version — there it is the version to move to. The decoder passes the range's end when the version is above it — there it is the last version that accepted those bytes, and moving to it is not what the submitter should do. One sentence cannot be actionable for both, and the neutral wording that covered them was accurate for both and useful to neither. So the message branches on which side was missed: "is not active at protocol version N: it becomes active at M" below the range, "is no longer accepted at protocol version N: protocol version M was the last that accepted it, and a newer transition version carries it from here" above it. The getter says the same in its doc, since a reader holding the number alone cannot tell the cases apart. The dispatcher doc, the v1 generation's doc and the protocol version 14 feature list each stated the reclassification without the rule that picks the boundary; all three now carry it. `coding-conventions.md:344-347` puts that contract in the dispatcher's doc comment. Also here: a line in the batch-cap note rewrapped to half again the paragraph's width in an earlier commit, and the `methods:` slot summary in `v14.rs` listed two of the entries its table moves while the merge had brought in four more — it now points at the per-slot comments that carry the full list. Co-Authored-By: Claude Opus 5 (1M context) --- .../state_transition_not_active_error.rs | 31 +++++++++++++++++-- .../decode_raw_state_transitions/mod.rs | 5 ++- .../decode_raw_state_transitions/v1/mod.rs | 5 ++- .../src/version/system_limits/mod.rs | 5 +-- .../rs-platform-version/src/version/v14.rs | 8 +++-- 5 files changed, 44 insertions(+), 10 deletions(-) diff --git a/packages/rs-dpp/src/errors/consensus/basic/state_transition/state_transition_not_active_error.rs b/packages/rs-dpp/src/errors/consensus/basic/state_transition/state_transition_not_active_error.rs index dee0ede33e7..713b5474358 100644 --- a/packages/rs-dpp/src/errors/consensus/basic/state_transition/state_transition_not_active_error.rs +++ b/packages/rs-dpp/src/errors/consensus/basic/state_transition/state_transition_not_active_error.rs @@ -21,9 +21,7 @@ use thiserror::Error; PlatformDeserializeUntrusted, DecodeUntrusted, )] -#[error( - "State transition type {state_transition_type} is not active at protocol version {current_protocol_version}. The boundary of its active range is protocol version {required_protocol_version}" -)] +#[error("{}", self.describe())] #[platform_serialize(unversioned)] pub struct StateTransitionNotActiveError { /* @@ -57,9 +55,36 @@ impl StateTransitionNotActiveError { self.current_protocol_version } + /// The boundary of the transition's active range that `current_protocol_version` missed, which + /// is the range's start when the version is below it and the range's end when it is above. + /// Read it with `current_protocol_version`: alone it does not say which side was missed, and a + /// version above the range is not reached by moving to the one reported. pub fn required_protocol_version(&self) -> ProtocolVersion { self.required_protocol_version } + + /// Both sides of the range read differently, so each is spelled out rather than sharing a + /// wording that would be accurate for both and actionable for neither. + fn describe(&self) -> String { + if self.current_protocol_version > self.required_protocol_version { + format!( + "State transition type {} is no longer accepted at protocol version {}: protocol \ + version {} was the last that accepted it, and a newer transition version carries \ + it from here", + self.state_transition_type, + self.current_protocol_version, + self.required_protocol_version + ) + } else { + format!( + "State transition type {} is not active at protocol version {}: it becomes active \ + at protocol version {}", + self.state_transition_type, + self.current_protocol_version, + self.required_protocol_version + ) + } + } } impl From for ConsensusError { diff --git a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/decode_raw_state_transitions/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/decode_raw_state_transitions/mod.rs index c9ed0a622bf..7cf86299060 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/decode_raw_state_transitions/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/decode_raw_state_transitions/mod.rs @@ -15,7 +15,10 @@ where /// Processes the given raw state transitions based on the `block_info` and `transaction`. /// /// Version 0 ignores bytes left over after a transition; version 1 (from protocol version - /// 14) refuses them as an invalid encoding. + /// 14) refuses them as an invalid encoding, and reports a transition whose version is outside + /// its active range as a coded consensus refusal rather than a decode failure. The boundary + /// that refusal names is the range's start when the version is below it and the range's end + /// when it is above. /// /// # Arguments /// diff --git a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/decode_raw_state_transitions/v1/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/decode_raw_state_transitions/v1/mod.rs index 93b7eb7e961..949a6040b74 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/decode_raw_state_transitions/v1/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/decode_raw_state_transitions/v1/mod.rs @@ -27,7 +27,10 @@ where /// anything appended decoded, validated and executed as the original. Here left over bytes /// are an `InvalidEncoding` carrying a `SerializedObjectParsingError`, which is unpaid. A /// transition whose version is not active at `platform_version` is an `InvalidEncoding` - /// carrying a `StateTransitionNotActiveError`, not a decode failure. + /// carrying a `StateTransitionNotActiveError`, not a decode failure. The range can be missed + /// from either side, and the boundary reported is the one that was missed: its start for a + /// version not active yet, its end for one already superseded, since naming the start there + /// would point at a version the chain is past. /// /// ## Arguments /// diff --git a/packages/rs-platform-version/src/version/system_limits/mod.rs b/packages/rs-platform-version/src/version/system_limits/mod.rs index c336f3e8f95..f8d93931fd7 100644 --- a/packages/rs-platform-version/src/version/system_limits/mod.rs +++ b/packages/rs-platform-version/src/version/system_limits/mod.rs @@ -107,8 +107,9 @@ pub struct SystemLimits { /// against a pool at all — so the same authorized bytes can sit in two shields of one /// batch: state validation runs per transition against the transaction before any /// operation applies, so the second cannot see the first's pending insert, and the - /// within-bundle check is scoped to one action set. The two inserts are then byte-identical in path, key and value, which a node - /// running the shipped batching default folds in silence while a node verifying batch + /// within-bundle check is scoped to one action set. The two inserts are then byte-identical + /// in path, key and value, which a node running the shipped batching default folds in + /// silence while a node verifying batch /// consistency refuses — the two disagree on one block and neither shows why. Raising the /// cap means batch-scoped nullifier deduplication and a delta-based pool balance write, /// not just making the ignored cases above pass. diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index 72d4d0bfbd8..7bd5ad14410 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -1221,8 +1221,10 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// (`SerializedObjectParsingError`, 10002), refused unpaid in `check_tx` and /// in block processing. Version 0 ignored them, so the transition with /// anything appended executed as the original under another transaction -/// hash. Version 1 also reports a transition whose version is not active -/// as `StateTransitionNotActiveError` (10603) instead of a decode failure. +/// hash. Version 1 also reports a transition whose version is outside its +/// active range as `StateTransitionNotActiveError` (10603) instead of a +/// decode failure, naming whichever boundary of that range was missed: its +/// start for a version not active yet, its end for one already superseded. /// /// 47. **A storage refund is clawed back from the epochs it was priced for**: /// removing data in epoch E refunds its owner the shares of epochs E+1 @@ -1346,7 +1348,7 @@ pub const PLATFORM_V14: PlatformVersion = PlatformVersion { drive: DRIVE_VERSION_V9, // changed: drive document method versions v4 — v2 index walkers (shared-prefix aggregate indexes become insertable) + the detect_ranked_mode slot; contract method versions v4: the moderation list trees, the document removal record trees and the moderation method table; apply_drive_operations 1 (a moderator's document deletion refunds nobody; every write of one identity balance, fee pot or prefunded specialized balance in a batch merged into one; a batch writing one token balance or supply twice refused; repaid identity debt credited to the processing fee pool); index uniqueness gains validate_restored_document_uniqueness (a moderator's document restore); vote method versions v3: the end-date cleanup of ended contested vote polls removes an end date only once none of its polls remain; token method versions v2: calculate_total_tokens_balance 1 (token shielded pool balances join token conservation) and evonode_participation_rewards 1 (an evonode's token claim covers only the epochs it read) drive_abci: DriveAbciVersion { structs: DRIVE_ABCI_STRUCTURE_VERSIONS_V2, // changed: saved platform state structure 1 keeps masternodes and validator sets as one aux entry each - methods: DRIVE_ABCI_METHOD_VERSIONS_V10, // changed: records the per-block total credits history for the daily withdrawal limit; record_token_shielded_pool_anchors records and prunes the anchors of the token pools a block touched + methods: DRIVE_ABCI_METHOD_VERSIONS_V10, // changed: records the per-block total credits history for the daily withdrawal limit; record_token_shielded_pool_anchors records and prunes the anchors of the token pools a block touched; decode_raw_state_transitions, execute_event, validate_fees_of_event and add_distribute_storage_fee_to_epochs_operations each move to 1 — the table's own per-slot comments carry the full list validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V10, // changed: contested-index cross-check + refersTo document reference validation; the ContractUserModeration gates and the batch transformer's contract_moderation_gate; the three shielded-fee token pool transitions gain basic structure validation and document_base_transition_state_validation 1 admits a document token cost paid from a token pool; the ShieldFromAssetLock transform_into_action 1 checks its bundle against the bound preimage withdrawal_constants: DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V3, // changed: prune bound for the total credits history query: DRIVE_ABCI_QUERY_VERSIONS_V3, // changed: ranked + boolean-HAVING routing gate; the v1 handler also resolves IN_TIME_RANGE from committed block time From e98f1474fa01f8121be8772f6272afa85e626529 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 12:02:13 +0700 Subject: [PATCH 44/94] test(drive-abci): stop a transition kind from losing its is_allowed phase unnoticed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two fixtures behind the `has_is_allowed_validation` tests are written by hand and carry 15 of the 29 kinds. A kind in neither list is invisible to both tests, so a kind can be moved out of the group that gets the check — or arrive without being placed at all — with everything still green. That is the shape of a defect this branch already had once: the merge dropped `Batch` from the group needing the check, which would have left the token shielded pool's pre-activation gate as dead code, and only an exhaustive match elsewhere refused to compile. So the expected answer for every kind is now stated in one exhaustive match. A new kind stops compiling there until someone says which side it belongs on, and the fixtures are checked against that statement for every kind they carry. The shielded and token kinds stay out of the fixtures: their `V0` bodies carry bundles, proofs and signatures and implement no `Default`, so a fixture entry would be a fabricated transition rather than a cheap one. What the gate does for them is pinned by the block-level tests; what this adds is that they cannot be quietly forgotten. Co-Authored-By: Claude Opus 5 (1M context) --- .../processor/traits/is_allowed.rs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs index d1ef5c08802..baffbe73767 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs @@ -542,6 +542,71 @@ mod tests { } } + /// The two fixtures are written by hand, so a transition kind absent from both is invisible + /// to the tests that consume them — which is how a kind can lose its `is_allowed` phase + /// with every test still green. The match below is exhaustive, so a new kind stops + /// compiling here until its expected answer is stated, and the fixtures are then held to + /// that answer for every kind they carry. + /// + /// The shielded and token kinds have no fixture entry: their `V0` bodies carry bundles, + /// proofs and signatures and implement no `Default`. What the gate does for them is pinned + /// by the block-level tests instead; what this test adds is that they cannot be forgotten. + #[test] + fn every_transition_kind_states_whether_it_has_an_is_allowed_phase() { + fn expected(st: &StateTransition) -> Option { + match st { + StateTransition::Batch(_) + | StateTransition::IdentityTopUpFromAddresses(_) + | StateTransition::IdentityCreateFromAddresses(_) + | StateTransition::AddressFundsTransfer(_) + | StateTransition::IdentityCreditTransferToAddresses(_) + | StateTransition::AddressFundingFromAssetLock(_) + | StateTransition::AddressCreditWithdrawal(_) + | StateTransition::Shield(_) + | StateTransition::ShieldedTransfer(_) + | StateTransition::Unshield(_) + | StateTransition::ShieldFromAssetLock(_) + | StateTransition::ShieldedWithdrawal(_) + | StateTransition::IdentityCreateFromShieldedPool(_) + | StateTransition::ShieldFromIdentity(_) + | StateTransition::IdentityTopUpFromShieldedPool(_) + | StateTransition::IdentityKeyLimitsUpdate(_) + | StateTransition::ContractUserModeration(_) + | StateTransition::ContractFeeClaim(_) + | StateTransition::TokenShieldedTransferWithShieldedFee(_) + | StateTransition::TokenUnshieldWithShieldedFee(_) + | StateTransition::TokenPurchaseFromShieldedPool(_) => Some(true), + StateTransition::IdentityCreate(_) + | StateTransition::IdentityTopUp(_) + | StateTransition::IdentityCreditWithdrawal(_) + | StateTransition::IdentityUpdate(_) + | StateTransition::IdentityCreditTransfer(_) + | StateTransition::MasternodeVote(_) => Some(false), + // These two answer from the contract they carry, not from their kind: a token + // configuration format the protocol version does not admit needs the check. + StateTransition::DataContractCreate(_) + | StateTransition::DataContractUpdate(_) => None, + } + } + + for st in transitions_requiring_allowed_validation() { + assert_ne!( + expected(&st), + Some(false), + "a kind in the requiring fixture is stated as needing no check: {:?}", + std::mem::discriminant(&st) + ); + } + for st in transitions_not_requiring_allowed_validation() { + assert_ne!( + expected(&st), + Some(true), + "a kind in the not-requiring fixture is stated as needing the check: {:?}", + std::mem::discriminant(&st) + ); + } + } + #[test] fn should_return_false_for_transitions_not_requiring_allowed_check() { for st in transitions_not_requiring_allowed_validation() { From b1c7bb1bf5042ce0e4cf31246a16e0d4f9032df3 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 12:08:32 +0700 Subject: [PATCH 45/94] test(drive-abci): drop an assertion that could not fire `assert_eq!(bundle.amount, SHIELDED)` guarded the bound bundle against the inputs `signed_shield` derives. It could not: `build_outputs_only_bundle_bound(value, _)` adds one output of `value` and sets `amount` from that same balance, so the two are equal by construction. Nor would it have guarded what it claimed even if it could, since the amount reaches the transition only as the credits of the inputs map, which `shield_funding_digest_v0` does not hash. Co-Authored-By: Claude Opus 5 (1M context) --- .../state_transition/state_transitions/shield/tests.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs index 6c3375da708..4b3463f9c56 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs @@ -1451,12 +1451,7 @@ mod tests { ); let extra_sighash_data = shield_extra_sighash_data(&inputs, platform_version) .expect("the binding of the funding inputs"); - let bundle = build_outputs_only_bundle_bound(SHIELDED, &extra_sighash_data); - assert_eq!( - bundle.amount, SHIELDED, - "the inputs signed_shield derives must be the ones the bundle is bound to" - ); - bundle + build_outputs_only_bundle_bound(SHIELDED, &extra_sighash_data) } /// A shield of `bundle` from `address` at `nonce`, signed by `signer`. From 1793ba161108814c3f0d9f4dfbaebd753cdd4f07 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 12:41:56 +0700 Subject: [PATCH 46/94] fix(drive): let a wallet verify a proof of a token pool transition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neither half of prove-then-verify worked for any of the seven kinds that write into a token shielded pool, and nothing said so: no test proved a pool transition and verified the proof, for any kind. Both faults sat in code the seven share with the eleven older token kinds, so both were invisible from the pool's own code. The verifier resolved the token history document type ahead of every branch. The pool kinds have no entry in the history contract — they keep no history, and no apply path writes one for them — so the lookup returned `DocumentTypeNotFound` before control reached their branches, and the branches at the end of that match were unreachable in every run. It is resolved inside the closure that needs it now, which only the kinds that keep history ever call, so the history contract does not have to gain names for documents nobody writes. The seven branches then asked to verify strictly. From protocol version 14 the prover merges the owner's credit balance into any batch's proof, so a strict read rejects the extra layer — GroveDB reports "proof contains extra lower layer … not required by query". They now pass `carries_owner_balance`, as the eleven older branches already did. The test that should have caught this exists now for `TokenShield`, through the real path: execute, commit, prove, verify. It fails on each fault on its own — `DocumentTypeNotFound` with the first restored, the extra-layer rejection with only the second — and passes with both fixed. It pins what the faults destroyed: the owner's token balance, and the credit balance riding in the same proof. The other six pool kinds still have no prove-then-verify test. The fixes are in code all seven share and the suite is green, but that is an argument, not coverage. Co-Authored-By: Claude Opus 5 (1M context) --- .../batch/tests/token/shielded/mod.rs | 94 +++++++++++++++++++ .../v0/mod.rs | 20 ++-- 2 files changed, 106 insertions(+), 8 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 759f60c6a06..288b55e7e8f 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -35,11 +35,13 @@ mod token_shielded_pool_tests { use dpp::state_transition::data_contract_create_transition::DataContractCreateTransition; use dpp::state_transition::data_contract_update_transition::methods::DataContractUpdateTransitionMethodsV0; use dpp::state_transition::data_contract_update_transition::DataContractUpdateTransition; + use dpp::state_transition::proof_result::StateTransitionProofResult; use dpp::state_transition::StateTransition; use dpp::tests::fixtures::get_data_contract_fixture; use dpp::tests::json_document::json_document_to_contract_with_ids; use dpp::version::feature_initial_protocol_versions::TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION; use drive::drive::shielded::paths::token_shielded_pool_anchors_path_vec; + use drive::drive::Drive; use drive::grovedb::Element; use grovedb_commitment_tree::{ Anchor, Authorized, Builder, Bundle, BundleType, ClientMemoryCommitmentTree, DashMemo, @@ -48,6 +50,7 @@ mod token_shielded_pool_tests { SpendingKey, }; use platform_version::version::PlatformVersion; + use std::collections::BTreeMap; /// The basic token fixture mints this much to the contract owner. pub(super) const OWNER_INITIAL_BALANCE: u64 = 100_000; @@ -398,6 +401,97 @@ mod token_shielded_pool_tests { .set_genesis_state() } + /// Proving a pool write and verifying that proof is how a wallet learns what the write left + /// behind, and no kind that writes into a pool had a test for it. Both halves of the path were + /// broken and nothing said so: the verifier asked the token history contract for a document + /// type these kinds do not have, and it read the proof strictly although the prover merges the + /// owner's balance into any batch's proof from protocol version 14. + #[tokio::test] + async fn should_prove_and_verify_a_token_shield() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9101); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + + let shield_bundle = build_shield_bundle( + SHIELD_AMOUNT, + 11, + TokenTransitionActionType::Shield, + token_id, + identity.id(), + ); + let shield = BatchTransition::new_token_shield_transition( + token_id, + identity.id(), + contract.id(), + 0, + SHIELD_AMOUNT, + shield_bundle, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token shield transition"); + + let result = process(&platform, &shield); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + let proof = platform + .drive + .prove_state_transition(&shield, None, platform_version) + .expect("prove the shield") + .into_data() + .expect("proof bytes rather than an error"); + + let known_contracts: BTreeMap = + BTreeMap::from([(contract.id(), contract.clone())]); + let (root_hash, outcome) = Drive::verify_state_transition_was_executed_with_proof( + &shield, + &BlockInfo::default(), + &proof, + &|id| Ok(known_contracts.get(id).cloned().map(std::sync::Arc::new)), + platform_version, + ) + .expect("verify the shield proof"); + + assert_ne!(root_hash, [0u8; 32], "the verified proof must carry a root"); + + // A shield's proof is an affected-state snapshot, not a proof that this transition ran: + // the pool write it leaves behind is not attributable to one transition. What it must + // carry is the owner's token balance after the write, and the owner's credit balance + // beside it — the prover merges that second query into any batch's proof from protocol + // version 14, so a verifier reading the proof strictly finds neither. + assert_matches!( + outcome.result(), + StateTransitionProofResult::VerifiedTokenBalance(owner, _) + if *owner == identity.id(), + "the proof must carry the shielding identity's token balance" + ); + assert!( + outcome.owner_balance().is_some(), + "the owner's credit balance rides along in the same proof, got {:?}", + outcome.owner_balance() + ); + } + #[tokio::test] async fn test_token_shield_unshield_and_shielded_transfer() { let platform_version = PlatformVersion::latest(); diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs index 353d8f360fb..69bf020e952 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs @@ -635,15 +635,19 @@ impl Drive { platform_version, )?; - let token_history_document_type = - token_transition.historical_document_type(&token_history_contract)?; - let token_config = contract.expected_token_configuration( token_transition.base().token_contract_position(), )?; let keeps_historical_document = token_config.keeps_history(); let historical_query = || { + // Resolved here rather than above because the kinds that write into a + // shielded pool keep no history and have no document type in the + // history contract. Only a kind that reaches this closure has one, so + // looking it up any earlier fails for every pool transition, whatever + // the token's history settings say. + let token_history_document_type = token_transition + .historical_document_type(&token_history_contract)?; let query = SingleDocumentDriveQuery { contract_id: token_history_contract.id().into_buffer(), document_type_name: token_history_document_type_name, @@ -1118,7 +1122,7 @@ impl Drive { proof, token_id.into_buffer(), owner_id.into_buffer(), - false, + carries_owner_balance, platform_version, )? else { @@ -1134,7 +1138,7 @@ impl Drive { proof, token_id.into_buffer(), recipient_id.into_buffer(), - false, + carries_owner_balance, platform_version, )? else { @@ -1156,7 +1160,7 @@ impl Drive { proof, token_id.into_buffer(), &nullifiers, - false, + carries_owner_balance, platform_version, )?; if statuses.len() != nullifiers.len() @@ -1175,7 +1179,7 @@ impl Drive { Drive::verify_token_shielded_pool_state( proof, token_id.into_buffer(), - false, + carries_owner_balance, platform_version, )? else { @@ -1199,7 +1203,7 @@ impl Drive { proof, token_id.into_buffer(), &nullifiers, - false, + carries_owner_balance, platform_version, )?; if statuses.len() != nullifiers.len() From b76c5365412c359e17b0672e55f4050bf4349a90 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 12:46:33 +0700 Subject: [PATCH 47/94] test(drive-abci): pin the per-token namespace for a bundle's dummy nullifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `test_a_nullifier_spent_in_one_token_pool_is_unspent_in_another` pins the property for a nullifier a spend reveals. The dummy nullifiers an outputs-only bundle reveals had no equivalent, and today they do not need one: both kinds travel `token_shielded_pool_update_operations` to be written and `has_token_pool_nullifier` to be read, and both take the token first, so one tree per token holds them all. That is an argument about the shape of today's code, which is exactly what stops being true across a refactor. Give the outputs-only path a writer or a reader of its own — and "record the dummies too" invites precisely that split — and the existing test stays green while dummies lose the namespace. The failure would surface either as one token's pool refusing an honest operation in another, or as a cross-pool repeat that nothing rejects. So the dummies get the same two assertions, over `bundle.actions`: recorded in their own token's pool, absent from a second token's. Co-Authored-By: Claude Opus 5 (1M context) --- .../batch/tests/token/shielded/mod.rs | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 288b55e7e8f..a82e5b38d50 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -1251,6 +1251,94 @@ mod token_shielded_pool_tests { assert_tokens_conserved(&platform); } + /// The sibling below pins the per-token namespace for a nullifier a *spend* reveals. The dummy + /// nullifiers an outputs-only bundle reveals travel the same two token-scoped functions today — + /// `token_shielded_pool_update_operations` writing, `has_token_pool_nullifier` reading, both + /// taking the token first — so the property holds for them by construction. Construction is + /// what one can say before a refactor, not after: give the outputs-only path a writer of its + /// own and the sibling stays green while dummies lose the namespace, which shows up either as + /// one token's pool refusing an honest operation in another, or as a cross-pool repeat nothing + /// rejects. + #[tokio::test] + async fn test_a_dummy_nullifier_recorded_in_one_token_pool_is_unrecorded_in_another() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9103); + + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract_a, token_a) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + // A contract id derives from its owner, so a second owner is what makes the second pool a + // different pool rather than the same one again. + let (other_owner, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.1)); + let (_contract_b, token_b) = create_token_contract_with_owner_identity( + &mut platform, + other_owner.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + assert_ne!(token_a, token_b, "the two tokens must own separate pools"); + + let bundle = build_shield_bundle( + SHIELD_AMOUNT, + 29, + TokenTransitionActionType::Shield, + token_a, + identity.id(), + ); + let dummy_nullifiers: Vec<[u8; 32]> = + bundle.actions.iter().map(|action| action.nullifier).collect(); + assert!( + !dummy_nullifiers.is_empty(), + "an outputs-only bundle reveals a dummy nullifier per action" + ); + + let shield = BatchTransition::new_token_shield_transition( + token_a, + identity.id(), + contract_a.id(), + 0, + SHIELD_AMOUNT, + bundle, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token shield transition"); + + let result = process(&platform, &shield); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + for nullifier in &dummy_nullifiers { + assert!( + nullifier_is_spent(&platform, token_a, nullifier), + "a shield records its dummy nullifiers in its own token's pool" + ); + assert!( + !nullifier_is_spent(&platform, token_b, nullifier), + "recording a dummy nullifier in one token's pool must not mark it in another" + ); + } + } + /// Spent nullifiers are namespaced by token id too: spending a note in one token's pool must /// not mark that nullifier spent in another's, or the first token to use a nullifier would /// make every other pool's note with the same nullifier unspendable. From 030ea540d3de7d6e8af0b76a9287940859d837dd Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 12:48:32 +0700 Subject: [PATCH 48/94] style(drive-abci): wrap the dummy nullifier collection as rustfmt asks Co-Authored-By: Claude Opus 5 (1M context) --- .../state_transitions/batch/tests/token/shielded/mod.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index a82e5b38d50..b76bb4c5ca5 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -1297,8 +1297,11 @@ mod token_shielded_pool_tests { token_a, identity.id(), ); - let dummy_nullifiers: Vec<[u8; 32]> = - bundle.actions.iter().map(|action| action.nullifier).collect(); + let dummy_nullifiers: Vec<[u8; 32]> = bundle + .actions + .iter() + .map(|action| action.nullifier) + .collect(); assert!( !dummy_nullifiers.is_empty(), "an outputs-only bundle reveals a dummy nullifier per action" From ead553ef0b58faadf2fb4b4e140abf5dd39a09b9 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 13:31:57 +0700 Subject: [PATCH 49/94] fix(drive-abci)!: hold a shielded-pool purchase to the pool's anonymity set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A purchase paid from the credit shielded pool moves the agreed price out of that pool and into the contract owner's identity balance. Credits leaving the pool for a named identity is what the minimum-notes threshold exists to gate — unshield, shielded withdrawal, and the identity create and top up paid from the pool all check it — and this one did not. The threshold at protocol version 14 is 250 notes, so the gap was live rather than latent: a contract owner could buy from itself out of a pool too small to hide where the credits came from, and the transition reported nothing unusual. It now calls `validate_minimum_pool_notes` before it spends the pool, as its four siblings do. The fee half is untouched: that leaves the pool for the fee pools, not for an identity, and whether a fee spend owes the same anonymity set is a separate question this does not answer. `test_token_purchase_from_shielded_pool` submits one purchase on both sides of the threshold — refused unpaid while the pool holds nothing, accepted after the pool is seeded, the same bundle both times since an unpaid refusal records nothing. Without the check the first submission succeeds, which is the vector. The three copy tests reach the purchase through `token_for_sale`, which now seeds the pool so they stay about what they are named for. Co-Authored-By: Claude Opus 5 (1M context) --- .../batch/tests/token/shielded/mod.rs | 36 +++++++++++++++++++ .../transform_into_action/v0/mod.rs | 16 +++++++++ 2 files changed, 52 insertions(+) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index b76bb4c5ca5..04b938d5e09 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -3677,6 +3677,7 @@ mod document_shielded_token_payment_tests { /// and a fee bundle in the credit pool, no identity anywhere. The test wallet owns notes in /// both pools; the credit pool is funded directly in state, the token pool through a shield. mod token_pool_paid_transitions_tests { + use crate::execution::validation::state_transition::state_transitions::test_helpers::insert_dummy_encrypted_notes; use super::token_shielded_pool_tests::{ assert_check_tx_accepts, assert_tokens_conserved, build_shield_bundle, dummy_bundle, enable_shielded_pool, identity_token_balance, insert_token_pool_anchor, nullifier_is_spent, @@ -4075,6 +4076,27 @@ mod token_pool_paid_transitions_tests { ) .expect("build transition"); + // The agreed price leaves the credit pool for the seller's balance, so the purchase owes + // the same anonymity set as every other way credits leave that pool. The same transition + // is refused while the pool is too small and accepted once it is not; the refusal is + // unpaid, so it records nothing and the retry is the same bundle. + let result = process(&platform, &transition); + assert_matches!( + result.execution_results().as_slice(), + [StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::StateError(StateError::InsufficientPoolNotesError(_)) + )], + "a purchase may not walk credits out of a pool too small to hide where they came from" + ); + insert_dummy_encrypted_notes( + &platform, + platform_version + .drive_abci + .validation_and_processing + .event_constants + .minimum_pool_notes_for_outgoing, + ); + let result = process(&platform, &transition); assert_matches!( result.execution_results().as_slice(), @@ -4480,6 +4502,7 @@ mod token_pool_paid_transitions_tests { /// bundle the verifier never accepted anywhere is refused for any reason at all. The repeat /// must then fail on the original's first dummy nullifier, and nothing it carries may move. mod token_pool_outputs_only_copy_tests { + use crate::execution::validation::state_transition::state_transitions::test_helpers::insert_dummy_encrypted_notes; use super::token_pool_paid_transitions_tests::{ credit_pool_balance, fund_credit_pool, spendable, wallet_address, Prover, CREDIT_NOTE, }; @@ -6751,6 +6774,19 @@ mod token_pool_outputs_only_copy_tests { process(platform, &set_price).execution_results().as_slice(), [StateTransitionExecutionResult::SuccessfulExecution { .. }] ); + // A purchase moves the agreed price out of the credit pool to the seller, so it owes the + // same anonymity set as every other way credits leave that pool. Seeding it here keeps the + // tests below about what they are named for; the threshold itself is pinned by + // `test_token_purchase_from_shielded_pool`, which submits the same purchase on both sides + // of it. + insert_dummy_encrypted_notes( + platform, + platform_version + .drive_abci + .validation_and_processing + .event_constants + .minimum_pool_notes_for_outgoing, + ); (seller.id(), contract.id(), token_id) } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs index 1080f82e1dd..976f7515cb2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs @@ -1,4 +1,5 @@ use crate::error::Error; +use crate::execution::validation::state_transition::state_transitions::shielded_common::validate_minimum_pool_notes; use crate::execution::validation::state_transition::state_transitions::token_pool_paid_common::{ resolve_pooled_token, validate_credit_pool_fee_spend, validate_token_pool_outputs, }; @@ -126,6 +127,21 @@ impl TokenPurchaseFromShieldedPoolStateTransitionTransformIntoActionValidationV0 )? { return Ok(rejection); } + // The agreed price leaves the credit pool for the contract owner's balance, so this is a + // value exit from the shielded set to a named identity and owes the same anonymity set as + // the other four: unshield, shielded withdrawal, and the identity create and top up paid + // from the pool. Without it a contract owner buys from itself and walks credits out of a + // pool too small to hide where they came from. + let mut drive_operations = vec![]; + if let Some(consensus_error) = validate_minimum_pool_notes( + drive, + transaction, + &mut drive_operations, + platform_version, + )? { + return Ok(consensus_error); + } + let fee_nullifiers: Vec<[u8; 32]> = v0.fee_actions.iter().map(|a| a.nullifier).collect(); let current_credit_pool_balance = match validate_credit_pool_fee_spend( drive, From c66ceb585693bf862fc59ceafbbc174532c44ba7 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 14:07:12 +0700 Subject: [PATCH 50/94] fix(dpp): drop a token config validator nothing calls, and the claim it earned MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `validate_token_config_update` has no caller anywhere outside its own module, and it had none in `v4.1.1` either: the dispatcher and its `v0` shipped dead. This branch added a `v1` to it, pointed `DPP_VALIDATION_VERSIONS_V5` at that `v1`, and recorded in `v14.rs` that "validate_token_config_update 1 keeps the shielded pool opt-in immutable after creation". The sentence is false. Nothing invokes the function, so it keeps nothing. What does keep it immutable is `DataContract::validate_update`, which refuses any change to a `TokenConfigurationV1` field and is pinned by `should_return_invalid_result_when_any_token_configuration_v1_field_is_updated` — a test that destructures the struct without `..`, so a field added later stops it compiling until someone decides what changing it should mean. The rule is guarded, and guarded better than the unreachable validator would have guarded it. Wiring the dispatcher in instead was the other option and is worse: it would give one rule two enforcement points that can disagree, and it would put new behaviour into a generation released versions select. So the `v1`, the dispatcher arm that reached it, the version-table entry and the false sentence all go, leaving the dispatcher exactly as it shipped. The test that went with the `v1` went too: it named protocol version 15, ran at 14, and exercised a function no transition can reach. Co-Authored-By: Claude Opus 5 (1M context) --- .../mod.rs | 10 +- .../v1/mod.rs | 91 ------------------- .../dpp_validation_versions/v5.rs | 2 +- .../rs-platform-version/src/version/v14.rs | 2 +- 4 files changed, 3 insertions(+), 102 deletions(-) delete mode 100644 packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/v1/mod.rs diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/mod.rs index 147905dbfde..a0a12019fbf 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/mod.rs @@ -9,7 +9,6 @@ use platform_version::version::PlatformVersion; use std::collections::BTreeMap; mod v0; -mod v1; impl TokenConfiguration { pub fn validate_token_config_update( @@ -34,16 +33,9 @@ impl TokenConfiguration { action_taker, goal, )), - 1 => Ok(self.validate_token_config_update_v1( - new_config, - contract_owner_id, - groups, - action_taker, - goal, - )), version => Err(ProtocolError::UnknownVersionMismatch { method: "validate_token_config_update".to_string(), - known_versions: vec![0, 1], + known_versions: vec![0], received: version, }), } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/v1/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/v1/mod.rs deleted file mode 100644 index 351026aabe9..00000000000 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_update/v1/mod.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::consensus::basic::data_contract::DataContractTokenConfigurationUpdateError; -use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Getters; -use crate::data_contract::associated_token::token_configuration::TokenConfiguration; -use crate::data_contract::group::Group; -use crate::data_contract::GroupContractPosition; -use crate::group::action_taker::{ActionGoal, ActionTaker}; -use crate::validation::SimpleConsensusValidationResult; -use platform_value::Identifier; -use std::collections::BTreeMap; - -impl TokenConfiguration { - pub(super) fn validate_token_config_update_v1( - &self, - new_config: &TokenConfiguration, - contract_owner_id: &Identifier, - groups: &BTreeMap, - action_taker: &ActionTaker, - goal: ActionGoal, - ) -> SimpleConsensusValidationResult { - let result = self.validate_token_config_update_v0( - new_config, - contract_owner_id, - groups, - action_taker, - goal, - ); - if !result.is_valid() { - return result; - } - // The shielded pool opt-in is decided at creation: enabling it later would need the - // pool subtree created by the update, and a pool holding notes can never be removed. - if self.has_shielded_pool() != new_config.has_shielded_pool() { - return SimpleConsensusValidationResult::new_with_error( - DataContractTokenConfigurationUpdateError::new( - "update".to_string(), - "hasShieldedPool".to_string(), - self.clone(), - new_config.clone(), - ) - .into(), - ); - } - - SimpleConsensusValidationResult::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters; - use crate::data_contract::associated_token::token_configuration::v0::TokenConfigurationV0; - use platform_version::version::PlatformVersion; - - #[test] - fn should_freeze_the_shielded_pool_opt_in_only_from_protocol_15() { - let owner = Identifier::from([1; 32]); - let groups = BTreeMap::new(); - let action_taker = ActionTaker::SingleIdentity(owner); - let plain = TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); - let mut shielded = plain.clone(); - shielded.set_has_shielded_pool(true); - - for platform_version in [PlatformVersion::get(13).unwrap(), PlatformVersion::latest()] { - for (old, new) in [ - (&plain, &shielded), - (&shielded, &plain), - (&shielded, &shielded), - ] { - let result = old - .validate_token_config_update( - new, - &owner, - &groups, - &action_taker, - ActionGoal::ActionCompletion, - platform_version, - ) - .expect("validate token configuration update"); - assert_eq!( - result.is_valid(), - platform_version.protocol_version < 14 - || old.has_shielded_pool() == new.has_shielded_pool(), - "unexpected update validation on protocol {}: {:?}", - platform_version.protocol_version, - result.errors, - ); - } - } - } -} diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v5.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v5.rs index 694d96714c6..0f8da7df9bb 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v5.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v5.rs @@ -23,7 +23,7 @@ pub const DPP_VALIDATION_VERSIONS_V5: DPPValidationVersions = DPPValidationVersi data_contract: DataContractValidationVersions { validate_config_update: 2, validate_once_per_identity_distribution: Some(0), - validate_token_config_update: 1, + validate_token_config_update: 0, ..DPP_VALIDATION_VERSIONS_V4.data_contract }, document_type: DocumentTypeValidationVersions { diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index 7bd5ad14410..8a48f870955 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -1356,7 +1356,7 @@ pub const PLATFORM_V14: PlatformVersion = PlatformVersion { }, dpp: DPPVersion { costs: DPP_COSTS_VERSIONS_V1, - validation: DPP_VALIDATION_VERSIONS_V5, // changed: validate_config_update 2 admits the contract moderation declaration of config V2; validate_token_config_update 1 keeps the shielded pool opt-in immutable after creation + validation: DPP_VALIDATION_VERSIONS_V5, // changed: validate_config_update 2 admits the contract moderation declaration of config V2 state_transition_serialization_versions: STATE_TRANSITION_SERIALIZATION_VERSIONS_V3, // changed: the indexOnly delete-by-values kind (documentIndexOnlyDelete) joins the wire; ShieldFromAssetLock moves to version 1 alone; the ContractUserModeration transition state_transition_conversion_versions: STATE_TRANSITION_CONVERSION_VERSIONS_V2, state_transition_method_versions: STATE_TRANSITION_METHOD_VERSIONS_V2, // changed: public keys in creation may carry a budget or an expiry From 256fe5154586a1186918218e5c598412acbf3162 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 14:08:28 +0700 Subject: [PATCH 51/94] fix(drive-abci): answer not-found for a shielded pool the chain does not have MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unproved shielded reads take `token_id` straight from the request and read the pool's subtree without checking that the pool exists. Any 32 bytes an unauthenticated caller makes up name a pool that was never created, and the six endpoints answered that in two wrong ways: - `notes_count` and `encrypted_notes` failed inside GroveDB with `PathParentLayerNotFound`, which reaches the client as gRPC `Internal` and writes a `tracing::error!` per request: unauthenticated log flooding, and an honest client told its own request was the node's fault. - `anchors`, `most_recent_anchor`, `nullifiers` and `pool_state` reported an empty pool. `nullifiers` answering `is_spent: false` for a pool that does not exist is the answer a wallet leans on to treat a note as still spendable. All six now refuse with `QueryError::NotFound`, which `query/service.rs` maps to `Code::NotFound` and logs at trace rather than error. Modelled on `Platform::kept_moderation_lists` in `query/contract_moderation_queries`: the same query surface, the same `Result, Error>` shape, and the same reason — a read of a tree the chain does not hold is refused in the handler rather than left to fail in GroveDB. The proved reads are left alone, deliberately. GroveDB proves the pool's absence, and `Drive::verify_pool_notes_count_v0` and its siblings read that proof against the root hash and report `None`, so a proving client gets an answer it can check instead of a status it must trust. `token_total_supply` already splits proved and unproved this way, and `most_recent_anchor` documents the same split for its empty index. A test pins the split so it cannot be tidied away silently. Test would have caught this in CI: `unproved_queries_answer_not_found_for_a_pool_the_chain_does_not_have` and `a_nullifier_is_never_reported_unspent_for_a_pool_the_chain_does_not_have` were both red before the fix and green after; the first reports all six endpoints' answers at once, so no endpoint's wrong answer hides behind another's. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/query/shielded/anchors/v0/mod.rs | 4 + .../query/shielded/encrypted_notes/v0/mod.rs | 4 + .../rs-drive-abci/src/query/shielded/mod.rs | 689 ++++++++++++++++++ .../shielded/most_recent_anchor/v0/mod.rs | 4 + .../src/query/shielded/notes_count/v0/mod.rs | 4 + .../src/query/shielded/nullifiers/v0/mod.rs | 4 + .../src/query/shielded/pool_state/v0/mod.rs | 4 + 7 files changed, 713 insertions(+) diff --git a/packages/rs-drive-abci/src/query/shielded/anchors/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/anchors/v0/mod.rs index 2e0077b8bf0..3f79647aa6d 100644 --- a/packages/rs-drive-abci/src/query/shielded/anchors/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/anchors/v0/mod.rs @@ -119,6 +119,10 @@ impl Platform { metadata: Some(self.response_metadata_v0(platform_state, grovedb_used)), } } else { + check_validation_result_with_data!( + pool.validate_pool_exists(&self.drive, platform_version)? + ); + // Anchors are stored as anchor_bytes → block_height_be; extract keys let anchors: Vec> = bounded_key_elements .into_iter() diff --git a/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs index 0199fa17406..4a23c4fa3f8 100644 --- a/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs @@ -117,6 +117,10 @@ impl Platform { metadata: Some(self.response_metadata_v0(platform_state, grovedb_used)), } } else { + check_validation_result_with_data!( + pool.validate_pool_exists(&self.drive, platform_version)? + ); + // Non-proved: loop over commitment_tree_get_value for each position let pool_path = pool.pool_path_vec(); let pool_subtree: SubtreePath> = pool_path.as_slice().into(); diff --git a/packages/rs-drive-abci/src/query/shielded/mod.rs b/packages/rs-drive-abci/src/query/shielded/mod.rs index ab01fb9ff4b..a109dbcd64a 100644 --- a/packages/rs-drive-abci/src/query/shielded/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/mod.rs @@ -6,6 +6,8 @@ mod nullifiers; mod pool_state; use crate::error::query::QueryError; +use crate::error::Error; +use dpp::identifier::Identifier; use dpp::version::feature_initial_protocol_versions::TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION; use dpp::version::PlatformVersion; use drive::drive::shielded::paths::{ @@ -14,7 +16,11 @@ use drive::drive::shielded::paths::{ token_shielded_pool_anchors_path_vec, token_shielded_pool_latest_recorded_anchor_path_query, token_shielded_pool_nullifiers_path_vec, token_shielded_pool_path_vec, }; +use drive::drive::tokens::paths::token_shielded_pools_root_path; +use drive::drive::Drive; use drive::grovedb::PathQuery; +use drive::grovedb_path::SubtreePath; +use drive::util::grove_operations::DirectQueryType; /// Which shielded pool a shielded query targets: the credit pool (no `token_id` in the request) /// or one token's pool. Every pool has the same subtree layout, so the handlers only differ in @@ -55,6 +61,54 @@ impl ShieldedPoolSelector { } } + /// Confirms the chain holds the pool this selector names, so that an unproved read of it has + /// something to read. + /// + /// A token pool only exists once a token that opted into one was registered, and any 32 bytes + /// an unauthenticated caller makes up name a pool that was never created. Its subtree is + /// absent, which no read underneath can tell apart from a pool that exists and is empty: a + /// read of the notes tree fails inside GroveDB as a missing path, and a range or membership + /// read of the anchors, nullifier or balance keys comes back empty, which would report a + /// nullifier in a pool that does not exist as unspent. "This pool holds no such entry" and + /// "there is no such pool" are different facts, so the second is refused here rather than + /// answered as the first. + /// + /// Only the unproved reads need this. A proof over a pool that does not exist is a proof of + /// its absence, which `Drive::verify_pool_notes_count_v0` and its siblings verify against the + /// root hash and report as `None` — an answer the client can check rather than has to trust. + pub(super) fn validate_pool_exists( + &self, + drive: &Drive, + platform_version: &PlatformVersion, + ) -> Result, Error> { + match self { + // The credit pool's subtree is part of the initial state structure of every version + // that serves these queries, so its absence would be corrupted state rather than a + // client naming a pool that was never created. + ShieldedPoolSelector::Credit => Ok(Ok(())), + ShieldedPoolSelector::Token(token_id) => { + let pools_root = token_shielded_pools_root_path(); + let pool_exists = drive.grove_has_raw( + SubtreePath::from(&pools_root), + token_id, + DirectQueryType::StatefulDirectQuery, + None, + &mut vec![], + &platform_version.drive, + )?; + + if pool_exists { + Ok(Ok(())) + } else { + Ok(Err(QueryError::NotFound(format!( + "shielded pool for token {} not found", + Identifier::new(*token_id) + )))) + } + } + } + } + /// The pool subtree path. pub(super) fn pool_path_vec(&self) -> Vec> { match self { @@ -97,6 +151,641 @@ impl ShieldedPoolSelector { #[cfg(test)] mod tests { use super::*; + use crate::platform_types::platform_state::PlatformState; + use crate::query::tests::setup_platform; + use crate::rpc::core::MockCoreRPCLike; + use crate::test::helpers::setup::TempPlatform; + use dapi_grpc::platform::v0::get_most_recent_shielded_anchor_request::{ + GetMostRecentShieldedAnchorRequestV0, Version as MostRecentAnchorRequestVersion, + }; + use dapi_grpc::platform::v0::get_most_recent_shielded_anchor_response::{ + get_most_recent_shielded_anchor_response_v0, Version as MostRecentAnchorResponseVersion, + }; + use dapi_grpc::platform::v0::get_shielded_anchors_request::{ + GetShieldedAnchorsRequestV0, Version as AnchorsRequestVersion, + }; + use dapi_grpc::platform::v0::get_shielded_anchors_response::{ + get_shielded_anchors_response_v0, Version as AnchorsResponseVersion, + }; + use dapi_grpc::platform::v0::get_shielded_encrypted_notes_request::{ + GetShieldedEncryptedNotesRequestV0, Version as EncryptedNotesRequestVersion, + }; + use dapi_grpc::platform::v0::get_shielded_encrypted_notes_response::{ + get_shielded_encrypted_notes_response_v0, Version as EncryptedNotesResponseVersion, + }; + use dapi_grpc::platform::v0::get_shielded_notes_count_request::{ + GetShieldedNotesCountRequestV0, Version as NotesCountRequestVersion, + }; + use dapi_grpc::platform::v0::get_shielded_notes_count_response::{ + get_shielded_notes_count_response_v0, Version as NotesCountResponseVersion, + }; + use dapi_grpc::platform::v0::get_shielded_nullifiers_request::{ + GetShieldedNullifiersRequestV0, Version as NullifiersRequestVersion, + }; + use dapi_grpc::platform::v0::get_shielded_nullifiers_response::{ + get_shielded_nullifiers_response_v0, Version as NullifiersResponseVersion, + }; + use dapi_grpc::platform::v0::get_shielded_pool_state_request::{ + GetShieldedPoolStateRequestV0, Version as PoolStateRequestVersion, + }; + use dapi_grpc::platform::v0::get_shielded_pool_state_response::{ + get_shielded_pool_state_response_v0, Version as PoolStateResponseVersion, + }; + use dapi_grpc::platform::v0::{ + GetMostRecentShieldedAnchorRequest, GetShieldedAnchorsRequest, + GetShieldedEncryptedNotesRequest, GetShieldedNotesCountRequest, + GetShieldedNullifiersRequest, GetShieldedPoolStateRequest, + }; + use dpp::dashcore::Network; + use std::sync::Arc; + + /// A token id the chain holds no shielded pool for. Any 32 bytes an unauthenticated caller + /// makes up land here. + const POOL_LESS_TOKEN_ID: [u8; 32] = [0xAB; 32]; + + /// The token id of the pool the fixtures create. + const POOLED_TOKEN_ID: [u8; 32] = [3; 32]; + + /// A platform holding one token shielded pool, so that a pool the chain has can be told + /// apart from one it does not. + fn setup_platform_with_a_token_pool<'a>() -> ( + TempPlatform, + Arc, + &'a PlatformVersion, + ) { + let (platform, state, platform_version) = setup_platform(None, Network::Testnet, None); + + let operations = platform + .drive + .create_token_shielded_pool_trees_operations( + POOLED_TOKEN_ID, + false, + &mut None, + None, + platform_version, + ) + .expect("expected the pool tree operations"); + platform + .drive + .apply_batch_low_level_drive_operations( + None, + None, + operations, + &mut vec![], + &platform_version.drive, + ) + .expect("expected the token shielded pool to be created"); + + (platform, state, platform_version) + } + + fn pool_not_found_message() -> String { + format!( + "shielded pool for token {} not found", + Identifier::new(POOL_LESS_TOKEN_ID) + ) + } + + fn assert_pool_not_found(endpoint: &str, errors: &[QueryError]) { + let expected = pool_not_found_message(); + match errors { + [QueryError::NotFound(message)] if message == &expected => {} + other => panic!("{endpoint}: expected {expected:?}, got {other:?}"), + } + } + + /// Every unproved shielded read of a pool the chain does not have answers `NotFound`, the + /// coded client answer the rest of this surface gives for a missing identity or contract. + /// + /// Two of these reads used to fail inside GroveDB as a missing path, which reaches the client + /// as gRPC `Internal` and writes a `tracing::error!` per request — an unauthenticated caller + /// choosing 32 arbitrary bytes could fill a node's log with them, and an honest client was + /// told its own request was the node's fault. + #[test] + fn unproved_queries_answer_not_found_for_a_pool_the_chain_does_not_have() { + let (platform, state, version) = setup_platform_with_a_token_pool(); + let token_id = Some(POOL_LESS_TOKEN_ID.to_vec()); + + // Collected rather than asserted one at a time: the six answered a missing pool in three + // different wrong ways, and a failure that stops at the first endpoint hides the rest. + let outcomes: Vec<(&str, Result, Error>)> = vec![ + ( + "notes_count", + platform + .query_shielded_notes_count( + GetShieldedNotesCountRequest { + version: Some(NotesCountRequestVersion::V0( + GetShieldedNotesCountRequestV0 { + prove: false, + token_id: token_id.clone(), + }, + )), + }, + &state, + version, + ) + .map(|result| result.errors), + ), + ( + "encrypted_notes", + platform + .query_shielded_encrypted_notes( + GetShieldedEncryptedNotesRequest { + version: Some(EncryptedNotesRequestVersion::V0( + GetShieldedEncryptedNotesRequestV0 { + start_index: 0, + count: 1, + prove: false, + token_id: token_id.clone(), + }, + )), + }, + &state, + version, + ) + .map(|result| result.errors), + ), + ( + "anchors", + platform + .query_shielded_anchors( + GetShieldedAnchorsRequest { + version: Some(AnchorsRequestVersion::V0(GetShieldedAnchorsRequestV0 { + prove: false, + token_id: token_id.clone(), + })), + }, + &state, + version, + ) + .map(|result| result.errors), + ), + ( + "most_recent_anchor", + platform + .query_most_recent_shielded_anchor( + GetMostRecentShieldedAnchorRequest { + version: Some(MostRecentAnchorRequestVersion::V0( + GetMostRecentShieldedAnchorRequestV0 { + prove: false, + token_id: token_id.clone(), + }, + )), + }, + &state, + version, + ) + .map(|result| result.errors), + ), + ( + "nullifiers", + platform + .query_shielded_nullifiers( + GetShieldedNullifiersRequest { + version: Some(NullifiersRequestVersion::V0( + GetShieldedNullifiersRequestV0 { + nullifiers: vec![vec![0x11; 32]], + prove: false, + token_id: token_id.clone(), + }, + )), + }, + &state, + version, + ) + .map(|result| result.errors), + ), + ( + "pool_state", + platform + .query_shielded_pool_state( + GetShieldedPoolStateRequest { + version: Some(PoolStateRequestVersion::V0( + GetShieldedPoolStateRequestV0 { + prove: false, + token_id, + }, + )), + }, + &state, + version, + ) + .map(|result| result.errors), + ), + ]; + + let expected = pool_not_found_message(); + let mut wrong = Vec::new(); + for (endpoint, outcome) in outcomes { + match outcome { + Ok(errors) => match errors.as_slice() { + [QueryError::NotFound(message)] if message == &expected => {} + other => wrong.push(format!("{endpoint}: answered with {other:?}")), + }, + Err(error) => wrong.push(format!("{endpoint}: failed the request with {error}")), + } + } + + assert!( + wrong.is_empty(), + "every unproved shielded read must answer {expected:?}:\n{}", + wrong.join("\n") + ); + } + + /// A nullifier is never reported unspent for a pool the chain does not have. + /// + /// `is_spent: false` is the answer a wallet leans on to decide a note is still spendable. + /// The spent set of a pool that was never created is a tree that does not exist, and the + /// lookup reads that as "no such key" — indistinguishable from an unspent nullifier in a + /// real pool. The caller must be told the pool is unknown instead, because "this pool holds + /// no record of that nullifier" and "there is no such pool" are different facts. + #[test] + fn a_nullifier_is_never_reported_unspent_for_a_pool_the_chain_does_not_have() { + let (platform, state, version) = setup_platform_with_a_token_pool(); + + let result = platform + .query_shielded_nullifiers( + GetShieldedNullifiersRequest { + version: Some(NullifiersRequestVersion::V0( + GetShieldedNullifiersRequestV0 { + nullifiers: vec![vec![0x11; 32]], + prove: false, + token_id: Some(POOL_LESS_TOKEN_ID.to_vec()), + }, + )), + }, + &state, + version, + ) + .expect("expected the query to complete"); + + assert_pool_not_found("nullifiers", &result.errors); + assert!( + result.data.is_none(), + "a spend status must not be handed back alongside the refusal: {:?}", + result.data + ); + } + + /// An empty pool and a pool that does not exist are different facts, and the unproved reads + /// must not answer the same way for both. Every value asserted here is one that a pool the + /// chain does not have used to return too. + #[test] + fn unproved_queries_tell_an_empty_pool_apart_from_a_pool_that_does_not_exist() { + let (platform, state, version) = setup_platform_with_a_token_pool(); + let token_id = Some(POOLED_TOKEN_ID.to_vec()); + + let notes_count = platform + .query_shielded_notes_count( + GetShieldedNotesCountRequest { + version: Some(NotesCountRequestVersion::V0( + GetShieldedNotesCountRequestV0 { + prove: false, + token_id: token_id.clone(), + }, + )), + }, + &state, + version, + ) + .expect("expected the query to complete"); + assert!(notes_count.errors.is_empty(), "{:?}", notes_count.errors); + match notes_count.data.and_then(|response| response.version) { + Some(NotesCountResponseVersion::V0(v0)) => assert!(matches!( + v0.result, + Some(get_shielded_notes_count_response_v0::Result::TotalNotesCount(0)) + )), + other => panic!("expected a notes count, got {other:?}"), + } + + let encrypted_notes = platform + .query_shielded_encrypted_notes( + GetShieldedEncryptedNotesRequest { + version: Some(EncryptedNotesRequestVersion::V0( + GetShieldedEncryptedNotesRequestV0 { + start_index: 0, + count: 1, + prove: false, + token_id: token_id.clone(), + }, + )), + }, + &state, + version, + ) + .expect("expected the query to complete"); + assert!( + encrypted_notes.errors.is_empty(), + "{:?}", + encrypted_notes.errors + ); + match encrypted_notes.data.and_then(|response| response.version) { + Some(EncryptedNotesResponseVersion::V0(v0)) => match v0.result { + Some(get_shielded_encrypted_notes_response_v0::Result::EncryptedNotes(notes)) => { + assert!(notes.entries.is_empty()) + } + other => panic!("expected encrypted notes, got {other:?}"), + }, + other => panic!("expected a v0 response, got {other:?}"), + } + + let anchors = platform + .query_shielded_anchors( + GetShieldedAnchorsRequest { + version: Some(AnchorsRequestVersion::V0(GetShieldedAnchorsRequestV0 { + prove: false, + token_id: token_id.clone(), + })), + }, + &state, + version, + ) + .expect("expected the query to complete"); + assert!(anchors.errors.is_empty(), "{:?}", anchors.errors); + match anchors.data.and_then(|response| response.version) { + Some(AnchorsResponseVersion::V0(v0)) => match v0.result { + Some(get_shielded_anchors_response_v0::Result::Anchors(anchors)) => { + assert!(anchors.anchors.is_empty()) + } + other => panic!("expected anchors, got {other:?}"), + }, + other => panic!("expected a v0 response, got {other:?}"), + } + + let most_recent_anchor = platform + .query_most_recent_shielded_anchor( + GetMostRecentShieldedAnchorRequest { + version: Some(MostRecentAnchorRequestVersion::V0( + GetMostRecentShieldedAnchorRequestV0 { + prove: false, + token_id: token_id.clone(), + }, + )), + }, + &state, + version, + ) + .expect("expected the query to complete"); + assert!( + most_recent_anchor.errors.is_empty(), + "{:?}", + most_recent_anchor.errors + ); + match most_recent_anchor + .data + .and_then(|response| response.version) + { + Some(MostRecentAnchorResponseVersion::V0(v0)) => match v0.result { + // The empty-index sentinel this endpoint documents. + Some(get_most_recent_shielded_anchor_response_v0::Result::Anchor(anchor)) => { + assert_eq!(anchor, vec![0; 32]) + } + other => panic!("expected an anchor, got {other:?}"), + }, + other => panic!("expected a v0 response, got {other:?}"), + } + + let nullifiers = platform + .query_shielded_nullifiers( + GetShieldedNullifiersRequest { + version: Some(NullifiersRequestVersion::V0( + GetShieldedNullifiersRequestV0 { + nullifiers: vec![vec![0x11; 32]], + prove: false, + token_id: token_id.clone(), + }, + )), + }, + &state, + version, + ) + .expect("expected the query to complete"); + assert!(nullifiers.errors.is_empty(), "{:?}", nullifiers.errors); + match nullifiers.data.and_then(|response| response.version) { + Some(NullifiersResponseVersion::V0(v0)) => match v0.result { + Some(get_shielded_nullifiers_response_v0::Result::NullifierStatuses(statuses)) => { + assert!(statuses.entries.iter().all(|status| !status.is_spent)) + } + other => panic!("expected nullifier statuses, got {other:?}"), + }, + other => panic!("expected a v0 response, got {other:?}"), + } + + let pool_state = platform + .query_shielded_pool_state( + GetShieldedPoolStateRequest { + version: Some(PoolStateRequestVersion::V0(GetShieldedPoolStateRequestV0 { + prove: false, + token_id, + })), + }, + &state, + version, + ) + .expect("expected the query to complete"); + assert!(pool_state.errors.is_empty(), "{:?}", pool_state.errors); + match pool_state.data.and_then(|response| response.version) { + Some(PoolStateResponseVersion::V0(v0)) => assert!(matches!( + v0.result, + Some(get_shielded_pool_state_response_v0::Result::TotalBalance(0)) + )), + other => panic!("expected a total balance, got {other:?}"), + } + } + + /// The proved reads keep answering a pool the chain does not have with a proof, deliberately. + /// + /// GroveDB proves the pool's absence, and the verifiers + /// (`Drive::verify_pool_notes_count_v0` and its siblings) read that proof against the root + /// hash and report `None`. That is an answer the client checks rather than has to trust, so + /// it is worth more than the `NotFound` the unproved reads give. Whoever changes this must + /// change the verifiers with it. + #[test] + fn proved_queries_answer_a_pool_the_chain_does_not_have_with_an_absence_proof() { + let (platform, state, version) = setup_platform_with_a_token_pool(); + let token_id = Some(POOL_LESS_TOKEN_ID.to_vec()); + + let notes_count = platform + .query_shielded_notes_count( + GetShieldedNotesCountRequest { + version: Some(NotesCountRequestVersion::V0( + GetShieldedNotesCountRequestV0 { + prove: true, + token_id: token_id.clone(), + }, + )), + }, + &state, + version, + ) + .expect("expected the query to complete"); + assert!(notes_count.errors.is_empty(), "{:?}", notes_count.errors); + match notes_count.data.and_then(|response| response.version) { + Some(NotesCountResponseVersion::V0(v0)) => assert!(matches!( + v0.result, + Some(get_shielded_notes_count_response_v0::Result::Proof(_)) + )), + other => panic!("expected a proof, got {other:?}"), + } + + let encrypted_notes = platform + .query_shielded_encrypted_notes( + GetShieldedEncryptedNotesRequest { + version: Some(EncryptedNotesRequestVersion::V0( + GetShieldedEncryptedNotesRequestV0 { + start_index: 0, + count: 1, + prove: true, + token_id: token_id.clone(), + }, + )), + }, + &state, + version, + ) + .expect("expected the query to complete"); + assert!( + encrypted_notes.errors.is_empty(), + "{:?}", + encrypted_notes.errors + ); + match encrypted_notes.data.and_then(|response| response.version) { + Some(EncryptedNotesResponseVersion::V0(v0)) => assert!(matches!( + v0.result, + Some(get_shielded_encrypted_notes_response_v0::Result::Proof(_)) + )), + other => panic!("expected a proof, got {other:?}"), + } + + let anchors = platform + .query_shielded_anchors( + GetShieldedAnchorsRequest { + version: Some(AnchorsRequestVersion::V0(GetShieldedAnchorsRequestV0 { + prove: true, + token_id: token_id.clone(), + })), + }, + &state, + version, + ) + .expect("expected the query to complete"); + assert!(anchors.errors.is_empty(), "{:?}", anchors.errors); + match anchors.data.and_then(|response| response.version) { + Some(AnchorsResponseVersion::V0(v0)) => assert!(matches!( + v0.result, + Some(get_shielded_anchors_response_v0::Result::Proof(_)) + )), + other => panic!("expected a proof, got {other:?}"), + } + + let most_recent_anchor = platform + .query_most_recent_shielded_anchor( + GetMostRecentShieldedAnchorRequest { + version: Some(MostRecentAnchorRequestVersion::V0( + GetMostRecentShieldedAnchorRequestV0 { + prove: true, + token_id: token_id.clone(), + }, + )), + }, + &state, + version, + ) + .expect("expected the query to complete"); + assert!( + most_recent_anchor.errors.is_empty(), + "{:?}", + most_recent_anchor.errors + ); + match most_recent_anchor + .data + .and_then(|response| response.version) + { + Some(MostRecentAnchorResponseVersion::V0(v0)) => assert!(matches!( + v0.result, + Some(get_most_recent_shielded_anchor_response_v0::Result::Proof( + _ + )) + )), + other => panic!("expected a proof, got {other:?}"), + } + + let nullifiers = platform + .query_shielded_nullifiers( + GetShieldedNullifiersRequest { + version: Some(NullifiersRequestVersion::V0( + GetShieldedNullifiersRequestV0 { + nullifiers: vec![vec![0x11; 32]], + prove: true, + token_id: token_id.clone(), + }, + )), + }, + &state, + version, + ) + .expect("expected the query to complete"); + assert!(nullifiers.errors.is_empty(), "{:?}", nullifiers.errors); + match nullifiers.data.and_then(|response| response.version) { + Some(NullifiersResponseVersion::V0(v0)) => assert!(matches!( + v0.result, + Some(get_shielded_nullifiers_response_v0::Result::Proof(_)) + )), + other => panic!("expected a proof, got {other:?}"), + } + + let pool_state = platform + .query_shielded_pool_state( + GetShieldedPoolStateRequest { + version: Some(PoolStateRequestVersion::V0(GetShieldedPoolStateRequestV0 { + prove: true, + token_id, + })), + }, + &state, + version, + ) + .expect("expected the query to complete"); + assert!(pool_state.errors.is_empty(), "{:?}", pool_state.errors); + match pool_state.data.and_then(|response| response.version) { + Some(PoolStateResponseVersion::V0(v0)) => assert!(matches!( + v0.result, + Some(get_shielded_pool_state_response_v0::Result::Proof(_)) + )), + other => panic!("expected a proof, got {other:?}"), + } + } + + /// The credit pool is built with the initial state structure, so a credit-pool read is never + /// gated on the pool's existence — gating it would refuse every shielded credit query. + #[test] + fn credit_pool_reads_are_not_gated_on_pool_existence() { + let (platform, _state, version) = setup_platform(None, Network::Testnet, None); + + assert!(matches!( + ShieldedPoolSelector::Credit.validate_pool_exists(&platform.drive, version), + Ok(Ok(())) + )); + } + + /// A token pool the chain holds passes the existence gate, and one it does not is refused — + /// the gate reads state rather than always answering one way. + #[test] + fn existence_gate_separates_a_held_token_pool_from_an_unheld_one() { + let (platform, _state, version) = setup_platform_with_a_token_pool(); + + assert!(matches!( + ShieldedPoolSelector::Token(POOLED_TOKEN_ID) + .validate_pool_exists(&platform.drive, version), + Ok(Ok(())) + )); + assert!(matches!( + ShieldedPoolSelector::Token(POOL_LESS_TOKEN_ID) + .validate_pool_exists(&platform.drive, version), + Ok(Err(QueryError::NotFound(_))) + )); + } #[test] fn selector_resolves_credit_pool_without_token_id() { diff --git a/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/v0/mod.rs index fbd300a3a56..259b1d2818d 100644 --- a/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/v0/mod.rs @@ -56,6 +56,10 @@ impl Platform { metadata: Some(self.response_metadata_v0(platform_state, grovedb_used)), } } else { + check_validation_result_with_data!( + pool.validate_pool_exists(&self.drive, platform_version)? + ); + let (results, _) = self.drive.grove_get_raw_path_query( &path_query, None, diff --git a/packages/rs-drive-abci/src/query/shielded/notes_count/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/notes_count/v0/mod.rs index 6eaed5d2541..606c22c6aae 100644 --- a/packages/rs-drive-abci/src/query/shielded/notes_count/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/notes_count/v0/mod.rs @@ -65,6 +65,10 @@ impl Platform { metadata: Some(self.response_metadata_v0(platform_state, grovedb_used)), } } else { + check_validation_result_with_data!( + pool.validate_pool_exists(&self.drive, platform_version)? + ); + let total_notes_count = match pool { ShieldedPoolSelector::Credit => { self.drive diff --git a/packages/rs-drive-abci/src/query/shielded/nullifiers/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/nullifiers/v0/mod.rs index bec4d767c4b..c938265bc99 100644 --- a/packages/rs-drive-abci/src/query/shielded/nullifiers/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/nullifiers/v0/mod.rs @@ -93,6 +93,10 @@ impl Platform { metadata: Some(self.response_metadata_v0(platform_state, grovedb_used)), } } else { + check_validation_result_with_data!( + pool.validate_pool_exists(&self.drive, platform_version)? + ); + let nullifiers_path = pool.nullifiers_path_vec(); let entries: Vec = nullifiers diff --git a/packages/rs-drive-abci/src/query/shielded/pool_state/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/pool_state/v0/mod.rs index 4f2df152bcf..2fc79374ef9 100644 --- a/packages/rs-drive-abci/src/query/shielded/pool_state/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/pool_state/v0/mod.rs @@ -51,6 +51,10 @@ impl Platform { metadata: Some(self.response_metadata_v0(platform_state, grovedb_used)), } } else { + check_validation_result_with_data!( + pool.validate_pool_exists(&self.drive, platform_version)? + ); + let pool_path = pool.pool_path_vec(); let total_balance = self From 3341739891d928e1a543fc8cf4f5ce59448ed6cf Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 14:09:43 +0700 Subject: [PATCH 52/94] fix(dpp)!: refuse a token configuration key its format version does not carry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A token JSON carrying "hasShieldedPool": true beside "$formatVersion": "0" decoded as a plain V0 configuration with the flag dropped. A token's configuration is immutable after creation and the pool subtree is built with the token's other trees, so the caller asked for a pool, got a token that can never have one, and was told nothing. TokenConfigurationV0 now refuses an unknown key. This follows the two-layer split the codebase already uses: the $formatVersion-tagged enum stays permissive so an unknown version is reported as one, and the struct for a pinned version denies, as ContractModerationConfig does for the same reason (a misspelled key would leave the contract without the list for good; see book/src/data-model/contract-moderation.md). Forward compatibility is not at risk here: V0's field set is closed, because new behaviour is a new generation, and a client with more fields sends "$formatVersion": "1". TokenConfigurationV1 reads the V0 fields through serde(flatten), which does not carry the refusal, so V1 decoding is unchanged. A test pins that, since a serde change there would otherwise break V1 silently. Two of this crate's own contract-JSON fixtures carried a top-level "directPurchaseRules" object, a duplicate of the real distributionRules.changeDirectPurchasePricingRules. The key names no field anywhere in the repository and had been dropped on every parse; the new refusal surfaced it, and it is removed from both fixtures. Neither test is about that key. Tests would have caught this in CI: should_refuse_a_pool_asked_for_at_format_version_0_rather_than_drop_it should_refuse_a_pool_asked_for_at_format_version_0_on_the_value_wire_too both FAILED before the fix with "got Ok(false)" — a decoded, pool-less token — and pass after it. BREAKING CHANGE: a token configuration JSON or Value with a key its $formatVersion does not define is now refused instead of silently losing that key. Co-Authored-By: Claude Opus 5 (1M context) --- .../token_configuration/mod.rs | 78 +++++++++++++++++++ .../token_configuration/v0/mod.rs | 10 ++- .../src/data_contract/conversion/json/mod.rs | 16 ---- 3 files changed, 87 insertions(+), 17 deletions(-) diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs index b9e2858b2f3..03ec11a417b 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs @@ -716,3 +716,81 @@ mod minimum_pool_notes_json_tests { assert_eq!(decoded.minimum_pool_notes_for_outgoing(), 0); } } + +#[cfg(all( + test, + feature = "json-conversion", + feature = "value-conversion", + feature = "serde-conversion" +))] +mod unknown_configuration_key_tests { + use super::*; + use crate::data_contract::associated_token::token_configuration::accessors::v1::TokenConfigurationV1Setters; + use crate::serialization::{JsonConvertible, ValueConvertible}; + + /// A V0 configuration's JSON with `hasShieldedPool` bolted on: what a caller writes when + /// they ask for a pool but leave the format version at 0. + fn v0_json_asking_for_a_pool() -> serde_json::Value { + let mut json = TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()) + .to_json() + .expect("to_json"); + json.as_object_mut() + .expect("configuration object") + .insert("hasShieldedPool".to_string(), serde_json::Value::Bool(true)); + json + } + + #[test] + fn should_refuse_a_pool_asked_for_at_format_version_0_rather_than_drop_it() { + let json = v0_json_asking_for_a_pool(); + let decoded = TokenConfiguration::from_json(json); + assert!( + decoded.is_err(), + "a configuration asking for a pool must not decode as a token that can never \ + have one, got {:?}", + decoded.map(|configuration| configuration.has_shielded_pool()) + ); + } + + #[test] + fn should_refuse_a_pool_asked_for_at_format_version_0_on_the_value_wire_too() { + let mut value = TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()) + .to_object() + .expect("to_object"); + value.as_map_mut().expect("configuration map").push(( + platform_value::Value::Text("hasShieldedPool".to_string()), + platform_value::Value::Bool(true), + )); + let decoded = TokenConfiguration::from_object(value); + assert!( + decoded.is_err(), + "the contract ingest path must refuse the same configuration, got {:?}", + decoded.map(|configuration| configuration.has_shielded_pool()) + ); + } + + /// The V0 fields reach a V1 configuration through `serde(flatten)`, which drops the + /// strictness the V0 variant is read with. A V1 configuration must keep decoding. + #[test] + fn should_still_decode_a_pooled_configuration_whose_v0_fields_are_flattened() { + let mut configuration = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + configuration.set_has_shielded_pool(true); + + let json = configuration.to_json().expect("to_json"); + assert_eq!( + json.get("$formatVersion").and_then(|v| v.as_str()), + Some("1") + ); + assert_eq!( + TokenConfiguration::from_json(json).expect("from_json"), + configuration + ); + + let value = configuration.to_object().expect("to_object"); + assert_eq!( + TokenConfiguration::from_object(value).expect("from_object"), + configuration + ); + } +} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/mod.rs index 6a0a4bb667a..bb2f3d2de08 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v0/mod.rs @@ -32,9 +32,17 @@ use std::fmt; /// /// This configuration is designed to be deterministic and versioned for compatibility /// across protocol upgrades and validation environments. +/// +/// An unknown key is refused rather than skipped. A token's configuration is fixed when the +/// token is created, so a key this format version does not carry — `hasShieldedPool` written +/// beside `$formatVersion: "0"`, say — would otherwise leave the token without what was asked +/// for and with no way to add it later. The field set is closed: a later format version is a +/// new generation, never a new field here. `TokenConfigurationV1` reads these fields through +/// `serde(flatten)`, which does not carry the refusal, so a version 1 configuration is not +/// held to it. #[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Serialize, Deserialize, Decode, Encode, Debug, Clone, PartialEq, Eq, DecodeUntrusted)] -#[serde(rename_all = "camelCase")] +#[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct TokenConfigurationV0 { /// Metadata conventions, including decimals and localizations. pub conventions: TokenConfigurationConvention, diff --git a/packages/rs-dpp/src/data_contract/conversion/json/mod.rs b/packages/rs-dpp/src/data_contract/conversion/json/mod.rs index 68cad843aa8..2ee8dc18d77 100644 --- a/packages/rs-dpp/src/data_contract/conversion/json/mod.rs +++ b/packages/rs-dpp/src/data_contract/conversion/json/mod.rs @@ -175,14 +175,6 @@ mod tests { "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, - "directPurchaseRules": { - "$formatVersion": "0", - "authorizedToMakeChange": {"$type": "noOne"}, - "adminActionTakers": {"$type": "noOne"}, - "changingAuthorizedActionTakersToNoOneAllowed": false, - "changingAdminActionTakersToNoOneAllowed": false, - "selfChangingAdminActionTakersAllowed": false - }, "mainControlGroup": null, "mainControlGroupCanBeModified": {"$type": "noOne"} } @@ -327,14 +319,6 @@ mod tests { "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, - "directPurchaseRules": { - "$formatVersion": "0", - "authorizedToMakeChange": {"$type": "noOne"}, - "adminActionTakers": {"$type": "noOne"}, - "changingAuthorizedActionTakersToNoOneAllowed": false, - "changingAdminActionTakersToNoOneAllowed": false, - "selfChangingAdminActionTakersAllowed": false - }, "mainControlGroup": null, "mainControlGroupCanBeModified": {"$type": "noOne"} } From 4dab68867147bbd386c8b8af59aa2d8736b46338 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 14:09:52 +0700 Subject: [PATCH 53/94] fix(dpp): show a pool mint or burn's public note on a pending group action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TokenEvent::public_note() ended in a catch-all `_ => None`, so MintToPool and BurnFromPool — both of which carry a note — returned None. A pending group action's publicNote came back undefined, and the co-signers deciding whether to sign could not read what the proposer wrote. The catch-all is replaced with every variant listed explicitly, which this repository requires on consensus paths and which turns a future variant that carries a note into a compile error rather than a note that silently goes missing. The note-bearing arms now bind the Option and use as_deref(), so a variant cannot be listed and still fall through to None. Test would have caught this in CI: should_show_the_proposers_note_on_a_pool_mint_or_burn FAILED before the fix with left: None, right: Some("quarterly issuance"), and passes after it. Co-Authored-By: Claude Opus 5 (1M context) --- packages/rs-dpp/src/tokens/token_event.rs | 104 +++++++++++++++++++--- 1 file changed, 93 insertions(+), 11 deletions(-) diff --git a/packages/rs-dpp/src/tokens/token_event.rs b/packages/rs-dpp/src/tokens/token_event.rs index dc9182bfb90..8bc0967c980 100644 --- a/packages/rs-dpp/src/tokens/token_event.rs +++ b/packages/rs-dpp/src/tokens/token_event.rs @@ -815,19 +815,31 @@ impl TokenEvent { } /// Returns a reference to the public note if the variant includes one. + /// + /// Every variant is listed: the co-signers of a pending group action read the proposer's + /// note here while deciding whether to sign, so a variant that carries one and is not + /// listed shows them nothing. Adding a variant is then a compile error rather than a note + /// that silently goes missing. pub fn public_note(&self) -> Option<&str> { match self { - TokenEvent::Mint(_, _, Some(note)) - | TokenEvent::Burn(_, _, Some(note)) - | TokenEvent::Freeze(_, Some(note)) - | TokenEvent::Unfreeze(_, Some(note)) - | TokenEvent::DestroyFrozenFunds(_, _, Some(note)) - | TokenEvent::Transfer(_, Some(note), _, _, _) - | TokenEvent::Claim(_, _, Some(note)) - | TokenEvent::EmergencyAction(_, Some(note)) - | TokenEvent::ConfigUpdate(_, Some(note)) - | TokenEvent::ChangePriceForDirectPurchase(_, Some(note)) => Some(note), - _ => None, + TokenEvent::Mint(_, _, note) + | TokenEvent::Burn(_, _, note) + | TokenEvent::Freeze(_, note) + | TokenEvent::Unfreeze(_, note) + | TokenEvent::DestroyFrozenFunds(_, _, note) + | TokenEvent::Transfer(_, note, _, _, _) + | TokenEvent::Claim(_, _, note) + | TokenEvent::EmergencyAction(_, note) + | TokenEvent::ConfigUpdate(_, note) + | TokenEvent::ChangePriceForDirectPurchase(_, note) + | TokenEvent::MintToPool(_, _, note) + | TokenEvent::BurnFromPool(_, _, note) => note.as_deref(), + TokenEvent::DirectPurchase(_, _) + | TokenEvent::Shield(_) + | TokenEvent::Unshield(_, _) + | TokenEvent::ShieldedTransfer + | TokenEvent::ClaimToPool(_) + | TokenEvent::DirectPurchaseToPool(_, _) => None, } } @@ -1212,3 +1224,73 @@ mod tests { assert_eq!(format_note(&Some("hello".to_string())), " (note: hello)"); } } + +#[cfg(test)] +mod public_note_tests { + use super::*; + use crate::group::action_event::GroupActionEvent; + + fn actions_digest() -> Identifier { + Identifier::from([7u8; 32]) + } + + /// The co-signers of a pending group action read the proposer's note off the event to + /// decide whether to sign it. A pool mint or burn carries one like any other proposal. + #[test] + fn should_show_the_proposers_note_on_a_pool_mint_or_burn() { + let mint = TokenEvent::MintToPool( + 100, + actions_digest(), + Some("quarterly issuance".to_string()), + ); + assert_eq!(mint.public_note(), Some("quarterly issuance")); + assert_eq!( + GroupActionEvent::TokenEvent(mint).public_note(), + Some("quarterly issuance") + ); + + let burn = TokenEvent::BurnFromPool( + 40, + actions_digest(), + Some("retiring treasury notes".to_string()), + ); + assert_eq!(burn.public_note(), Some("retiring treasury notes")); + assert_eq!( + GroupActionEvent::TokenEvent(burn).public_note(), + Some("retiring treasury notes") + ); + } + + /// A pool mint or burn the proposer left unannotated reads as no note, not as an empty one. + #[test] + fn should_report_no_note_on_an_unannotated_pool_mint_or_burn() { + assert_eq!( + TokenEvent::MintToPool(100, actions_digest(), None).public_note(), + None + ); + assert_eq!( + TokenEvent::BurnFromPool(40, actions_digest(), None).public_note(), + None + ); + } + + /// The events that carry no note at all: nothing to show a co-signer. + #[test] + fn should_report_no_note_on_the_events_that_carry_none() { + for event in [ + TokenEvent::DirectPurchase(5, 50), + TokenEvent::Shield(5), + TokenEvent::Unshield(Identifier::from([1u8; 32]), 5), + TokenEvent::ShieldedTransfer, + TokenEvent::ClaimToPool(5), + TokenEvent::DirectPurchaseToPool(5, 50), + ] { + assert_eq!( + event.public_note(), + None, + "{} carries no note", + event.associated_document_type_name() + ); + } + } +} From c8370ce25e48c2e97bfb4db3753ea49c39b915cb Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 14:12:55 +0700 Subject: [PATCH 54/94] fix(drive-abci)!: bound a token pool mint by the supply's sum-item ceiling A mint into a token shielded pool writes the supply with allow_saturation=false, but its state validation only checked an explicitly configured max supply. With no max supply set, an amount that overflows the supply reached Drive and came back as CorruptedCodeExecution: the transition was reported as an internal error, stripped from the block, and the submitter paid nothing, so it could be resubmitted forever. CorruptedCodeExecution is also how a node says its own code is broken, which this is not. The supply lives in a sum item, so i64::MAX bounds it whether or not a max supply is configured, and a pool mint cannot absorb that bound by saturating: the bundle proves exactly the minted amount entering the pool, so a supply that stopped short would leave the pool holding value the supply does not record. All four pool mints now derive the effective ceiling the way the transparent TokenClaim does for its once-per-identity kind (token_claim_transition_action/state_v0) and return TokenMintPastMaxSupplyError: - token_claim_to_pool - token_mint_to_pool - token_direct_purchase_to_pool - token_purchase_from_shielded_pool TokenClaim applies the ceiling only to its once-per-identity kind because its other kinds mint with allow_saturation=true. No pool mint saturates, so every kind is bounded here. Test would have caught this in CI: before the fix both new cases failed with a claim past the supply ceiling must be a paid TokenMintPastMaxSupplyError, got [InternalError("storage: drive: corrupted code execution error: trying to add an amount that would overflow total supply")] and after it `cargo test -p drive-abci --lib token_claim_to_pool` is 4 passed, 0 failed. The new cases drive ClaimToPool only; the other three sites share the writer and the gate and are covered by their existing configured-max-supply tests (32 tests green across the token_claim_to_pool, to_pool and purchase_from_shielded_pool filters), not by a new case each. The transparent mint has the same defect but is shipped, so it is left for a separate change: v4.1.1 declares LATEST_VERSION = PROTOCOL_VERSION_13, PV13 selects DRIVE_ABCI_VALIDATION_VERSIONS_V9 with token_mint_transition_state_validation: 0, and that generation carries the same gate at token_mint_transition_action/state_v0:121 while its writer passes allow_saturation: false. In-place changes to shipped generations: none. All four edited modules are new on this branch and absent from v4.1-dev, so no released protocol version selects them. Co-Authored-By: Claude Opus 5 (1M context) --- .../state_v0/mod.rs | 70 ++++---- .../state_v0/mod.rs | 69 ++++---- .../state_v0/mod.rs | 68 ++++---- .../batch/tests/token/shielded/mod.rs | 158 ++++++++++++++++++ .../transform_into_action/v0/mod.rs | 39 +++-- 5 files changed, 301 insertions(+), 103 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs index 1dd3b19e48e..101de6455c7 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_claim_to_pool_transition_action/state_v0/mod.rs @@ -43,7 +43,7 @@ pub(in crate::execution::validation::state_transition::state_transitions::batch: impl TokenClaimToPoolTransitionActionStateValidationV0 for TokenClaimToPoolTransitionAction { /// The claim itself (who may claim, how much, up to which moment) was resolved when the action - /// was built; here the max supply is checked and the outputs-only bundle must carry exactly + /// was built; here the supply ceiling is checked and the outputs-only bundle must carry exactly /// the resolved amount. fn validate_state_v0( &self, @@ -89,36 +89,46 @@ impl TokenClaimToPoolTransitionActionStateValidationV0 for TokenClaimToPoolTrans let contract = &self.data_contract_fetch_info_ref().contract; let token_configuration = contract.expected_token_configuration(self.token_position())?; - if let Some(max_supply) = token_configuration.max_supply() { - let (token_total_supply, fee) = platform.drive.fetch_token_total_supply_with_cost( - token_id.to_buffer(), - block_info, - transaction, - platform_version, - )?; - execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee)); - let Some(token_total_supply) = token_total_supply else { - return Err(Error::Drive(drive::error::Error::Drive( - DriveError::CorruptedDriveState(format!( - "token {} total supply not found", - token_id + // A mint into the pool never saturates: the bundle proves exactly `amount` entering the + // pool, so a supply that stopped short would leave the pool holding value the supply does + // not record. The supply lives in a sum item, so `i64::MAX` is a ceiling of its own, and it + // bounds the claim even where the token configures no max supply. Without that bound the + // release reaches the writer as an overflow and the node reports its own code as broken, + // dropping the claim from the block unpaid and leaving it resubmittable forever. + let max_supply = token_configuration + .max_supply() + .map_or(i64::MAX as u64, |max_supply| { + max_supply.min(i64::MAX as u64) + }); + + let (token_total_supply, fee) = platform.drive.fetch_token_total_supply_with_cost( + token_id.to_buffer(), + block_info, + transaction, + platform_version, + )?; + execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee)); + let Some(token_total_supply) = token_total_supply else { + return Err(Error::Drive(drive::error::Error::Drive( + DriveError::CorruptedDriveState(format!( + "token {} total supply not found", + token_id + )), + ))); + }; + match token_total_supply.checked_add(self.amount()) { + Some(total_supply_after) if total_supply_after <= max_supply => {} + _ => { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::TokenMintPastMaxSupplyError( + TokenMintPastMaxSupplyError::new( + token_id, + self.amount(), + token_total_supply, + max_supply, + ), )), - ))); - }; - match token_total_supply.checked_add(self.amount()) { - Some(total_supply_after) if total_supply_after <= max_supply => {} - _ => { - return Ok(SimpleConsensusValidationResult::new_with_error( - ConsensusError::StateError(StateError::TokenMintPastMaxSupplyError( - TokenMintPastMaxSupplyError::new( - token_id, - self.amount(), - token_total_supply, - max_supply, - ), - )), - )); - } + )); } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs index 9ee6d3f9761..93281f34781 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_direct_purchase_to_pool_transition_action/state_v0/mod.rs @@ -90,36 +90,47 @@ impl TokenDirectPurchaseToPoolTransitionActionStateValidationV0 let contract = &self.data_contract_fetch_info_ref().contract; let token_configuration = contract.expected_token_configuration(self.token_position())?; - if let Some(max_supply) = token_configuration.max_supply() { - let (token_total_supply, fee) = platform.drive.fetch_token_total_supply_with_cost( - token_id.to_buffer(), - block_info, - transaction, - platform_version, - )?; - execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee)); - let Some(token_total_supply) = token_total_supply else { - return Err(Error::Drive(drive::error::Error::Drive( - DriveError::CorruptedDriveState(format!( - "token {} total supply not found", - token_id + // A mint into the pool never saturates: the bundle proves exactly the purchased count + // entering the pool, so a supply that stopped short would leave the pool holding value the + // supply does not record. The supply lives in a sum item, so `i64::MAX` is a ceiling of its + // own, and it bounds the purchase even where the token configures no max supply. Without + // that bound the count reaches the writer as an overflow and the node reports its own code + // as broken, dropping the purchase from the block unpaid and leaving it resubmittable + // forever. + let max_supply = token_configuration + .max_supply() + .map_or(i64::MAX as u64, |max_supply| { + max_supply.min(i64::MAX as u64) + }); + + let (token_total_supply, fee) = platform.drive.fetch_token_total_supply_with_cost( + token_id.to_buffer(), + block_info, + transaction, + platform_version, + )?; + execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee)); + let Some(token_total_supply) = token_total_supply else { + return Err(Error::Drive(drive::error::Error::Drive( + DriveError::CorruptedDriveState(format!( + "token {} total supply not found", + token_id + )), + ))); + }; + match token_total_supply.checked_add(self.token_count()) { + Some(total_supply_after) if total_supply_after <= max_supply => {} + _ => { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::TokenMintPastMaxSupplyError( + TokenMintPastMaxSupplyError::new( + token_id, + self.token_count(), + token_total_supply, + max_supply, + ), )), - ))); - }; - match token_total_supply.checked_add(self.token_count()) { - Some(total_supply_after) if total_supply_after <= max_supply => {} - _ => { - return Ok(SimpleConsensusValidationResult::new_with_error( - ConsensusError::StateError(StateError::TokenMintPastMaxSupplyError( - TokenMintPastMaxSupplyError::new( - token_id, - self.token_count(), - token_total_supply, - max_supply, - ), - )), - )); - } + )); } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs index 4d77bcc752c..78609233e79 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_mint_to_pool_transition_action/state_v0/mod.rs @@ -165,36 +165,46 @@ impl TokenMintToPoolTransitionActionStateValidationV0 for TokenMintToPoolTransit } } - if let Some(max_supply) = token_configuration.max_supply() { - let (token_total_supply, fee) = platform.drive.fetch_token_total_supply_with_cost( - token_id.to_buffer(), - block_info, - transaction, - platform_version, - )?; - execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee)); - let Some(token_total_supply) = token_total_supply else { - return Err(Error::Drive(drive::error::Error::Drive( - DriveError::CorruptedDriveState(format!( - "token {} total supply not found", - token_id + // A mint into the pool never saturates: the bundle proves exactly `amount` entering the + // pool, so a supply that stopped short would leave the pool holding value the supply does + // not record. The supply lives in a sum item, so `i64::MAX` is a ceiling of its own, and it + // bounds the mint even where the token configures no max supply. Without that bound the + // amount reaches the writer as an overflow and the node reports its own code as broken, + // dropping the mint from the block unpaid and leaving it resubmittable forever. + let max_supply = token_configuration + .max_supply() + .map_or(i64::MAX as u64, |max_supply| { + max_supply.min(i64::MAX as u64) + }); + + let (token_total_supply, fee) = platform.drive.fetch_token_total_supply_with_cost( + token_id.to_buffer(), + block_info, + transaction, + platform_version, + )?; + execution_context.add_operation(ValidationOperation::PrecalculatedOperation(fee)); + let Some(token_total_supply) = token_total_supply else { + return Err(Error::Drive(drive::error::Error::Drive( + DriveError::CorruptedDriveState(format!( + "token {} total supply not found", + token_id + )), + ))); + }; + match token_total_supply.checked_add(self.amount()) { + Some(total_supply_after) if total_supply_after <= max_supply => {} + _ => { + return Ok(SimpleConsensusValidationResult::new_with_error( + ConsensusError::StateError(StateError::TokenMintPastMaxSupplyError( + TokenMintPastMaxSupplyError::new( + token_id, + self.amount(), + token_total_supply, + max_supply, + ), )), - ))); - }; - match token_total_supply.checked_add(self.amount()) { - Some(total_supply_after) if total_supply_after <= max_supply => {} - _ => { - return Ok(SimpleConsensusValidationResult::new_with_error( - ConsensusError::StateError(StateError::TokenMintPastMaxSupplyError( - TokenMintPastMaxSupplyError::new( - token_id, - self.amount(), - token_total_supply, - max_supply, - ), - )), - )); - } + )); } } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 04b938d5e09..cf0da6a63f7 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -2938,6 +2938,164 @@ mod token_pool_mint_burn_claim_purchase_tests { assert_eq!(pool_balance(&platform, token_id), 445); } + /// A claim into the pool that would push the supply past the sum item's ceiling is a paid + /// consensus error, even where the token configures no max supply. + /// + /// The supply lives in a sum item, so `i64::MAX` bounds it whether or not a max supply is + /// set, and a mint into the pool cannot absorb that bound by saturating: the bundle proves + /// exactly the claimed amount entering the pool, so a supply that stopped short would leave + /// the pool holding value the supply does not record. Reaching the ceiling therefore has to + /// be refused as a consensus error the claimant pays for. Reported as an internal error + /// instead, the claim is stripped from the block for free and can be resubmitted forever, + /// and the node blames its own code for a release it merely cannot honour. + #[tokio::test] + async fn should_charge_a_token_claim_to_pool_past_the_supply_ceiling_once_per_identity() { + assert_claim_to_pool_past_the_supply_ceiling_is_charged( + TokenDistributionType::OncePerIdentity, + ) + .await; + } + + /// A transparent claim lets a pre-programmed release saturate the supply, so only its + /// once-per-identity kind needs the ceiling. A claim into the pool never saturates, which + /// leaves every one of its kinds bounded by it. + #[tokio::test] + async fn should_charge_a_token_claim_to_pool_past_the_supply_ceiling_pre_programmed() { + assert_claim_to_pool_past_the_supply_ceiling_is_charged( + TokenDistributionType::PreProgrammed, + ) + .await; + } + + async fn assert_claim_to_pool_past_the_supply_ceiling_is_charged( + distribution_type: TokenDistributionType, + ) { + // The largest supply a sum item can hold: one more token of it overflows. + const SUPPLY_CEILING: u64 = i64::MAX as u64; + const RELEASE: u64 = 445; + + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9131); + + let (owner, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (claimant, signer, key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let claimant_id = claimant.id(); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + owner.id(), + Some(move |configuration: &mut TokenConfiguration| { + configuration.set_has_shielded_pool(true); + configuration.set_base_supply(SUPPLY_CEILING); + configuration.set_max_supply(None); + if distribution_type == TokenDistributionType::OncePerIdentity { + configuration + .distribution_rules_mut() + .set_once_per_identity_distribution(Some( + TokenOncePerIdentityDistribution::V0( + TokenOncePerIdentityDistributionV0 { amount: RELEASE }, + ), + )); + } else { + configuration + .distribution_rules_mut() + .set_pre_programmed_distribution(Some(TokenPreProgrammedDistribution::V0( + TokenPreProgrammedDistributionV0 { + distributions: [(100, [(claimant_id, RELEASE)].into())].into(), + }, + ))); + } + }), + None, + None, + None, + platform_version, + ); + // Only the ceiling can refuse this release: the token sets no max supply of its own. + assert_eq!( + contract + .expected_token_configuration(0) + .expect("token configuration") + .max_supply(), + None + ); + assert_eq!(total_supply(&platform, token_id), SUPPLY_CEILING); + + fast_forward_to_block(&platform, 100, 40, 42, 1, false); + let platform_state = platform.state.load(); + let block_info = BlockInfo { + time_ms: 200, + height: 41, + core_height: 42, + epoch: Epoch::new(1).unwrap(), + }; + + let claim = BatchTransition::new_token_claim_to_pool_transition( + token_id, + claimant_id, + contract.id(), + 0, + distribution_type, + None, + build_shield_bundle( + RELEASE, + 29, + TokenTransitionActionType::ClaimToPool, + token_id, + claimant_id, + ), + None, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token claim to pool transition"); + let transaction = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &[claim.serialize_to_bytes().expect("serialize")], + &platform_state, + &block_info, + &transaction, + platform_version, + false, + None, + ) + .expect("process state transition"); + let [StateTransitionExecutionResult::PaidConsensusError { + error: ConsensusError::StateError(StateError::TokenMintPastMaxSupplyError(_)), + actual_fees, + .. + }] = result.execution_results().as_slice() + else { + panic!( + "a claim past the supply ceiling must be a paid TokenMintPastMaxSupplyError, got {:?}", + result.execution_results() + ); + }; + assert!( + actual_fees.processing_fee > 0, + "the claimant must pay for the refused claim, or it can be resubmitted for free" + ); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("commit"); + + // Nothing of the claim landed. + assert_eq!(total_supply(&platform, token_id), SUPPLY_CEILING); + assert_eq!(pool_balance(&platform, token_id), 0); + assert_eq!(pool_notes_count(&platform, token_id), 0); + } + #[tokio::test] async fn test_token_direct_purchase_to_pool() { let platform_version = PlatformVersion::latest(); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs index 976f7515cb2..8f0e81d84e1 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_purchase_from_shielded_pool/transform_into_action/v0/mod.rs @@ -73,7 +73,13 @@ impl TokenPurchaseFromShieldedPoolStateTransitionTransformIntoActionValidationV0 ) { return Ok(ConsensusValidationResult::new_with_error(consensus_error)); } - // Minting into the pool must respect the max supply. + // Minting into the pool must respect the supply ceiling. A mint into the pool never + // saturates: the bundle proves exactly the purchased count entering the pool, so a supply + // that stopped short would leave the pool holding value the supply does not record. The + // supply lives in a sum item, so `i64::MAX` is a ceiling of its own, and it bounds the + // purchase even where the token configures no max supply. Without that bound the count + // reaches the writer as an overflow and the node reports its own code as broken, dropping + // the purchase from the block unpaid and leaving it resubmittable forever. let Some(total_supply) = drive.fetch_token_total_supply( v0.token_id.to_buffer(), transaction, @@ -88,20 +94,23 @@ impl TokenPurchaseFromShieldedPoolStateTransitionTransformIntoActionValidationV0 .into(), )); }; - if let Some(max_supply) = configuration.max_supply() { - match total_supply.checked_add(v0.token_count) { - Some(after) if after <= max_supply => {} - _ => { - return Ok(ConsensusValidationResult::new_with_error( - TokenMintPastMaxSupplyError::new( - v0.token_id, - v0.token_count, - total_supply, - max_supply, - ) - .into(), - )); - } + let max_supply = configuration + .max_supply() + .map_or(i64::MAX as u64, |max_supply| { + max_supply.min(i64::MAX as u64) + }); + match total_supply.checked_add(v0.token_count) { + Some(after) if after <= max_supply => {} + _ => { + return Ok(ConsensusValidationResult::new_with_error( + TokenMintPastMaxSupplyError::new( + v0.token_id, + v0.token_count, + total_supply, + max_supply, + ) + .into(), + )); } } // The credit bundle carries the price plus the fee; the fee was pinned by the From d5dcf15db06001f14614edcde3d0c168fb6b19a8 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 14:34:25 +0700 Subject: [PATCH 55/94] fix(dpp)!: price a token unshield's balance write as the insert it is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `TokenUnshieldWithShieldedFee` credits the recipient's token balance, and the transform requires only that the recipient be an existing identity, not an existing holder. So the balance sum item usually does not exist yet and the write creates it — but the fee priced it as `SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES`, the 8-byte allowance for rewriting an item that already exists. Measured at protocol version 14: the insert really costs 6,102,000 credits of storage (223 effective bytes at 27,400 credits/byte) against 219,200 charged — a shortfall of 5,882,800 credits per unshield. `execute_event` books a pool-paid transition as `storage = min(real_storage, carved_fee)` and pays the proposer only the remainder, so the shortfall came out of the proposer's reward for the Halo 2 proof it verified and left the storage pool short of an item the chain then carries forever. Anyone holding pool notes could force it at will by unshielding to an identity that has never held the token. Priced as the insert for every recipient alike, via a new named `SHIELDED_TOKEN_BALANCE_INSERT_STORAGE_BYTES = 230` (223 measured, headroom for the node layout gaining a few bytes). Two independent reasons, and the second is the one that settles it. The first is the standing invariant that an estimate may never fall below the actual fee. Recipient state is not reachable where the number is needed in any case: the fee is fixed by the SDK builder on the client and re-derived by the stateless `validate_minimum_shielded_fee` gate, which takes only the transition and `platform_version` so it can run ahead of proof verification. The second holds even where that state IS reachable, and is why the cheap case must not be split out later as an optimisation: this transition is `amount_is_pure_fee`, so the public `credit_amount` must equal the fee EXACTLY. A fee that varied with the recipient's holdings would therefore publish whether the recipient holds this token for the first time — the fee fingerprint the shielded design exists to deny. Erring high is not caution here, it is the only correct option. Pricing the worst case follows the house rather than making an exception: the shipped `SHIELDED_UNSHIELD_ADDRESS_STORAGE_BYTES = 222` sizes an `AddBalanceToAddress` to its new-address worst case, and the estimation branch of `add_to_identity_token_balance_operations` already assumes the insert ("worse case is that we insert"). What it costs an ordinary unshield to an existing holder: the allowance goes from 219,200 to 6,302,000 credits, so a minimal two-action-per-bundle unshield goes from 228,499,200 to 234,582,000 — +6,082,800 credits, +2.66%. The over-collection is not burned; `processing_fee = carved_fee - storage_fee` routes it to the proposer. Consensus scope: no shipped generation is edited. The changed function `compute_token_unshield_with_shielded_fee_fee`, the constant it replaces and the `TokenUnshieldWithShieldedFee` transition itself are all absent from `origin/v4.1-dev` and exist only on this feature branch, so no released protocol version can reach them. `compute_minimum_shielded_fee_v0` and the other shipped fee functions in the same module are untouched, and `minimum_shielded_fee_changes_only_at_protocol_14` still pins protocol 12 and 13 byte-for-byte. Tests: `test_token_unshield_with_shielded_fee_covers_a_first_time_holders_balance_insert` measures the real metered insert and reads the allowance as the unshield fee minus the two bundle bases, so it pins the component rather than any one constant. It would have caught this in CI: ✖ before the fix ("the unshield fee sets aside 219200 credits (8 effective bytes) ... really costs up to 6102000 credits of storage (223 effective bytes) ... shortfall of 5882800 credits"), ✔ after. The dpp test additionally pins that the insert allowance stays strictly above the rewrite allowance and that the top-up fee does not move, so the two constants cannot be folded back together. Not fixed, checked and sound: `TokenPurchaseFromShieldedPool` keeps the 8-byte allowance for its supply item because `create_token_trees` pre-writes `SumItem(0)`, so `allow_first_mint: true` never reaches the insert branch, and its other write credits an owner identity that already exists. `IdentityTopUpFromShieldedPool` tops up an existing identity, and `TokenShieldedTransferWithShieldedFee` writes nothing outside the pools. Co-Authored-By: Claude Opus 5 (1M context) --- .../compute_minimum_shielded_fee/mod.rs | 11 ++- .../compute_minimum_shielded_fee/v0/mod.rs | 68 +++++++++++++ packages/rs-dpp/src/shielded/mod.rs | 34 +++++++ .../batch/tests/token/shielded/mod.rs | 95 +++++++++++++++++++ 4 files changed, 206 insertions(+), 2 deletions(-) diff --git a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/mod.rs b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/mod.rs index eac5481ba47..3ef8fa52b89 100644 --- a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/mod.rs +++ b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/mod.rs @@ -3,6 +3,7 @@ mod v0; use crate::fee::Credits; use crate::shielded::{ SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES, SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES, + SHIELDED_TOKEN_BALANCE_INSERT_STORAGE_BYTES, }; use crate::ProtocolError; use platform_version::version::PlatformVersion; @@ -269,7 +270,13 @@ pub fn compute_token_shielded_transfer_with_shielded_fee_fee( } /// The fee of a `TokenUnshieldWithShieldedFee`: both bundles plus the recipient's token -/// balance item. +/// balance item, priced as the insert it is for a recipient who has never held this token. +/// +/// The recipient only has to be an existing identity, not an existing holder, so the balance item +/// usually does not exist yet and the write creates it. The component is the insert's cost for +/// every recipient alike: `credit_amount` is public and must equal this fee exactly, so a fee that +/// distinguished the two cases would publish whether the recipient is holding this token for the +/// first time. See [`SHIELDED_TOKEN_BALANCE_INSERT_STORAGE_BYTES`]. pub fn compute_token_unshield_with_shielded_fee_fee( token_actions: usize, fee_actions: usize, @@ -278,7 +285,7 @@ pub fn compute_token_unshield_with_shielded_fee_fee( compute_token_pool_paid_shielded_fee( token_actions, fee_actions, - SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES, + SHIELDED_TOKEN_BALANCE_INSERT_STORAGE_BYTES, platform_version, ) } diff --git a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs index e56e41389be..4c99e1ff13e 100644 --- a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs +++ b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs @@ -563,4 +563,72 @@ mod tests { ); } } + + /// A token unshield credits the recipient's token balance, and the recipient only has to be an + /// existing identity — not an existing holder — so the fee has to cover CREATING that balance + /// item, not merely rewriting one. Its component is therefore the insert allowance, strictly + /// dearer than the replace-only allowance an `IdentityTopUpFromShieldedPool` needs for a + /// balance that already exists. Folding the two back into one constant would either underfund + /// the insert or overcharge the top-up, so both halves of that relationship are pinned. + /// + /// The allowance is flat and identical for every recipient, which is not merely conservatism: + /// `credit_amount` is public and must equal this fee exactly, so a component that varied with + /// the recipient's holdings would publish whether they hold this token for the first time. + #[test] + fn compute_token_unshield_with_shielded_fee_fee_prices_a_balance_insert_for_every_recipient() { + use crate::shielded::{ + compute_token_shielded_transfer_with_shielded_fee_fee, + compute_token_unshield_with_shielded_fee_fee, + SHIELDED_TOKEN_BALANCE_INSERT_STORAGE_BYTES, + }; + + let platform_version = PlatformVersion::latest(); + let storage = &platform_version.fee_version.storage; + let per_byte_rate = + storage.storage_disk_usage_credit_per_byte + storage.storage_processing_credit_per_byte; + let insert_cost = SHIELDED_TOKEN_BALANCE_INSERT_STORAGE_BYTES * per_byte_rate; + + for (token_actions, fee_actions) in [(2usize, 2usize), (2, 5), (7, 2), (16, 16)] { + let unshield = compute_token_unshield_with_shielded_fee_fee( + token_actions, + fee_actions, + platform_version, + ) + .expect("token unshield fee"); + // A shielded transfer writes nothing outside the pools, so it is the two bundles alone. + let both_bundles = compute_token_shielded_transfer_with_shielded_fee_fee( + token_actions, + fee_actions, + platform_version, + ) + .expect("token shielded transfer fee"); + assert_eq!( + unshield, + both_bundles + insert_cost, + "a token unshield of {token_actions}+{fee_actions} actions must be the two \ + bundles plus exactly one {SHIELDED_TOKEN_BALANCE_INSERT_STORAGE_BYTES}-byte \ + balance-insert component" + ); + assert_eq!( + unshield - both_bundles, + insert_cost, + "the balance-insert component must be flat (independent of action count)" + ); + } + + assert!( + SHIELDED_TOKEN_BALANCE_INSERT_STORAGE_BYTES + > SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES, + "creating a balance item costs more than rewriting one, so the unshield's allowance \ + ({SHIELDED_TOKEN_BALANCE_INSERT_STORAGE_BYTES} bytes) must exceed the replace-only \ + one ({SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES} bytes)" + ); + // The top-up tops up an identity that already exists, so it keeps the replace-only one. + assert_eq!( + compute_shielded_identity_top_up_fee_v0(2, platform_version).expect("top up fee"), + compute_minimum_shielded_fee_v0(2, platform_version).expect("minimum shielded fee") + + SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES * per_byte_rate, + "pricing the unshield's insert must not move the top-up off its rewrite allowance" + ); + } } diff --git a/packages/rs-dpp/src/shielded/mod.rs b/packages/rs-dpp/src/shielded/mod.rs index 47f810148f0..9ec203d5658 100644 --- a/packages/rs-dpp/src/shielded/mod.rs +++ b/packages/rs-dpp/src/shielded/mod.rs @@ -160,6 +160,40 @@ pub const SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES: u64 = 8; /// [`compute_minimum_shielded_fee::compute_shielded_identity_balance_write_fee`]. pub const SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES: u64 = 20; +/// Flat component (in effective bytes at the per-byte storage rate) for the recipient's token +/// balance item a `TokenUnshieldWithShieldedFee` writes on top of its per-action nullifier and +/// note writes. +/// +/// It prices the write as an INSERT, not a rewrite. A recipient who already holds the token has a +/// balance sum item to replace, which adds no storage; a recipient who has never held it has no +/// item, so the write creates one and it is real new storage. Measured at protocol version 14: +/// 6,102,000 credits of storage, the same for the smallest balance and the widest, because the +/// item is fixed-width — 223 effective bytes at 27,400 credits/byte, against 8 for the rewrite. +/// 230 leaves headroom for the node layout gaining a few bytes without a fresh calibration. +/// +/// The expensive case is priced unconditionally, and there are two independent reasons for that. +/// +/// The first is that the component may never fall below what the write really costs. +/// `execute_event` books a pool-paid transition as `storage = min(real_storage, carved_fee)` and +/// pays the proposer only the remainder, so a component under the real cost comes out of the +/// proposer's reward for the proof it verified and leaves the storage pool short of an item the +/// chain then carries forever. Recipient state is not reachable where the number is needed in any +/// case: the fee is fixed by the SDK builder on the client and re-derived by the stateless +/// `validate_minimum_shielded_fee` gate, and neither reads a balance. +/// +/// The second reason is decisive even where that state IS reachable, and it is why the cheap case +/// must not be split out later as an optimisation: `credit_amount` is public and must equal this +/// fee EXACTLY, so a fee that varied with the recipient's holdings would publish whether the +/// recipient holds this token for the first time. That is precisely the fee fingerprint the +/// shielded design exists to deny. +/// +/// Pricing the worst case is the standing choice here, not an exception: +/// `SHIELDED_UNSHIELD_ADDRESS_STORAGE_BYTES` sizes an `AddBalanceToAddress` to its new-address +/// worst case, and the estimation branch of `add_to_identity_token_balance_operations` assumes +/// the insert for the same reason. See +/// [`compute_minimum_shielded_fee::compute_token_unshield_with_shielded_fee_fee`]. +pub const SHIELDED_TOKEN_BALANCE_INSERT_STORAGE_BYTES: u64 = 230; + /// Common Orchard bundle parameters shared across all shielded transition types. /// /// Groups the fields that every shielded transition carries identically: diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 04b938d5e09..fb10da32654 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -3990,6 +3990,101 @@ mod token_pool_paid_transitions_tests { assert_tokens_conserved(&platform); } + /// A token unshield's flat fee sets one storage component aside for the recipient's token + /// balance item, and that component has to cover the write in the shape it actually takes. + /// A recipient who has never held this token has no balance item, so the write INSERTS a sum + /// item instead of replacing one — an insert that costs two orders of magnitude more than a + /// rewrite of an existing item, because the item and its tree node are new storage rather + /// than replaced bytes. + /// + /// Underpricing it is not absorbed by the sender: `execute_event` books a pool-paid + /// transition as `storage = min(real_storage, carved_fee)` and hands the proposer only + /// `carved_fee - storage`, so a component below the real cost comes out of the proposer's + /// reward for the Halo 2 proof it verified, and the storage pool is left short of the cost + /// of an item the chain now carries forever. + /// + /// The allowance is read as the difference between the unshield's fee and the two bundle + /// bases it is built from, so this pins the component itself rather than any one constant. + #[tokio::test] + async fn test_token_unshield_with_shielded_fee_covers_a_first_time_holders_balance_insert() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9412); + let block_info = BlockInfo::default(); + + let (owner, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (_, token_id) = create_token_contract_with_owner_identity( + &mut platform, + owner.id(), + Some(enable_shielded_pool), + None, + None, + None, + platform_version, + ); + + let credit = |holder: Identifier, amount: u64| { + platform + .drive + .add_to_identity_token_balance( + token_id.to_buffer(), + holder.to_buffer(), + amount, + &block_info, + true, + None, + platform_version, + None, + ) + .expect("credit a token balance") + }; + + // A holder the token has never seen: crediting them inserts the sum item. The balance + // rides in that item, so a wider balance is a wider insert. Balances live in a sum tree + // whose total may not exceed `i64::MAX`, so the widest one measurable here leaves room + // for the other holders this test credits. + const WIDEST_MEASURABLE_BALANCE: u64 = 1 << 62; + let smallest_insert = credit(Identifier::from(rng.gen::<[u8; 32]>()), 1); + let largest_insert = credit( + Identifier::from(rng.gen::<[u8; 32]>()), + WIDEST_MEASURABLE_BALANCE, + ); + let worst_insert = smallest_insert.storage_fee.max(largest_insert.storage_fee); + // A holder who already has the item: this one only replaces bytes, so it adds no storage. + let existing_holder = Identifier::from(rng.gen::<[u8; 32]>()); + credit(existing_holder, 1); + let replace = credit(existing_holder, 1); + + let storage = &platform_version.fee_version.storage; + let per_byte_rate = + storage.storage_disk_usage_credit_per_byte + storage.storage_processing_credit_per_byte; + let allowance = + dpp::shielded::compute_token_unshield_with_shielded_fee_fee(1, 1, platform_version) + .expect("unshield fee") + - 2 * dpp::shielded::compute_minimum_shielded_fee(1, platform_version) + .expect("bundle base"); + + assert!( + replace.storage_fee < worst_insert, + "a rewrite of an existing balance item must cost less storage than inserting one, \ + otherwise this test is not measuring the insert: insert {worst_insert} vs replace {}", + replace.storage_fee + ); + assert!( + allowance >= worst_insert, + "the unshield fee sets aside {allowance} credits ({} effective bytes) for the \ + recipient's balance item, but inserting one for a first-time holder really costs up \ + to {worst_insert} credits of storage ({} effective bytes: {} for the smallest \ + balance, {} for the largest) — the shortfall of {} credits is taken out of the \ + proposer's processing reward", + allowance / per_byte_rate, + worst_insert.div_ceil(per_byte_rate), + smallest_insert.storage_fee, + largest_insert.storage_fee, + worst_insert.saturating_sub(allowance), + ); + } + #[tokio::test] async fn test_token_purchase_from_shielded_pool() { let platform_version = PlatformVersion::latest(); From bd707cc369d65dd17d6c3447db9bd0768601bf3c Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 14:37:13 +0700 Subject: [PATCH 56/94] docs(drive): put two detached doc paragraphs back on the code they describe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three tidying fixes in the token pool work, no behaviour change. The `SetPrices` resolver's doc comment had come loose and was sitting on `resolve_direct_purchase_price`, where its first line described a function the reader was not looking at. It is back on `resolve_set_prices_direct_purchase_price`, and the paragraph that explains why an empty schedule must never be assumed away now states the hazard rather than narrating which earlier code had it. `TokenShieldTransitionAction::validate_state_v0` carried a truncated paragraph that restarted the sentence above it and trailed off. The only fact it added — that the bundle's anchor is not checked against the pool, because with spends disabled the anchor is not consumed — is kept; the dangling half is gone. `bump_with_errors` spelled every type as a fully qualified inline path, against the convention the rest of the file follows. Its signature now reads in one line per parameter against top-of-file imports. Co-Authored-By: Claude Opus 5 (1M context) --- .../state_v0/mod.rs | 5 ++- .../token_transition/mod.rs | 31 +++++++++++-------- .../v0/transformer.rs | 21 ++++++------- 3 files changed, 30 insertions(+), 27 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs index 7264b41f63f..b11665d5ad1 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_shield_transition_action/state_v0/mod.rs @@ -45,9 +45,8 @@ impl TokenShieldTransitionActionStateValidationV0 for TokenShieldTransitionActio /// token can never freeze an account, so no frozen check is read or billed. It then /// verifies the outputs-only bundle. /// - /// Historical note: the pool - /// opt-in, then verifies the outputs-only bundle. The bundle's anchor is not checked - /// against the pool: with spends disabled the anchor is not consumed. + /// The bundle's anchor is not checked against the pool: with spends disabled the anchor is + /// not consumed. fn validate_state_v0( &self, platform: &PlatformStateRef, diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/mod.rs index 3211f052539..38d4b451267 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/mod.rs @@ -58,10 +58,17 @@ use dpp::data_contract::associated_token::token_keeps_history_rules::accessors:: use dpp::data_contract::document_type::DocumentTypeRef; use dpp::document::Document; use dpp::identifier::Identifier; -use dpp::prelude::{DataContract, IdentityNonce}; +use dpp::consensus::ConsensusError; +use dpp::fee::fee_result::FeeResult; +use dpp::prelude::{ + ConsensusValidationResult, DataContract, IdentityNonce, UserFeeIncrease, +}; +use dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition; use dpp::ProtocolError; use platform_version::version::PlatformVersion; use crate::error::Error; +use crate::state_transition_action::batch::BatchedTransitionAction; +use crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction; use crate::state_transition_action::batch::batched_transition::token_transition::token_base_transition_action::{TokenBaseTransitionAction, TokenBaseTransitionActionAccessorsV0}; use crate::state_transition_action::batch::batched_transition::token_transition::token_burn_transition_action::{TokenBurnTransitionAction, TokenBurnTransitionActionAccessorsV0}; use crate::state_transition_action::batch::batched_transition::token_transition::token_config_update_transition_action::{TokenConfigUpdateTransitionAction, TokenConfigUpdateTransitionActionAccessorsV0}; @@ -276,25 +283,23 @@ impl TokenTransitionAction { /// owner's contract nonce carrying `errors`, returned with the fee accrued so far so the /// failure is paid. pub(crate) fn bump_with_errors( - base: &dpp::state_transition::batch_transition::token_base_transition::TokenBaseTransition, - owner_id: dpp::identifier::Identifier, - user_fee_increase: dpp::prelude::UserFeeIncrease, - errors: Vec, - fee_result: dpp::fee::fee_result::FeeResult, + base: &TokenBaseTransition, + owner_id: Identifier, + user_fee_increase: UserFeeIncrease, + errors: Vec, + fee_result: FeeResult, ) -> ( - dpp::prelude::ConsensusValidationResult< - crate::state_transition_action::batch::BatchedTransitionAction, - >, - dpp::fee::fee_result::FeeResult, + ConsensusValidationResult, + FeeResult, ) { - let bump_action = crate::state_transition_action::system::bump_identity_data_contract_nonce_action::BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( + let bump_action = BumpIdentityDataContractNonceAction::from_borrowed_token_base_transition( base, owner_id, user_fee_increase, ); ( - dpp::prelude::ConsensusValidationResult::new_with_data_and_errors( - crate::state_transition_action::batch::BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action), + ConsensusValidationResult::new_with_data_and_errors( + BatchedTransitionAction::BumpIdentityDataContractNonce(bump_action), errors, ), fee_result, diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/transformer.rs index 9254f47ca2e..28c2e1e2d35 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/transformer.rs @@ -170,17 +170,6 @@ impl TokenDirectPurchaseTransitionActionV0 { } } -/// Resolves the required total price for a `SetPrices` (tiered) direct purchase. -/// -/// Returns the required total in credits on success, or the consensus error that must reject -/// the purchase. This is a pure function so every rejection branch — in particular the -/// empty-schedule case — can be unit-tested directly without standing up a `Drive`. -/// -/// An empty `SetPrices` map is a representable, storable value, so this function must NOT -/// assume the map is non-empty: the original inline code did -/// `set_prices.keys().next().expect("Map is not empty")`, which panics on an empty map. That -/// panic was uncaught during per-state-transition processing and would deterministically halt -/// the chain across the quorum. Here an empty schedule resolves to `TokenNotForDirectSale`. /// Resolves the credits a direct purchase of `token_count` must pay right now, from the token's /// current pricing schedule: an error when the token is not for sale, the buyer agreed to too /// little, or the amount is under the minimum sale amount. Shared by `TokenDirectPurchase` and @@ -269,6 +258,16 @@ pub fn required_direct_purchase_price( Ok(required_price) } +/// Resolves the required total price for a `SetPrices` (tiered) direct purchase. +/// +/// Returns the required total in credits on success, or the consensus error that must reject the +/// purchase. Kept pure so every rejection branch — in particular the empty-schedule case — can be +/// unit-tested without standing up a `Drive`. +/// +/// An empty `SetPrices` map is a representable, storable value, so the schedule must never be +/// assumed non-empty: taking a key unconditionally would panic, and a panic during +/// per-state-transition processing deterministically halts the chain across the quorum. An empty +/// schedule resolves to `TokenNotForDirectSale` instead. fn resolve_set_prices_direct_purchase_price( token_id: Identifier, set_prices: &BTreeMap, From 4fb3a1843ee6200db7d23374af0361fde78a5832 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 14:51:51 +0700 Subject: [PATCH 57/94] fix(dpp): decode a shielded payment's byte fields from base64, not base58 `TokenShieldedPayment`'s `TryFrom>` read every raw byte field with `remove_hash256_bytes` / `remove_bytes`, whose `Value::Text` arms decode base58. The same struct writes those fields as base64 in JSON: `#[json_safe_fields]` injects `serde_bytes` on each `[u8; N]` and `serde_bytes_var` on `proof`, and a JSON string stays `Value::Text` through `From for Value`. So a client holding the payment info as JSON could not parse it back through the map path at all. The JSON side is the one that conforms. Base64 is this repo's encoding for raw bytes crossing the human-readable boundary -- `BinaryData`, `Bytes20`, `Bytes32`, `Bytes36`, `DocumentPropertyType::ByteArray` and every `*_binary_bytes` call site use it -- while base58 is reserved for identifiers (`into_identifier_bytes`, `Identifier`, `DocumentPropertyType:: Identifier`). This parser was the only caller of the base58 `remove_bytes` / `remove_hash256_bytes` helpers in the repository. Switched to `remove_bytes_32` and `remove_binary_data`, the base64 members of the same helper family. Both also pass byte-valued `Value`s through, so the binary map shape that `to_object` produces keeps parsing unchanged. Scope: `TokenPaymentInfo::V1` and `TokenShieldedPayment` are new at protocol version 14, which is unreleased (`LATEST_VERSION = PROTOCOL_VERSION_14`). No released version ever accepted `$formatVersion: "1"`, so no released version's accepted, signed or stored bytes change -- this is an in-place edit of an unshipped generation. The bincode `Encode`/`Decode` consensus representation is untouched either way; only `Value::Text` decoding moves. Test would have caught this in CI -- red before the fix, green after: before: test tokens::token_payment_info::json_convertible_tests:: v1_parses_a_map_whose_byte_fields_arrived_as_base64_json_text ... FAILED panicked at packages/rs-dpp/src/tokens/token_payment_info/mod.rs:384:58: the JSON map parses: ValueError(StructureError("value was a string, but could not be decoded from base 58")) test result: FAILED. 0 passed; 1 failed after: test tokens::token_payment_info::json_convertible_tests:: v1_parses_a_map_whose_byte_fields_arrived_as_base64_json_text ... ok test result: ok. 5 passed; 0 failed The pre-existing `v1_json_and_value_round_trip` passed throughout: it feeds the parser `to_object()` output, whose byte fields are `Value::Bytes32` / `Value::Bytes`, never `Value::Text`, which is why the defect survived it. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/tokens/token_payment_info/mod.rs | 28 +++++++++++++++++ .../src/tokens/token_payment_info/v1/mod.rs | 30 +++++++++++++------ 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/packages/rs-dpp/src/tokens/token_payment_info/mod.rs b/packages/rs-dpp/src/tokens/token_payment_info/mod.rs index 38812d6e01d..217bfaac5e2 100644 --- a/packages/rs-dpp/src/tokens/token_payment_info/mod.rs +++ b/packages/rs-dpp/src/tokens/token_payment_info/mod.rs @@ -357,6 +357,34 @@ mod json_convertible_tests { assert_eq!(original, recovered); } + /// A client that holds the payment info as JSON hands its map to the map + /// parser. JSON carries raw byte fields as base64 strings — the shape + /// `to_json` writes for them — and a JSON string becomes `Value::Text`, so + /// the parser has to read `Value::Text` as base64 to agree with the JSON + /// the same type produces. Base58 is this repo's identifier encoding, not + /// its raw-byte encoding. + #[test] + fn v1_parses_a_map_whose_byte_fields_arrived_as_base64_json_text() { + use crate::serialization::JsonConvertible; + let original = v1_fixture(); + let json = original.to_json().expect("to_json"); + // Pin the JSON encoding the parser has to match: base64, padded. + assert_eq!( + json["shieldedPayment"]["anchor"], + json!("BwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwc=") + ); + assert_eq!(json["shieldedPayment"]["proof"], json!("CAgICAgICAgICA==")); + assert_eq!( + json["shieldedPayment"]["actions"][0]["nullifier"], + json!("AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=") + ); + let map = Value::from(json) + .into_btree_string_map() + .expect("the JSON object becomes a string-keyed map"); + let recovered: TokenPaymentInfo = map.try_into().expect("the JSON map parses"); + assert_eq!(original, recovered); + } + #[test] fn setting_and_clearing_the_shielded_payment_switches_the_format_version() { let mut info = fixture(); diff --git a/packages/rs-dpp/src/tokens/token_payment_info/v1/mod.rs b/packages/rs-dpp/src/tokens/token_payment_info/v1/mod.rs index 1bbcf7f0875..e0cf7613d23 100644 --- a/packages/rs-dpp/src/tokens/token_payment_info/v1/mod.rs +++ b/packages/rs-dpp/src/tokens/token_payment_info/v1/mod.rs @@ -78,27 +78,39 @@ impl TryFrom> for TokenShieldedPayment { type Error = ProtocolError; fn try_from(mut map: BTreeMap) -> Result { + // These are raw cryptographic bytes, so they cross the JSON boundary as + // base64 — the encoding this type's own JSON form writes, and the one + // `Bytes32` / `BinaryData` use. `remove_bytes_32` and + // `remove_binary_data` decode base64 text and pass byte-valued + // `Value`s through, so both the JSON and the binary map shape parse. + // Base58 is this repo's identifier encoding and does not apply here. let actions = map .remove_inner_value_array::>("actions")? .into_iter() .map(|value| { let mut action = value.into_btree_string_map()?; Ok(SerializedAction { - nullifier: action.remove_hash256_bytes("nullifier")?, - rk: action.remove_hash256_bytes("rk")?, - cmx: action.remove_hash256_bytes("cmx")?, - encrypted_note: action.remove_bytes("encryptedNote")?, - cv_net: action.remove_hash256_bytes("cvNet")?, - spend_auth_sig: fixed_64(action.remove_bytes("spendAuthSig")?, "spendAuthSig")?, + nullifier: action.remove_bytes_32("nullifier")?.to_buffer(), + rk: action.remove_bytes_32("rk")?.to_buffer(), + cmx: action.remove_bytes_32("cmx")?.to_buffer(), + encrypted_note: action.remove_binary_data("encryptedNote")?.0, + cv_net: action.remove_bytes_32("cvNet")?.to_buffer(), + spend_auth_sig: fixed_64( + action.remove_binary_data("spendAuthSig")?.0, + "spendAuthSig", + )?, }) }) .collect::, ProtocolError>>()?; Ok(TokenShieldedPayment { amount: map.remove_integer("amount")?, actions, - anchor: map.remove_hash256_bytes("anchor")?, - proof: map.remove_bytes("proof")?, - binding_signature: fixed_64(map.remove_bytes("bindingSignature")?, "bindingSignature")?, + anchor: map.remove_bytes_32("anchor")?.to_buffer(), + proof: map.remove_binary_data("proof")?.0, + binding_signature: fixed_64( + map.remove_binary_data("bindingSignature")?.0, + "bindingSignature", + )?, }) } } From dfe2d36f59fb54e1c0afb72c71603d094455533e Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 15:12:38 +0700 Subject: [PATCH 58/94] fix(wasm-dpp2): name the TokenEvent fields the serializer actually emits The `TokenEventObject` doc block is the only place a TypeScript consumer learns a variant's field names -- the interface itself is an index signature, so nothing checked the prose. Four entries named fields that do not exist, and a JS consumer reading any of them got `undefined`: Unshield recipientId -> recipient Transfer personalEncryptedNote -> privateEncryptedNote EmergencyAction emergencyAction -> action ConfigUpdate configChange -> configurationChange The doc is the wrong side in all four. `TokenEvent`'s `Serialize` and `Deserialize` agree with each other on every one of these names, so the emitted name is also the only name a producer can send: the deserializer matches `"recipient"`, `"privateEncryptedNote"`, `"action"` and `"configurationChange"`, and a map without them fails with a missing-field error. Renaming the wire instead would have broken both directions. `recipient` is also the name this surface already uses for the concept. Mint and Transfer both serialize it as `recipient`, all three variants type it as the `RecipientIdentifier` alias, and the doc block's own Mint and Transfer entries say `recipient`. `recipientId` belongs to a different surface -- it is a data contract document property in the moderation charters contract, not a TokenEvent field. Only `Unshield` was reported; the test found the other three, which are the same defect and the same one-word correction. The test cross-checks the doc against the serializer rather than asserting a string: it serializes one instance of every variant, reads the field list the doc promises for that variant back out of the doc block, and compares the two sets. So it fails on a name that exists only in the doc, and equally on a serialized field renamed without updating the doc. `variant_name` matches every variant with no catch-all, so a new variant stops compiling until it is documented. To let the test read the doc, the `typescript_custom_section` attribute is now applied only for the wasm target, leaving the constant itself visible to Rust. Shipped behaviour is unchanged: on wasm32 the cfg_attr expands to exactly the previous attribute, and the wasm32 build still carries both the doc text and the `__wasm_bindgen_unstable` section marker. Test would have caught this in CI -- red before the fix, green after: before: test group::token_event::tests:: the_documented_field_names_are_the_ones_the_serializer_emits ... FAILED the TypeScript doc block promises field names `TokenEvent`'s Serialize does not emit: Transfer: doc says ["amount", "personalEncryptedNote", "publicNote", "recipient", "sharedEncryptedNote"], serializer emits ["amount", "privateEncryptedNote", "publicNote", "recipient", "sharedEncryptedNote"] EmergencyAction: doc says ["emergencyAction", "publicNote"], serializer emits ["action", "publicNote"] ConfigUpdate: doc says ["configChange", "publicNote"], serializer emits ["configurationChange", "publicNote"] Unshield: doc says ["amount", "recipientId"], serializer emits ["amount", "recipient"] test result: FAILED. 0 passed; 1 failed after: test group::token_event::tests:: the_documented_field_names_are_the_ones_the_serializer_emits ... ok test result: ok. 13 passed; 0 failed Co-Authored-By: Claude Opus 5 (1M context) --- packages/wasm-dpp2/src/group/token_event.rs | 150 +++++++++++++++++++- 1 file changed, 145 insertions(+), 5 deletions(-) diff --git a/packages/wasm-dpp2/src/group/token_event.rs b/packages/wasm-dpp2/src/group/token_event.rs index 74d0fb8071d..0058ef5e6f0 100644 --- a/packages/wasm-dpp2/src/group/token_event.rs +++ b/packages/wasm-dpp2/src/group/token_event.rs @@ -3,7 +3,11 @@ use crate::impl_wasm_type_info; use dpp::tokens::token_event::TokenEvent; use wasm_bindgen::prelude::wasm_bindgen; -#[wasm_bindgen(typescript_custom_section)] +// The custom section is a wasm-target artifact, so only the attribute is gated +// on it. The constant itself stays visible to Rust, which lets the field names +// this doc promises be checked against the ones the serializer emits. +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(typescript_custom_section))] +#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] const TS_TYPES: &str = r#" /** * TokenEvent serialized as a plain object. @@ -19,14 +23,14 @@ const TS_TYPES: &str = r#" * - Unfreeze:{ $type: "unfreeze",frozenIdentifier, publicNote } * - DestroyFrozenFunds: { $type, frozenIdentifier, amount, publicNote } * - Transfer:{ $type, recipient, publicNote, sharedEncryptedNote, - * personalEncryptedNote, amount } + * privateEncryptedNote, amount } * - Claim: { $type, distributionType, amount, publicNote } - * - EmergencyAction: { $type, emergencyAction, publicNote } - * - ConfigUpdate: { $type, configChange, publicNote } + * - EmergencyAction: { $type, action, publicNote } + * - ConfigUpdate: { $type, configurationChange, publicNote } * - ChangePriceForDirectPurchase: { $type, pricingSchedule, publicNote } * - DirectPurchase: { $type, amount, credits } * - Shield: { $type: "shield", amount } - * - Unshield:{ $type: "unshield", recipientId, amount } + * - Unshield:{ $type: "unshield", recipient, amount } * - ShieldedTransfer: { $type: "shieldedTransfer" } * - MintToPool: { $type: "mintToPool", amount, actionsDigest, publicNote } * - BurnFromPool: { $type: "burnFromPool", amount, actionsDigest, publicNote } @@ -139,3 +143,139 @@ impl_wasm_conversions_inner!( TokenEventJSONJs ); impl_wasm_type_info!(TokenEventWasm, TokenEvent); + +#[cfg(test)] +mod tests { + use super::TS_TYPES; + use dpp::data_contract::associated_token::token_configuration_item::TokenConfigurationChangeItem; + use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionTypeWithResolvedRecipient; + use dpp::identifier::Identifier; + use dpp::tokens::emergency_action::TokenEmergencyAction; + use dpp::tokens::token_event::TokenEvent; + + /// `TokenEventObject` is an index signature, so the per-variant field names + /// a TypeScript consumer works from live only in the doc block's prose and + /// nothing checks them. This reads the field list the doc promises for each + /// variant back out of it and compares that with the keys `TokenEvent`'s + /// `Serialize` actually emits. A name that exists only in the doc — a + /// property a JS consumer would read as `undefined` — fails here, and so + /// does renaming a serialized field without updating the doc. + #[test] + fn the_documented_field_names_are_the_ones_the_serializer_emits() { + // A comment block: entries wrap across lines, each carrying a leading + // `*`. Flatten it so one entry is one contiguous span of text. + let doc = TS_TYPES.replace(['\n', '*'], " "); + + let mut disagreements = Vec::new(); + for event in every_variant() { + let serialized = serde_json::to_value(&event).expect("a TokenEvent serializes"); + let object = serialized + .as_object() + .expect("a TokenEvent serializes as a map keyed by field name"); + let mut emitted: Vec = object + .keys() + .filter(|key| key.as_str() != "$type") + .cloned() + .collect(); + emitted.sort(); + + let variant = variant_name(&event); + let mut documented = documented_fields(&doc, variant); + documented.sort(); + + if documented != emitted { + disagreements.push(format!( + " {variant}: doc says {documented:?}, serializer emits {emitted:?}" + )); + } + } + + assert!( + disagreements.is_empty(), + "the TypeScript doc block promises field names `TokenEvent`'s Serialize \ + does not emit:\n{}", + disagreements.join("\n") + ); + } + + /// The payload field names the doc block promises for one variant, read out + /// of its `- Variant: { $type, field, ... }` entry. `$type` is the + /// discriminator rather than a payload field, so it is dropped. + fn documented_fields(doc: &str, variant: &str) -> Vec { + let entry = doc + .split_once(&format!("- {variant}:")) + .unwrap_or_else(|| panic!("the doc block has an entry for `{variant}`")) + .1; + let fields = entry + .split_once('{') + .expect("the entry opens its field list") + .1 + .split_once('}') + .expect("the entry closes its field list") + .0; + fields + .split(',') + .map(str::trim) + .filter(|field| !field.is_empty() && !field.starts_with("$type")) + .map(str::to_string) + .collect() + } + + /// The label the doc block uses for a variant's entry. Exhaustive, so a new + /// variant stops compiling until it is documented and added below. + fn variant_name(event: &TokenEvent) -> &'static str { + match event { + TokenEvent::Mint(..) => "Mint", + TokenEvent::Burn(..) => "Burn", + TokenEvent::Freeze(..) => "Freeze", + TokenEvent::Unfreeze(..) => "Unfreeze", + TokenEvent::DestroyFrozenFunds(..) => "DestroyFrozenFunds", + TokenEvent::Transfer(..) => "Transfer", + TokenEvent::Claim(..) => "Claim", + TokenEvent::EmergencyAction(..) => "EmergencyAction", + TokenEvent::ConfigUpdate(..) => "ConfigUpdate", + TokenEvent::ChangePriceForDirectPurchase(..) => "ChangePriceForDirectPurchase", + TokenEvent::DirectPurchase(..) => "DirectPurchase", + TokenEvent::Shield(..) => "Shield", + TokenEvent::Unshield(..) => "Unshield", + TokenEvent::ShieldedTransfer => "ShieldedTransfer", + TokenEvent::MintToPool(..) => "MintToPool", + TokenEvent::BurnFromPool(..) => "BurnFromPool", + TokenEvent::ClaimToPool(..) => "ClaimToPool", + TokenEvent::DirectPurchaseToPool(..) => "DirectPurchaseToPool", + } + } + + /// One instance of every variant. Which keys the serializer emits depends + /// on the variant alone, never on the payload, so the values are stand-ins. + fn every_variant() -> Vec { + let id = Identifier::new([1; 32]); + vec![ + TokenEvent::Mint(1, id, None), + TokenEvent::Burn(1, id, None), + TokenEvent::Freeze(id, None), + TokenEvent::Unfreeze(id, None), + TokenEvent::DestroyFrozenFunds(id, 1, None), + TokenEvent::Transfer(id, None, None, None, 1), + TokenEvent::Claim( + TokenDistributionTypeWithResolvedRecipient::PreProgrammed(id), + 1, + None, + ), + TokenEvent::EmergencyAction(TokenEmergencyAction::Pause, None), + TokenEvent::ConfigUpdate( + TokenConfigurationChangeItem::TokenConfigurationNoChange, + None, + ), + TokenEvent::ChangePriceForDirectPurchase(None, None), + TokenEvent::DirectPurchase(1, 1), + TokenEvent::Shield(1), + TokenEvent::Unshield(id, 1), + TokenEvent::ShieldedTransfer, + TokenEvent::MintToPool(1, id, None), + TokenEvent::BurnFromPool(1, id, None), + TokenEvent::ClaimToPool(1), + TokenEvent::DirectPurchaseToPool(1, 1), + ] + } +} From b460834ea3215a867fb0f2bca0d82c5544298122 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 15:24:41 +0700 Subject: [PATCH 59/94] refactor(dpp): give the token shielded pool kinds one home MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Which token transition kinds touch a shielded pool" was written out by hand in five places. Three of them ended in a catch-all, so a new pool kind compiled into all three as a non-pool kind and was silently dropped from: - the Orchard action count CheckTx admits proof work for, which would let the new kind's bundle be verified outside the per-nonce budget; - the admission key a batch carrying pool transitions is limited under, which would leave the new kind with no limiter at all; - the pools a processed transition wrote to, which feeds the block-end anchor hook, so the pool would take notes and never record an anchor for them. `TokenTransition::shielded_pool_actions` is now that home: it hands back the bundle's actions for the seven pool kinds and nothing for the rest, naming every kind so a new one cannot compile without choosing a side. The three sites above read their answer from it and have no catch-all left. This is a behaviour-preserving refactor — there is no defect to reproduce, so there is no red phase. What it buys is shown directly: adding an eighth pool kind to the enum now stops the build at the one place that decides, instead of compiling silently into three wrong answers. packages/.../batched_transition/token_transition.rs:658:15: error[E0004]: non-exhaustive patterns: `&TokenTransition::ExperimentalPool(_)` not covered Two tests pin the classification either way: test ...token_transition::tests::every_token_shielded_pool_kind_reports_its_bundle ... ok test ...token_transition::tests::a_kind_without_a_pool_bundle_reports_none ... ok test result: ok. 25 passed; 0 failed; 0 ignored; 0 measured; 5019 filtered out The two remaining hand-written lists are left alone on purpose: the proof verifier's transition classifier is being edited concurrently, and the `is_allowed` gate's arms carry a per-kind name string that reaches an error message, so converting it would change what that error says. Co-Authored-By: Claude Opus 5 (1M context) --- .../batched_transition/token_transition.rs | 182 +++++++++++++++++- .../process_raw_state_transitions/v0/mod.rs | 17 +- .../processor/traits/shielded_proof.rs | 45 ++--- 3 files changed, 198 insertions(+), 46 deletions(-) diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs index 5e514e3902b..4883a5bb608 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/token_transition.rs @@ -41,10 +41,12 @@ use crate::state_transition::batch_transition::token_direct_purchase_transition: use crate::state_transition::batch_transition::token_direct_purchase_transition::v0::v0_methods::TokenDirectPurchaseTransitionV0Methods; use crate::state_transition::batch_transition::token_set_price_for_direct_purchase_transition::v0::v0_methods::TokenSetPriceForDirectPurchaseTransitionV0Methods; use crate::state_transition::batch_transition::token_transfer_transition::v0::v0_methods::TokenTransferTransitionV0Methods; -use crate::shielded::serialized_actions_digest; +use crate::shielded::{serialized_actions_digest, SerializedAction}; use crate::state_transition::batch_transition::token_shield_transition::v0::v0_methods::TokenShieldTransitionV0Methods; +use crate::state_transition::batch_transition::token_shielded_transfer_transition::v0::v0_methods::TokenShieldedTransferTransitionV0Methods; use crate::state_transition::batch_transition::token_mint_to_pool_transition::v0::v0_methods::TokenMintToPoolTransitionV0Methods; use crate::state_transition::batch_transition::token_burn_from_pool_transition::v0::v0_methods::TokenBurnFromPoolTransitionV0Methods; +use crate::state_transition::batch_transition::token_claim_to_pool_transition::v0::v0_methods::TokenClaimToPoolTransitionV0Methods; use crate::state_transition::batch_transition::token_direct_purchase_to_pool_transition::v0::v0_methods::TokenDirectPurchaseToPoolTransitionV0Methods; use crate::state_transition::batch_transition::token_unshield_transition::v0::v0_methods::TokenUnshieldTransitionV0Methods; use crate::state_transition::batch_transition::token_unfreeze_transition::v0::v0_methods::TokenUnfreezeTransitionV0Methods; @@ -498,6 +500,18 @@ pub trait TokenTransitionV0Methods { ) -> Option>; fn can_calculate_action_id(&self) -> bool; + + /// The Orchard actions of the bundle this transition carries against the token's shielded + /// pool, or `None` for a kind that does not touch a pool. + /// + /// This is the single home for "which token transition kinds touch a shielded pool". The + /// question is asked wherever a pool's notes have to be accounted for outside the pool + /// validators — the mempool identifiers a batch claims, the proof work admission budgets, + /// the pools a processed transition wrote to — and every one of those follows from this + /// answer rather than repeating the list of kinds. The match names all kinds, so a new kind + /// has to declare which side it is on before it compiles. + fn shielded_pool_actions(&self) -> Option<&[SerializedAction]>; + /// Historical document type name for the token history contract fn historical_document_type_name(&self) -> &str; /// Historical document type for the token history contract @@ -637,6 +651,29 @@ impl TokenTransitionV0Methods for TokenTransition { } } + fn shielded_pool_actions(&self) -> Option<&[SerializedAction]> { + match self { + TokenTransition::Shield(t) => Some(t.actions()), + TokenTransition::Unshield(t) => Some(t.actions()), + TokenTransition::ShieldedTransfer(t) => Some(t.actions()), + TokenTransition::MintToPool(t) => Some(t.actions()), + TokenTransition::BurnFromPool(t) => Some(t.actions()), + TokenTransition::ClaimToPool(t) => Some(t.actions()), + TokenTransition::DirectPurchaseToPool(t) => Some(t.actions()), + TokenTransition::Burn(_) + | TokenTransition::Mint(_) + | TokenTransition::Transfer(_) + | TokenTransition::Freeze(_) + | TokenTransition::Unfreeze(_) + | TokenTransition::DestroyFrozenFunds(_) + | TokenTransition::Claim(_) + | TokenTransition::EmergencyAction(_) + | TokenTransition::ConfigUpdate(_) + | TokenTransition::DirectPurchase(_) + | TokenTransition::SetPriceForDirectPurchase(_) => None, + } + } + fn set_data_contract_id(&mut self, id: Identifier) { self.base_mut().set_data_contract_id(id); } @@ -870,8 +907,15 @@ mod tests { use crate::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; use crate::data_contract::change_control_rules::v0::ChangeControlRulesV0; use crate::data_contract::change_control_rules::ChangeControlRules; + use crate::state_transition::batch_transition::batched_transition::token_burn_from_pool_transition::TokenBurnFromPoolTransitionV0; use crate::state_transition::batch_transition::batched_transition::token_burn_transition::TokenBurnTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_claim_to_pool_transition::TokenClaimToPoolTransitionV0; use crate::state_transition::batch_transition::batched_transition::token_claim_transition::TokenClaimTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_direct_purchase_to_pool_transition::TokenDirectPurchaseToPoolTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_mint_to_pool_transition::TokenMintToPoolTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_shield_transition::TokenShieldTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_shielded_transfer_transition::TokenShieldedTransferTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_unshield_transition::TokenUnshieldTransitionV0; use crate::state_transition::batch_transition::batched_transition::token_config_update_transition::TokenConfigUpdateTransitionV0; use crate::state_transition::batch_transition::batched_transition::token_destroy_frozen_funds_transition::TokenDestroyFrozenFundsTransitionV0; use crate::state_transition::batch_transition::batched_transition::token_direct_purchase_transition::TokenDirectPurchaseTransitionV0; @@ -1800,4 +1844,140 @@ mod tests { other => panic!("Expected Claim, got {:?}", other), } } + + /// `count` distinguishable Orchard actions. + fn pool_actions(count: u8) -> Vec { + (0..count) + .map(|i| SerializedAction { + nullifier: [i; 32], + rk: [i; 32], + cmx: [i; 32], + encrypted_note: vec![i; 216], + cv_net: [i; 32], + spend_auth_sig: [i; 64], + }) + .collect() + } + + /// The seven kinds that carry a bundle against the token's shielded pool must hand it over, + /// each with the actions it actually holds rather than another kind's. A caller that has to + /// account for the pool's notes — the mempool identifiers, the admitted proof work, the + /// pools a block wrote to — reads them from here, so a kind reporting nothing or reporting + /// the wrong field would drop those notes from every one of them at once. + #[test] + fn every_token_shielded_pool_kind_reports_its_bundle() { + let cases: Vec<(TokenTransition, u8)> = vec![ + ( + TokenTransition::Shield(TokenShieldTransition::V0(TokenShieldTransitionV0 { + actions: pool_actions(1), + ..Default::default() + })), + 1, + ), + ( + TokenTransition::Unshield(TokenUnshieldTransition::V0(TokenUnshieldTransitionV0 { + actions: pool_actions(2), + ..Default::default() + })), + 2, + ), + ( + TokenTransition::ShieldedTransfer(TokenShieldedTransferTransition::V0( + TokenShieldedTransferTransitionV0 { + actions: pool_actions(3), + ..Default::default() + }, + )), + 3, + ), + ( + TokenTransition::MintToPool(TokenMintToPoolTransition::V0( + TokenMintToPoolTransitionV0 { + actions: pool_actions(4), + ..Default::default() + }, + )), + 4, + ), + ( + TokenTransition::BurnFromPool(TokenBurnFromPoolTransition::V0( + TokenBurnFromPoolTransitionV0 { + actions: pool_actions(5), + ..Default::default() + }, + )), + 5, + ), + ( + TokenTransition::ClaimToPool(TokenClaimToPoolTransition::V0( + TokenClaimToPoolTransitionV0 { + actions: pool_actions(6), + ..Default::default() + }, + )), + 6, + ), + ( + TokenTransition::DirectPurchaseToPool(TokenDirectPurchaseToPoolTransition::V0( + TokenDirectPurchaseToPoolTransitionV0 { + actions: pool_actions(7), + ..Default::default() + }, + )), + 7, + ), + ]; + + for (transition, expected) in cases { + let actions = transition + .shielded_pool_actions() + .unwrap_or_else(|| panic!("{} must report its bundle", transition.name_for_test())); + assert_eq!( + actions, + pool_actions(expected).as_slice(), + "{} must report its own actions", + transition.name_for_test() + ); + } + } + + /// A kind that touches no pool must report nothing: were it to report an empty bundle + /// instead, the callers would treat it as a pool transition and, among other things, claim + /// the pool as one the transition writes to. + #[test] + fn a_kind_without_a_pool_bundle_reports_none() { + let transitions = vec![ + TokenTransition::Burn(TokenBurnTransition::V0(TokenBurnTransitionV0 { + base: TokenBaseTransition::default(), + burn_amount: 1, + public_note: None, + })), + TokenTransition::Mint(TokenMintTransition::V0(TokenMintTransitionV0 { + base: TokenBaseTransition::default(), + issued_to_identity_id: None, + amount: 1, + public_note: None, + })), + TokenTransition::Claim(TokenClaimTransition::V0(TokenClaimTransitionV0 { + base: TokenBaseTransition::default(), + distribution_type: TokenDistributionType::Perpetual, + public_note: None, + })), + ]; + + for transition in transitions { + assert!( + transition.shielded_pool_actions().is_none(), + "{} touches no shielded pool", + transition.name_for_test() + ); + } + } + + impl TokenTransition { + /// The kind's name, for a failure message that says which case failed. + fn name_for_test(&self) -> &str { + self.historical_document_type_name() + } + } } diff --git a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs index ddaec719bc5..f88e25ddddf 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs @@ -7,9 +7,7 @@ use dpp::consensus::codes::ErrorWithCode; use dpp::fee::Credits; use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; -use dpp::state_transition::batch_transition::batched_transition::token_transition::{ - TokenTransition, TokenTransitionV0Methods, -}; +use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransitionV0Methods; use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRef; use dpp::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; use dpp::state_transition::batch_transition::document_base_transition::v1::v1_methods::DocumentBaseTransitionV1Methods; @@ -408,15 +406,9 @@ fn token_shielded_pools_touched(state_transition: &StateTransition) -> Vec<[u8; StateTransition::Batch(batch) => batch .transitions_iter() .filter_map(|transition| match transition { - BatchedTransitionRef::Token( - token_transition @ (TokenTransition::Shield(_) - | TokenTransition::Unshield(_) - | TokenTransition::ShieldedTransfer(_) - | TokenTransition::MintToPool(_) - | TokenTransition::BurnFromPool(_) - | TokenTransition::ClaimToPool(_) - | TokenTransition::DirectPurchaseToPool(_)), - ) => Some(token_transition.token_id().to_buffer()), + BatchedTransitionRef::Token(token_transition) => token_transition + .shielded_pool_actions() + .map(|_| token_transition.token_id().to_buffer()), BatchedTransitionRef::Document(document_transition) => { let base = document_transition.base(); base.token_payment_info_ref() @@ -424,7 +416,6 @@ fn token_shielded_pools_touched(state_transition: &StateTransition) -> Vec<[u8; .filter(|info| info.shielded_payment().is_some()) .map(|info| info.token_id(base.data_contract_id()).to_buffer()) } - _ => None, }) .collect(), StateTransition::TokenShieldedTransferWithShieldedFee(st) => { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs index f5ee67639d5..0251938ea08 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs @@ -40,7 +40,6 @@ use dpp::state_transition::batch_transition::token_shielded_transfer_transition: use dpp::state_transition::batch_transition::token_unshield_transition::v0::v0_methods::TokenUnshieldTransitionV0Methods; use dpp::state_transition::batch_transition::token_mint_to_pool_transition::v0::v0_methods::TokenMintToPoolTransitionV0Methods; use dpp::state_transition::batch_transition::token_burn_from_pool_transition::v0::v0_methods::TokenBurnFromPoolTransitionV0Methods; -use dpp::state_transition::batch_transition::token_claim_to_pool_transition::v0::v0_methods::TokenClaimToPoolTransitionV0Methods; use dpp::state_transition::batch_transition::token_direct_purchase_to_pool_transition::v0::v0_methods::TokenDirectPurchaseToPoolTransitionV0Methods; use dpp::state_transition::batch_transition::BatchTransition; use dpp::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; @@ -217,23 +216,10 @@ impl StateTransitionHasShieldedProofValidationV0 for StateTransition { StateTransition::Batch(batch) => batch .transitions_iter() .map(|transition| match transition { - BatchedTransitionRef::Token(TokenTransition::Shield(t)) => t.actions().len(), - BatchedTransitionRef::Token(TokenTransition::Unshield(t)) => t.actions().len(), - BatchedTransitionRef::Token(TokenTransition::ShieldedTransfer(t)) => { - t.actions().len() - } - BatchedTransitionRef::Token(TokenTransition::MintToPool(t)) => { - t.actions().len() - } - BatchedTransitionRef::Token(TokenTransition::BurnFromPool(t)) => { - t.actions().len() - } - BatchedTransitionRef::Token(TokenTransition::ClaimToPool(t)) => { - t.actions().len() - } - BatchedTransitionRef::Token(TokenTransition::DirectPurchaseToPool(t)) => { - t.actions().len() - } + BatchedTransitionRef::Token(token_transition) => token_transition + .shielded_pool_actions() + .map(|actions| actions.len()) + .unwrap_or(0), BatchedTransitionRef::Document(document_transition) => document_transition .base() .token_payment_info_ref() @@ -241,7 +227,6 @@ impl StateTransitionHasShieldedProofValidationV0 for StateTransition { .and_then(|info| info.shielded_payment()) .map(|payment| payment.actions.len()) .unwrap_or(0), - _ => 0, }) .sum(), _ => 0, @@ -261,19 +246,15 @@ impl StateTransitionHasShieldedProofValidationV0 for StateTransition { batch .transitions_iter() .find_map(|transition| match transition { - BatchedTransitionRef::Token( - token_transition @ (TokenTransition::Shield(_) - | TokenTransition::Unshield(_) - | TokenTransition::ShieldedTransfer(_) - | TokenTransition::MintToPool(_) - | TokenTransition::BurnFromPool(_) - | TokenTransition::ClaimToPool(_) - | TokenTransition::DirectPurchaseToPool(_)), - ) => Some(ShieldedProofAdmissionKey::IdentityContract { - identity_id: owner_id, - contract_id: token_transition.data_contract_id().to_buffer(), - nonce: token_transition.identity_contract_nonce(), - }), + BatchedTransitionRef::Token(token_transition) + if token_transition.shielded_pool_actions().is_some() => + { + Some(ShieldedProofAdmissionKey::IdentityContract { + identity_id: owner_id, + contract_id: token_transition.data_contract_id().to_buffer(), + nonce: token_transition.identity_contract_nonce(), + }) + } BatchedTransitionRef::Document(document_transition) if document_transition .base() From 65a9bc77e9b1cb7788f47620ab6c100d9b3f85f2 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 15:31:29 +0700 Subject: [PATCH 60/94] fix(drive): verify a token pool group action's proof while it is still open MintToPool and BurnFromPool can be submitted as group actions, and a group action writes nothing into the pool until the last required signature arrives: the operation converters push the pool write only when the action closes, while the signer's power entry is recorded on every signature. The verifier ignored that. It demanded every nullifier of a BurnFromPool be shown spent, which rejects the proof of a perfectly good open proposal, and it reported a grouped MintToPool as a bare pool balance with no signer power and no action status, claiming the pool total was the result of a mint that had not run. The six older grouped token branches already establish the pattern: verify the signer and the total power, read the action status from the proof, and demand the effect only once the status is ActionClosed. Both pool branches now follow it, and the execution-guarantee classifier follows with them. A grouped mint into the pool is now execution-proved for the same reason every other grouped kind is, the signer's entry sitting under the action id the transition derives, and the burn keeps its unconditional guarantee, which the group-action entry supports for an open action and the spent nullifiers for an ungrouped one. Two variants are appended at the tail of StateTransitionProofResult to carry what the older variants cannot: the pool balance and the nullifier statuses next to the signer power and the action status. VerifiedTokenShieldedPoolBalance also gains the json_safe_u64 its sibling balance variants carry, so a JS consumer no longer loses precision above 2^53. Protocol version 14 is unreleased. The shared verifier body is selected by released versions too, but every arm touched here is a token pool transition kind, and validate_is_allowed refuses all of them below TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION, so no released version reaches them. Red before green, staged so each defect failed on its own: # a grouped MintToPool reported as a bare pool balance assertion failed: `VerifiedTokenShieldedPoolBalance(Identifier(...), 10000)` does not match `StateTransitionProofResult::VerifiedTokenGroupActionWithShieldedPoolBalance(power, status, balance,)` test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured # the classifier denied the guarantee the group-action proof gives the signer's power entry sits under the action id derived from this transition, so the proof binds this proposal's execution test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured # an open BurnFromPool proposal's proof rejected outright Proof(IncorrectProof("proof did not show every nullifier of the token burn from pool as spent")) test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured # after the fix test ... test_a_pool_group_action_proposal_proof_verifies_as_an_open_action ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 3688 filtered out The JS-safety fix reverted on its own, then restored: left: Number(9007199254740993) right: String("9007199254740993") test result: FAILED. 7 passed; 1 failed; 0 ignored; 0 measured test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured The drive-abci token suite passes 355, the rs-drive verify suite 274, and clippy is clean on dpp/drive/drive-abci with --all-features and on wasm-dpp2 for wasm32. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/state_transition/proof_result.rs | 89 ++++- .../batch/tests/token/shielded/mod.rs | 304 ++++++++++++++++++ .../v0/mod.rs | 177 +++++++--- .../state_transitions/proof_result/convert.rs | 30 +- .../state_transitions/proof_result/helpers.rs | 20 ++ .../proof_result/shielded.rs | 83 ++++- .../state_transitions/proof_result/token.rs | 38 +++ 7 files changed, 699 insertions(+), 42 deletions(-) diff --git a/packages/rs-dpp/src/state_transition/proof_result.rs b/packages/rs-dpp/src/state_transition/proof_result.rs index 4517ff67ff5..2c7d6373285 100644 --- a/packages/rs-dpp/src/state_transition/proof_result.rs +++ b/packages/rs-dpp/src/state_transition/proof_result.rs @@ -106,7 +106,14 @@ pub enum StateTransitionProofResult { VerifiedShieldedNullifiers(Vec<(Vec, bool)>), /// The proven total balance of a token's shielded pool (token id, balance). Returned by the /// pool transitions that only create notes (mint, claim and purchase into the pool). - VerifiedTokenShieldedPoolBalance(Identifier, TokenAmount), + VerifiedTokenShieldedPoolBalance( + Identifier, + #[cfg_attr( + feature = "json-conversion", + serde(with = "crate::serialization::json_safe_u64") + )] + TokenAmount, + ), VerifiedShieldedNullifiersWithAddressInfos( Vec<(Vec, bool)>, #[cfg_attr( @@ -164,6 +171,30 @@ pub enum StateTransitionProofResult { /// and carries the transition's reason. A document id is produced at most once, so the /// record is of this document and of no other. VerifiedContractDocumentRemoval(Identifier, String, Identifier, ContractDocumentRemoval), + /// Returned by a `MintToPool` submitted as a group action: the signer's recorded power, the + /// action's status, and the token shielded pool's total balance. A group action writes into + /// the pool only once the last required signature arrives, so while the action is active the + /// balance is the one the pool already held, and is absent for a pool that has never held a + /// note. A closed action has minted, so its balance is present. + VerifiedTokenGroupActionWithShieldedPoolBalance( + GroupSumPower, + GroupActionStatus, + #[cfg_attr( + feature = "json-conversion", + serde(with = "crate::serialization::json_safe_option_u64") + )] + Option, + ), + /// Returned by a `BurnFromPool` submitted as a group action: the signer's recorded power, the + /// action's status, and the spend status of every nullifier the burn names + /// (`(nullifier_bytes, spent)`). A group action spends nothing until the last required + /// signature arrives, so while the action is active every nullifier reads unspent; a closed + /// action has spent them all. + VerifiedTokenGroupActionWithShieldedNullifiers( + GroupSumPower, + GroupActionStatus, + Vec<(Vec, bool)>, + ), } /// The guarantee a verified state-transition proof establishes. @@ -413,6 +444,62 @@ mod json_convertible_tests { assert_eq!(original, recovered); } + /// A token shielded pool's balance is a `u64` in a tuple variant that `#[json_safe_fields]` + /// cannot reach, so the JS-safe helper has to sit on the field itself. Past + /// `Number.MAX_SAFE_INTEGER` it must serialize as a JSON string, or a JS consumer reads a + /// silently rounded pool balance. + #[test] + fn verified_token_shielded_pool_balance_large_amount_serializes_as_string() { + use crate::serialization::JsonConvertible; + let original = StateTransitionProofResult::VerifiedTokenShieldedPoolBalance( + Identifier::new([0xcd; 32]), + 9_007_199_254_740_993, // 2^53 + 1 + ); + let json = original.to_json().expect("to_json"); + assert_eq!( + json["VerifiedTokenShieldedPoolBalance"][1], + json!("9007199254740993") + ); + let recovered = StateTransitionProofResult::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + /// The pool balance a group action's proof carries is an optional `u64`, equally out of the + /// macro's reach, and equally lossy in JS without the helper. + #[test] + fn verified_token_group_action_with_shielded_pool_balance_large_amount_serializes_as_string() { + use crate::serialization::JsonConvertible; + let original = StateTransitionProofResult::VerifiedTokenGroupActionWithShieldedPoolBalance( + 1, + GroupActionStatus::ActionActive, + Some(9_007_199_254_740_993), // 2^53 + 1 + ); + let json = original.to_json().expect("to_json"); + assert_eq!( + json["VerifiedTokenGroupActionWithShieldedPoolBalance"][2], + json!("9007199254740993") + ); + let recovered = StateTransitionProofResult::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + + /// A group action burning out of a pool reports each named nullifier's spend status. An + /// action still gathering signatures has spent nothing, and the round trip has to preserve + /// those `false` flags rather than dropping them — they are what tells a client the burn has + /// not run yet. + #[test] + fn verified_token_group_action_with_shielded_nullifiers_round_trips_unspent_flags() { + use crate::serialization::JsonConvertible; + let original = StateTransitionProofResult::VerifiedTokenGroupActionWithShieldedNullifiers( + 1, + GroupActionStatus::ActionActive, + vec![(vec![1u8, 2, 3], false), (vec![4u8, 5, 6], false)], + ); + let json = original.to_json().expect("to_json"); + let recovered = StateTransitionProofResult::from_json(json).expect("from_json"); + assert_eq!(original, recovered); + } + /// The outcome carries the owner's balance next to the result; past /// `Number.MAX_SAFE_INTEGER` it serializes as a string, and a proof made /// before protocol version 14 carries none. diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index cf0da6a63f7..83934fcd8ee 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -1814,15 +1814,20 @@ mod token_pool_mint_burn_claim_purchase_tests { use dpp::data_contract::associated_token::token_perpetual_distribution::TokenPerpetualDistribution; use dpp::data_contract::associated_token::token_pre_programmed_distribution::v0::TokenPreProgrammedDistributionV0; use dpp::data_contract::associated_token::token_pre_programmed_distribution::TokenPreProgrammedDistribution; + use dpp::data_contract::DataContract; + use dpp::group::group_action_status::GroupActionStatus; use dpp::identity::accessors::IdentityGettersV0; use dpp::shielded::{serialized_actions_digest, token_burn_from_pool_extra_sighash_data_v0}; + use dpp::state_transition::proof_result::StateTransitionProofResult; use dpp::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType; use dpp::state_transition::batch_transition::{ TokenBurnFromPoolTransition, TokenSetPriceForDirectPurchaseTransition, }; use dpp::state_transition::StateTransition; use dpp::tokens::token_pricing_schedule::TokenPricingSchedule; + use drive::drive::Drive; use platform_version::version::PlatformVersion; + use std::collections::BTreeMap; fn total_supply(platform: &TempPlatform, token_id: Identifier) -> u64 { platform @@ -2541,6 +2546,305 @@ mod token_pool_mint_burn_claim_purchase_tests { assert_tokens_conserved(&platform); } + /// Proving a pool group action's proposal. A group action writes nothing into the pool until + /// the last required signature arrives: the operation converters push the pool write only + /// when the action closes, while the signer's power entry is recorded on every signature. A + /// proposal's proof therefore shows an untouched pool — no notes created, no nullifiers + /// spent — and the verifier has to read it as an open action rather than as a burn or mint + /// that failed to happen. + #[tokio::test] + async fn test_a_pool_group_action_proposal_proof_verifies_as_an_open_action() { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(9107); + + let (proposer, proposer_signer, proposer_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (confirmer, confirmer_signer, confirmer_key) = + setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + proposer.id(), + Some(|token_configuration: &mut TokenConfiguration| { + enable_shielded_pool(token_configuration); + let group_rules = |position| { + ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::Group(position), + admin_action_takers: AuthorizedActionTakers::NoOne, + changing_authorized_action_takers_to_no_one_allowed: false, + changing_admin_action_takers_to_no_one_allowed: false, + self_changing_admin_action_takers_allowed: false, + }) + }; + // Burning and minting are put under groups of their own so that the burn can + // close while the mint proposal is still open: a proof's action status is read + // from the group's active and closed action trees, so one group holding an open + // action and a closed one at the same time cannot be resolved to either. + token_configuration.set_manual_burning_rules(group_rules(0)); + token_configuration.set_manual_minting_rules(group_rules(1)); + }), + None, + Some( + [ + ( + 0, + Group::V0(GroupV0 { + members: [(proposer.id(), 1), (confirmer.id(), 1)].into(), + required_power: 2, + }), + ), + ( + 1, + Group::V0(GroupV0 { + members: [(proposer.id(), 1), (confirmer.id(), 1)].into(), + required_power: 2, + }), + ), + ] + .into(), + ), + None, + platform_version, + ); + let token = token_id.to_buffer(); + let known_contracts: BTreeMap = + BTreeMap::from([(contract.id(), contract.clone())]); + let verify = |transition: &StateTransition| { + let proof = platform + .drive + .prove_state_transition(transition, None, platform_version) + .expect("prove the pool group transition") + .into_data() + .expect("proof bytes rather than an error"); + Drive::verify_state_transition_was_executed_with_proof( + transition, + &BlockInfo::default(), + &proof, + &|id| Ok(known_contracts.get(id).cloned().map(std::sync::Arc::new)), + platform_version, + ) + }; + + // Fund the pool so the burn below has a note to spend. + let shield = BatchTransition::new_token_shield_transition( + token_id, + proposer.id(), + contract.id(), + 0, + 10_000, + build_shield_bundle( + 10_000, + 31, + TokenTransitionActionType::Shield, + token_id, + proposer.id(), + ), + &proposer_key, + 2, + 0, + &proposer_signer, + platform_version, + None, + ) + .await + .expect("token shield transition"); + assert_matches!( + process(&platform, &shield).execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + // --- A mint into the pool, proposed by one of two required signers. + let mint_amount = 2_000; + let mint_proposal = BatchTransition::new_token_mint_to_pool_transition( + token_id, + proposer.id(), + contract.id(), + 0, + mint_amount, + build_shield_bundle( + mint_amount, + 34, + TokenTransitionActionType::MintToPool, + token_id, + proposer.id(), + ), + None, + Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(1)), + &proposer_key, + 3, + 0, + &proposer_signer, + platform_version, + None, + ) + .await + .expect("token mint to pool proposal"); + assert_matches!( + process(&platform, &mint_proposal) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + // One signature of two: the pool holds only what the shield put there. + assert_eq!(pool_balance(&platform, token_id), 10_000); + + let (_root_hash, outcome) = verify(&mint_proposal).expect( + "an open mint-to-pool proposal's proof must verify: nothing was minted yet, so the \ + proof can only show the group action and the pool as it stands", + ); + assert_matches!( + outcome.result(), + StateTransitionProofResult::VerifiedTokenGroupActionWithShieldedPoolBalance( + power, + status, + balance, + ) => { + assert_eq!(*power, 1, "one of the two required signatures is recorded"); + assert_eq!(*status, GroupActionStatus::ActionActive); + assert_eq!( + *balance, + Some(10_000), + "the mint has not run, so the pool still holds only the shielded amount" + ); + }, + "an open mint-to-pool proposal must be reported as a group action over the pool" + ); + assert!( + outcome.is_execution_proved(), + "the signer's power entry sits under the action id derived from this transition, so \ + the proof binds this proposal's execution" + ); + + // --- A burn out of the pool, proposed by one of two required signers. + let (note, anchor, merkle_path) = spendable_note(6_000, 32); + insert_token_pool_anchor(&platform, token_id, &anchor); + let burn_amount = 4_000; + let extra = token_burn_from_pool_extra_sighash_data_v0( + &token, + &proposer.id().to_buffer(), + burn_amount, + ); + let (burn_bundle, _) = + build_spend_bundle(note, merkle_path, anchor, burn_amount, &extra, 33); + let proposer_nonce = 4; + let burn_proposal = BatchTransition::new_token_burn_from_pool_transition( + token_id, + proposer.id(), + contract.id(), + 0, + burn_amount, + burn_bundle.clone(), + None, + Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(0)), + &proposer_key, + proposer_nonce, + 0, + &proposer_signer, + platform_version, + None, + ) + .await + .expect("token burn from pool proposal"); + assert_matches!( + process(&platform, &burn_proposal) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert!(!nullifier_is_spent( + &platform, + token_id, + &burn_bundle.actions[0].nullifier + )); + + let (_root_hash, outcome) = verify(&burn_proposal).expect( + "an open burn-from-pool proposal's proof must verify: no nullifier is spent until \ + the action closes, so demanding every nullifier spent rejects a valid proposal", + ); + assert_matches!( + outcome.result(), + StateTransitionProofResult::VerifiedTokenGroupActionWithShieldedNullifiers( + power, + status, + statuses, + ) => { + assert_eq!(*power, 1, "one of the two required signatures is recorded"); + assert_eq!(*status, GroupActionStatus::ActionActive); + assert_eq!(statuses.len(), burn_bundle.actions.len()); + assert!( + statuses.iter().all(|(_, spent)| !*spent), + "an open action has spent nothing, got {statuses:?}" + ); + }, + "an open burn-from-pool proposal must be reported as a group action over the pool" + ); + + // --- The confirmation closes the action, and now the burn really has happened. + let action_id = TokenBurnFromPoolTransition::calculate_action_id_with_fields( + &token, + proposer.id().as_bytes(), + proposer_nonce, + burn_amount, + &serialized_actions_digest(&burn_bundle.actions), + ); + let burn_confirmation = BatchTransition::new_token_burn_from_pool_transition( + token_id, + confirmer.id(), + contract.id(), + 0, + burn_amount, + burn_bundle.clone(), + None, + Some( + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: 0, + action_id, + action_is_proposer: false, + }, + ), + ), + &confirmer_key, + 2, + 0, + &confirmer_signer, + platform_version, + None, + ) + .await + .expect("token burn from pool confirmation"); + assert_matches!( + process(&platform, &burn_confirmation) + .execution_results() + .as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert!(nullifier_is_spent( + &platform, + token_id, + &burn_bundle.actions[0].nullifier + )); + + let (_root_hash, outcome) = + verify(&burn_confirmation).expect("the closing signature's proof must verify"); + assert_matches!( + outcome.result(), + StateTransitionProofResult::VerifiedTokenGroupActionWithShieldedNullifiers( + power, + status, + statuses, + ) => { + assert_eq!(*power, 2, "both required signatures are recorded"); + assert_eq!(*status, GroupActionStatus::ActionClosed); + assert!( + statuses.iter().all(|(_, spent)| *spent), + "a closed burn has spent every nullifier it named, got {statuses:?}" + ); + }, + "the closing signature must still be reported as a group action over the pool" + ); + } + #[tokio::test] async fn test_token_claim_to_pool_pre_programmed() { assert_token_claim_to_pool(TokenDistributionType::PreProgrammed).await; diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs index 69bf020e952..96866e83cdf 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs @@ -68,7 +68,7 @@ use dpp::state_transition::identity_credit_withdrawal_transition::accessors::Ide use dpp::state_transition::identity_topup_transition::accessors::IdentityTopUpTransitionAccessorsV0; use dpp::state_transition::masternode_vote_transition::accessors::MasternodeVoteTransitionAccessorsV0; use dpp::state_transition::proof_result::StateTransitionProofOutcome; -use dpp::state_transition::proof_result::StateTransitionProofResult::{VerifiedAddressInfos, VerifiedContractDocumentRemoval, VerifiedContractFeeClaim, VerifiedContractModerationListStatuses, VerifiedBalanceTransfer, VerifiedDataContract, VerifiedDocuments, VerifiedIdentity, VerifiedIdentityFullWithAddressInfos, VerifiedIdentityWithAddressInfos, VerifiedMasternodeVote, VerifiedPartialIdentity, VerifiedTokenActionWithDocument, VerifiedTokenBalance, VerifiedTokenGroupActionWithDocument, VerifiedTokenGroupActionWithTokenBalance, VerifiedTokenGroupActionWithTokenIdentityInfo, VerifiedTokenGroupActionWithTokenPricingSchedule, VerifiedTokenIdentitiesBalances, VerifiedTokenIdentityInfo, VerifiedTokenPricingSchedule, VerifiedTokenShieldedPoolBalance, VerifiedShieldedNullifiers}; +use dpp::state_transition::proof_result::StateTransitionProofResult::{VerifiedAddressInfos, VerifiedContractDocumentRemoval, VerifiedContractFeeClaim, VerifiedContractModerationListStatuses, VerifiedBalanceTransfer, VerifiedDataContract, VerifiedDocuments, VerifiedIdentity, VerifiedIdentityFullWithAddressInfos, VerifiedIdentityWithAddressInfos, VerifiedMasternodeVote, VerifiedPartialIdentity, VerifiedTokenActionWithDocument, VerifiedTokenBalance, VerifiedTokenGroupActionWithDocument, VerifiedTokenGroupActionWithShieldedNullifiers, VerifiedTokenGroupActionWithShieldedPoolBalance, VerifiedTokenGroupActionWithTokenBalance, VerifiedTokenGroupActionWithTokenIdentityInfo, VerifiedTokenGroupActionWithTokenPricingSchedule, VerifiedTokenIdentitiesBalances, VerifiedTokenIdentityInfo, VerifiedTokenPricingSchedule, VerifiedTokenShieldedPoolBalance, VerifiedShieldedNullifiers}; use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; use dpp::tokens::info::v0::IdentityTokenInfoV0Accessors; use dpp::voting::vote_polls::VotePoll; @@ -1175,22 +1175,69 @@ impl Drive { TokenTransition::MintToPool(_) | TokenTransition::ClaimToPool(_) | TokenTransition::DirectPurchaseToPool(_) => { - let (root_hash, Some(balance)) = - Drive::verify_token_shielded_pool_state( - proof, - token_id.into_buffer(), - carries_owner_balance, - platform_version, - )? - else { - return Err(Error::Proof(ProofError::IncorrectProof( - "proof did not contain the token shielded pool balance expected to exist because of state transition (notes created in the pool)".to_string(), - ))); - }; - Ok(( - root_hash, - VerifiedTokenShieldedPoolBalance(token_id, balance), - )) + // Of these three only a mint into the pool can be proposed as a + // group action; a claim and a direct purchase into the pool + // derive no action id. The branch is on the group info all the + // same, because that is what the prover branches on when it + // decides whether to merge the group signer query into the proof. + if let Some(group_state_transition_info) = + token_transition.base().using_group_info() + { + let (_root_hash, status, sum_power) = + Drive::verify_action_signer_and_total_power( + proof, + data_contract_id, + group_state_transition_info.group_contract_position, + None, + group_state_transition_info.action_id, + owner_id, + true, + platform_version, + )?; + + let (root_hash, balance) = + Drive::verify_token_shielded_pool_state( + proof, + token_id.into_buffer(), + true, + platform_version, + )?; + // A group action creates no notes until the last required + // signature arrives, so only a closed action must show a + // pool. While it is active the balance is whatever the pool + // already held, and is absent for a pool never yet used. + if status == GroupActionStatus::ActionClosed + && balance.is_none() + { + return Err(Error::Proof(ProofError::IncorrectProof( + "proof did not contain the token shielded pool balance expected to exist because the closed group action created notes in the pool".to_string(), + ))); + } + + Ok(( + root_hash, + VerifiedTokenGroupActionWithShieldedPoolBalance( + sum_power, status, balance, + ), + )) + } else { + let (root_hash, Some(balance)) = + Drive::verify_token_shielded_pool_state( + proof, + token_id.into_buffer(), + carries_owner_balance, + platform_version, + )? + else { + return Err(Error::Proof(ProofError::IncorrectProof( + "proof did not contain the token shielded pool balance expected to exist because of state transition (notes created in the pool)".to_string(), + ))); + }; + Ok(( + root_hash, + VerifiedTokenShieldedPoolBalance(token_id, balance), + )) + } } TokenTransition::BurnFromPool(token_burn_from_pool_transition) => { let nullifiers: Vec> = token_burn_from_pool_transition @@ -1198,22 +1245,70 @@ impl Drive { .iter() .map(|action| action.nullifier.to_vec()) .collect(); - let (root_hash, statuses) = - Drive::verify_token_shielded_pool_nullifiers( - proof, - token_id.into_buffer(), - &nullifiers, - carries_owner_balance, - platform_version, - )?; - if statuses.len() != nullifiers.len() - || statuses.iter().any(|(_, spent)| !spent) + if let Some(group_state_transition_info) = + token_transition.base().using_group_info() { - return Err(Error::Proof(ProofError::IncorrectProof( - "proof did not show every nullifier of the token burn from pool as spent".to_string(), - ))); + let (_root_hash, status, sum_power) = + Drive::verify_action_signer_and_total_power( + proof, + data_contract_id, + group_state_transition_info.group_contract_position, + None, + group_state_transition_info.action_id, + owner_id, + true, + platform_version, + )?; + + let (root_hash, statuses) = + Drive::verify_token_shielded_pool_nullifiers( + proof, + token_id.into_buffer(), + &nullifiers, + true, + platform_version, + )?; + if statuses.len() != nullifiers.len() { + return Err(Error::Proof(ProofError::IncorrectProof( + "proof did not show the spend status of every nullifier of the token burn from pool".to_string(), + ))); + } + // A group action spends nothing until the last required + // signature arrives, so only a closed action must show every + // nullifier spent. While it is active they all read unspent, + // and that is what tells a client the burn has not run. + if status == GroupActionStatus::ActionClosed + && statuses.iter().any(|(_, spent)| !spent) + { + return Err(Error::Proof(ProofError::IncorrectProof( + "proof did not show every nullifier of the closed group action burning from the pool as spent".to_string(), + ))); + } + + Ok(( + root_hash, + VerifiedTokenGroupActionWithShieldedNullifiers( + sum_power, status, statuses, + ), + )) + } else { + let (root_hash, statuses) = + Drive::verify_token_shielded_pool_nullifiers( + proof, + token_id.into_buffer(), + &nullifiers, + carries_owner_balance, + platform_version, + )?; + if statuses.len() != nullifiers.len() + || statuses.iter().any(|(_, spent)| !spent) + { + return Err(Error::Proof(ProofError::IncorrectProof( + "proof did not show every nullifier of the token burn from pool as spent".to_string(), + ))); + } + Ok((root_hash, VerifiedShieldedNullifiers(statuses))) } - Ok((root_hash, VerifiedShieldedNullifiers(statuses))) } } } @@ -2988,17 +3083,23 @@ impl Drive { | TokenTransition::EmergencyAction(_) | TokenTransition::ConfigUpdate(_) | TokenTransition::Claim(_) => true, - // Balance snapshots: the moved amount cannot be tied to one shield or - // unshield. A shielded transfer's spent nullifiers exist only if it - // executed. + // Balance snapshots: the moved amount cannot be tied to one shield, one + // unshield, or one creation of notes in a pool. TokenTransition::Shield(_) | TokenTransition::Unshield(_) - | TokenTransition::MintToPool(_) | TokenTransition::ClaimToPool(_) | TokenTransition::DirectPurchaseToPool(_) => false, - TokenTransition::ShieldedTransfer(_) | TokenTransition::BurnFromPool(_) => { - true - } + // A mint into the pool leaves only the pool's new total behind, which no + // single transition owns. Proposed as a group action it is bound the way + // every other grouped kind is: by the signer's recorded entry under the + // action id this transition derives. + TokenTransition::MintToPool(_) => grouped, + // A shielded transfer's spent nullifiers exist only if it executed. + TokenTransition::ShieldedTransfer(_) => true, + // Ungrouped, the pool nullifiers this burn spends exist only if it + // executed. Grouped, the signer's recorded entry binds it whether or not + // the action has gathered enough power to close. + TokenTransition::BurnFromPool(_) => true, } } }, diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result/convert.rs b/packages/wasm-dpp2/src/state_transitions/proof_result/convert.rs index 7e3ccb30e35..4666fa5eaaa 100644 --- a/packages/wasm-dpp2/src/state_transitions/proof_result/convert.rs +++ b/packages/wasm-dpp2/src/state_transitions/proof_result/convert.rs @@ -22,10 +22,12 @@ use super::shielded::{ VerifiedIdentityWithShieldedNullifiersWasm, VerifiedShieldedNullifiersWasm, VerifiedShieldedNullifiersWithAddressInfosWasm, VerifiedShieldedNullifiersWithWithdrawalDocumentWasm, + VerifiedTokenGroupActionWithShieldedNullifiersWasm, }; use super::token::{ VerifiedTokenActionWithDocumentWasm, VerifiedTokenBalanceAbsenceWasm, VerifiedTokenBalanceWasm, - VerifiedTokenGroupActionWithDocumentWasm, VerifiedTokenGroupActionWithTokenBalanceWasm, + VerifiedTokenGroupActionWithDocumentWasm, VerifiedTokenGroupActionWithShieldedPoolBalanceWasm, + VerifiedTokenGroupActionWithTokenBalanceWasm, VerifiedTokenGroupActionWithTokenIdentityInfoWasm, VerifiedTokenGroupActionWithTokenPricingScheduleWasm, VerifiedTokenIdentitiesBalancesWasm, VerifiedTokenIdentityInfoWasm, VerifiedTokenPricingScheduleWasm, @@ -78,7 +80,9 @@ export type StateTransitionProofResultType = | VerifiedIdentityWithShieldedNullifiers | VerifiedContractModerationListStatuses | VerifiedContractFeeClaim - | VerifiedContractDocumentRemoval; + | VerifiedContractDocumentRemoval + | VerifiedTokenGroupActionWithShieldedPoolBalance + | VerifiedTokenGroupActionWithShieldedNullifiers; "#; #[wasm_bindgen] @@ -300,6 +304,28 @@ pub fn convert_proof_result( .into() } + StateTransitionProofResult::VerifiedTokenGroupActionWithShieldedPoolBalance( + power, + status, + maybe_balance, + ) => VerifiedTokenGroupActionWithShieldedPoolBalanceWasm { + group_power: power, + action_status: action_status_to_string(status), + balance: maybe_balance, + } + .into(), + + StateTransitionProofResult::VerifiedTokenGroupActionWithShieldedNullifiers( + power, + status, + nullifiers, + ) => VerifiedTokenGroupActionWithShieldedNullifiersWasm::new( + power, + action_status_to_string(status), + build_nullifier_map(nullifiers), + ) + .into(), + StateTransitionProofResult::VerifiedShieldedNullifiersWithAddressInfos( nullifiers, infos, diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result/helpers.rs b/packages/wasm-dpp2/src/state_transitions/proof_result/helpers.rs index ffdbd9e08e2..b8a1efe8c0a 100644 --- a/packages/wasm-dpp2/src/state_transitions/proof_result/helpers.rs +++ b/packages/wasm-dpp2/src/state_transitions/proof_result/helpers.rs @@ -22,6 +22,26 @@ pub(super) fn js_obj(entries: &[(&str, JsValue)]) -> JsValue { obj.into() } +/// Read a numeric property from an ingested JS value, as the `u32` a group power is. +pub(super) fn read_u32_property(value: &JsValue, name: &str) -> WasmDppResult { + js_sys::Reflect::get(value, &name.into()) + .ok() + .and_then(|raw| raw.as_f64()) + .and_then(|number| { + (number.fract() == 0.0 && (0.0..=f64::from(u32::MAX)).contains(&number)) + .then_some(number as u32) + }) + .ok_or_else(|| WasmDppError::generic(format!("Property {} must be a u32 number", name))) +} + +/// Read a string property from an ingested JS value. +pub(super) fn read_string_property(value: &JsValue, name: &str) -> WasmDppResult { + js_sys::Reflect::get(value, &name.into()) + .ok() + .and_then(|raw| raw.as_string()) + .ok_or_else(|| WasmDppError::generic(format!("Property {} must be a string", name))) +} + /// Read a `Map`-shaped property from an ingested JS value. /// /// `toJSON` normalizes a `Map` to a plain object so it survives diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result/shielded.rs b/packages/wasm-dpp2/src/state_transitions/proof_result/shielded.rs index dc12325413b..cee15b184bc 100644 --- a/packages/wasm-dpp2/src/state_transitions/proof_result/shielded.rs +++ b/packages/wasm-dpp2/src/state_transitions/proof_result/shielded.rs @@ -3,7 +3,7 @@ //! These types were extracted from `proof_result` to keep shielded-specific //! code in its own module. -use super::helpers::{js_obj, read_map_property}; +use super::helpers::{js_obj, read_map_property, read_string_property, read_u32_property}; use crate::IdentityWasm; use crate::error::{WasmDppError, WasmDppResult}; use crate::impl_wasm_conversions_serde; @@ -339,6 +339,87 @@ impl_wasm_type_info!( VerifiedShieldedNullifiersWithAddressInfos ); +// --- VerifiedTokenGroupActionWithShieldedNullifiers --- + +/// A burn out of a token's shielded pool submitted as a group action: the signer's recorded +/// power, the action's status, and the spend status of every nullifier the burn names. The burn +/// runs only once the last required signature arrives, so while the action is active every +/// nullifier reads unspent. +#[wasm_bindgen(js_name = "VerifiedTokenGroupActionWithShieldedNullifiers")] +#[derive(Clone)] +pub struct VerifiedTokenGroupActionWithShieldedNullifiersWasm { + group_power: u32, + action_status: String, + nullifiers: Map, // Map +} + +#[wasm_bindgen(js_class = VerifiedTokenGroupActionWithShieldedNullifiers)] +impl VerifiedTokenGroupActionWithShieldedNullifiersWasm { + #[wasm_bindgen(getter = "groupPower")] + pub fn group_power(&self) -> u32 { + self.group_power + } + + #[wasm_bindgen(getter = "actionStatus")] + pub fn action_status(&self) -> String { + self.action_status.clone() + } + + #[wasm_bindgen(getter)] + pub fn nullifiers(&self) -> Map { + self.nullifiers.clone() + } + + #[wasm_bindgen(js_name = toObject)] + pub fn to_object(&self) -> JsValue { + js_obj(&[ + ("groupPower", JsValue::from(self.group_power)), + ("actionStatus", JsValue::from(self.action_status.clone())), + ("nullifiers", self.nullifiers.clone().into()), + ]) + } + + /// Returns a `JSON.stringify`-friendly form: the `Map` is normalised to a + /// plain object so its entries survive serialisation. + #[wasm_bindgen(js_name = toJSON)] + pub fn to_json(&self) -> WasmDppResult { + normalize_js_value_for_json(&self.to_object()) + } + + #[wasm_bindgen(js_name = fromObject)] + pub fn from_object( + value: JsValue, + ) -> WasmDppResult { + Ok(VerifiedTokenGroupActionWithShieldedNullifiersWasm { + group_power: read_u32_property(&value, "groupPower")?, + action_status: read_string_property(&value, "actionStatus")?, + nullifiers: read_map_property(&value, "nullifiers")?, + }) + } + + #[wasm_bindgen(js_name = fromJSON)] + pub fn from_json( + value: JsValue, + ) -> WasmDppResult { + Self::from_object(value) + } +} + +impl VerifiedTokenGroupActionWithShieldedNullifiersWasm { + pub fn new(group_power: u32, action_status: String, nullifiers: Map) -> Self { + Self { + group_power, + action_status, + nullifiers, + } + } +} + +impl_wasm_type_info!( + VerifiedTokenGroupActionWithShieldedNullifiersWasm, + VerifiedTokenGroupActionWithShieldedNullifiers +); + // --- VerifiedShieldedNullifiersWithWithdrawalDocument --- #[wasm_bindgen(js_name = "VerifiedShieldedNullifiersWithWithdrawalDocument")] diff --git a/packages/wasm-dpp2/src/state_transitions/proof_result/token.rs b/packages/wasm-dpp2/src/state_transitions/proof_result/token.rs index 6b5498a22c0..591d1fd947d 100644 --- a/packages/wasm-dpp2/src/state_transitions/proof_result/token.rs +++ b/packages/wasm-dpp2/src/state_transitions/proof_result/token.rs @@ -81,6 +81,44 @@ impl_wasm_conversions_serde!( VerifiedTokenShieldedPoolBalance ); +// --- VerifiedTokenGroupActionWithShieldedPoolBalance --- + +/// A mint into a token's shielded pool submitted as a group action: the signer's recorded power, +/// the action's status, and the pool's total balance. The mint runs only once the last required +/// signature arrives, so an active action's balance is the one the pool already held, and is +/// absent for a pool that has never held a note. +#[dpp_json_convertible_derive::json_safe_fields(crate = "dpp")] +#[wasm_bindgen(js_name = "VerifiedTokenGroupActionWithShieldedPoolBalance")] +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VerifiedTokenGroupActionWithShieldedPoolBalanceWasm { + #[wasm_bindgen(getter_with_clone, js_name = "groupPower")] + pub group_power: u32, + #[wasm_bindgen(getter_with_clone, js_name = "actionStatus")] + pub action_status: String, + pub(super) balance: Option, +} + +#[wasm_bindgen(js_class = VerifiedTokenGroupActionWithShieldedPoolBalance)] +impl VerifiedTokenGroupActionWithShieldedPoolBalanceWasm { + #[wasm_bindgen(getter)] + pub fn balance(&self) -> JsValue { + match self.balance { + Some(b) => BigInt::from(b).into(), + None => JsValue::undefined(), + } + } +} + +impl_wasm_type_info!( + VerifiedTokenGroupActionWithShieldedPoolBalanceWasm, + VerifiedTokenGroupActionWithShieldedPoolBalance +); +impl_wasm_conversions_serde!( + VerifiedTokenGroupActionWithShieldedPoolBalanceWasm, + VerifiedTokenGroupActionWithShieldedPoolBalance +); + // --- VerifiedTokenIdentityInfo --- #[wasm_bindgen(js_name = "VerifiedTokenIdentityInfo")] From d9f638fb90338e449987849d332d1dbb222ef4ff Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 15:33:17 +0700 Subject: [PATCH 61/94] fix(dpp): name a batch's shielded pool nullifiers among its mempool identifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A batch's `unique_identifiers` listed one owner-contract-nonce key per transition and nothing about the shielded pool notes the batch spends. Two batches that spend the same pool note could therefore sit in the mempool at once with nothing in common: whichever lands records the nullifier and the other is refused on it, so one of the two was always a block slot spent on a transition the node already had the means to know could not execute. Their nonces need not be alike, and nothing else about them has to be. The credit pool's shielded transitions have always identified themselves by their spent nullifiers alone, hex-encoded. A batch now appends the same values in the same encoding: one per Orchard action of each token pool transition it carries, and of each document whose token cost is paid out of a pool, which spends pool notes exactly as an unshield does. Worth knowing about the fix's reach: only `unique_identifiers().first()` is handed to Tenderdash, as the `sender` of the CheckTx response, and the existing note above that call says multi-sender support is what the rest waits on. The nullifier keys are appended behind the nonce keys for that reason — the transmitted key must stay the one that stops a nonce being replayed — so this completes the set the method is documented to return without changing which key travels today. Not a consensus change: `unique_identifiers` is read only by CheckTx, which hands it to the mempool. It is not hashed, not signed, not serialized and not stored, so `BatchTransitionV1` is edited in place although released protocol versions also select it. For a batch that touches no pool, the returned list is byte-identical to before. Test would have caught this in CI, red before the fix and green after: v1_unique_identifiers_collide_on_a_pool_nullifier_two_batches_both_spend ... FAILED v1_unique_identifiers_collide_on_a_document_shielded_payment_nullifier ... FAILED v1_unique_identifiers_keep_the_nonce_keys_in_front_of_the_nullifier_keys ... FAILED two batches spending one pool note must share an identifier, got ["AgIC...=-u7u7...=-7"] and ["AgIC...=-u7u7...=-8"] test result: FAILED. 2 passed; 3 failed v1_unique_identifiers_collide_on_a_pool_nullifier_two_batches_both_spend ... ok v1_unique_identifiers_collide_on_a_document_shielded_payment_nullifier ... ok v1_unique_identifiers_keep_the_nonce_keys_in_front_of_the_nullifier_keys ... ok v1_unique_identifiers_are_unchanged_for_a_batch_without_a_pool_bundle ... ok test result: ok. 5 passed; 0 failed cargo test -p dpp --all-features --lib test result: ok. 5042 passed; 0 failed; 6 ignored Co-Authored-By: Claude Opus 5 (1M context) --- .../document/batch_transition/tests.rs | 133 ++++++++++++++++++ .../v1/state_transition_like.rs | 39 ++++- 2 files changed, 170 insertions(+), 2 deletions(-) diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/tests.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/tests.rs index c8011b7ec59..b77b83aabd7 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/tests.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/tests.rs @@ -17,6 +17,8 @@ mod batch_transition_tests { use crate::state_transition::batch_transition::batched_transition::document_transition_action_type::{ DocumentTransitionActionType, DocumentTransitionActionTypeGetter, }; + use crate::shielded::SerializedAction; + use crate::state_transition::batch_transition::batched_transition::document_base_transition::v1::DocumentBaseTransitionV1; use crate::state_transition::batch_transition::batched_transition::token_base_transition::v0::TokenBaseTransitionV0; use crate::state_transition::batch_transition::batched_transition::token_base_transition::TokenBaseTransition; use crate::state_transition::batch_transition::batched_transition::token_burn_transition::v0::TokenBurnTransitionV0; @@ -24,6 +26,11 @@ mod batch_transition_tests { use crate::state_transition::batch_transition::batched_transition::token_claim_transition::v0::TokenClaimTransitionV0; use crate::state_transition::batch_transition::batched_transition::token_claim_transition::TokenClaimTransition; use crate::state_transition::batch_transition::batched_transition::token_transfer_transition::v0::TokenTransferTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_unshield_transition::v0::TokenUnshieldTransitionV0; + use crate::state_transition::batch_transition::batched_transition::token_unshield_transition::TokenUnshieldTransition; + use crate::tokens::token_payment_info::v0::TokenPaymentInfoV0; + use crate::tokens::token_payment_info::v1::{TokenPaymentInfoV1, TokenShieldedPayment}; + use crate::tokens::token_payment_info::TokenPaymentInfo; use crate::state_transition::batch_transition::batched_transition::token_transfer_transition::TokenTransferTransition; use crate::state_transition::batch_transition::batched_transition::token_transition::TokenTransition; use crate::state_transition::batch_transition::batched_transition::token_transition_action_type::{ @@ -118,6 +125,50 @@ mod batch_transition_tests { })) } + /// One Orchard action spending the note whose nullifier is `nullifier`. + fn make_pool_action(nullifier: [u8; 32]) -> SerializedAction { + SerializedAction { + nullifier, + rk: [0x11; 32], + cmx: [0x22; 32], + encrypted_note: vec![0x33; 216], + cv_net: [0x44; 32], + spend_auth_sig: [0x55; 64], + } + } + + /// An unshield out of the token's pool, spending the note `nullifier` names. + fn make_token_unshield_transition(nonce: u64, nullifier: [u8; 32]) -> TokenTransition { + TokenTransition::Unshield(TokenUnshieldTransition::V0(TokenUnshieldTransitionV0 { + base: make_token_base(nonce), + amount: 1_000, + recipient_id: Identifier::new([0xEE; 32]), + actions: vec![make_pool_action(nullifier)], + ..Default::default() + })) + } + + /// A document delete whose token cost is paid out of the token's shielded pool, spending the + /// note `nullifier` names. + fn make_document_with_shielded_payment(nonce: u64, nullifier: [u8; 32]) -> DocumentTransition { + DocumentTransition::Delete(DocumentDeleteTransition::V0(DocumentDeleteTransitionV0 { + base: DocumentBaseTransition::V1(DocumentBaseTransitionV1 { + id: Identifier::new([nonce as u8; 32]), + identity_contract_nonce: nonce, + document_type_name: "test_doc".to_string(), + data_contract_id: Identifier::new([0xAA; 32]), + token_payment_info: Some(TokenPaymentInfo::V1(TokenPaymentInfoV1::from_v0( + TokenPaymentInfoV0::default(), + TokenShieldedPayment { + amount: 5, + actions: vec![make_pool_action(nullifier)], + ..Default::default() + }, + ))), + }), + })) + } + fn make_token_claim_transition(nonce: u64) -> TokenTransition { TokenTransition::Claim(TokenClaimTransition::V0(TokenClaimTransitionV0 { base: make_token_base(nonce), @@ -577,6 +628,88 @@ mod batch_transition_tests { } } + /// Two batches that spend the same shielded pool note can never both execute: whichever + /// lands first records the nullifier and the other is refused on it. Nothing else about them + /// need be alike — here they carry different nonces — so the nullifier is the only thing + /// their identifier sets can collide on, and collide they must, or the mempool holds a batch + /// it already knows cannot execute. + #[test] + fn v1_unique_identifiers_collide_on_a_pool_nullifier_two_batches_both_spend() { + let nullifier = [0x5a; 32]; + let first = make_batch_v1(vec![BatchedTransition::Token( + make_token_unshield_transition(7, nullifier), + )]); + let second = make_batch_v1(vec![BatchedTransition::Token( + make_token_unshield_transition(8, nullifier), + )]); + + let first_ids = first.unique_identifiers(); + let second_ids = second.unique_identifiers(); + assert!( + first_ids.iter().any(|id| second_ids.contains(id)), + "two batches spending one pool note must share an identifier, got {first_ids:?} \ + and {second_ids:?}" + ); + } + + /// A document's token cost can be paid out of the token's shielded pool, which spends pool + /// notes exactly as an unshield does, so the same collision has to hold for it. + #[test] + fn v1_unique_identifiers_collide_on_a_document_shielded_payment_nullifier() { + let nullifier = [0x6b; 32]; + let first = make_batch_v1(vec![BatchedTransition::Document( + make_document_with_shielded_payment(7, nullifier), + )]); + let second = make_batch_v1(vec![BatchedTransition::Document( + make_document_with_shielded_payment(8, nullifier), + )]); + + let first_ids = first.unique_identifiers(); + let second_ids = second.unique_identifiers(); + assert!( + first_ids.iter().any(|id| second_ids.contains(id)), + "two batches spending one pool note must share an identifier, got {first_ids:?} \ + and {second_ids:?}" + ); + } + + /// Only the first identifier reaches the mempool today, so the per-transition owner, + /// contract and nonce keys have to stay at the front of the list: they are what stops a + /// second batch replaying one nonce, and that must not be traded away for the nullifier + /// keys, which are appended behind them. + #[test] + fn v1_unique_identifiers_keep_the_nonce_keys_in_front_of_the_nullifier_keys() { + let nullifier = [0x7c; 32]; + let batch = make_batch_v1(vec![ + BatchedTransition::Token(make_token_unshield_transition(7, nullifier)), + BatchedTransition::Token(make_token_burn_transition(8, 100)), + ]); + + let ids = batch.unique_identifiers(); + assert_eq!( + ids.len(), + 3, + "one key per transition plus one per pool nullifier, got {ids:?}" + ); + assert!( + ids[0].contains('-') && ids[1].contains('-'), + "the nonce keys come first, got {ids:?}" + ); + assert_eq!(ids[2], hex::encode(nullifier)); + } + + /// A batch that touches no pool keeps the identifiers it had: its mempool key must not move + /// because pool kinds exist. + #[test] + fn v1_unique_identifiers_are_unchanged_for_a_batch_without_a_pool_bundle() { + let batch = make_batch_v1(vec![ + BatchedTransition::Document(make_delete_transition(1)), + BatchedTransition::Token(make_token_burn_transition(2, 100)), + ]); + let ids = batch.unique_identifiers(); + assert_eq!(ids.len(), 2, "no nullifier keys to add, got {ids:?}"); + } + // ----------------------------------------------------------------------- // BatchTransitionV1: StateTransitionIdentitySigned (purpose_requirement) // ----------------------------------------------------------------------- diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/state_transition_like.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/state_transition_like.rs index 35fcbcf681c..8c079aaf323 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/state_transition_like.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/state_transition_like.rs @@ -11,6 +11,8 @@ use platform_value::{BinaryData, Identifier}; use crate::state_transition::batch_transition::batched_transition::BatchedTransition; use crate::state_transition::batch_transition::batched_transition::token_transition::TokenTransitionV0Methods; use crate::state_transition::batch_transition::document_base_transition::v0::v0_methods::DocumentBaseTransitionV0Methods; +use crate::state_transition::batch_transition::document_base_transition::v1::v1_methods::DocumentBaseTransitionV1Methods; +use crate::tokens::token_payment_info::v1::v1_accessors::TokenPaymentInfoAccessorsV1; impl From for StateTransition { fn from(value: BatchTransitionV1) -> Self { @@ -42,8 +44,19 @@ impl StateTransitionLike for BatchTransitionV1 { } /// We create a list of unique identifiers for the batch + /// + /// Each transition contributes the owner, contract and nonce that replay-protect it. A batch + /// that spends notes from a token's shielded pool contributes each spent nullifier as well: + /// two batches naming one nullifier can never both execute, whichever of them lands records + /// it and the other is refused on it, and their nonces need not be alike, so without the + /// nullifier nothing about them collides. The credit pool's shielded transitions identify + /// themselves by their nullifiers alone; these are the same values in the same encoding. + /// + /// The nonce keys stay in front. Only the first identifier reaches the mempool today, and + /// that one has to remain the key that stops a second batch replaying a nonce. fn unique_identifiers(&self) -> Vec { - self.transitions + let mut identifiers: Vec = self + .transitions .iter() .map(|transition| match transition { BatchedTransition::Document(document_transition) => { @@ -63,7 +76,29 @@ impl StateTransitionLike for BatchTransitionV1 { ) } }) - .collect() + .collect(); + + identifiers.extend(self.transitions.iter().flat_map(|transition| { + let actions = match transition { + BatchedTransition::Token(token_transition) => { + token_transition.shielded_pool_actions() + } + // A document whose token cost is paid out of the token's shielded pool spends + // pool notes exactly as an unshield does. + BatchedTransition::Document(document_transition) => document_transition + .base() + .token_payment_info_ref() + .as_ref() + .and_then(|info| info.shielded_payment()) + .map(|payment| payment.actions.as_slice()), + }; + actions + .unwrap_or_default() + .iter() + .map(|action| hex::encode(action.nullifier)) + })); + + identifiers } } From 6951d7c510fd84e115252549eada39b29c86c7da Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 15:34:41 +0700 Subject: [PATCH 62/94] test(drive-abci): name the credit pool in the encrypted notes tests The chunk-aligned range read arrived from v4.2-dev with tests written against the query before it could select a pool: they call the credit pool's path helper, which the non-test code no longer uses, and build a request without the field that names a pool. Both are test-only and invisible to `cargo check`, which does not compile tests. Co-Authored-By: Claude Opus 5 (1M context) --- .../rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs index c9bb1890986..9bcf735a9cb 100644 --- a/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs @@ -172,6 +172,7 @@ impl Platform { #[cfg(test)] mod tests { use super::*; + use drive::drive::shielded::paths::shielded_credit_pool_path; use crate::query::tests::setup_platform; use crate::rpc::core::MockCoreRPCLike; use crate::test::helpers::setup::TempPlatform; @@ -526,6 +527,7 @@ mod tests { start_index, count, prove: false, + token_id: None, }, &state, version, From 9b4eaf0fcb90b9b7630c18b8fb98cacef361ccc7 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 15:40:20 +0700 Subject: [PATCH 63/94] fix(drive): meter a token pool's total-balance read when estimating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `token_shielded_pool_update_operations` read the pool's total balance only when applying. Fee validation builds the very same operations with `apply = false` to quote the batch, so the quote booked nothing at all for a read execution then performs and meters — 14180 credits per operation that moves the total, which is every token shield, unshield, mint into the pool and burn from it, and every document paid from a pool. The estimate now books the read as well, through the stateless query Drive prices every other estimated read by: it costs the read against the pool's modelled layer instead of reading the value stored there. Pricing it from the model and not from the value is also what keeps the fee independent of how much of the token is shielded, so the quote cannot be read back as the pool total. A shielded transfer leaves the total alone and still reads nothing in either mode, for the same reason. `read_pool_total_balance_v0` takes the query type as a parameter now and the credit pool passes the stateful query the function used to hardcode, so every protocol version that selects this generation sees the same operation against the same tree. The token pool path is reachable only from protocol version 14, whose `CONTRACT_VERSIONS_V6` is what admits the `TokenConfigurationV1` carrying `has_shielded_pool`; it is unreleased and edited in place. Residual, not addressed here: GroveDB's stateless model is an average case (2 seeks / 173 bytes / 0 hash calls) and the stateful read costs more (3 / 369 / 2), so the quote still books 7460 where execution meters 14180. That gap belongs to every stateless read in Drive — the crate has no worst-case caller anywhere — and closing it on this surface alone would fork the convention. Test would have caught this in CI: before the fix ---- should_meter_the_pool_total_read_when_estimating_as_well_as_when_applying ---- Add(1) meters 1 read(s) worth 14180 credits when applied but only 0 worth 0 when estimated, so the difference is charged to nobody test result: FAILED. 0 passed; 1 failed after the fix test drive::tokens::shielded_pool::tests:: should_meter_the_pool_total_read_when_estimating_as_well_as_when_applying ... ok test result: ok. 12 passed; 0 failed Co-Authored-By: Claude Opus 5 (1M context) --- .../document_shielded_token_payment/mod.rs | 2 + .../state_v0/mod.rs | 2 + .../state_v0/mod.rs | 2 + .../batch/tests/token/shielded/mod.rs | 2 + .../token_pool_paid_common/mod.rs | 2 + .../src/drive/shielded/estimated_costs.rs | 5 + .../drive/shielded/read_total_balance/mod.rs | 8 + .../shielded/read_total_balance/v0/mod.rs | 22 ++- .../src/drive/tokens/shielded_pool/mod.rs | 159 +++++++++++++++--- 9 files changed, 177 insertions(+), 27 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_shielded_token_payment/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_shielded_token_payment/mod.rs index 1288d9a780f..eee940ea291 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_shielded_token_payment/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/document/document_shielded_token_payment/mod.rs @@ -9,6 +9,7 @@ //! unspent, the pool must hold the amount, and the spend bundle must verify with the token id, //! the batch owner, the document's contract and id and the amount bound into its sighash. +use drive::util::grove_operations::DirectQueryType; use crate::error::execution::ExecutionError; use crate::error::Error; use crate::execution::types::execution_operation::ValidationOperation; @@ -199,6 +200,7 @@ pub(in crate::execution::validation::state_transition::state_transitions::batch) let pool_balance = platform.drive.read_token_shielded_pool_total_balance( &token_id_bytes, + DirectQueryType::StatefulDirectQuery, transaction, &mut drive_operations, platform_version, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_burn_from_pool_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_burn_from_pool_transition_action/state_v0/mod.rs index 664eb256ce1..0598528092c 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_burn_from_pool_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_burn_from_pool_transition_action/state_v0/mod.rs @@ -1,3 +1,4 @@ +use drive::util::grove_operations::DirectQueryType; use crate::error::Error; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; use crate::execution::validation::state_transition::batch::action_validation::token::token_base_transition_action::TokenBaseTransitionActionValidation; @@ -208,6 +209,7 @@ impl TokenBurnFromPoolTransitionActionStateValidationV0 for TokenBurnFromPoolTra let pool_balance = platform.drive.read_token_shielded_pool_total_balance( &token_id_bytes, + DirectQueryType::StatefulDirectQuery, transaction, &mut drive_operations, platform_version, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/state_v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/state_v0/mod.rs index ca55fe222bb..fd4a85d56f1 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/state_v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/action_validation/token/token_unshield_transition_action/state_v0/mod.rs @@ -1,3 +1,4 @@ +use drive::util::grove_operations::DirectQueryType; use crate::error::Error; use crate::execution::types::state_transition_execution_context::StateTransitionExecutionContext; use crate::execution::validation::state_transition::batch::action_validation::token::token_base_transition_action::TokenBaseTransitionActionValidation; @@ -162,6 +163,7 @@ impl TokenUnshieldTransitionActionStateValidationV0 for TokenUnshieldTransitionA let pool_balance = platform.drive.read_token_shielded_pool_total_balance( &token_id_bytes, + DirectQueryType::StatefulDirectQuery, transaction, &mut drive_operations, platform_version, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index cf0da6a63f7..8aec97e68c2 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -1,4 +1,5 @@ use super::*; +use drive::util::grove_operations::DirectQueryType; mod minimum_pool_notes; @@ -230,6 +231,7 @@ mod token_shielded_pool_tests { .drive .read_token_shielded_pool_total_balance( &token_id.to_buffer(), + DirectQueryType::StatefulDirectQuery, None, &mut vec![], PlatformVersion::latest(), diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_pool_paid_common/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_pool_paid_common/mod.rs index 7d2e057f86c..5c7919eddea 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_pool_paid_common/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/token_pool_paid_common/mod.rs @@ -30,6 +30,7 @@ use drive::drive::contract::DataContractFetchInfo; use drive::drive::Drive; use drive::grovedb::TransactionArg; use drive::state_transition_action::StateTransitionAction; +use drive::util::grove_operations::DirectQueryType; use std::sync::Arc; /// A resolved pooled token: its contract and its configuration. @@ -220,6 +221,7 @@ pub(crate) fn validate_token_pool_holds( let mut drive_operations = vec![]; let pool_balance = drive.read_token_shielded_pool_total_balance( &token_id.to_buffer(), + DirectQueryType::StatefulDirectQuery, transaction, &mut drive_operations, platform_version, diff --git a/packages/rs-drive/src/drive/shielded/estimated_costs.rs b/packages/rs-drive/src/drive/shielded/estimated_costs.rs index 7a1a5d61ed2..817066b46e1 100644 --- a/packages/rs-drive/src/drive/shielded/estimated_costs.rs +++ b/packages/rs-drive/src/drive/shielded/estimated_costs.rs @@ -25,6 +25,11 @@ pub(in crate::drive) const ANCHOR_KEY_SIZE: u8 = 32; /// Size of an anchor value (u64 big-endian block height = 8 bytes) pub(in crate::drive) const ANCHOR_VALUE_SIZE: u32 = 8; +/// Size of a pool's total balance value (a u64 sum item = 8 bytes). Read by the stateless query +/// that prices the total's read while a batch is being estimated, so it must stay at or above +/// what the stored value occupies, and it is the same size the pool layer declares for the item. +pub(in crate::drive) const TOTAL_BALANCE_VALUE_SIZE: u32 = 8; + impl Drive { /// Adds estimation costs for shielded pool operations. /// diff --git a/packages/rs-drive/src/drive/shielded/read_total_balance/mod.rs b/packages/rs-drive/src/drive/shielded/read_total_balance/mod.rs index 3a83d43681e..6b3b6893f03 100644 --- a/packages/rs-drive/src/drive/shielded/read_total_balance/mod.rs +++ b/packages/rs-drive/src/drive/shielded/read_total_balance/mod.rs @@ -5,6 +5,7 @@ use crate::drive::Drive; use crate::error::drive::DriveError; use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; +use crate::util::grove_operations::DirectQueryType; use dpp::balances::credits::TokenAmount; use dpp::fee::Credits; use dpp::version::PlatformVersion; @@ -47,9 +48,15 @@ impl Drive { /// Reads a TOKEN shielded pool's total balance (the amount of the token currently /// shielded). Returns 0 if the pool has no balance item yet. Same versioning as /// [`Drive::read_shielded_pool_total_balance`]. + /// + /// `direct_query_type` decides whether the read consults state or only prices itself: a + /// caller building the operations of a batch it is estimating asks for a stateless query, so + /// the read it will perform when the batch is applied is metered without reading anything, + /// and takes the balance to price from elsewhere because a stateless query returns 0. pub fn read_token_shielded_pool_total_balance( &self, token_id: &[u8; 32], + direct_query_type: DirectQueryType, transaction: TransactionArg, drive_operations: &mut Vec, platform_version: &PlatformVersion, @@ -59,6 +66,7 @@ impl Drive { let pool_path = token_shielded_pool_path(token_id); self.read_pool_total_balance_v0( &pool_path, + direct_query_type, transaction, drive_operations, platform_version, diff --git a/packages/rs-drive/src/drive/shielded/read_total_balance/v0/mod.rs b/packages/rs-drive/src/drive/shielded/read_total_balance/v0/mod.rs index 42d107b6cb2..95e80fd809b 100644 --- a/packages/rs-drive/src/drive/shielded/read_total_balance/v0/mod.rs +++ b/packages/rs-drive/src/drive/shielded/read_total_balance/v0/mod.rs @@ -19,17 +19,27 @@ impl Drive { platform_version: &PlatformVersion, ) -> Result { let pool_path = shielded_credit_pool_path(); - // The pool's path is a parameter now, and the credit pool passes the very path this - // function used to build for itself: every protocol version that selects this - // generation sees the same operations against the same tree. - self.read_pool_total_balance_v0(&pool_path, transaction, drive_operations, platform_version) + // The pool's path and the query type are parameters now, and the credit pool passes the + // very path this function used to build for itself and the stateful query it used to + // hardcode: every protocol version that selects this generation sees the same operations + // against the same tree. + self.read_pool_total_balance_v0( + &pool_path, + DirectQueryType::StatefulDirectQuery, + transaction, + drive_operations, + platform_version, + ) } /// Reads the total balance SumItem of the pool at `pool_path`, whichever shielded pool - /// (credit or token) it is. Returns 0 if the key does not exist yet. + /// (credit or token) it is. Returns 0 if the key does not exist yet, and also whenever + /// `direct_query_type` is stateless: such a query reads nothing and only books what the read + /// costs, so a caller estimating a batch must supply the balance it wants priced itself. pub(in crate::drive) fn read_pool_total_balance_v0( &self, pool_path: &[&[u8]], + direct_query_type: DirectQueryType, transaction: TransactionArg, drive_operations: &mut Vec, platform_version: &PlatformVersion, @@ -38,7 +48,7 @@ impl Drive { .grove_get_raw_value_u64_from_encoded_var_vec( pool_path.into(), &[SHIELDED_TOTAL_BALANCE_KEY], - DirectQueryType::StatefulDirectQuery, + direct_query_type, transaction, drive_operations, &platform_version.drive, diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs index ec69ec27ab9..b4bf540358f 100644 --- a/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs @@ -21,6 +21,7 @@ mod shield; mod shielded_transfer; mod unshield; +use crate::drive::shielded::estimated_costs::TOTAL_BALANCE_VALUE_SIZE; use crate::drive::tokens::paths::token_shielded_pools_root_path; use crate::drive::Drive; use crate::error::drive::DriveError; @@ -28,10 +29,11 @@ use crate::error::Error; use crate::fees::op::LowLevelDriveOperation; use crate::state_transition_action::shielded::ShieldedActionNote; use crate::util::grove_operations::DirectQueryType; +use crate::util::grove_operations::QueryTarget::QueryTargetValue; use dpp::balances::credits::TokenAmount; use dpp::version::PlatformVersion; use grovedb::batch::KeyInfoPath; -use grovedb::{EstimatedLayerInformation, TransactionArg}; +use grovedb::{EstimatedLayerInformation, TransactionArg, TreeType}; use std::collections::HashMap; /// How a pool operation moves the pool's total balance. @@ -113,6 +115,23 @@ impl Drive { let apply = estimated_costs_only_with_layer_info.is_none(); + // Moving the total means reading it, and fee validation quotes a batch without state + // while execution meters the same batch against it, so a read only the apply path books + // is charged to nobody. An estimate therefore books the read as well, as the stateless + // query every other estimated read in Drive is priced by: it prices the read from the + // pool's modelled layer instead of reading the value stored there. Pricing it from the + // model rather than from the value is also what keeps the fee from depending on how much + // of the token is shielded. A shielded transfer leaves the total alone and so reads + // nothing in either mode. + let direct_query_type = if apply { + DirectQueryType::StatefulDirectQuery + } else { + DirectQueryType::StatelessDirectQuery { + in_tree_type: TreeType::SumTree, + query_target: QueryTargetValue(TOTAL_BALANCE_VALUE_SIZE), + } + }; + // The total is read from state and written back as an absolute value, so two pool // operations on the same token in one batch would each start from the pre-batch // total and the second write would discard the first. Every state transition applies @@ -122,16 +141,15 @@ impl Drive { let new_total_balance = match balance_change { TokenPoolBalanceChange::Unchanged => None, TokenPoolBalanceChange::Add(amount) => { - let current = if apply { - self.read_token_shielded_pool_total_balance( - &token_id, - transaction, - &mut drive_operations, - platform_version, - )? - } else { - 0 - }; + // A stateless read returns 0, which is also the balance an estimate assumes: a + // fresh pool is the worst case for the write. + let current = self.read_token_shielded_pool_total_balance( + &token_id, + direct_query_type, + transaction, + &mut drive_operations, + platform_version, + )?; Some(current.checked_add(amount).ok_or_else(|| { Error::Drive(DriveError::CorruptedDriveState( "token shielded pool total balance overflow when shielding".to_string(), @@ -139,16 +157,17 @@ impl Drive { })?) } TokenPoolBalanceChange::Remove(amount) => { - let current = if apply { - self.read_token_shielded_pool_total_balance( - &token_id, - transaction, - &mut drive_operations, - platform_version, - )? - } else { - amount - }; + let current = self.read_token_shielded_pool_total_balance( + &token_id, + direct_query_type, + transaction, + &mut drive_operations, + platform_version, + )?; + // A stateless read returns 0, which would take the subtraction below negative, + // so an estimate prices the write of a pool holding exactly what leaves it. The + // sum item is a fixed width, so the value it carries does not change the cost. + let current = if apply { current } else { amount }; Some(current.checked_sub(amount).ok_or_else(|| { Error::Drive(DriveError::CorruptedDriveState( "token shielded pool total balance would go negative when unshielding" @@ -285,6 +304,7 @@ mod tests { drive .read_token_shielded_pool_total_balance( &TOKEN_ID, + DirectQueryType::StatefulDirectQuery, None, &mut vec![], PlatformVersion::latest(), @@ -667,6 +687,103 @@ mod tests { assert_eq!(token_balance(&drive, identity_id), Some(1_000)); } + /// Fee validation quotes a batch with `apply = false` and execution meters the same batch + /// against state, so a read execution performs that the quote never booked is charged to + /// nobody. The worst-case layer information the estimate registers covers the batch's + /// writes, and that margin narrows as a pool fills towards the modelled depth, so it is not + /// what pays for a read: every read the apply path performs has to be metered on the + /// estimate path too. + /// + /// Only a change of the total reads it. A shielded transfer leaves the total alone and so + /// meters no read in either mode, which is what keeps what a transfer costs from revealing + /// how much of the token is shielded. + #[test] + fn should_meter_the_pool_total_read_when_estimating_as_well_as_when_applying() { + let platform_version = PlatformVersion::latest(); + let (drive, identity_id) = setup(1_000); + + // Fill the pool first, so the total the apply path reads is a stored value rather than + // the absent key a fresh pool has: that read is the one the estimate has to cover. + let seed = drive + .token_shield_operations( + TOKEN_ID, + identity_id, + 1_000, + &[note(1)], + &mut None, + None, + platform_version, + ) + .expect("seed shield operations"); + apply(&drive, seed); + + // What the pool update meters, apart from the grove operations of the batch: those are + // priced when the batch is applied, against the layer information when estimating and + // against the trees when applying, so they are no part of what a read costs. + let metered = |balance_change: TokenPoolBalanceChange, estimating: bool| { + let mut layer_info = estimating.then(HashMap::new); + let costs: Vec = drive + .token_shielded_pool_update_operations( + TOKEN_ID, + balance_change, + &[], + &[note(3)], + &mut layer_info, + None, + platform_version, + ) + .expect("pool update operations") + .into_iter() + .filter(|operation| { + matches!( + operation, + LowLevelDriveOperation::CalculatedCostOperation(_) + ) + }) + .collect(); + let reads = costs.len(); + let fee = Drive::calculate_fee( + None, + Some(costs), + &BlockInfo::default().epoch, + drive.config.epochs_per_era, + platform_version, + None, + ) + .expect("price the metered operations") + .processing_fee; + (reads, fee) + }; + + // A shield and a mint into the pool add to the total; an unshield and a burn remove from + // it. Either way the total is read, so either way the estimate has to meter that read. + for balance_change in [ + TokenPoolBalanceChange::Add(1), + TokenPoolBalanceChange::Remove(1), + ] { + let (estimated_reads, estimated_fee) = metered(balance_change, true); + let (actual_reads, actual_fee) = metered(balance_change, false); + assert!( + estimated_reads >= actual_reads, + "{balance_change:?} meters {actual_reads} read(s) worth {actual_fee} credits when \ + applied but only {estimated_reads} worth {estimated_fee} when estimated, so the \ + difference is charged to nobody" + ); + } + + assert_eq!( + metered(TokenPoolBalanceChange::Unchanged, true), + (0, 0), + "a shielded transfer must meter no read while estimating" + ); + assert_eq!( + metered(TokenPoolBalanceChange::Unchanged, false), + (0, 0), + "a shielded transfer must meter no read while applying, so that what it costs cannot \ + reveal the pool total" + ); + } + #[test] fn should_record_and_prune_pool_anchors() { let (drive, identity_id) = setup(1_000); From 6e9cc747a88506744ced17abeea62349e414fab2 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 15:40:54 +0700 Subject: [PATCH 64/94] docs(platform): record why each in-place edit to a shipped generation is inert Protocol version 14 is unreleased; versions 1 through 13 are shipped. The token shielded pools work modified 68 files in place under a /vN/ path, and the ones whose generation a released version selects are frozen unless the edit provably cannot move consensus there. This adds the comment that carries that proof at the edited lines, so a reader with no access to the review can check it. The arguments are of three shapes. Most are unreachability: the token pool transition kinds, the pool-threshold configuration items and a document cost paid from a pool are all refused unpaid by the batch's `is_allowed` gate, which is the first check `process_state_transition_v0` makes, before signature, nonce or fee. Some are an optional versioned method whose feature version is None in every shipped table, so the dispatcher returns without touching state. The rest are output-identity: a body extracted behind a path or a parameter, or an accessor replacing a destructure that stopped compiling when its enum gained a variant, where the released input yields the same operations, the same order and the same errors. Two comments the branch already carried were corrected rather than added to, because both named a gate that does not hold: - batch/transformer/v0 said protocol versions 1 through 11 select it and that the version admitting token pools does not, so the new arms were only exhaustiveness filler. `try_into_action_v0` is the shared transformer body of `transform_into_action` 0, 1 and 2, so the released span is 1 through 13 and version 14 reaches those arms as its live path. - batch/state/v0 justified the document shielded payment by `TokenPaymentInfo::V1`'s `active_version_range`. No such range exists; the only one is `StateTransition::active_version_range`, whose Batch arm narrows on the document base's own feature version, so a V1 payment info on a version 1 base is not caught at decode. The `is_allowed` gate is the whole argument. Comments only, no logic change: 235 added lines and 9 replaced, every one a comment. Verified with `cargo fmt --check --all`, `cargo check -p dpp -p drive -p drive-abci`, and `cargo test -p drive-abci --lib --no-run` for the test code `check` skips. Co-Authored-By: Claude Opus 5 (1M context) --- .../apply_token_configuration_item/v0/mod.rs | 7 +++++++ .../v0/mod.rs | 7 +++++++ .../v0/mod.rs | 7 +++++++ .../v1/mod.rs | 8 ++++++++ .../src/document/extended_document/v0/mod.rs | 4 ++++ .../v0/state_transition_validation.rs | 5 +++++ .../engine/run_block_proposal/v0/mod.rs | 6 ++++++ .../v0/mod.rs | 6 ++++++ .../process_raw_state_transitions/v0/mod.rs | 13 +++++++++++++ .../v0/mod.rs | 7 +++++++ .../state_transitions/batch/state/v0/mod.rs | 19 ++++++++++++++----- .../batch/transformer/v0/mod.rs | 15 +++++++++++---- .../group_queries/group_actions/v0/mod.rs | 8 ++++++++ .../src/query/shielded/anchors/v0/mod.rs | 9 +++++++++ .../query/shielded/encrypted_notes/v0/mod.rs | 7 +++++++ .../shielded/most_recent_anchor/v0/mod.rs | 7 +++++++ .../src/query/shielded/notes_count/v0/mod.rs | 7 +++++++ .../src/query/shielded/nullifiers/v0/mod.rs | 9 +++++++++ .../src/query/shielded/pool_state/v0/mod.rs | 7 +++++++ .../prove/prove_state_transition/v0/mod.rs | 9 +++++++++ .../document_base_transition_action/v0/mod.rs | 4 ++++ .../v0/transformer.rs | 6 ++++++ .../token_claim_transition_action/v0/mod.rs | 4 ++++ .../v0/transformer.rs | 8 ++++++++ .../v0/mod.rs | 4 ++++ .../v0/transformer.rs | 6 ++++++ .../state_transition_action/batch/v0/mod.rs | 4 ++++ .../shield_from_asset_lock/v0/transformer.rs | 5 +++++ .../v0/mod.rs | 6 ++++++ .../verify_shielded_anchors/v0/mod.rs | 6 ++++++ .../verify_shielded_encrypted_notes/v0/mod.rs | 6 ++++++ .../verify_shielded_notes_count/v0/mod.rs | 6 ++++++ .../verify_shielded_nullifiers/v0/mod.rs | 6 ++++++ .../verify_shielded_pool_state/v0/mod.rs | 6 ++++++ 34 files changed, 235 insertions(+), 9 deletions(-) diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/v0/mod.rs index d146de9f2ef..39a94c44ff2 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/v0/mod.rs @@ -153,6 +153,13 @@ impl TokenConfigurationV0 { } // A V0 configuration has no shielded pool and so no threshold to change; // `can_apply_token_configuration_item` refuses these items for it. + // The three pool threshold items below cannot reach this generation on the protocol + // versions that select it. They are appended at the tail of + // `TokenConfigurationChangeItem`, so no existing item's encoding or group action key + // moves, and this match has no catch-all, so nothing previously handled is rerouted. + // A batch carrying one is refused unpaid by the batch's `is_allowed` gate below the + // version that admits token pools, so no transition and no stored group action can + // present one here. TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(_) | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup(_) | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(_) => {} diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/v0/mod.rs index 4bdd68490a9..2f51be340c0 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/v0/mod.rs @@ -135,6 +135,13 @@ impl TokenConfigurationV0 { .trade_mode_change_rules() .admin_action_takers(), // A V0 configuration has no shielded pool and so no threshold for anyone to change. + // The three pool threshold items below cannot reach this generation on the protocol + // versions that select it. They are appended at the tail of + // `TokenConfigurationChangeItem`, so no existing item's encoding or group action key + // moves, and this match has no catch-all, so nothing previously handled is rerouted. + // A batch carrying one is refused unpaid by the batch's `is_allowed` gate below the + // version that admits token pools, so no transition and no stored group action can + // present one here. TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(_) | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup(_) | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(_) => { diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/v0/mod.rs index 7e46417ba91..6f19a2c937d 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/v0/mod.rs @@ -318,6 +318,13 @@ impl TokenConfigurationV0 { goal, ), // A V0 configuration has no shielded pool and so no threshold for anyone to change. + // The three pool threshold items below cannot reach this generation on the protocol + // versions that select it. They are appended at the tail of + // `TokenConfigurationChangeItem`, so no existing item's encoding or group action key + // moves, and this match has no catch-all, so nothing previously handled is rerouted. + // A batch carrying one is refused unpaid by the batch's `is_allowed` gate below the + // version that admits token pools, so no transition and no stored group action can + // present one here. TokenConfigurationChangeItem::MinimumPoolNotesForOutgoing(_) | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingControlGroup(_) | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(_) => false, diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_groups_exist/v1/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_groups_exist/v1/mod.rs index 7a39e2b69f2..c90417e8cdc 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_groups_exist/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_groups_exist/v1/mod.rs @@ -20,6 +20,14 @@ impl TokenConfiguration { return legacy_result; } + // Reading the rules through the enum rather than the narrowed V0 base is + // output-identical on the protocol version that selects this generation, which admits + // only a version 0 token configuration: the enum's `all_change_control_rules` delegates + // straight to the V0 list for a V0 configuration, so the same rules arrive in the same + // order and the same first violation wins, and `main_control_group` reads the same V0 + // field. A configuration of a later format is refused unpaid before any structure + // validation runs. + // // Iterate through the enum rather than narrowing to the V0 base first. The narrowed // view cannot see rules a later configuration version adds, and the v0 pass above is // the only other place those are reached — through `all_used_group_positions`. Reading diff --git a/packages/rs-dpp/src/document/extended_document/v0/mod.rs b/packages/rs-dpp/src/document/extended_document/v0/mod.rs index bb9b2e0f2eb..44b46cf1d01 100644 --- a/packages/rs-dpp/src/document/extended_document/v0/mod.rs +++ b/packages/rs-dpp/src/document/extended_document/v0/mod.rs @@ -403,6 +403,10 @@ impl ExtendedDocumentV0 { property_names::DATA_CONTRACT_ID.to_string(), Value::Identifier(self.data_contract_id.to_buffer()), ); + // Borrowing and cloning the payment info rather than moving it out of `self` is forced + // by the type no longer being `Copy`; every protocol version selects this generation and + // the cloned value is the one that was previously copied, so the object this builds is + // unchanged. if let Some(token_payment_info) = &self.token_payment_info { object.insert( property_names::TOKEN_PAYMENT_INFO.to_string(), diff --git a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/state_transition_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/state_transition_validation.rs index b14402858da..713c4c4805a 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/state_transition_validation.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/shielded/shield_from_asset_lock_transition/v0/state_transition_validation.rs @@ -27,6 +27,11 @@ impl StateTransitionStructureValidation for ShieldFromAssetLockTransitionV0 { /// The structure rules of a `ShieldFromAssetLock`, shared by every transition version: the /// versions differ only in the preimage their bundle is proved against, not in their fields. +/// +/// Extracting this body out of the version 0 implementation is output-identical for the two +/// protocol versions that admit a version 0 transition: the six checks keep their order, so the +/// same first failure wins, and each returns the same error with the same message. Those versions +/// admit version 0 alone, and the version that admits version 1 refuses version 0 outright. pub(in crate::state_transition::state_transitions::shielded::shield_from_asset_lock_transition) fn validate_shield_from_asset_lock_structure( actions: &[SerializedAction], value_balance: u64, diff --git a/packages/rs-drive-abci/src/execution/engine/run_block_proposal/v0/mod.rs b/packages/rs-drive-abci/src/execution/engine/run_block_proposal/v0/mod.rs index 6cf2f1c273e..22745544ad1 100644 --- a/packages/rs-drive-abci/src/execution/engine/run_block_proposal/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/engine/run_block_proposal/v0/mod.rs @@ -415,6 +415,12 @@ where phases.end_phase("prune_shielded_pool_anchors"); // Token shielded pools: record the new anchor of every pool this block wrote to and // prune that pool's stale anchors (touch-driven, so untouched pools cost nothing). + // + // Every protocol version selects this generation, so the call has to be inert at the ones + // that predate token pools, and it is: `record_token_shielded_pool_anchors` is an + // optional versioned method, and its dispatcher returns `Ok(())` without reading or + // writing state when the feature version is `None`, which is what every + // `DRIVE_ABCI_METHOD_VERSIONS_V*` below the token pool protocol version sets it to. self.record_token_shielded_pool_anchors( state_transitions_result.token_shielded_pools_touched(), block_proposal.height, diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index 4208d4b0f0f..1c8e00a5b13 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -840,6 +840,12 @@ impl Platform { // CONSENSUS-CRITICAL: the genesis path (`Drive::create_initial_state_structure_v4`) calls // the same helper, so a chain born at this version and one upgraded to it build a // byte-identical `[Tokens]` subtree. + // + // Earlier protocol versions select this generation too, directly or through the later + // ones that delegate to it, but they cannot reach this insert: the enclosing + // `transition_to_version_14` is called only under the + // `platform_version.protocol_version >= 14` guard in + // `perform_events_on_first_block_of_protocol_change_v0`. self.drive .insert_token_shielded_pools_root_tree(Some(transaction), platform_version)?; Ok(()) diff --git a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs index ddaec719bc5..bfdde76e003 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/state_transition_processing/process_raw_state_transitions/v0/mod.rs @@ -204,6 +204,15 @@ where // Remembered before the transition is consumed: the pools an applied // batch touched get their anchor recorded at block end. + // + // Every protocol version selects this generation, so this has to be inert + // at the ones that predate token pools, and it is twice over: the kinds + // `token_shielded_pools_touched` reports are exactly the ones + // `validate_is_allowed` refuses unpaid there before execution, so the set + // stays empty, and the set's only consumer, + // `record_token_shielded_pool_anchors`, is `None` in those versions' + // tables and returns without touching state. The set lives on the + // in-memory processing result, which is neither serialized nor hashed. let token_shielded_pools_touched = token_shielded_pools_touched(&state_transition); @@ -403,6 +412,10 @@ fn error_to_internal_error_execution_result( /// The token shielded pools a state transition writes to: the token ids of every token pool /// transition in a batch, of every document whose token cost is paid from a pool, and of the /// identity-less token pool transitions. +/// +/// Every kind matched here is one that `StateTransitionIsAllowedValidationV0::validate_is_allowed` +/// refuses, unpaid and before execution, below the protocol version that introduces token pools, +/// so this returns an empty list at every earlier version. fn token_shielded_pools_touched(state_transition: &StateTransition) -> Vec<[u8; 32]> { match state_transition { StateTransition::Batch(batch) => batch diff --git a/packages/rs-drive-abci/src/execution/platform_events/tokens/validate_token_aggregated_balance/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/tokens/validate_token_aggregated_balance/v0/mod.rs index 96da77bf13b..6408070828f 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/tokens/validate_token_aggregated_balance/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/tokens/validate_token_aggregated_balance/v0/mod.rs @@ -20,6 +20,13 @@ impl Platform { .map_err(Error::Drive)?; if !token_balance.ok()? { + // The balance side of the conservation equation includes the token shielded + // pools. Every protocol version selects this generation, and that is inert at + // the ones that predate those pools: below the token pool protocol version + // `calculate_total_tokens_balance` reports a pool total of zero, so + // `total_balances` is the identity balance total it always was. The line is on + // the diagnostic of an already-failed invariant in any case; the accept or + // reject decision is `TotalTokensBalance::ok`. return Err(Error::Execution( ExecutionError::CorruptedTokensNotBalanced(format!( "tokens are not balanced after block execution {:?} off by {}", diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs index f996b6db9b8..c97fe3d282a 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/state/v0/mod.rs @@ -300,11 +300,13 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { transaction, platform_version, )?, - // Every token shielded pool arm below is unreachable on the protocol - // versions that select this generation. The pool transition kinds are refused - // at the batch's `is_allowed` gate until the version that admits them, and a - // document cost paid out of a pool travels in `TokenPaymentInfo::V1`, whose - // `active_version_range` opens at that same version. + // Every protocol version selects this generation, so the token shielded pool + // arms below have to be unreachable on the released ones, 1 through 13, and + // they are: a batch carrying any of these pool transition kinds is refused, + // unpaid, at the batch's `is_allowed` gate below the version that admits + // token pools, and that gate is the first check a state transition meets, + // well before this stage runs. From that version on the arms are the live + // path. TokenTransitionAction::ShieldAction(shield_action) => shield_action .validate_state( platform, @@ -390,6 +392,13 @@ impl DocumentsBatchStateTransitionStateValidationV0 for BatchTransition { // A document whose token cost is paid out of the token's shielded pool: the pool // side and the bundle are validated once the document action itself is valid. + // + // Unreachable on protocol versions 1 through 13, which select this generation along + // with every later one: only a `TokenPaymentInfo::V1` carries a shielded payment, and + // the same `is_allowed` gate refuses a batch holding a document whose payment info + // carries one. Note that nothing narrows the batch's `active_version_range` for it -- + // that narrows on the document base's own feature version -- so the `is_allowed` + // gate is the whole of the argument here. if transition_validation_result.is_valid() { if let BatchedTransitionAction::DocumentAction(document_action) = &transition { if let Some(payment) = document_action.base().shielded_token_payment() { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs index d8cc8eb46dd..00d61c9b8d1 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/mod.rs @@ -753,10 +753,11 @@ impl BatchTransitionInternalTransformerV0 for BatchTransition { Ok(batched_action) } - // Every token shielded pool arm below is unreachable in this generation. The - // protocol versions that select it, 1 through 11, refuse each pool transition kind at - // the batch's `is_allowed` gate, and the version that admits those kinds does not - // select this generation at all. The arms exist so the match stays exhaustive. + // Every token shielded pool arm below is refused before it is reached on protocol + // versions 1 through 13: the batch's `is_allowed` gate rejects each pool transition + // kind unpaid until the version that admits them. From that version on these arms + // are the live path, because `try_into_action_v0` is the shared transformer body of + // `transform_into_action` 0, 1 and 2 -- they are not exhaustiveness filler. TokenTransition::Shield(token_shield) => { let (batched_action, fee_result) = TokenShieldTransitionAction::try_from_borrowed_token_shield_transition_with_contract_lookup(drive, owner_id, token_shield, approximate_for_costs, transaction, block_info, user_fee_increase, |_identifier| { Ok(data_contract_fetch_info.clone()) @@ -877,6 +878,12 @@ impl BatchTransitionInternalTransformerV0 for BatchTransition { // Halo 2 verification and per-action work GroveDB cannot meter: charged here, before // anything can fail, so CheckTx admission and block execution price it identically and // a rejected document still pays for the proof it made the validators check. + // + // Inert on protocol versions 1 through 13, which select this shared transformer body + // through `transform_into_action` 0 and 1: only a `TokenPaymentInfo::V1` carries a + // shielded payment at all, and a batch holding a document whose payment info carries one + // is refused unpaid at the batch's `is_allowed` gate, which runs before the transformer, + // so this charge is never reached and no fee moves there. if let Some(payment) = transition .base() .token_payment_info_ref() diff --git a/packages/rs-drive-abci/src/query/group_queries/group_actions/v0/mod.rs b/packages/rs-drive-abci/src/query/group_queries/group_actions/v0/mod.rs index 7fc8666edfb..88934fb835a 100644 --- a/packages/rs-drive-abci/src/query/group_queries/group_actions/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/group_queries/group_actions/v0/mod.rs @@ -224,6 +224,14 @@ impl Platform { })), }) } + // The pool events rendered here and refused below + // cannot be in the state a released protocol version + // reads. Each is appended at the tail of `TokenEvent`, so + // no stored event's discriminant moves and an event + // written by an earlier version still decodes as it did, + // and the batch transitions that record them are refused + // unpaid by the batch's `is_allowed` gate below the + // version that admits token pools. TokenEvent::MintToPool(amount, actions_digest, public_note) => { group_action_event::EventType::TokenEvent(TokenEventResponse { r#type: Some(token_event::Type::MintToPool(MintToPoolEvent { diff --git a/packages/rs-drive-abci/src/query/shielded/anchors/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/anchors/v0/mod.rs index 3f79647aa6d..3e960de5eab 100644 --- a/packages/rs-drive-abci/src/query/shielded/anchors/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/anchors/v0/mod.rs @@ -51,6 +51,15 @@ impl Platform { platform_state: &PlatformState, platform_version: &PlatformVersion, ) -> Result, Error> { + // Protocol versions 1 through 13 select this generation as well, and the selector + // leaves a request any of them can make untouched: `token_id` is absent there, + // `from_request` maps that to the credit pool without consulting the version, the credit + // pool's path is the same one this handler used to build inline, and + // `validate_pool_exists` is a no-op for it. The query, the proof and the response + // therefore all stay as they were. A `token_id` is refused outright below the version + // that admits token pools. + // Resolving the pool ahead of the checks below cannot reorder errors for such a + // request either, because the credit arm of `from_request` cannot fail. let pool = match ShieldedPoolSelector::from_request(token_id, platform_version) { Ok(pool) => pool, Err(error) => return Ok(QueryValidationResult::new_with_error(error)), diff --git a/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs index 4a23c4fa3f8..e38dff656a3 100644 --- a/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs @@ -32,6 +32,13 @@ impl Platform { platform_state: &PlatformState, platform_version: &PlatformVersion, ) -> Result, Error> { + // Protocol versions 1 through 13 select this generation as well, and the selector + // leaves a request any of them can make untouched: `token_id` is absent there, + // `from_request` maps that to the credit pool without consulting the version, the credit + // pool's path is the same one this handler used to build inline, and + // `validate_pool_exists` is a no-op for it. The query, the proof and the response + // therefore all stay as they were. A `token_id` is refused outright below the version + // that admits token pools. let pool = match ShieldedPoolSelector::from_request(token_id, platform_version) { Ok(pool) => pool, Err(error) => return Ok(QueryValidationResult::new_with_error(error)), diff --git a/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/v0/mod.rs index 259b1d2818d..4b7f0e4e03a 100644 --- a/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/most_recent_anchor/v0/mod.rs @@ -31,6 +31,13 @@ impl Platform { platform_state: &PlatformState, platform_version: &PlatformVersion, ) -> Result, Error> { + // Protocol versions 1 through 13 select this generation as well, and the selector + // leaves a request any of them can make untouched: `token_id` is absent there, + // `from_request` maps that to the credit pool without consulting the version, the credit + // pool's path is the same one this handler used to build inline, and + // `validate_pool_exists` is a no-op for it. The query, the proof and the response + // therefore all stay as they were. A `token_id` is refused outright below the version + // that admits token pools. let pool = match ShieldedPoolSelector::from_request(token_id, platform_version) { Ok(pool) => pool, Err(error) => return Ok(QueryValidationResult::new_with_error(error)), diff --git a/packages/rs-drive-abci/src/query/shielded/notes_count/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/notes_count/v0/mod.rs index 606c22c6aae..f67ca1ee2fe 100644 --- a/packages/rs-drive-abci/src/query/shielded/notes_count/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/notes_count/v0/mod.rs @@ -33,6 +33,13 @@ impl Platform { platform_state: &PlatformState, platform_version: &PlatformVersion, ) -> Result, Error> { + // Protocol versions 1 through 13 select this generation as well, and the selector + // leaves a request any of them can make untouched: `token_id` is absent there, + // `from_request` maps that to the credit pool without consulting the version, the credit + // pool's path is the same one this handler used to build inline, and + // `validate_pool_exists` is a no-op for it. The query, the proof and the response + // therefore all stay as they were. A `token_id` is refused outright below the version + // that admits token pools. let pool = match ShieldedPoolSelector::from_request(token_id, platform_version) { Ok(pool) => pool, Err(error) => return Ok(QueryValidationResult::new_with_error(error)), diff --git a/packages/rs-drive-abci/src/query/shielded/nullifiers/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/nullifiers/v0/mod.rs index c938265bc99..69fbf03f4c0 100644 --- a/packages/rs-drive-abci/src/query/shielded/nullifiers/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/nullifiers/v0/mod.rs @@ -30,6 +30,15 @@ impl Platform { platform_state: &PlatformState, platform_version: &PlatformVersion, ) -> Result, Error> { + // Protocol versions 1 through 13 select this generation as well, and the selector + // leaves a request any of them can make untouched: `token_id` is absent there, + // `from_request` maps that to the credit pool without consulting the version, the credit + // pool's path is the same one this handler used to build inline, and + // `validate_pool_exists` is a no-op for it. The query, the proof and the response + // therefore all stay as they were. A `token_id` is refused outright below the version + // that admits token pools. + // Resolving the pool ahead of the checks below cannot reorder errors for such a + // request either, because the credit arm of `from_request` cannot fail. let pool = match ShieldedPoolSelector::from_request(token_id, platform_version) { Ok(pool) => pool, Err(error) => return Ok(QueryValidationResult::new_with_error(error)), diff --git a/packages/rs-drive-abci/src/query/shielded/pool_state/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/pool_state/v0/mod.rs index 2fc79374ef9..d7af3a67316 100644 --- a/packages/rs-drive-abci/src/query/shielded/pool_state/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/pool_state/v0/mod.rs @@ -22,6 +22,13 @@ impl Platform { platform_state: &PlatformState, platform_version: &PlatformVersion, ) -> Result, Error> { + // Protocol versions 1 through 13 select this generation as well, and the selector + // leaves a request any of them can make untouched: `token_id` is absent there, + // `from_request` maps that to the credit pool without consulting the version, the credit + // pool's path is the same one this handler used to build inline, and + // `validate_pool_exists` is a no-op for it. The query, the proof and the response + // therefore all stay as they were. A `token_id` is refused outright below the version + // that admits token pools. let pool = match ShieldedPoolSelector::from_request(token_id, platform_version) { Ok(pool) => pool, Err(error) => return Ok(QueryValidationResult::new_with_error(error)), diff --git a/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs b/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs index 57cd1201142..15820329321 100644 --- a/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs +++ b/packages/rs-drive/src/prove/prove_state_transition/v0/mod.rs @@ -630,6 +630,10 @@ impl Drive { grovedb::SizedQuery::new(query, Some(1), None), ); + // Reading the surplus output through the accessor rather than destructuring + // the version 0 body is forced by the transition becoming a versioned enum. For + // a version 0 transition the accessor returns that same field, so the proof this + // builds for a transition the released protocol versions admit is byte-identical. match st.surplus_output() { Some(surplus_address) => { // Mirror the Unshield arm: also prove the balance of the signed @@ -687,6 +691,11 @@ impl Drive { } // The token bundle's spent nullifiers in the token pool bind the exact actions of // this transfer. + // The three arms below are not dead code: this body is shared by both prove + // generations, and the later one reaches them. They cannot be reached on the released + // protocol versions, because `active_version_range` places all three of these + // transitions at the version that admits token pools and above, and + // `decode_untrusted_in_version` refuses a transition outside its range. StateTransition::TokenShieldedTransferWithShieldedFee(st) => { use crate::drive::shielded::paths::token_shielded_pool_nullifiers_path_query; use dpp::state_transition::token_shielded_transfer_with_shielded_fee_transition::accessors::TokenShieldedTransferWithShieldedFeeTransitionAccessorsV0; diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/mod.rs index f853d092136..18bde696250 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/mod.rs @@ -86,6 +86,10 @@ pub struct DocumentBaseTransitionActionV0 { pub declared_action_fee: Option>, /// The spend bundle paying `token_cost` out of the token's shielded pool instead of the /// owner's token balance (`TokenPaymentInfo::V1`). Only set when there is a token cost. + /// This action is built fresh in memory for each block and is never encoded, hashed or + /// written to GroveDB, so adding a field changes no stored or signed bytes. It is `None` for + /// every document a protocol version below the one admitting token pools accepts, and the + /// token cost path falls through to the balance-paid operations when it is. pub shielded_token_payment: Option>, } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/transformer.rs index d033249678c..e7ada7cb484 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_base_transition_action/v0/transformer.rs @@ -116,6 +116,12 @@ impl DocumentBaseTransitionActionV0 { } // A shielded payment pays exactly what its bundle proves: the document type's cost, // or the document is rejected before the proof is verified. + // + // This body runs for every document transition at every protocol version, so the two + // branches keyed on a shielded payment have to be unreachable on the released ones, + // and they are: only a `TokenPaymentInfo::V1` carries a shielded payment, and a batch + // whose document base carries one is refused unpaid at the batch's `is_allowed` gate, + // the first check a state transition meets, before this transformer runs. if let Some(payment) = token_payment_info.shielded_payment() { if payment.amount != document_action_token_cost.token_amount { return Ok(ConsensusValidationResult::new_with_error( diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/mod.rs index 8d735276776..5f6ee893204 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/mod.rs @@ -1,4 +1,8 @@ mod transformer; +// The claim resolution body is shared with the claim-into-pool transition rather than copied. +// Re-exporting it changes no behaviour for a plain claim: the shared function takes the moment to +// claim up to as a parameter, and the plain claim passes none, which leaves the current interval +// exactly as it was computed before. pub(crate) use transformer::resolve_token_claim; use std::sync::Arc; diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs index ab3c0e2b5ab..4c1607af0c1 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs @@ -242,6 +242,14 @@ impl TokenClaimTransitionActionV0 { /// Shared by `TokenClaim` (`claim_up_to = None`, the current interval) and `TokenClaimToPool`, /// whose bundle must prove the exact amount, so the client names the cycle-aligned moment the /// perpetual claim pays out to. Read costs are added to `fee_result`. +/// +/// This action has a single version, so the body runs at every protocol version that accepts a +/// claim, and extracting it had to leave a plain claim's outcome alone. It does: the state reads +/// and the fee accumulations keep their order and their position relative to every early return, +/// each rejection carries the same single error as the site it replaced, and the caller rebuilds +/// the same nonce bump from the same three arguments. `claim_up_to` is `None` for a plain claim, +/// which rebinds the current interval to itself; only the claim-into-pool transition passes a +/// value, and that transition is refused unpaid below the version admitting token pools. #[allow(clippy::too_many_arguments)] pub(crate) fn resolve_token_claim( drive: &Drive, diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/mod.rs index 3cf4832fa3f..29c1ecaf70c 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/mod.rs @@ -1,4 +1,8 @@ mod transformer; +// The price resolution body is shared with the purchase-into-pool transition rather than +// copied. Re-exporting it changes no behaviour for a plain direct purchase: the extracted +// functions keep the original order of the price fetch, the fee accumulation and the two +// rejections, and return the same values. pub use transformer::required_direct_purchase_price; pub(crate) use transformer::resolve_direct_purchase_price; diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/transformer.rs index 9254f47ca2e..e843e60113a 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_direct_purchase_transition_action/v0/transformer.rs @@ -185,6 +185,12 @@ impl TokenDirectPurchaseTransitionActionV0 { /// current pricing schedule: an error when the token is not for sale, the buyer agreed to too /// little, or the amount is under the minimum sale amount. Shared by `TokenDirectPurchase` and /// `TokenDirectPurchaseToPool`. Read costs are added to `fee_result`. +/// +/// This action has a single version, so the body runs at every protocol version that accepts a +/// direct purchase, and extracting it had to leave one alone. It does: the price fetch, the fee +/// accumulation and the two rejections keep their order, the arithmetic is the same saturating +/// multiply compared against the same required price, and each rejection carries the same single +/// error as the site it replaced. #[allow(clippy::too_many_arguments)] pub(crate) fn resolve_direct_purchase_price( drive: &Drive, diff --git a/packages/rs-drive/src/state_transition_action/batch/v0/mod.rs b/packages/rs-drive/src/state_transition_action/batch/v0/mod.rs index be1b1899f6c..6ef507c1f80 100644 --- a/packages/rs-drive/src/state_transition_action/batch/v0/mod.rs +++ b/packages/rs-drive/src/state_transition_action/batch/v0/mod.rs @@ -236,6 +236,10 @@ impl BatchTransitionActionV0 { TokenTransitionAction::DirectPurchaseAction(token_purchase), ) => Some(token_purchase.total_agreed_price()), BatchedTransitionAction::TokenAction( + // Previously swallowed by the catch-all below. The action cannot be in a + // batch on the released protocol versions: the purchase-into-pool kind is + // refused unpaid at the batch's `is_allowed` gate before the batch becomes + // actions, so the fold sees the same sequence of prices there. TokenTransitionAction::DirectPurchaseToPoolAction(token_purchase), ) => Some(token_purchase.total_agreed_price()), _ => None, diff --git a/packages/rs-drive/src/state_transition_action/shielded/shield_from_asset_lock/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/shielded/shield_from_asset_lock/v0/transformer.rs index 66a98d065ea..83a66b3ef64 100644 --- a/packages/rs-drive/src/state_transition_action/shielded/shield_from_asset_lock/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/shielded/shield_from_asset_lock/v0/transformer.rs @@ -8,6 +8,11 @@ use dpp::state_transition::state_transitions::shielded::shield_from_asset_lock_t impl ShieldFromAssetLockTransitionActionV0 { /// Transforms the shield from asset lock transition into an action. Every transition version /// carries the same fields, so the action does not depend on which one it came from. + /// + /// Taking the versioned transition and reading it through accessors, rather than the version 0 + /// body directly, is forced by the transition becoming a versioned enum. For a version 0 + /// input the action is field for field what it was, and the two protocol versions that select + /// this generation admit version 0 alone. #[allow(clippy::too_many_arguments)] pub fn try_from_transition( value: &ShieldFromAssetLockTransition, diff --git a/packages/rs-drive/src/verify/shielded/verify_most_recent_shielded_anchor/v0/mod.rs b/packages/rs-drive/src/verify/shielded/verify_most_recent_shielded_anchor/v0/mod.rs index 1847b2a607d..cac549d1ff7 100644 --- a/packages/rs-drive/src/verify/shielded/verify_most_recent_shielded_anchor/v0/mod.rs +++ b/packages/rs-drive/src/verify/shielded/verify_most_recent_shielded_anchor/v0/mod.rs @@ -23,6 +23,12 @@ impl Drive { verify_subset_of_proof: bool, platform_version: &PlatformVersion, ) -> Result<(RootHash, Option<[u8; 32]>), Error> { + // Every protocol version selects this generation, so splitting the body out behind a + // path parameter has to leave the credit pool alone, and it does: this entry point passes + // the same path the body used to build inline, so an existing credit pool proof is checked + // by the same PathQuery and yields the same root hash and the same result. A token pool + // path reaches the shared body only from a caller that cannot exist below the version + // that admits token pools. Self::verify_pool_most_recent_anchor_v0( proof, shielded_latest_recorded_anchor_path_query(), diff --git a/packages/rs-drive/src/verify/shielded/verify_shielded_anchors/v0/mod.rs b/packages/rs-drive/src/verify/shielded/verify_shielded_anchors/v0/mod.rs index 455a52e123e..c52dc57558e 100644 --- a/packages/rs-drive/src/verify/shielded/verify_shielded_anchors/v0/mod.rs +++ b/packages/rs-drive/src/verify/shielded/verify_shielded_anchors/v0/mod.rs @@ -12,6 +12,12 @@ impl Drive { verify_subset_of_proof: bool, platform_version: &PlatformVersion, ) -> Result<(RootHash, Vec<[u8; 32]>), Error> { + // Every protocol version selects this generation, so splitting the body out behind a + // path parameter has to leave the credit pool alone, and it does: this entry point passes + // the same path the body used to build inline, so an existing credit pool proof is checked + // by the same PathQuery and yields the same root hash and the same result. A token pool + // path reaches the shared body only from a caller that cannot exist below the version + // that admits token pools. Self::verify_pool_anchors_v0( proof, shielded_credit_pool_anchors_path_vec(), diff --git a/packages/rs-drive/src/verify/shielded/verify_shielded_encrypted_notes/v0/mod.rs b/packages/rs-drive/src/verify/shielded/verify_shielded_encrypted_notes/v0/mod.rs index 1b761a17715..033dcb99047 100644 --- a/packages/rs-drive/src/verify/shielded/verify_shielded_encrypted_notes/v0/mod.rs +++ b/packages/rs-drive/src/verify/shielded/verify_shielded_encrypted_notes/v0/mod.rs @@ -38,6 +38,12 @@ impl Drive { verify_subset_of_proof: bool, platform_version: &PlatformVersion, ) -> Result<(RootHash, Vec, u64), Error> { + // Every protocol version selects this generation, so splitting the body out behind a + // path parameter has to leave the credit pool alone, and it does: this entry point passes + // the same path the body used to build inline, so an existing credit pool proof is checked + // by the same PathQuery and yields the same root hash and the same result. A token pool + // path reaches the shared body only from a caller that cannot exist below the version + // that admits token pools. Self::verify_pool_encrypted_notes_v0( proof, shielded_credit_pool_path_vec(), diff --git a/packages/rs-drive/src/verify/shielded/verify_shielded_notes_count/v0/mod.rs b/packages/rs-drive/src/verify/shielded/verify_shielded_notes_count/v0/mod.rs index 50776dce93e..59b40c6028b 100644 --- a/packages/rs-drive/src/verify/shielded/verify_shielded_notes_count/v0/mod.rs +++ b/packages/rs-drive/src/verify/shielded/verify_shielded_notes_count/v0/mod.rs @@ -12,6 +12,12 @@ impl Drive { verify_subset_of_proof: bool, platform_version: &PlatformVersion, ) -> Result<(RootHash, Option), Error> { + // Every protocol version selects this generation, so splitting the body out behind a + // path parameter has to leave the credit pool alone, and it does: this entry point passes + // the same path the body used to build inline, so an existing credit pool proof is checked + // by the same PathQuery and yields the same root hash and the same result. A token pool + // path reaches the shared body only from a caller that cannot exist below the version + // that admits token pools. Self::verify_pool_notes_count_v0( proof, shielded_credit_pool_path_vec(), diff --git a/packages/rs-drive/src/verify/shielded/verify_shielded_nullifiers/v0/mod.rs b/packages/rs-drive/src/verify/shielded/verify_shielded_nullifiers/v0/mod.rs index 5e922d945a3..511eb7840bd 100644 --- a/packages/rs-drive/src/verify/shielded/verify_shielded_nullifiers/v0/mod.rs +++ b/packages/rs-drive/src/verify/shielded/verify_shielded_nullifiers/v0/mod.rs @@ -13,6 +13,12 @@ impl Drive { verify_subset_of_proof: bool, platform_version: &PlatformVersion, ) -> Result<(RootHash, Vec<(Vec, bool)>), Error> { + // Every protocol version selects this generation, so splitting the body out behind a + // path parameter has to leave the credit pool alone, and it does: this entry point passes + // the same path the body used to build inline, so an existing credit pool proof is checked + // by the same PathQuery and yields the same root hash and the same result. A token pool + // path reaches the shared body only from a caller that cannot exist below the version + // that admits token pools. Self::verify_pool_nullifiers_v0( proof, shielded_credit_pool_nullifiers_path_vec(), diff --git a/packages/rs-drive/src/verify/shielded/verify_shielded_pool_state/v0/mod.rs b/packages/rs-drive/src/verify/shielded/verify_shielded_pool_state/v0/mod.rs index 10dc1d171b8..7a715ff5379 100644 --- a/packages/rs-drive/src/verify/shielded/verify_shielded_pool_state/v0/mod.rs +++ b/packages/rs-drive/src/verify/shielded/verify_shielded_pool_state/v0/mod.rs @@ -12,6 +12,12 @@ impl Drive { verify_subset_of_proof: bool, platform_version: &PlatformVersion, ) -> Result<(RootHash, Option), Error> { + // Every protocol version selects this generation, so splitting the body out behind a + // path parameter has to leave the credit pool alone, and it does: this entry point passes + // the same path the body used to build inline, so an existing credit pool proof is checked + // by the same PathQuery and yields the same root hash and the same result. A token pool + // path reaches the shared body only from a caller that cannot exist below the version + // that admits token pools. Self::verify_pool_state_v0( proof, shielded_credit_pool_path_vec(), From 1c159fc4d164e3936c14e7955f2bfffbd453e73b Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 15:57:02 +0700 Subject: [PATCH 65/94] docs(drive): state why two shipped verifier edits cannot move consensus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The audit of this branch's in-place edits could not reach these two lines because another change held the file. The token history document type is now resolved inside the closure, and the surplus output read through an accessor; neither changes what a released protocol version verifies, and both now say so where they sit. Also pins what `TryInto` does with the new pool balance variant. Because `TokenAmount` and `Credits` are both `u64`, it shares its field-type list, and so its generated impl, with `VerifiedTokenBalance` — a successful conversion does not say which variant was held. That is the enum's existing shape rather than something the pool variants introduce: `VerifiedMasternodeVote` and `VerifiedNextDistribution` are both `(Vote)` and have always shared an impl the same way. The test records it so a change to the derive or to either field type surfaces there instead of in a caller. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/state_transition/proof_result.rs | 25 +++++++++++++++++++ .../v0/mod.rs | 9 +++++++ 2 files changed, 34 insertions(+) diff --git a/packages/rs-dpp/src/state_transition/proof_result.rs b/packages/rs-dpp/src/state_transition/proof_result.rs index 2c7d6373285..37117db10d4 100644 --- a/packages/rs-dpp/src/state_transition/proof_result.rs +++ b/packages/rs-dpp/src/state_transition/proof_result.rs @@ -556,4 +556,29 @@ mod json_convertible_tests { let recovered = StateTransitionProofResult::from_object(value).expect("from_object"); assert_eq!(original, recovered); } + /// `TryInto` cannot tell a pool balance from a token balance, and is not meant to. + /// + /// `derive_more::TryInto` writes one impl per distinct field-type list, so variants that share + /// one share an impl and a successful conversion says nothing about which variant was held. + /// `TokenAmount` and `Credits` are both `u64`, so the pool balance shares its list with + /// `VerifiedTokenBalance`. This is the enum's existing shape, not something the pool variants + /// introduce: `VerifiedMasternodeVote` and `VerifiedNextDistribution` are both `(Vote)` and + /// have always shared an impl the same way. Match on the variant to discriminate; this test + /// exists so that a change to the derive, or to either field type, shows up here rather than + /// silently in a caller. + #[test] + fn should_convert_a_pool_balance_through_the_token_balance_impl_it_shares() { + let id = Identifier::from([7u8; 32]); + let pool = StateTransitionProofResult::VerifiedTokenShieldedPoolBalance(id, 42); + let converted: (Identifier, u64) = pool + .try_into() + .expect("the shared impl accepts a pool balance"); + assert_eq!(converted, (id, 42)); + + let balance = StateTransitionProofResult::VerifiedTokenBalance(id, 42); + let converted: (Identifier, u64) = balance + .try_into() + .expect("and accepts a token balance identically"); + assert_eq!(converted, (id, 42)); + } } diff --git a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs index 96866e83cdf..fb1e1f29c88 100644 --- a/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs +++ b/packages/rs-drive/src/verify/state_transition/verify_state_transition_was_executed_with_proof/v0/mod.rs @@ -646,6 +646,12 @@ impl Drive { // history contract. Only a kind that reaches this closure has one, so // looking it up any earlier fails for every pool transition, whatever // the token's history settings say. + // + // Moving the lookup cannot change what a released version verifies: + // every kind those versions admit resolves a document type in both + // schema versions of the history contract, which this change leaves + // untouched, so the lookup that used to run before the branch still + // succeeds here and yields the same type and the same errors. let token_history_document_type = token_transition .historical_document_type(&token_history_contract)?; let query = SingleDocumentDriveQuery { @@ -2260,6 +2266,9 @@ impl Drive { })?; let outpoint_bytes: [u8; 36] = outpoint.into(); + // Read through the accessor rather than by destructuring the transition, which + // stopped compiling once the enum gained a variant. Same field, same value, so + // nothing a released version verifies moves. let surplus_output = st.surplus_output(); // Build the outpoint sub-query exactly as the prove side does (same path, same key). From f4896bc065037d64c1d1e63b87a6f2c6f149f7b1 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 16:06:00 +0700 Subject: [PATCH 66/94] fix(drive-abci): read a token pool's spent nullifiers before verifying a batch's bundles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CheckTx verified every Orchard bundle a batch carries against the token's shielded pool without ever reading the pool. A batch naming a nullifier the pool already records can only fail — block validation refuses it on exactly that record — yet admission paid for a Halo 2 verification per bundle before concluding anything, and an unauthenticated submitter could buy that work with a bundle that was already dead. Batches are the only shielded transitions this applied to. The pool-paid transitions run their nullifier check inside their own action transformer, which CheckTx runs; a batch's pool checks live in its per-action state validation, which CheckTx does not run, so nothing in the mempool passes looked at the pool. Admission now reads the record first, which costs a membership lookup per nullifier, and refuses on it. Not a consensus change. `validate_batch_token_shielded_proofs`, the function this sits in front of, is reached only from `check_tx`: block processing verifies a batch's bundles inside each pool action's state validation instead, and reads the nullifier record there for itself with no memory of what admission decided. What admission refuses is refused unpaid, and an unpaid refusal never reaches a committed block — the proposer leaves it out and `process_proposal` rejects a block carrying one — so only which unpaid error a submitter sees changes. Which error that is, is the point of the test: the batch it submits carries both a tampered proof and a spent nullifier, so the error names the check that ran first. check_tx_refuses_a_spent_pool_nullifier_without_verifying_the_bundle ... FAILED assertion failed: `[StateError(InvalidShieldedProofError(InvalidShieldedProofError { message: "bundle verification failed: proof, spend auth signatures, or binding signature invalid" }))]` does not match `[ConsensusError::StateError(StateError::NullifierAlreadySpentError(error))]`: the pool read must refuse this batch before its proof is verified check_tx_refuses_a_spent_pool_nullifier_without_verifying_the_bundle ... ok test result: ok. 1 passed; 0 failed Six existing tests asserted the behaviour this replaces — that CheckTx admits a repeat of a landed bundle because it never reads the pool — and now assert the refusal instead. Their block half is unchanged and still driven directly, because a paid refusal in the block is what has to hold when a repeat reaches a block anyway: a proposer of an older build, or one whose pool gained the nullifier between admitting the repeat and proposing it. cargo test -p drive-abci --all-features --lib batch::tests::token::shielded test result: ok. 70 passed; 0 failed; 0 ignored (one unrelated test excluded) cargo test -p drive-abci --all-features --lib execution::check_tx test result: ok. 25 passed; 0 failed; 1 ignored cargo test -p drive-abci --all-features --lib abci::handler::check_tx test result: ok. 5 passed; 0 failed Co-Authored-By: Claude Opus 5 (1M context) --- .../src/execution/check_tx/v0/mod.rs | 22 +- .../processor/traits/shielded_proof.rs | 94 ++++++++ .../batch/tests/token/shielded/mod.rs | 54 +++-- .../shielded/pool_nullifier_admission.rs | 213 ++++++++++++++++++ 4 files changed, 364 insertions(+), 19 deletions(-) create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/pool_nullifier_admission.rs diff --git a/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs b/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs index 704ed92b487..cf6514a9482 100644 --- a/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs @@ -3,7 +3,8 @@ use crate::execution::check_tx::{CheckTxLevel, CheckTxResult}; use crate::execution::validation::state_transition::check_tx_verification::state_transition_to_execution_event_for_check_tx; use crate::execution::validation::state_transition::processor::traits::shielded_proof::ShieldedProofAdmissionKey; use crate::execution::validation::state_transition::processor::traits::shielded_proof::{ - StateTransitionHasShieldedProofValidationV0, StateTransitionShieldedProofValidationV0, + validate_batch_token_pool_nullifiers_unspent, StateTransitionHasShieldedProofValidationV0, + StateTransitionShieldedProofValidationV0, }; use crate::platform_types::check_tx_proof_verifier::IdentityProofVerification; @@ -26,7 +27,6 @@ use crate::execution::types::state_transition_container::v0::{ use crate::execution::validation::state_transition::processor::process_state_transition; #[cfg(test)] use dpp::serialization::PlatformDeserializableUntrusted; -#[cfg(test)] use dpp::state_transition::StateTransition; use dpp::util::hash::hash_single; use dpp::validation::ValidationResult; @@ -194,6 +194,24 @@ where check_tx_result.fee_result = Some(estimated_fee_result); + // A batch that spends a token shielded pool note the pool already records can only + // fail: block validation refuses it on that record. Reading the record costs a few + // key lookups against a pool tree; verifying the bundles the batch carries costs a + // Halo 2 verification per bundle, which the node would otherwise pay for before + // discovering the free reason to refuse. Batches are the only transitions whose pool + // bundles are checked against state nowhere in the mempool passes: the pool-paid + // transitions do it in their own transformer, which runs here too. + if errors.is_empty() && matches!(check_tx_level, CheckTxLevel::FirstTimeCheck) { + if let StateTransition::Batch(batch) = &state_transition { + let result = validate_batch_token_pool_nullifiers_unspent( + batch, + platform_ref.drive, + platform_version, + )?; + errors.extend(result.errors); + } + } + // Orchard verification is intentionally last. // The preliminary balance floor includes an identity-write allowance; // the execution-event fee check remains the authoritative check diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs index 0251938ea08..1f4f95fe2fc 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs @@ -13,6 +13,7 @@ use dpp::consensus::basic::state_transition::{ }; use dpp::consensus::basic::BasicError; use dpp::consensus::state::shielded::insufficient_shielded_fee_error::InsufficientShieldedFeeError; +use dpp::consensus::state::shielded::nullifier_already_spent_error::NullifierAlreadySpentError; use dpp::consensus::state::state_error::StateError; use dpp::consensus::ConsensusError; use dpp::serialization::{PlatformMessageSignable, Signable}; @@ -51,6 +52,11 @@ use dpp::state_transition::{StateTransition, StateTransitionOwned}; use dpp::util::hash::hash_single; use dpp::validation::SimpleConsensusValidationResult; use dpp::version::PlatformVersion; +use drive::drive::tokens::paths::token_shielded_pools_root_path; +use drive::drive::Drive; +use drive::grovedb_path::SubtreePath; +use drive::util::grove_operations::DirectQueryType; +use std::collections::HashSet; /// The (identity, nonce) pair CheckTx's `CheckTxProofVerifier` admits Orchard proof work under /// for identity-signed shielded transitions, so an identity cannot start unbounded verification @@ -904,6 +910,94 @@ fn key_not_allowed_in_shielded_creation( }) } +/// Refuses a batch that names a token shielded pool nullifier the pool has already recorded. +/// +/// CheckTx only. A batch's pool bundles are checked against the pool in block validation, inside +/// each pool action's state validation, where a failure is a paid one — the identity contract +/// nonce is consumed and the fee charged. Nothing in the mempool passes reads the pool: a batch +/// whose note has been spent is admitted, and the node pays for the Halo 2 verification of a +/// bundle that block validation will refuse on a handful of key lookups. This is those lookups, +/// run before the verification, so the free reason to refuse is found first. +/// +/// It cannot change a consensus outcome. What it refuses is refused unpaid, and an unpaid refusal +/// never reaches a committed block: a proposer leaves such a transition out and `process_proposal` +/// rejects a block that carries one. Block validation reads the same record for itself and +/// decides on its own, with no memory of what admission concluded. +pub(crate) fn validate_batch_token_pool_nullifiers_unspent( + batch: &BatchTransition, + drive: &Drive, + platform_version: &PlatformVersion, +) -> Result { + let mut drive_operations = vec![]; + let mut pools_without_a_subtree = HashSet::new(); + + for transition in batch.transitions_iter() { + let (token_id, actions) = match transition { + BatchedTransitionRef::Token(token_transition) => { + match token_transition.shielded_pool_actions() { + Some(actions) => (token_transition.token_id().to_buffer(), actions), + None => continue, + } + } + // A document whose token cost is paid out of the token's shielded pool spends pool + // notes exactly as an unshield does. + BatchedTransitionRef::Document(document_transition) => { + let base = document_transition.base(); + let Some(token_payment_info) = base.token_payment_info_ref().as_ref() else { + continue; + }; + let Some(payment) = token_payment_info.shielded_payment() else { + continue; + }; + ( + token_payment_info + .token_id(base.data_contract_id()) + .to_buffer(), + payment.actions.as_slice(), + ) + } + }; + + if pools_without_a_subtree.contains(&token_id) { + continue; + } + // A token that never opted into a shielded pool has no pool subtree, and a membership + // read under a subtree that is not there is not a read of an empty pool. Such a batch is + // refused in the block for naming a pool the token does not have, so there is nothing + // for this to find. + if !drive.grove_has_raw( + SubtreePath::from(&token_shielded_pools_root_path()), + &token_id, + DirectQueryType::StatefulDirectQuery, + None, + &mut drive_operations, + &platform_version.drive, + )? { + pools_without_a_subtree.insert(token_id); + continue; + } + + for action in actions { + if drive.has_token_pool_nullifier( + &token_id, + &action.nullifier, + None, + &mut drive_operations, + platform_version, + )? { + return Ok(SimpleConsensusValidationResult::new_with_error( + StateError::NullifierAlreadySpentError(NullifierAlreadySpentError::new( + action.nullifier, + )) + .into(), + )); + } + } + } + + Ok(SimpleConsensusValidationResult::new()) +} + /// Verifies every token shielded bundle a batch carries, statelessly. fn validate_batch_token_shielded_proofs( batch: &BatchTransition, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index cf0da6a63f7..dbd0e088de8 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -1,6 +1,7 @@ use super::*; mod minimum_pool_notes; +mod pool_nullifier_admission; /// Token shielded pool transitions: shield, unshield and shielded transfer inside a batch. /// @@ -4795,12 +4796,17 @@ mod token_pool_outputs_only_copy_tests { /// Submits a bundle again after it has landed and returns what the submitter was charged for /// it. /// - /// CheckTx admits it: it does not read the pool. Where CheckTx verifies the kind's bundle and - /// the submitter is the bundle's owner, that admission also shows the bundle is valid as the - /// submitter's transition; a claim's bundle CheckTx does not verify at all. Block execution - /// must then refuse it on the original's first dummy nullifier, which it checks before the - /// proof, as a paid failure: the submitter's nonce advances to `nonce` and its fee is - /// charged, nothing else moves. + /// Both passes refuse it on the original's first dummy nullifier, which the pool now records, + /// and each reaches that for its own reasons: + /// + /// - CheckTx reads the pool's record before verifying any bundle the batch carries, so a + /// repeat never reaches the mempool and no node pays for its Halo 2 verification. + /// - Block execution reads the record for itself, with no memory of what admission decided, + /// and refuses the repeat as a paid failure: the submitter's nonce advances to `nonce` and + /// its fee is charged, nothing else moves. This half is what holds if the repeat reaches a + /// block anyway — a proposer of an older build, or one whose pool gained the nullifier + /// between admitting the repeat and proposing it — so the block is driven directly rather + /// than through CheckTx. fn assert_repeat_refused_in_block( platform: &TempPlatform, repeat: &StateTransition, @@ -4810,13 +4816,17 @@ mod token_pool_outputs_only_copy_tests { original: &OrchardBundleParams, block_info: Option<&BlockInfo>, ) -> Credits { - assert_check_tx_accepts(platform, repeat); + let first = original.actions[0].nullifier; + assert_matches!( + assert_check_tx_rejects(platform, repeat).as_slice(), + [ConsensusError::StateError(StateError::NullifierAlreadySpentError(error))] + if error.nullifier() == first + ); let credits_before = credits(platform, submitter_id); let result = match block_info { Some(block_info) => process_at(platform, repeat, block_info), None => process(platform, repeat), }; - let first = original.actions[0].nullifier; assert_matches!( result.execution_results().as_slice(), [StateTransitionExecutionResult::PaidConsensusError { @@ -6156,8 +6166,15 @@ mod token_pool_outputs_only_copy_tests { ) .await .expect("token mint to pool confirmation"); - assert_check_tx_accepts(&platform, &close_first); let first_nullifier = bundle.actions[0].nullifier; + // Admission refuses it on the record the mint left, so no node verifies this bundle a + // second time. The block is driven directly, because that refusal is what has to hold if + // the signature reaches a block anyway. + assert_matches!( + assert_check_tx_rejects(&platform, &close_first).as_slice(), + [ConsensusError::StateError(StateError::NullifierAlreadySpentError(error))] + if error.nullifier() == first_nullifier + ); assert_matches!( process(&platform, &close_first) .execution_results() @@ -6178,9 +6195,10 @@ mod token_pool_outputs_only_copy_tests { assert_tokens_conserved(&platform); } - /// CheckTx cannot verify a claim's bundle, whose amount is only known against state, so it - /// admits another claimant's copy; in the block, the original's recorded dummy nullifier - /// refuses the copy before its proof is looked at. + /// CheckTx cannot verify a claim's bundle, whose amount is only known against state, so the + /// pool's record is all it has to go on for another claimant's copy — and once the original + /// has landed, that record refuses the copy. In the block, the same record refuses it again, + /// before its proof is looked at. #[tokio::test] async fn test_a_claim_to_pool_bundle_copied_by_another_claimant_after_it_landed_is_refused() { let platform_version = PlatformVersion::latest(); @@ -6540,6 +6558,7 @@ mod token_pool_outputs_only_copy_tests { .expect("client-built token shield"); assert_check_tx_accepts(&platform, &shield); let bundle = proven_bundle(&shield); + let bundle_nullifier = bundle.actions[0].nullifier; let copy = BatchTransition::new_token_shield_transition( token_id, @@ -6586,8 +6605,10 @@ mod token_pool_outputs_only_copy_tests { Some(OWNER_INITIAL_BALANCE - 2 * amount) ); - // The bundle that just executed, offered again under the copier: CheckTx, which never - // reads the pool's record, still refuses it on the owner. + // The bundle that just executed, offered again under the copier. CheckTx now has two + // reasons to refuse it — the owner its sighash names, and the nullifiers the executed + // original left in the pool — and reports the pool's record, because reading it is what + // CheckTx does before verifying a bundle. let late_copy = BatchTransition::new_token_shield_transition( token_id, copier.id(), @@ -6606,9 +6627,8 @@ mod token_pool_outputs_only_copy_tests { .expect("token shield transition"); assert_matches!( assert_check_tx_rejects(&platform, &late_copy).as_slice(), - [ConsensusError::StateError( - StateError::InvalidShieldedProofError(_) - )] + [ConsensusError::StateError(StateError::NullifierAlreadySpentError(error))] + if error.nullifier() == bundle_nullifier ); assert_tokens_conserved(&platform); } diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/pool_nullifier_admission.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/pool_nullifier_admission.rs new file mode 100644 index 00000000000..3ee58864317 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/pool_nullifier_admission.rs @@ -0,0 +1,213 @@ +//! What the mempool does with a batch whose token shielded pool nullifiers are already spent. +//! +//! Such a batch can never execute: the pool holds the nullifier, so block validation refuses it +//! on that. Admission has to reach the same conclusion without paying for the batch's Halo 2 +//! verification first, so that an unauthenticated submitter cannot buy proof work with a bundle +//! that is already dead. + +use super::token_shielded_pool_tests::{ + build_shield_bundle, build_spend_bundle, insert_token_pool_anchor, nullifier_is_spent, + platform_with_latest_version, process, spendable_note, SHIELD_AMOUNT, +}; +use super::*; +use crate::execution::check_tx::CheckTxLevel; +use crate::platform_types::platform::PlatformRef; +use crate::rpc::core::MockCoreRPCLike; +use crate::test::helpers::setup::TempPlatform; +use dpp::data_contract::DataContract; +use dpp::identity::accessors::IdentityGettersV0; +use dpp::identity::{Identity, IdentityPublicKey}; +use dpp::shielded::{token_unshield_extra_sighash_data_v0, OrchardBundleParams}; +use dpp::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionType; +use dpp::state_transition::StateTransition; +use simple_signer::signer::SimpleSigner; + +/// The errors CheckTx refuses `transition` with at `level`, or a panic if it admits it. +fn check_tx_errors( + platform: &TempPlatform, + transition: &StateTransition, + level: CheckTxLevel, +) -> Vec { + let state = platform.state.load(); + let platform_ref = PlatformRef { + drive: &platform.drive, + state: &state, + config: &platform.config, + core_rpc: &platform.core_rpc, + }; + let result = platform + .check_tx( + &transition + .serialize_to_bytes() + .expect("serialize transition"), + level, + &platform_ref, + PlatformVersion::latest(), + ) + .expect("check tx"); + assert!( + !result.is_valid(), + "CheckTx at {level:?} admitted a batch whose pool nullifier is already spent" + ); + result.errors +} + +/// A pool holding one spent note, and everything needed to build another unshield that spends it +/// again: the unshield's bundle (valid, and the same bytes the landed one carried) and the +/// nullifier the pool now records. +struct SpentNote { + platform: TempPlatform, + identity: Identity, + signer: SimpleSigner, + key: IdentityPublicKey, + contract: DataContract, + token_id: Identifier, + recipient: Identifier, + amount: u64, + bundle: OrchardBundleParams, + nullifier: [u8; 32], +} + +/// Shields into a token's pool, then unshields one note out of it, so that the note's nullifier +/// is recorded and any further spend of it is refused. +async fn pool_with_a_spent_note() -> SpentNote { + let platform_version = PlatformVersion::latest(); + let mut platform = platform_with_latest_version(); + let mut rng = StdRng::seed_from_u64(70_401); + + let (identity, signer, key) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.5)); + let (recipient, _, _) = setup_identity(&mut platform, rng.gen(), dash_to_credits!(0.1)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + Some(super::token_shielded_pool_tests::enable_shielded_pool), + None, + None, + None, + platform_version, + ); + + let shield_bundle = build_shield_bundle( + SHIELD_AMOUNT, + 71, + TokenTransitionActionType::Shield, + token_id, + identity.id(), + ); + let shield = BatchTransition::new_token_shield_transition( + token_id, + identity.id(), + contract.id(), + 0, + SHIELD_AMOUNT, + shield_bundle, + &key, + 2, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token shield transition"); + assert_matches!( + process(&platform, &shield).execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + + let (note, anchor, merkle_path) = spendable_note(6_000, 1); + insert_token_pool_anchor(&platform, token_id, &anchor); + let amount = 4_000; + let extra = token_unshield_extra_sighash_data_v0( + &token_id.to_buffer(), + &identity.id().to_buffer(), + &recipient.id().to_buffer(), + amount, + ); + let (bundle, _) = build_spend_bundle(note, merkle_path, anchor, amount, &extra, 72); + let nullifier = bundle.actions[0].nullifier; + + let unshield = BatchTransition::new_token_unshield_transition( + token_id, + identity.id(), + contract.id(), + 0, + amount, + recipient.id(), + bundle.clone(), + &key, + 3, + 0, + &signer, + platform_version, + None, + ) + .await + .expect("token unshield transition"); + assert_matches!( + process(&platform, &unshield).execution_results().as_slice(), + [StateTransitionExecutionResult::SuccessfulExecution { .. }] + ); + assert!( + nullifier_is_spent(&platform, token_id, &nullifier), + "the landed unshield must have recorded the note's nullifier" + ); + + SpentNote { + platform, + identity, + signer, + key, + contract, + token_id, + recipient: recipient.id(), + amount, + bundle, + nullifier, + } +} + +impl SpentNote { + /// Another unshield of the same note, carrying `bundle`, at nonce 4. + async fn unshield_again(&self, bundle: OrchardBundleParams) -> StateTransition { + BatchTransition::new_token_unshield_transition( + self.token_id, + self.identity.id(), + self.contract.id(), + 0, + self.amount, + self.recipient, + bundle, + &self.key, + 4, + 0, + &self.signer, + PlatformVersion::latest(), + None, + ) + .await + .expect("token unshield transition") + } +} + +/// The batch carries a proof that does not verify AND a nullifier the pool already holds. Both +/// are grounds to refuse it, and which error comes back says which check ran first: reading the +/// pool costs a handful of key lookups, verifying the bundle costs a Halo 2 verification, and an +/// unauthenticated submitter must not be able to buy the second with a bundle the first would +/// have thrown away. +#[tokio::test] +async fn check_tx_refuses_a_spent_pool_nullifier_without_verifying_the_bundle() { + let fixture = pool_with_a_spent_note().await; + + let mut tampered = fixture.bundle.clone(); + tampered.proof[0] ^= 0xff; + let repeat = fixture.unshield_again(tampered).await; + + let errors = check_tx_errors(&fixture.platform, &repeat, CheckTxLevel::FirstTimeCheck); + assert_matches!( + errors.as_slice(), + [ConsensusError::StateError(StateError::NullifierAlreadySpentError(error))] + if error.nullifier() == fixture.nullifier, + "the pool read must refuse this batch before its proof is verified, got {errors:?}" + ); +} From 3954a983fcf67570ec9c650d7f52f577f80b8a82 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 16:16:52 +0700 Subject: [PATCH 67/94] fix(drive-abci): evict a batch from the mempool once its pool note is spent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The re-check after every block did not read the token shielded pool either, so a batch admitted while its note was still unspent stayed in the mempool after another transition spent that note. It could then only fail, and it failed where it costs the most: a proposer put it in a block, it was refused there on the nullifier record, and the submitter paid for a block slot nobody could use. A batch's pool checks live in its per-action state validation, which the re-check does not run — unlike the pool-paid transitions, whose transformer runs in both passes and has always done this check. The nullifier read the first check gained now runs at both levels, which is the whole change: the mempool drops such a batch at the first re-check after the block that spent the note. The test drives the re-check as Tenderdash does, with a batch carrying a valid bundle so that only the pool's record can refuse it: recheck_evicts_a_batch_whose_pool_nullifier_has_been_spent ... FAILED CheckTx at Recheck admitted a batch whose pool nullifier is already spent recheck_evicts_a_batch_whose_pool_nullifier_has_been_spent ... ok check_tx_refuses_a_spent_pool_nullifier_without_verifying_the_bundle ... ok test result: ok. 2 passed; 0 failed cargo test -p drive-abci --all-features --lib batch::tests::token::shielded test result: ok. 71 passed; 0 failed; 0 ignored Co-Authored-By: Claude Opus 5 (1M context) --- .../src/execution/check_tx/v0/mod.rs | 8 +++++- .../shielded/pool_nullifier_admission.rs | 27 ++++++++++++++++--- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs b/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs index cf6514a9482..4132509e661 100644 --- a/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs @@ -201,7 +201,13 @@ where // discovering the free reason to refuse. Batches are the only transitions whose pool // bundles are checked against state nowhere in the mempool passes: the pool-paid // transitions do it in their own transformer, which runs here too. - if errors.is_empty() && matches!(check_tx_level, CheckTxLevel::FirstTimeCheck) { + // + // Both levels read it. The first check keeps such a batch out; the re-check after + // every block is what evicts one that was admitted while its note was still unspent, + // and without it that batch sits in the mempool until a proposer spends a block slot + // on it. A batch's pool checks are in its per-action state validation, which a + // re-check does not run. + if errors.is_empty() { if let StateTransition::Batch(batch) = &state_transition { let result = validate_batch_token_pool_nullifiers_unspent( batch, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/pool_nullifier_admission.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/pool_nullifier_admission.rs index 3ee58864317..b5b8e846656 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/pool_nullifier_admission.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/pool_nullifier_admission.rs @@ -1,9 +1,11 @@ //! What the mempool does with a batch whose token shielded pool nullifiers are already spent. //! //! Such a batch can never execute: the pool holds the nullifier, so block validation refuses it -//! on that. Admission has to reach the same conclusion without paying for the batch's Halo 2 -//! verification first, so that an unauthenticated submitter cannot buy proof work with a bundle -//! that is already dead. +//! on that. Both mempool passes have to reach the same conclusion without paying for the batch's +//! Halo 2 verification first — admission, so an unauthenticated submitter cannot buy proof work +//! with a bundle that is already dead, and the re-check after every block, so a batch that was +//! admitted before the note was spent leaves the mempool rather than waiting for a proposer to +//! spend a block slot on it. use super::token_shielded_pool_tests::{ build_shield_bundle, build_spend_bundle, insert_token_pool_anchor, nullifier_is_spent, @@ -211,3 +213,22 @@ async fn check_tx_refuses_a_spent_pool_nullifier_without_verifying_the_bundle() "the pool read must refuse this batch before its proof is verified, got {errors:?}" ); } + +/// A batch admitted while its note was still unspent is re-checked after every block. Once the +/// note is spent the batch can only fail, so the re-check must drop it: left in the mempool it +/// occupies a slot in a proposer's block and lands there as a paid failure. +#[tokio::test] +async fn recheck_evicts_a_batch_whose_pool_nullifier_has_been_spent() { + let fixture = pool_with_a_spent_note().await; + + // A valid bundle, so nothing but the pool's record can refuse it. + let repeat = fixture.unshield_again(fixture.bundle.clone()).await; + + let errors = check_tx_errors(&fixture.platform, &repeat, CheckTxLevel::Recheck); + assert_matches!( + errors.as_slice(), + [ConsensusError::StateError(StateError::NullifierAlreadySpentError(error))] + if error.nullifier() == fixture.nullifier, + "the re-check must evict this batch, got {errors:?}" + ); +} From 25bf31a86807a62a36a503dc6e945a1b3bf07d1d Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 16:39:25 +0700 Subject: [PATCH 68/94] refactor(drive-abci): gate every token shielded pool kind from one question MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The protocol-version gate that refuses token shielded pool transitions before the version which introduced them listed the seven pool kinds by hand and ended in a catch-all. Of the five places that carried that list, this was the one where a kind missed from it would do real damage: the arm falls through to `None`, so a new pool kind would not be gated at all and would be accepted by nodes of every protocol version, months before the version that knows how to execute it. It now asks `shielded_pool_actions`, the one place that decides which kinds touch a pool, so a kind added there is gated here without anyone having to remember this site. The names are unchanged: each was the action type's own name under a `Token` prefix, and that is now how they are built, from a match that is itself exhaustive. No behaviour changes, so there is no red phase. What the names are is part of the answer an older node gives a client, and nothing pinned them, so a test now does — it would have failed had the conversion got any of the seven wrong: every_pool_kind_is_refused_under_its_own_name_before_the_pools_exist ... ok no_pool_kind_is_refused_once_the_pools_exist ... ok Co-Authored-By: Claude Opus 5 (1M context) --- .../processor/traits/is_allowed.rs | 158 +++++++++++++++--- 1 file changed, 135 insertions(+), 23 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs index baffbe73767..a30bede5522 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs @@ -6,7 +6,10 @@ use dpp::data_contract::associated_token::token_configuration_item::TokenConfigu use dpp::prelude::ConsensusValidationResult; use dpp::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; -use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransition; +use dpp::state_transition::batch_transition::batched_transition::token_transition::{ + TokenTransition, TokenTransitionV0Methods, +}; +use dpp::state_transition::batch_transition::batched_transition::token_transition_action_type::TokenTransitionActionTypeGetter; use dpp::state_transition::batch_transition::batched_transition::BatchedTransitionRef; use dpp::state_transition::batch_transition::document_base_transition::v1::v1_methods::DocumentBaseTransitionV1Methods; use dpp::state_transition::batch_transition::token_config_update_transition::v0::v0_methods::TokenConfigUpdateTransitionV0Methods; @@ -108,27 +111,16 @@ impl StateTransitionIsAllowedValidationV0 for StateTransition { if let Some(transition) = st.transitions_iter() .find_map(|transition| match transition { - BatchedTransitionRef::Token(TokenTransition::Shield(_)) => { - Some("TokenShield") - } - BatchedTransitionRef::Token(TokenTransition::Unshield(_)) => { - Some("TokenUnshield") - } - BatchedTransitionRef::Token(TokenTransition::ShieldedTransfer( - _, - )) => Some("TokenShieldedTransfer"), - BatchedTransitionRef::Token(TokenTransition::MintToPool(_)) => { - Some("TokenMintToPool") - } - BatchedTransitionRef::Token(TokenTransition::BurnFromPool(_)) => { - Some("TokenBurnFromPool") - } - BatchedTransitionRef::Token(TokenTransition::ClaimToPool(_)) => { - Some("TokenClaimToPool") + // Every kind that carries a bundle against a token's shielded + // pool, asked as one question so that a kind added later is + // gated by this without anyone having to remember to list it. + // The names are what they have always been: the action type's + // own name under a `Token` prefix. + BatchedTransitionRef::Token(token_transition) + if token_transition.shielded_pool_actions().is_some() => + { + Some(format!("Token{}", token_transition.action_type())) } - BatchedTransitionRef::Token( - TokenTransition::DirectPurchaseToPool(_), - ) => Some("TokenDirectPurchaseToPool"), // Only a token with a pool has an outgoing notes threshold, // and software older than it cannot decode these items. BatchedTransitionRef::Token(TokenTransition::ConfigUpdate( @@ -140,7 +132,9 @@ impl StateTransitionIsAllowedValidationV0 for StateTransition { | TokenConfigurationChangeItem::MinimumPoolNotesForOutgoingAdminGroup(_) ) => { - Some("TokenConfigUpdateMinimumPoolNotesForOutgoing") + Some( + "TokenConfigUpdateMinimumPoolNotesForOutgoing".to_string(), + ) } BatchedTransitionRef::Document(document_transition) if document_transition @@ -149,7 +143,7 @@ impl StateTransitionIsAllowedValidationV0 for StateTransition { .as_ref() .is_some_and(|info| info.shielded_payment().is_some()) => { - Some("DocumentShieldedTokenPayment") + Some("DocumentShieldedTokenPayment".to_string()) } _ => None, }) @@ -618,4 +612,122 @@ mod tests { } } } + + mod token_shielded_pool_gate { + use super::*; + use assert_matches::assert_matches; + use dpp::consensus::basic::BasicError; + use dpp::consensus::ConsensusError; + use dpp::state_transition::batch_transition::batched_transition::token_burn_from_pool_transition::{TokenBurnFromPoolTransition, TokenBurnFromPoolTransitionV0}; + use dpp::state_transition::batch_transition::batched_transition::token_claim_to_pool_transition::{TokenClaimToPoolTransition, TokenClaimToPoolTransitionV0}; + use dpp::state_transition::batch_transition::batched_transition::token_direct_purchase_to_pool_transition::{TokenDirectPurchaseToPoolTransition, TokenDirectPurchaseToPoolTransitionV0}; + use dpp::state_transition::batch_transition::batched_transition::token_mint_to_pool_transition::{TokenMintToPoolTransition, TokenMintToPoolTransitionV0}; + use dpp::state_transition::batch_transition::batched_transition::token_shield_transition::{TokenShieldTransition, TokenShieldTransitionV0}; + use dpp::state_transition::batch_transition::batched_transition::token_shielded_transfer_transition::{TokenShieldedTransferTransition, TokenShieldedTransferTransitionV0}; + use dpp::state_transition::batch_transition::batched_transition::token_unshield_transition::{TokenUnshieldTransition, TokenUnshieldTransitionV0}; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::BatchTransitionV1; + use dpp::platform_value::BinaryData; + + /// A batch whose only transition is `token_transition`. + fn batch_with(token_transition: TokenTransition) -> StateTransition { + StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: Default::default(), + transitions: vec![BatchedTransition::Token(token_transition)], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: BinaryData::default(), + })) + } + + /// Every kind that carries a bundle against a token's shielded pool, with the name the + /// gate reports it under. + fn every_pool_kind() -> Vec<(TokenTransition, &'static str)> { + vec![ + ( + TokenTransition::Shield(TokenShieldTransition::V0( + TokenShieldTransitionV0::default(), + )), + "TokenShield", + ), + ( + TokenTransition::Unshield(TokenUnshieldTransition::V0( + TokenUnshieldTransitionV0::default(), + )), + "TokenUnshield", + ), + ( + TokenTransition::ShieldedTransfer(TokenShieldedTransferTransition::V0( + TokenShieldedTransferTransitionV0::default(), + )), + "TokenShieldedTransfer", + ), + ( + TokenTransition::MintToPool(TokenMintToPoolTransition::V0( + TokenMintToPoolTransitionV0::default(), + )), + "TokenMintToPool", + ), + ( + TokenTransition::BurnFromPool(TokenBurnFromPoolTransition::V0( + TokenBurnFromPoolTransitionV0::default(), + )), + "TokenBurnFromPool", + ), + ( + TokenTransition::ClaimToPool(TokenClaimToPoolTransition::V0( + TokenClaimToPoolTransitionV0::default(), + )), + "TokenClaimToPool", + ), + ( + TokenTransition::DirectPurchaseToPool(TokenDirectPurchaseToPoolTransition::V0( + TokenDirectPurchaseToPoolTransitionV0::default(), + )), + "TokenDirectPurchaseToPool", + ), + ] + } + + /// Before the version that introduced the token shielded pools, every pool kind is + /// refused, and refused under the name a client is told. The names are part of the + /// answer a node of an older version gives, so they are pinned here rather than left to + /// whatever the gate happens to build them from. + #[test] + fn every_pool_kind_is_refused_under_its_own_name_before_the_pools_exist() { + let platform_version = + PlatformVersion::get(TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION - 1) + .expect("the version before token shielded pools"); + + for (token_transition, expected_name) in every_pool_kind() { + let result = batch_with(token_transition) + .validate_is_allowed(platform_version) + .expect("should not error"); + assert_matches!( + result.errors.as_slice(), + [ConsensusError::BasicError(BasicError::StateTransitionNotActiveError(error))] + if error.state_transition_type() == expected_name + && error.required_protocol_version() + == TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION, + "expected {expected_name} to be refused, got {:?}", + result.errors + ); + } + } + + /// And from that version on, none of them is refused by this gate. + #[test] + fn no_pool_kind_is_refused_once_the_pools_exist() { + let platform_version = + PlatformVersion::get(TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION) + .expect("the version that introduced token shielded pools"); + + for (token_transition, name) in every_pool_kind() { + let result = batch_with(token_transition) + .validate_is_allowed(platform_version) + .expect("should not error"); + assert!(result.is_valid(), "{name}: {:?}", result.errors); + } + } + } } From f8fb7fc423f92a33335e6ed2bbf5020ac778c6b1 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 16:39:44 +0700 Subject: [PATCH 69/94] test(drive-abci): pin that an absent token pool reads as nothing spent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pool nullifier read admission gained looks a token up under the token shielded pools root, and that root is only created by the version that introduced the pools — at a v14 genesis, or by the v14 transition on a chain upgraded from v13. Before it the whole subtree is absent, and GroveDB answers some reads of a subtree that is not there with an error rather than with "absent": the shielded note-count and encrypted-note queries were fixed for precisely that, where the error reached the client as gRPC Internal and wrote a per-request error log. I expected the same here and went looking for it with a test, on the theory that the read needed a protocol-version guard in front of it. It does not. A membership read of an absent subtree answers absent, so the read already gives the answer this wants, and the guard I was about to write would have been dead code justified by a story rather than by evidence: a_version_with_no_token_pools_root_reads_as_nothing_spent ... ok a_token_with_no_pool_holds_no_spent_nullifier ... ok test result: ok. 2 passed; 0 failed Which read errors and which answers absent is GroveDB's behaviour, not something this function arranges, and nothing in this crate pinned it. These two tests do: one for a version with no pools root at all, one for a token under an existing root that never opted into a pool. Either answering "spent" would refuse a batch for a note it has not spent; either erroring would turn a mempool decision into an internal error. If GroveDB's answer ever changes, these go red here rather than in production. Tests only — no production change, and nothing to be red before it. Co-Authored-By: Claude Opus 5 (1M context) --- .../processor/traits/shielded_proof.rs | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs index 1f4f95fe2fc..6d784fe6647 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs @@ -928,6 +928,11 @@ pub(crate) fn validate_batch_token_pool_nullifiers_unspent( drive: &Drive, platform_version: &PlatformVersion, ) -> Result { + // The pools root subtree is created by the version that introduced token shielded pools, and + // a membership read under a subtree that is not there fails inside GroveDB rather than + // answering "absent". Before that version `is_allowed` refuses any batch naming a pool kind + // on the first check, so nothing pool-bearing is in the mempool for the re-check to read + // either. let mut drive_operations = vec![]; let mut pools_without_a_subtree = HashSet::new(); @@ -2210,4 +2215,93 @@ mod tests { } } } + + mod validate_batch_token_pool_nullifiers_unspent_tests { + use super::*; + use crate::execution::validation::state_transition::state_transitions::test_helpers::{ + create_dummy_serialized_action, setup_platform, + }; + use crate::test::helpers::setup::TestPlatformBuilder; + use dpp::prelude::Identifier; + use dpp::version::feature_initial_protocol_versions::TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION; + use dpp::state_transition::batch_transition::batched_transition::token_base_transition::v0::TokenBaseTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::token_base_transition::TokenBaseTransition; + use dpp::state_transition::batch_transition::batched_transition::token_unshield_transition::v0::TokenUnshieldTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::token_unshield_transition::TokenUnshieldTransition; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::BatchTransitionV1; + + const TOKEN_ID: [u8; 32] = [0xcc; 32]; + + /// A batch whose only transition unshields from `TOKEN_ID`'s pool, spending `nullifier`. + /// Unsigned and unproven: this path reads neither. + fn pool_bearing_batch(nullifier: [u8; 32]) -> BatchTransition { + let mut action = create_dummy_serialized_action(); + action.nullifier = nullifier; + BatchTransition::V1(BatchTransitionV1 { + owner_id: Identifier::new([1u8; 32]), + transitions: vec![BatchedTransition::Token(TokenTransition::Unshield( + TokenUnshieldTransition::V0(TokenUnshieldTransitionV0 { + base: TokenBaseTransition::V0(TokenBaseTransitionV0 { + identity_contract_nonce: 1, + token_contract_position: 0, + data_contract_id: Identifier::new([2u8; 32]), + token_id: Identifier::new(TOKEN_ID), + using_group_info: None, + }), + amount: 1, + recipient_id: Identifier::new([3u8; 32]), + actions: vec![action], + ..Default::default() + }), + ))], + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + }) + } + + /// The token shielded pools root is only created by the version that introduced the + /// pools, so before it the whole subtree this reads under is absent. GroveDB answers some + /// reads of a subtree that is not there with an error rather than with "absent" — the + /// shielded note-count and encrypted-note queries were fixed for exactly that — and an + /// error here would turn a mempool decision into an internal error the node logs at error + /// level. A membership read is not one of those: it answers absent, which is the answer + /// this needs. That is a GroveDB behaviour rather than something this function arranges, + /// so it is pinned here. + #[test] + fn a_version_with_no_token_pools_root_reads_as_nothing_spent() { + let before_pools = TOKEN_SHIELDED_POOL_INITIAL_PROTOCOL_VERSION - 1; + let platform = TestPlatformBuilder::new() + .with_initial_protocol_version(before_pools) + .build_with_mock_rpc() + .set_genesis_state(); + let platform_version = + PlatformVersion::get(before_pools).expect("the version before token pools"); + + let result = validate_batch_token_pool_nullifiers_unspent( + &pool_bearing_batch([9u8; 32]), + &platform.drive, + platform_version, + ) + .expect("must answer rather than fail inside GroveDB"); + assert!(result.is_valid(), "got {:?}", result.errors); + } + + /// A token that never opted into a pool has no subtree under the root either. "There is + /// no such pool" must read as nothing spent and never as a spent nullifier, which would + /// refuse a batch for a note it has not spent. + #[test] + fn a_token_with_no_pool_holds_no_spent_nullifier() { + let platform = setup_platform(); + + let result = validate_batch_token_pool_nullifiers_unspent( + &pool_bearing_batch([9u8; 32]), + &platform.drive, + PlatformVersion::latest(), + ) + .expect("must answer rather than fail inside GroveDB"); + assert!(result.is_valid(), "got {:?}", result.errors); + } + } } From 75a423ca8da366247890631c9a580340bab481ca Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 16:46:11 +0700 Subject: [PATCH 70/94] fix(drive-abci): drop a pool-existence read the nullifier check does not need MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two comments justified a membership read of the pools root before reading a token's spent nullifiers, both claiming that a membership read under a subtree that is not there fails inside GroveDB instead of answering absent. It answers absent, which `a_version_with_no_token_pools_root_reads_as_nothing_spent` and `a_token_with_no_pool_holds_no_spent_nullifier` pin. So the read found nothing the nullifier read would not have found, and it cost one read per distinct token on the CheckTx path for a token that has a pool — which is every token these transitions are for. It saved reads only for a batch naming a pool its token does not have, which the block refuses anyway. Gone, along with the set that memoized it and both comments. Co-Authored-By: Claude Opus 5 (1M context) --- .../processor/traits/shielded_proof.rs | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs index 6d784fe6647..a56e185ce35 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs @@ -52,11 +52,7 @@ use dpp::state_transition::{StateTransition, StateTransitionOwned}; use dpp::util::hash::hash_single; use dpp::validation::SimpleConsensusValidationResult; use dpp::version::PlatformVersion; -use drive::drive::tokens::paths::token_shielded_pools_root_path; use drive::drive::Drive; -use drive::grovedb_path::SubtreePath; -use drive::util::grove_operations::DirectQueryType; -use std::collections::HashSet; /// The (identity, nonce) pair CheckTx's `CheckTxProofVerifier` admits Orchard proof work under /// for identity-signed shielded transitions, so an identity cannot start unbounded verification @@ -928,13 +924,7 @@ pub(crate) fn validate_batch_token_pool_nullifiers_unspent( drive: &Drive, platform_version: &PlatformVersion, ) -> Result { - // The pools root subtree is created by the version that introduced token shielded pools, and - // a membership read under a subtree that is not there fails inside GroveDB rather than - // answering "absent". Before that version `is_allowed` refuses any batch naming a pool kind - // on the first check, so nothing pool-bearing is in the mempool for the re-check to read - // either. let mut drive_operations = vec![]; - let mut pools_without_a_subtree = HashSet::new(); for transition in batch.transitions_iter() { let (token_id, actions) = match transition { @@ -963,25 +953,6 @@ pub(crate) fn validate_batch_token_pool_nullifiers_unspent( } }; - if pools_without_a_subtree.contains(&token_id) { - continue; - } - // A token that never opted into a shielded pool has no pool subtree, and a membership - // read under a subtree that is not there is not a read of an empty pool. Such a batch is - // refused in the block for naming a pool the token does not have, so there is nothing - // for this to find. - if !drive.grove_has_raw( - SubtreePath::from(&token_shielded_pools_root_path()), - &token_id, - DirectQueryType::StatefulDirectQuery, - None, - &mut drive_operations, - &platform_version.drive, - )? { - pools_without_a_subtree.insert(token_id); - continue; - } - for action in actions { if drive.has_token_pool_nullifier( &token_id, From f78f4cb4d02541a0cb885a475a8fc1c167f8aa72 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 18:09:29 +0700 Subject: [PATCH 71/94] style(drive-abci): put the credit pool path import in rustfmt order The import restoring the credit pool path to the encrypted notes tests was committed out of order, so the committed tree fails `cargo fmt --check` even though the working tree passed it. Co-Authored-By: Claude Opus 5 (1M context) --- .../rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs b/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs index 8d28d272759..31485ba8348 100644 --- a/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/encrypted_notes/v0/mod.rs @@ -179,11 +179,11 @@ impl Platform { #[cfg(test)] mod tests { use super::*; - use drive::drive::shielded::paths::shielded_credit_pool_path; use crate::query::tests::setup_platform; use crate::rpc::core::MockCoreRPCLike; use crate::test::helpers::setup::TempPlatform; use dpp::dashcore::Network; + use drive::drive::shielded::paths::shielded_credit_pool_path; use grovedb_commitment_tree::{DashMemo, NoteBytesData, TransmittedNoteCiphertext}; /// MMR chunk size used for alignment. Derived from From d3f8d918692cd7631a383880bdb965439a46bf52 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 18:28:23 +0700 Subject: [PATCH 72/94] docs(dpp): correct five comments that justified an edit by an untrue claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three arms for the pool threshold items said they "cannot reach this generation on the protocol versions that select it". These methods dispatch on the configuration variant, not on the protocol version: at version 14 a threshold change against a token with no pool carries a version 0 configuration, reaches all three, and is refused there — which is what makes it a paid UnauthorizedTokenActionError. `should_refuse_the_threshold_items_for_a_token_without_a_pool` drives exactly that path. Anyone who believed the comment and wrote `unreachable!()` would have halted block execution. The batch's basic structure validation said the same of its pool arms, on a generation that is the only one there is, so version 14 runs them as its live path rather than as filler. `validate_token_configuration_groups_exist` v1 claimed the one version that selects it admits only a version 0 configuration; a later version selects it as well and can present a version 1, where reading rules through the enum is the point rather than an equivalence. Co-Authored-By: Claude Opus 5 (1M context) --- .../apply_token_configuration_item/v0/mod.rs | 10 +- .../v0/mod.rs | 6 +- .../v0/mod.rs | 6 +- .../v1/mod.rs | 13 +-- .../validate_basic_structure/v0/mod.rs | 10 +- .../v0/transformer.rs | 94 +++++++++++++++++++ 6 files changed, 121 insertions(+), 18 deletions(-) diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/v0/mod.rs index 39a94c44ff2..5135b6218c0 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/apply_token_configuration_item/v0/mod.rs @@ -151,10 +151,12 @@ impl TokenConfigurationV0 { .trade_mode_change_rules_mut() .set_admin_action_takers(admin_group); } - // A V0 configuration has no shielded pool and so no threshold to change; - // `can_apply_token_configuration_item` refuses these items for it. - // The three pool threshold items below cannot reach this generation on the protocol - // versions that select it. They are appended at the tail of + // A V0 configuration has no shielded pool and so no threshold to change. This arm is + // reached at protocol version 14 for such a token, and never applies anything: + // `can_apply_token_configuration_item` has already refused the item, so the change + // ends as a paid `UnauthorizedTokenActionError` rather than here. + // Released protocol versions cannot reach these items at all: they are appended at + // the tail of // `TokenConfigurationChangeItem`, so no existing item's encoding or group action key // moves, and this match has no catch-all, so nothing previously handled is rerouted. // A batch carrying one is refused unpaid by the batch's `is_allowed` gate below the diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/v0/mod.rs index 2f51be340c0..1f674663610 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/authorized_action_takers_for_configuration_item/v0/mod.rs @@ -135,8 +135,10 @@ impl TokenConfigurationV0 { .trade_mode_change_rules() .admin_action_takers(), // A V0 configuration has no shielded pool and so no threshold for anyone to change. - // The three pool threshold items below cannot reach this generation on the protocol - // versions that select it. They are appended at the tail of + // This arm is reached at protocol version 14 for such a token: it answers `NoOne`, + // which is what turns the change into a paid `UnauthorizedTokenActionError`. + // Released protocol versions cannot reach these items at all: they are appended at + // the tail of // `TokenConfigurationChangeItem`, so no existing item's encoding or group action key // moves, and this match has no catch-all, so nothing previously handled is rerouted. // A batch carrying one is refused unpaid by the batch's `is_allowed` gate below the diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/v0/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/v0/mod.rs index 6f19a2c937d..eaed1db6889 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/v0/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/can_apply_token_configuration_item/v0/mod.rs @@ -318,8 +318,10 @@ impl TokenConfigurationV0 { goal, ), // A V0 configuration has no shielded pool and so no threshold for anyone to change. - // The three pool threshold items below cannot reach this generation on the protocol - // versions that select it. They are appended at the tail of + // This arm is reached at protocol version 14 for such a token and refuses the item, + // which is what turns the change into a paid `UnauthorizedTokenActionError`. + // Released protocol versions cannot reach these items at all: they are appended at + // the tail of // `TokenConfigurationChangeItem`, so no existing item's encoding or group action key // moves, and this match has no catch-all, so nothing previously handled is rerouted. // A batch carrying one is refused unpaid by the batch's `is_allowed` gate below the diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_groups_exist/v1/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_groups_exist/v1/mod.rs index c90417e8cdc..9f132ba75de 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_groups_exist/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/methods/validate_token_configuration_groups_exist/v1/mod.rs @@ -21,12 +21,13 @@ impl TokenConfiguration { } // Reading the rules through the enum rather than the narrowed V0 base is - // output-identical on the protocol version that selects this generation, which admits - // only a version 0 token configuration: the enum's `all_change_control_rules` delegates - // straight to the V0 list for a V0 configuration, so the same rules arrive in the same - // order and the same first violation wins, and `main_control_group` reads the same V0 - // field. A configuration of a later format is refused unpaid before any structure - // validation runs. + // output-identical for a version 0 token configuration: the enum's + // `all_change_control_rules` delegates straight to the V0 list, so the same rules arrive + // in the same order and the same first violation wins, and `main_control_group` reads the + // same V0 field. Every released protocol version admits only a version 0 configuration, + // so nothing they validate moves. A later version selects this generation too and can + // present a version 1 configuration, where reading through the enum is the point rather + // than an equivalence: it reaches that format's own rules. // // Iterate through the enum rather than narrowing to the V0 base first. The narrowed // view cannot see rules a later configuration version adds, and the v0 pass above is diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs index 1607d179b0d..de098140a44 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/validation/validate_basic_structure/v0/mod.rs @@ -251,10 +251,12 @@ impl BatchTransition { ) => { set_price_for_direct_purchase_transition.validate_structure(platform_version)? } - // Every token shielded pool arm below is unreachable on the protocol versions - // that select this generation: the batch's `is_allowed` gate refuses each pool - // transition kind until the version that admits them. The arms keep the match - // exhaustive and carry the same structure validation that version runs. + // Released protocol versions cannot reach the token shielded pool arms below: + // the batch's `is_allowed` gate refuses each pool transition kind until the + // version that admits them. This generation is the only one there is, so the + // version that admits them runs these arms as its live path — they are the + // structure validation for a pool transition, not filler to keep the match + // exhaustive. TokenTransition::Shield(shield_transition) => { shield_transition.validate_structure(platform_version)? } diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs index 4c1607af0c1..bedfb524e91 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs @@ -941,6 +941,100 @@ mod tests { assert_eq!(recipient, TokenDistributionRecipient::Identity(id)); } + /// Reimplements the closure passed to `rewards_in_interval` inside the + /// `EvonodesByParticipation` branch and checks its single-epoch output + /// matches the blocks proposed by the owner. + #[test] + fn evonodes_reward_ratio_single_epoch() { + let me = owner(); + let other = stranger(); + let mut block_proposers: BTreeMap = BTreeMap::new(); + block_proposers.insert(me, 7); + block_proposers.insert(other, 3); + let total_blocks_in_epoch = 10u64; + + let ratio = RewardRatio { + numerator: block_proposers.get(&me).copied().unwrap_or_default(), + denominator: total_blocks_in_epoch, + }; + assert_eq!(ratio.numerator, 7); + assert_eq!(ratio.denominator, 10); + } + + /// Multi-epoch branch should sum totals and only count blocks proposed by the owner. + #[test] + fn evonodes_reward_ratio_multi_epoch_sum() { + let me = owner(); + let mut epochs: BTreeMap)> = BTreeMap::new(); + + let mut bp1 = BTreeMap::new(); + bp1.insert(me, 5); + epochs.insert(0, (20, bp1)); + + let mut bp2 = BTreeMap::new(); + bp2.insert(me, 2); + epochs.insert(1, (30, bp2)); + + let mut total_blocks = 0u64; + let mut total_proposed_blocks = 0u64; + for idx in 0u16..=1u16 { + if let Some((blocks, proposers)) = epochs.get(&idx) { + total_blocks += *blocks; + total_proposed_blocks += proposers.get(&me).copied().unwrap_or_default(); + } + } + assert_eq!(total_blocks, 50); + assert_eq!(total_proposed_blocks, 7); + let ratio = if total_blocks > 0 { + Some(RewardRatio { + numerator: total_proposed_blocks, + denominator: total_blocks, + }) + } else { + None + }; + assert_eq!( + ratio, + Some(RewardRatio { + numerator: 7, + denominator: 50, + }) + ); + } + + /// With no epochs in range, total_blocks remains 0 so the ratio is None. + #[test] + fn evonodes_reward_ratio_empty_range_returns_none() { + let total_blocks = 0u64; + let total_proposed_blocks = 0u64; + let ratio = if total_blocks > 0 { + Some(RewardRatio { + numerator: total_proposed_blocks, + denominator: total_blocks, + }) + } else { + None + }; + assert!(ratio.is_none()); + } + + /// When an epoch index exists but the owner never proposed, numerator is 0. + #[test] + fn evonodes_reward_ratio_owner_has_no_proposed_blocks() { + let me = owner(); + let other = stranger(); + let mut bp: BTreeMap = BTreeMap::new(); + bp.insert(other, 4); + let total_blocks = 10u64; + + let ratio = RewardRatio { + numerator: bp.get(&me).copied().unwrap_or_default(), + denominator: total_blocks, + }; + assert_eq!(ratio.numerator, 0); + assert_eq!(ratio.denominator, 10); + } + /// The `last_paid_moment.unwrap_or(contract_creation_cycle_start)` fallback /// should pick the explicit last-paid if set, otherwise the cycle start. #[test] From 24773121333d1d8e50f6a5d16022978c7dd81c34 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 18:28:37 +0700 Subject: [PATCH 73/94] docs: stop four comments claiming more than is true or tested Two comments described the mempool as reading nothing from the pool, in the present tense, inside the code that changed it. They now say what held before the check rather than describing the state it replaced as current. The pool-existence refusal claimed the verifiers read an absence proof and report `None`. The test behind it asserts only that a proof comes back; nothing runs a verifier against a missing pool, so the claim is marked as the intent and the verifier's side as uncovered. The credit pool's no-op arm gave a reason that is false: versions 1 through 11 serve these queries and the credit pool only exists from 12. What actually makes the arm right is that a client cannot name a credit pool the way it names a token's. A batch's identifier list said two batches naming one nullifier can never both execute. A group action's proposer and its confirmers carry the same bundle and are meant to, and the values are not namespaced by token. Both are harmless while only the first identifier is used, and both must be settled before a consumer looks past it. The unshield fee's rationale credited an SDK builder on the client; that builder has no caller outside tests. The argument is about what the code can read, not about who calls it, and now says so. Co-Authored-By: Claude Opus 5 (1M context) --- packages/rs-dpp/src/shielded/mod.rs | 6 +++-- .../v1/state_transition_like.rs | 14 ++++++++--- .../src/execution/check_tx/v0/mod.rs | 7 +++--- .../processor/traits/shielded_proof.rs | 6 ++--- .../rs-drive-abci/src/query/shielded/mod.rs | 25 +++++++++++-------- 5 files changed, 36 insertions(+), 22 deletions(-) diff --git a/packages/rs-dpp/src/shielded/mod.rs b/packages/rs-dpp/src/shielded/mod.rs index 9ec203d5658..729d57f32a6 100644 --- a/packages/rs-dpp/src/shielded/mod.rs +++ b/packages/rs-dpp/src/shielded/mod.rs @@ -178,8 +178,10 @@ pub const SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES: u64 = 20; /// pays the proposer only the remainder, so a component under the real cost comes out of the /// proposer's reward for the proof it verified and leaves the storage pool short of an item the /// chain then carries forever. Recipient state is not reachable where the number is needed in any -/// case: the fee is fixed by the SDK builder on the client and re-derived by the stateless -/// `validate_minimum_shielded_fee` gate, and neither reads a balance. +/// case: the builder that fixes the fee takes no drive and no transaction, and the stateless +/// `validate_minimum_shielded_fee` gate that re-derives it takes neither either, so no balance is +/// reachable from where the number is decided. (That builder has no caller outside tests yet; the +/// argument is about what it can read, not about who calls it.) /// /// The second reason is decisive even where that state IS reachable, and it is why the cheap case /// must not be split out later as an optimisation: `credit_amount` is public and must equal this diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/state_transition_like.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/state_transition_like.rs index 8c079aaf323..627dbd4ef51 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/state_transition_like.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/v1/state_transition_like.rs @@ -46,12 +46,18 @@ impl StateTransitionLike for BatchTransitionV1 { /// We create a list of unique identifiers for the batch /// /// Each transition contributes the owner, contract and nonce that replay-protect it. A batch - /// that spends notes from a token's shielded pool contributes each spent nullifier as well: - /// two batches naming one nullifier can never both execute, whichever of them lands records - /// it and the other is refused on it, and their nonces need not be alike, so without the - /// nullifier nothing about them collides. The credit pool's shielded transitions identify + /// that spends notes from a token's shielded pool contributes each spent nullifier as well, + /// because two independent batches naming one nullifier cannot both execute — whichever lands + /// records it and the other is refused on it — and their nonces need not be alike, so without + /// the nullifier nothing about them collides. The credit pool's shielded transitions identify /// themselves by their nullifiers alone; these are the same values in the same encoding. /// + /// Sharing a nullifier does not always mean mutual exclusion, so a consumer must not read it + /// that way: a group action's proposer and its confirmers carry the same bundle, and are meant + /// to. The values are also not namespaced by token, so two pools' nullifiers are compared as + /// if they were one pool's. Neither matters while only the first identifier is used, and both + /// have to be settled before any consumer looks past it. + /// /// The nonce keys stay in front. Only the first identifier reaches the mempool today, and /// that one has to remain the key that stops a second batch replaying a nonce. fn unique_identifiers(&self) -> Vec { diff --git a/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs b/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs index 4132509e661..fb6d7e4da69 100644 --- a/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/check_tx/v0/mod.rs @@ -198,9 +198,10 @@ where // fail: block validation refuses it on that record. Reading the record costs a few // key lookups against a pool tree; verifying the bundles the batch carries costs a // Halo 2 verification per bundle, which the node would otherwise pay for before - // discovering the free reason to refuse. Batches are the only transitions whose pool - // bundles are checked against state nowhere in the mempool passes: the pool-paid - // transitions do it in their own transformer, which runs here too. + // discovering the free reason to refuse. A batch needed this read of its own: the + // pool-paid transitions check their bundles against state in their own transformer, + // which runs here, while a batch's pool checks live in per-action state validation, + // which does not. // // Both levels read it. The first check keeps such a batch out; the re-check after // every block is what evicts one that was admitted while its note was still unspent, diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs index a56e185ce35..6be3f347c16 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/shielded_proof.rs @@ -910,9 +910,9 @@ fn key_not_allowed_in_shielded_creation( /// /// CheckTx only. A batch's pool bundles are checked against the pool in block validation, inside /// each pool action's state validation, where a failure is a paid one — the identity contract -/// nonce is consumed and the fee charged. Nothing in the mempool passes reads the pool: a batch -/// whose note has been spent is admitted, and the node pays for the Halo 2 verification of a -/// bundle that block validation will refuse on a handful of key lookups. This is those lookups, +/// nonce is consumed and the fee charged. Before this check, no mempool pass read the pool at all: +/// a batch whose note had been spent was admitted, and the node paid for the Halo 2 verification of +/// a bundle that block validation would refuse on a handful of key lookups. This is those lookups, /// run before the verification, so the free reason to refuse is found first. /// /// It cannot change a consensus outcome. What it refuses is refused unpaid, and an unpaid refusal diff --git a/packages/rs-drive-abci/src/query/shielded/mod.rs b/packages/rs-drive-abci/src/query/shielded/mod.rs index a109dbcd64a..b06fbfbad6b 100644 --- a/packages/rs-drive-abci/src/query/shielded/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/mod.rs @@ -74,17 +74,20 @@ impl ShieldedPoolSelector { /// answered as the first. /// /// Only the unproved reads need this. A proof over a pool that does not exist is a proof of - /// its absence, which `Drive::verify_pool_notes_count_v0` and its siblings verify against the - /// root hash and report as `None` — an answer the client can check rather than has to trust. + /// its absence, which is an answer the client can check against the root hash rather than has + /// to trust. That a verifier walks across the missing pool layer and reports `None` is the + /// intent; the proved path is covered only as far as returning a proof, so treat the + /// verifier's side of it as untested. pub(super) fn validate_pool_exists( &self, drive: &Drive, platform_version: &PlatformVersion, ) -> Result, Error> { match self { - // The credit pool's subtree is part of the initial state structure of every version - // that serves these queries, so its absence would be corrupted state rather than a - // client naming a pool that was never created. + // The credit pool is not named by the client, so there is nothing here to refuse: a + // caller cannot ask for a credit pool that was never created the way it can ask for a + // token's. Versions that serve these queries before the credit pool exists read it as + // empty, which is what they did before this check was added. ShieldedPoolSelector::Credit => Ok(Ok(())), ShieldedPoolSelector::Token(token_id) => { let pools_root = token_shielded_pools_root_path(); @@ -596,11 +599,13 @@ mod tests { /// The proved reads keep answering a pool the chain does not have with a proof, deliberately. /// - /// GroveDB proves the pool's absence, and the verifiers - /// (`Drive::verify_pool_notes_count_v0` and its siblings) read that proof against the root - /// hash and report `None`. That is an answer the client checks rather than has to trust, so - /// it is worth more than the `NotFound` the unproved reads give. Whoever changes this must - /// change the verifiers with it. + /// GroveDB proves the pool's absence, which is an answer the client checks against the root + /// hash rather than has to trust, so it is worth more than the `NotFound` the unproved reads + /// give. Whoever changes this must change the verifiers with it. + /// + /// This test asserts only that a proof comes back. Whether + /// `Drive::verify_pool_notes_count_v0` and its siblings can walk across the absent pool layer + /// and report `None` is not covered here and is worth its own test. #[test] fn proved_queries_answer_a_pool_the_chain_does_not_have_with_an_absence_proof() { let (platform, state, version) = setup_platform_with_a_token_pool(); From b153be91e093b8a5afe74718849bffbcfc75327b Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 18:28:52 +0700 Subject: [PATCH 74/94] Revert "test(drive): drop four evonode reward-ratio tests that test only themselves" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The four tests are upstream's, still present on v4.2-dev, and deleting them here buys this branch nothing while guaranteeing a conflict on the next base merge. They really are tautological — each builds a local map, computes the ratio in the test body and asserts on its own expression — so the deletion is worth proposing upstream on its own. The reverted commit message was also wrong. It said the reward ratio returns `Some` for an empty range "and always has"; the shipped generation that protocol versions up to 13 run returns `Some` only `if total_blocks > 0`, so it returns `None` there, and only the new generation returns `Some(0/1)` unconditionally. The deleted test's name described the shipped rule correctly. Restored with the two imports the test module needs of its own, since it does not read `super`. All four pass. Co-Authored-By: Claude Opus 5 (1M context) --- .../token_claim_transition_action/v0/transformer.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs index bedfb524e91..e0d96af7bb8 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs @@ -636,6 +636,8 @@ mod tests { TokenDistributionRecipient, TokenDistributionResolvedRecipient, }; use dpp::data_contract::associated_token::token_perpetual_distribution::reward_distribution_moment::RewardDistributionMoment; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::reward_ratio::RewardRatio; + use dpp::block::epoch::EpochIndex; use dpp::identifier::Identifier; use dpp::prelude::TimestampMillis; use std::collections::BTreeMap; From 7da86223946b25050e4e7465d7025fa412bbb606 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 18:28:52 +0700 Subject: [PATCH 75/94] fix(dpp): keep the pool result variants out of the TryInto derive `derive_more::TryInto` writes one `TryFrom` per distinct field-type list and ORs every matching variant into it. `TokenAmount` and `Credits` are both `u64`, so `VerifiedTokenShieldedPoolBalance` shares its list with `VerifiedTokenBalance` and `VerifiedTokenGroupActionWithShieldedPoolBalance` shares its list with `VerifiedTokenGroupActionWithTokenBalance`. Adding them turned two conversions that identified one variant each into conversions that accept either, so a caller using the tuple type to tell them apart silently stopped doing so. Test would have caught this in CI: before, converting a pool balance into `(Identifier, u64)` returned `Ok(42)`; after, it is refused and the pre-existing conversion still identifies a token balance. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/state_transition/proof_result.rs | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/packages/rs-dpp/src/state_transition/proof_result.rs b/packages/rs-dpp/src/state_transition/proof_result.rs index 37117db10d4..4dcd7746587 100644 --- a/packages/rs-dpp/src/state_transition/proof_result.rs +++ b/packages/rs-dpp/src/state_transition/proof_result.rs @@ -106,6 +106,11 @@ pub enum StateTransitionProofResult { VerifiedShieldedNullifiers(Vec<(Vec, bool)>), /// The proven total balance of a token's shielded pool (token id, balance). Returned by the /// pool transitions that only create notes (mint, claim and purchase into the pool). + // Kept out of the `TryInto` derive: this shares a field-type list with a variant that + // predates it, and `derive_more` writes one `TryFrom` per distinct list. Including it + // would make that conversion accept either variant, so a caller using the tuple type to + // tell them apart would silently stop doing so. Match on the variant instead. + #[try_into(ignore)] VerifiedTokenShieldedPoolBalance( Identifier, #[cfg_attr( @@ -176,6 +181,11 @@ pub enum StateTransitionProofResult { /// the pool only once the last required signature arrives, so while the action is active the /// balance is the one the pool already held, and is absent for a pool that has never held a /// note. A closed action has minted, so its balance is present. + // Kept out of the `TryInto` derive: this shares a field-type list with a variant that + // predates it, and `derive_more` writes one `TryFrom` per distinct list. Including it + // would make that conversion accept either variant, so a caller using the tuple type to + // tell them apart would silently stop doing so. Match on the variant instead. + #[try_into(ignore)] VerifiedTokenGroupActionWithShieldedPoolBalance( GroupSumPower, GroupActionStatus, @@ -556,29 +566,29 @@ mod json_convertible_tests { let recovered = StateTransitionProofResult::from_object(value).expect("from_object"); assert_eq!(original, recovered); } - /// `TryInto` cannot tell a pool balance from a token balance, and is not meant to. + /// A pool balance must not convert through a pre-existing variant's `TryInto` impl. /// - /// `derive_more::TryInto` writes one impl per distinct field-type list, so variants that share - /// one share an impl and a successful conversion says nothing about which variant was held. - /// `TokenAmount` and `Credits` are both `u64`, so the pool balance shares its list with - /// `VerifiedTokenBalance`. This is the enum's existing shape, not something the pool variants - /// introduce: `VerifiedMasternodeVote` and `VerifiedNextDistribution` are both `(Vote)` and - /// have always shared an impl the same way. Match on the variant to discriminate; this test - /// exists so that a change to the derive, or to either field type, shows up here rather than - /// silently in a caller. + /// `derive_more::TryInto` writes one `TryFrom` per distinct field-type list and ORs every + /// matching variant into it. `TokenAmount` and `Credits` are both `u64`, so a pool balance's + /// list is identical to `VerifiedTokenBalance`'s, and including it would turn a conversion + /// that used to identify one variant into one that accepts either. The pool variants are + /// therefore excluded from the derive, and this pins that: the pre-existing conversion still + /// works and still means what it meant, and the pool variant is refused. #[test] - fn should_convert_a_pool_balance_through_the_token_balance_impl_it_shares() { + fn should_refuse_to_convert_a_pool_balance_through_a_pre_existing_variants_impl() { let id = Identifier::from([7u8; 32]); - let pool = StateTransitionProofResult::VerifiedTokenShieldedPoolBalance(id, 42); - let converted: (Identifier, u64) = pool - .try_into() - .expect("the shared impl accepts a pool balance"); - assert_eq!(converted, (id, 42)); let balance = StateTransitionProofResult::VerifiedTokenBalance(id, 42); let converted: (Identifier, u64) = balance .try_into() - .expect("and accepts a token balance identically"); + .expect("the pre-existing conversion still identifies a token balance"); assert_eq!(converted, (id, 42)); + + let pool = StateTransitionProofResult::VerifiedTokenShieldedPoolBalance(id, 42); + let refused: Result<(Identifier, u64), _> = pool.try_into(); + assert!( + refused.is_err(), + "a pool balance must not pass for a token balance" + ); } } From ae86bd0321e4554653bd1712a31eb128621a59d1 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 18:37:28 +0700 Subject: [PATCH 76/94] test(drive-abci): compare the Tokens Merk's own shape, not only its children MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `collect_subtree_diffs` walks the children at a root path and below, so it never sees the shape of the Merk at that path — which is exactly where a new root-level key is inserted. Two lineages can agree on every child and still carry different `root_key`s, and a different `root_key` is a different root hash. The equivalence guard now also compares the element that holds it. The token shielded pools root passes: one shared helper inserts one key into an already-existing non-empty parent, so neither insertion order nor the batch versus sequential mechanism can move the shape. The check binds. Pointed at `WithdrawalTransactions`, whose protocol-14 keys the genesis path batches and the upgrade path inserts sequentially, it reports `left: tree: [hex: 03]` against `right: tree: [hex: 02]`. Co-Authored-By: Claude Opus 5 (1M context) --- .../v0/mod.rs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs index 20b15d1b9b0..cdbe8ff9f99 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/protocol_upgrade/perform_events_on_first_block_of_protocol_change/v0/mod.rs @@ -3391,6 +3391,34 @@ mod tests { in-place-upgraded v14 node.\n{}", diffs.join("\n"), ); + + // `collect_subtree_diffs` walks the children AT `[Tokens]` and below, so it cannot see + // the shape of the `[Tokens]` Merk itself — and a new root-level key is inserted exactly + // there. Two lineages can therefore agree on every child and still carry different + // `root_key`s, which is a different app hash. Read the element that holds it. + let root_element = |platform: &crate::platform_types::platform::Platform< + crate::rpc::core::MockCoreRPCLike, + >, + txn: drive::grovedb::TransactionArg| { + platform + .drive + .grove + .get_raw( + drive::grovedb_path::SubtreePath::empty(), + &[RootTree::Tokens as u8], + txn, + &platform_version_14.drive.grove_version, + ) + .unwrap() + .expect("expected to read the [Tokens] root element") + }; + assert_eq!( + root_element(&platform_a, None), + root_element(&platform_b, Some(&txn_b)), + "CONSENSUS FORK: the [Tokens] Merk itself is shaped differently on a fresh genesis-v14 \ + node and an in-place-upgraded one, so their root hashes differ even though every \ + child matches" + ); } /// CONSENSUS-CRITICAL equivalence guard for the v11→v12 boundary. From 1ff727f9daf914fb884ec8bc8e73cefaff097d82 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 23:39:32 +0700 Subject: [PATCH 77/94] refactor(wasm-dpp): report shielded-pool token transitions as unsupported wasm-dpp is the legacy binding scheduled for removal, so it carries only what the compiler forces. The shielded-pool token transition kinds forced two exhaustive matches over TokenTransition. The one that converts to a JS value had been satisfied by adding seven wrapper classes that no JS or TS consumer ever reached. Drop those wrappers and have toTransition report the kinds as unsupported, matching how StateTransitionFactory already declines transition kinds this binding does not cover. All seven kinds are named explicitly rather than folded into a catch-all, so an eighth kind stays a compile error here. The TokenTransitionType variants stay. From<&TokenTransition> for TokenTransitionType is infallible, so dropping them would mean either misnaming a pool kind as an unrelated one or replacing the conversion with a fallible one: more code, plus a signature change on getTransitionType for every kind. Naming a kind is not supporting it; toTransition is where the binding declines. Making toTransition fallible does not change its generated TypeScript signature, which stays toTransition(): any. Net 341 lines removed from packages/wasm-dpp (369 deleted, 28 added). Verified: cargo check -p wasm-dpp --target wasm32-unknown-unknown; the crate build entry points yarn build:wasm and yarn build:js; yarn test:node (235 passing) and yarn test:types; cargo fmt --check --all. The generated wasm_dpp.d.ts no longer exports the seven classes. Browser tests (karma) were not run. Co-Authored-By: Claude Opus 5 (1M context) --- .../token_transition/burn_from_pool.rs | 46 ----------- .../token_transition/claim_to_pool.rs | 56 ------------- .../direct_purchase_to_pool.rs | 46 ----------- .../token_transition/mint_to_pool.rs | 46 ----------- .../batch_transition/token_transition/mod.rs | 79 +++++++------------ .../token_transition/shield.rs | 41 ---------- .../token_transition/shielded_transfer.rs | 36 --------- .../token_transition/unshield.rs | 47 ----------- 8 files changed, 28 insertions(+), 369 deletions(-) delete mode 100644 packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/burn_from_pool.rs delete mode 100644 packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/claim_to_pool.rs delete mode 100644 packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/direct_purchase_to_pool.rs delete mode 100644 packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/mint_to_pool.rs delete mode 100644 packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/shield.rs delete mode 100644 packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/shielded_transfer.rs delete mode 100644 packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/unshield.rs diff --git a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/burn_from_pool.rs b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/burn_from_pool.rs deleted file mode 100644 index 45ae46bc0fc..00000000000 --- a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/burn_from_pool.rs +++ /dev/null @@ -1,46 +0,0 @@ -use dpp::state_transition::batch_transition::token_burn_from_pool_transition::v0::v0_methods::TokenBurnFromPoolTransitionV0Methods; -use dpp::state_transition::batch_transition::TokenBurnFromPoolTransition; -use wasm_bindgen::prelude::wasm_bindgen; - -#[wasm_bindgen(js_name=TokenBurnFromPoolTransition)] -#[derive(Debug, Clone)] -pub struct TokenBurnFromPoolTransitionWasm(TokenBurnFromPoolTransition); - -impl From for TokenBurnFromPoolTransitionWasm { - fn from(value: TokenBurnFromPoolTransition) -> Self { - Self(value) - } -} - -#[wasm_bindgen(js_class = TokenBurnFromPoolTransition)] -impl TokenBurnFromPoolTransitionWasm { - #[wasm_bindgen(js_name=getAmount)] - pub fn amount(&self) -> u64 { - self.0.amount() - } - - #[wasm_bindgen(js_name=getPublicNote)] - pub fn public_note(&self) -> Option { - self.0.public_note().cloned() - } - - #[wasm_bindgen(js_name=getActionsCount)] - pub fn actions_count(&self) -> u32 { - self.0.actions().len() as u32 - } - - #[wasm_bindgen(js_name=getAnchor)] - pub fn anchor(&self) -> Vec { - self.0.anchor().to_vec() - } - - #[wasm_bindgen(js_name=getProof)] - pub fn proof(&self) -> Vec { - self.0.proof().to_vec() - } - - #[wasm_bindgen(js_name=getBindingSignature)] - pub fn binding_signature(&self) -> Vec { - self.0.binding_signature().to_vec() - } -} diff --git a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/claim_to_pool.rs b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/claim_to_pool.rs deleted file mode 100644 index 861412dab1c..00000000000 --- a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/claim_to_pool.rs +++ /dev/null @@ -1,56 +0,0 @@ -use dpp::data_contract::associated_token::token_distribution_key::TokenDistributionType; -use dpp::state_transition::batch_transition::token_claim_to_pool_transition::v0::v0_methods::TokenClaimToPoolTransitionV0Methods; -use dpp::state_transition::batch_transition::TokenClaimToPoolTransition; -use wasm_bindgen::prelude::wasm_bindgen; - -#[wasm_bindgen(js_name=TokenClaimToPoolTransition)] -#[derive(Debug, Clone)] -pub struct TokenClaimToPoolTransitionWasm(TokenClaimToPoolTransition); - -impl From for TokenClaimToPoolTransitionWasm { - fn from(value: TokenClaimToPoolTransition) -> Self { - Self(value) - } -} - -#[wasm_bindgen(js_class = TokenClaimToPoolTransition)] -impl TokenClaimToPoolTransitionWasm { - #[wasm_bindgen(js_name=getDistributionType)] - pub fn distribution_type(&self) -> u8 { - match self.0.distribution_type() { - TokenDistributionType::PreProgrammed => 0, - TokenDistributionType::Perpetual => 1, - TokenDistributionType::OncePerIdentity => 2, - } - } - - #[wasm_bindgen(js_name=getClaimUpTo)] - pub fn claim_up_to(&self) -> Option { - self.0.claim_up_to() - } - - #[wasm_bindgen(js_name=getPublicNote)] - pub fn public_note(&self) -> Option { - self.0.public_note().cloned() - } - - #[wasm_bindgen(js_name=getActionsCount)] - pub fn actions_count(&self) -> u32 { - self.0.actions().len() as u32 - } - - #[wasm_bindgen(js_name=getAnchor)] - pub fn anchor(&self) -> Vec { - self.0.anchor().to_vec() - } - - #[wasm_bindgen(js_name=getProof)] - pub fn proof(&self) -> Vec { - self.0.proof().to_vec() - } - - #[wasm_bindgen(js_name=getBindingSignature)] - pub fn binding_signature(&self) -> Vec { - self.0.binding_signature().to_vec() - } -} diff --git a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/direct_purchase_to_pool.rs b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/direct_purchase_to_pool.rs deleted file mode 100644 index 3ac95ad9c4a..00000000000 --- a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/direct_purchase_to_pool.rs +++ /dev/null @@ -1,46 +0,0 @@ -use dpp::state_transition::batch_transition::token_direct_purchase_to_pool_transition::v0::v0_methods::TokenDirectPurchaseToPoolTransitionV0Methods; -use dpp::state_transition::batch_transition::TokenDirectPurchaseToPoolTransition; -use wasm_bindgen::prelude::wasm_bindgen; - -#[wasm_bindgen(js_name=TokenDirectPurchaseToPoolTransition)] -#[derive(Debug, Clone)] -pub struct TokenDirectPurchaseToPoolTransitionWasm(TokenDirectPurchaseToPoolTransition); - -impl From for TokenDirectPurchaseToPoolTransitionWasm { - fn from(value: TokenDirectPurchaseToPoolTransition) -> Self { - Self(value) - } -} - -#[wasm_bindgen(js_class = TokenDirectPurchaseToPoolTransition)] -impl TokenDirectPurchaseToPoolTransitionWasm { - #[wasm_bindgen(js_name=getTokenCount)] - pub fn token_count(&self) -> u64 { - self.0.token_count() - } - - #[wasm_bindgen(js_name=getTotalAgreedPrice)] - pub fn total_agreed_price(&self) -> u64 { - self.0.total_agreed_price() - } - - #[wasm_bindgen(js_name=getActionsCount)] - pub fn actions_count(&self) -> u32 { - self.0.actions().len() as u32 - } - - #[wasm_bindgen(js_name=getAnchor)] - pub fn anchor(&self) -> Vec { - self.0.anchor().to_vec() - } - - #[wasm_bindgen(js_name=getProof)] - pub fn proof(&self) -> Vec { - self.0.proof().to_vec() - } - - #[wasm_bindgen(js_name=getBindingSignature)] - pub fn binding_signature(&self) -> Vec { - self.0.binding_signature().to_vec() - } -} diff --git a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/mint_to_pool.rs b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/mint_to_pool.rs deleted file mode 100644 index be22dee4481..00000000000 --- a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/mint_to_pool.rs +++ /dev/null @@ -1,46 +0,0 @@ -use dpp::state_transition::batch_transition::token_mint_to_pool_transition::v0::v0_methods::TokenMintToPoolTransitionV0Methods; -use dpp::state_transition::batch_transition::TokenMintToPoolTransition; -use wasm_bindgen::prelude::wasm_bindgen; - -#[wasm_bindgen(js_name=TokenMintToPoolTransition)] -#[derive(Debug, Clone)] -pub struct TokenMintToPoolTransitionWasm(TokenMintToPoolTransition); - -impl From for TokenMintToPoolTransitionWasm { - fn from(value: TokenMintToPoolTransition) -> Self { - Self(value) - } -} - -#[wasm_bindgen(js_class = TokenMintToPoolTransition)] -impl TokenMintToPoolTransitionWasm { - #[wasm_bindgen(js_name=getAmount)] - pub fn amount(&self) -> u64 { - self.0.amount() - } - - #[wasm_bindgen(js_name=getPublicNote)] - pub fn public_note(&self) -> Option { - self.0.public_note().cloned() - } - - #[wasm_bindgen(js_name=getActionsCount)] - pub fn actions_count(&self) -> u32 { - self.0.actions().len() as u32 - } - - #[wasm_bindgen(js_name=getAnchor)] - pub fn anchor(&self) -> Vec { - self.0.anchor().to_vec() - } - - #[wasm_bindgen(js_name=getProof)] - pub fn proof(&self) -> Vec { - self.0.proof().to_vec() - } - - #[wasm_bindgen(js_name=getBindingSignature)] - pub fn binding_signature(&self) -> Vec { - self.0.binding_signature().to_vec() - } -} diff --git a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/mod.rs b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/mod.rs index c1d5a7a1356..9c999f1860b 100644 --- a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/mod.rs +++ b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/mod.rs @@ -1,40 +1,26 @@ pub mod burn; -pub mod burn_from_pool; mod claim; -pub mod claim_to_pool; pub mod config; pub mod destroy; pub mod direct_purchase; -pub mod direct_purchase_to_pool; pub mod emergency_action; pub mod freeze; pub mod mint; -pub mod mint_to_pool; pub mod set_price_for_direct_purchase; -pub mod shield; -pub mod shielded_transfer; pub mod transfer; pub mod unfreeze; -pub mod unshield; use crate::batch_transition::token_transition::burn::TokenBurnTransitionWasm; -use crate::batch_transition::token_transition::burn_from_pool::TokenBurnFromPoolTransitionWasm; use crate::batch_transition::token_transition::claim::TokenClaimTransitionWasm; -use crate::batch_transition::token_transition::claim_to_pool::TokenClaimToPoolTransitionWasm; use crate::batch_transition::token_transition::config::TokenConfigUpdateTransitionWasm; use crate::batch_transition::token_transition::destroy::TokenDestroyFrozenFundsTransitionWasm; use crate::batch_transition::token_transition::direct_purchase::TokenDirectPurchaseTransitionWasm; -use crate::batch_transition::token_transition::direct_purchase_to_pool::TokenDirectPurchaseToPoolTransitionWasm; use crate::batch_transition::token_transition::emergency_action::TokenEmergencyActionTransitionWasm; use crate::batch_transition::token_transition::freeze::TokenFreezeTransitionWasm; use crate::batch_transition::token_transition::mint::TokenMintTransitionWasm; -use crate::batch_transition::token_transition::mint_to_pool::TokenMintToPoolTransitionWasm; use crate::batch_transition::token_transition::set_price_for_direct_purchase::TokenSetPriceForDirectPurchaseTransitionWasm; -use crate::batch_transition::token_transition::shield::TokenShieldTransitionWasm; -use crate::batch_transition::token_transition::shielded_transfer::TokenShieldedTransferTransitionWasm; use crate::batch_transition::token_transition::transfer::TokenTransferTransitionWasm; use crate::batch_transition::token_transition::unfreeze::TokenUnfreezeTransitionWasm; -use crate::batch_transition::token_transition::unshield::TokenUnshieldTransitionWasm; use crate::identifier::IdentifierWrapper; use dpp::prelude::IdentityNonce; use dpp::state_transition::batch_transition::batched_transition::token_transition::{ @@ -148,57 +134,48 @@ impl TokenTransitionWasm { } #[wasm_bindgen(js_name=toTransition)] - pub fn to_transition(&self) -> JsValue { + pub fn to_transition(&self) -> Result { match &self.0 { - TokenTransition::Burn(burn) => TokenBurnTransitionWasm::from(burn.clone()).into(), - TokenTransition::Mint(mint) => TokenMintTransitionWasm::from(mint.clone()).into(), + TokenTransition::Burn(burn) => Ok(TokenBurnTransitionWasm::from(burn.clone()).into()), + TokenTransition::Mint(mint) => Ok(TokenMintTransitionWasm::from(mint.clone()).into()), TokenTransition::Transfer(transfer) => { - TokenTransferTransitionWasm::from(transfer.clone()).into() + Ok(TokenTransferTransitionWasm::from(transfer.clone()).into()) } TokenTransition::Freeze(freeze) => { - TokenFreezeTransitionWasm::from(freeze.clone()).into() + Ok(TokenFreezeTransitionWasm::from(freeze.clone()).into()) } TokenTransition::Unfreeze(unfreeze) => { - TokenUnfreezeTransitionWasm::from(unfreeze.clone()).into() - } - TokenTransition::DestroyFrozenFunds(destroy_frozen_funds) => { - TokenDestroyFrozenFundsTransitionWasm::from(destroy_frozen_funds.clone()).into() + Ok(TokenUnfreezeTransitionWasm::from(unfreeze.clone()).into()) } + TokenTransition::DestroyFrozenFunds(destroy_frozen_funds) => Ok( + TokenDestroyFrozenFundsTransitionWasm::from(destroy_frozen_funds.clone()).into(), + ), TokenTransition::EmergencyAction(emergency_action) => { - TokenEmergencyActionTransitionWasm::from(emergency_action.clone()).into() + Ok(TokenEmergencyActionTransitionWasm::from(emergency_action.clone()).into()) } TokenTransition::ConfigUpdate(config_update) => { - TokenConfigUpdateTransitionWasm::from(config_update.clone()).into() + Ok(TokenConfigUpdateTransitionWasm::from(config_update.clone()).into()) + } + TokenTransition::Claim(claim) => { + Ok(TokenClaimTransitionWasm::from(claim.clone()).into()) } - TokenTransition::Claim(claim) => TokenClaimTransitionWasm::from(claim.clone()).into(), TokenTransition::DirectPurchase(direct_purchase) => { - TokenDirectPurchaseTransitionWasm::from(direct_purchase.clone()).into() + Ok(TokenDirectPurchaseTransitionWasm::from(direct_purchase.clone()).into()) } TokenTransition::SetPriceForDirectPurchase(set_price) => { - TokenSetPriceForDirectPurchaseTransitionWasm::from(set_price.clone()).into() - } - TokenTransition::Shield(shield) => { - TokenShieldTransitionWasm::from(shield.clone()).into() - } - TokenTransition::Unshield(unshield) => { - TokenUnshieldTransitionWasm::from(unshield.clone()).into() - } - TokenTransition::ShieldedTransfer(shielded_transfer) => { - TokenShieldedTransferTransitionWasm::from(shielded_transfer.clone()).into() - } - TokenTransition::MintToPool(mint_to_pool) => { - TokenMintToPoolTransitionWasm::from(mint_to_pool.clone()).into() - } - TokenTransition::BurnFromPool(burn_from_pool) => { - TokenBurnFromPoolTransitionWasm::from(burn_from_pool.clone()).into() - } - TokenTransition::ClaimToPool(claim_to_pool) => { - TokenClaimToPoolTransitionWasm::from(claim_to_pool.clone()).into() - } - TokenTransition::DirectPurchaseToPool(direct_purchase_to_pool) => { - TokenDirectPurchaseToPoolTransitionWasm::from(direct_purchase_to_pool.clone()) - .into() - } + Ok(TokenSetPriceForDirectPurchaseTransitionWasm::from(set_price.clone()).into()) + } + // The shielded-pool token transition kinds have no wrapper class here, so there is + // nothing to hand back; wasm-dpp2 is the binding that exposes them. + TokenTransition::Shield(_) + | TokenTransition::Unshield(_) + | TokenTransition::ShieldedTransfer(_) + | TokenTransition::MintToPool(_) + | TokenTransition::BurnFromPool(_) + | TokenTransition::ClaimToPool(_) + | TokenTransition::DirectPurchaseToPool(_) => Err(JsValue::from_str( + "shielded and pool-backed token transitions are not supported in wasm-dpp TokenTransition; use wasm-dpp2", + )), } } } diff --git a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/shield.rs b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/shield.rs deleted file mode 100644 index 2f138c4bc41..00000000000 --- a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/shield.rs +++ /dev/null @@ -1,41 +0,0 @@ -use dpp::state_transition::batch_transition::token_shield_transition::v0::v0_methods::TokenShieldTransitionV0Methods; -use dpp::state_transition::batch_transition::TokenShieldTransition; -use wasm_bindgen::prelude::wasm_bindgen; - -#[wasm_bindgen(js_name=TokenShieldTransition)] -#[derive(Debug, Clone)] -pub struct TokenShieldTransitionWasm(TokenShieldTransition); - -impl From for TokenShieldTransitionWasm { - fn from(value: TokenShieldTransition) -> Self { - Self(value) - } -} - -#[wasm_bindgen(js_class = TokenShieldTransition)] -impl TokenShieldTransitionWasm { - #[wasm_bindgen(js_name=getAmount)] - pub fn amount(&self) -> u64 { - self.0.amount() - } - - #[wasm_bindgen(js_name=getActionsCount)] - pub fn actions_count(&self) -> u32 { - self.0.actions().len() as u32 - } - - #[wasm_bindgen(js_name=getAnchor)] - pub fn anchor(&self) -> Vec { - self.0.anchor().to_vec() - } - - #[wasm_bindgen(js_name=getProof)] - pub fn proof(&self) -> Vec { - self.0.proof().to_vec() - } - - #[wasm_bindgen(js_name=getBindingSignature)] - pub fn binding_signature(&self) -> Vec { - self.0.binding_signature().to_vec() - } -} diff --git a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/shielded_transfer.rs b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/shielded_transfer.rs deleted file mode 100644 index ff52a8a1f69..00000000000 --- a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/shielded_transfer.rs +++ /dev/null @@ -1,36 +0,0 @@ -use dpp::state_transition::batch_transition::token_shielded_transfer_transition::v0::v0_methods::TokenShieldedTransferTransitionV0Methods; -use dpp::state_transition::batch_transition::TokenShieldedTransferTransition; -use wasm_bindgen::prelude::wasm_bindgen; - -#[wasm_bindgen(js_name=TokenShieldedTransferTransition)] -#[derive(Debug, Clone)] -pub struct TokenShieldedTransferTransitionWasm(TokenShieldedTransferTransition); - -impl From for TokenShieldedTransferTransitionWasm { - fn from(value: TokenShieldedTransferTransition) -> Self { - Self(value) - } -} - -#[wasm_bindgen(js_class = TokenShieldedTransferTransition)] -impl TokenShieldedTransferTransitionWasm { - #[wasm_bindgen(js_name=getActionsCount)] - pub fn actions_count(&self) -> u32 { - self.0.actions().len() as u32 - } - - #[wasm_bindgen(js_name=getAnchor)] - pub fn anchor(&self) -> Vec { - self.0.anchor().to_vec() - } - - #[wasm_bindgen(js_name=getProof)] - pub fn proof(&self) -> Vec { - self.0.proof().to_vec() - } - - #[wasm_bindgen(js_name=getBindingSignature)] - pub fn binding_signature(&self) -> Vec { - self.0.binding_signature().to_vec() - } -} diff --git a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/unshield.rs b/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/unshield.rs deleted file mode 100644 index 5cb5ee7527d..00000000000 --- a/packages/wasm-dpp/src/document/state_transition/batch_transition/token_transition/unshield.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::identifier::IdentifierWrapper; -use dpp::state_transition::batch_transition::token_unshield_transition::v0::v0_methods::TokenUnshieldTransitionV0Methods; -use dpp::state_transition::batch_transition::TokenUnshieldTransition; -use wasm_bindgen::prelude::wasm_bindgen; - -#[wasm_bindgen(js_name=TokenUnshieldTransition)] -#[derive(Debug, Clone)] -pub struct TokenUnshieldTransitionWasm(TokenUnshieldTransition); - -impl From for TokenUnshieldTransitionWasm { - fn from(value: TokenUnshieldTransition) -> Self { - Self(value) - } -} - -#[wasm_bindgen(js_class = TokenUnshieldTransition)] -impl TokenUnshieldTransitionWasm { - #[wasm_bindgen(js_name=getAmount)] - pub fn amount(&self) -> u64 { - self.0.amount() - } - - #[wasm_bindgen(js_name=getRecipientId)] - pub fn recipient_id(&self) -> IdentifierWrapper { - self.0.recipient_id().into() - } - - #[wasm_bindgen(js_name=getActionsCount)] - pub fn actions_count(&self) -> u32 { - self.0.actions().len() as u32 - } - - #[wasm_bindgen(js_name=getAnchor)] - pub fn anchor(&self) -> Vec { - self.0.anchor().to_vec() - } - - #[wasm_bindgen(js_name=getProof)] - pub fn proof(&self) -> Vec { - self.0.proof().to_vec() - } - - #[wasm_bindgen(js_name=getBindingSignature)] - pub fn binding_signature(&self) -> Vec { - self.0.binding_signature().to_vec() - } -} From e63e1eb856984bd5c260adb6bdd6092bdab850a8 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 23:42:17 +0700 Subject: [PATCH 78/94] fix(dpp): refuse a fresh mint-to-pool bundle for a group co-signer `build_token_mint_to_pool_transition` accepted `GroupStateTransitionInfoOtherSigner` and proved a brand new outputs-only bundle for it. Its sibling `build_token_burn_from_pool_transition` already refuses exactly that, and for a reason that applies identically here: a group action pins the digest of its actions, and the bundle's sighash binds the proposer as the minter. A co-signer that proves its own bundle is therefore substituting different actions under the proposer's action id, which consensus rejects as a modification of the group action. The co-signer's job is to confirm the proposer's bundle, not to replace it, so it submits that bundle unchanged through `new_token_mint_to_pool_transition`. The builder's doc comment already described this rule; only the code did not enforce it. Adopt the burn builder's guard, its error and its wording. Test would have caught this in CI: test ...token_mint_to_pool::tests::rejects_a_fresh_bundle_for_another_group_signer ... FAILED test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 5066 filtered out after the fix: test ...token_mint_to_pool::tests::rejects_a_fresh_bundle_for_another_group_signer ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 5065 filtered out Co-Authored-By: Claude Opus 5 (1M context) --- .../shielded/builder/token_mint_to_pool.rs | 58 +++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/packages/rs-dpp/src/shielded/builder/token_mint_to_pool.rs b/packages/rs-dpp/src/shielded/builder/token_mint_to_pool.rs index ef1f86a3b6b..0b12603d1bb 100644 --- a/packages/rs-dpp/src/shielded/builder/token_mint_to_pool.rs +++ b/packages/rs-dpp/src/shielded/builder/token_mint_to_pool.rs @@ -21,10 +21,13 @@ use super::{build_output_only_bundle, serialize_authorized_bundle, OrchardProver /// it in a batch transition signed by `owner_id`, who must be authorized to mint and pays the /// fee in credits. `using_group_info` sets up a group action exactly like a transparent mint. /// -/// The bundle's sighash binds `owner_id` as the minter, which is right for a direct mint and for -/// the proposal of a group action. Every other signer of a group action must submit the -/// proposer's bundle unchanged, through `new_token_mint_to_pool_transition`, rather than prove -/// a bundle of its own here. +/// A group action mint is proven once, by the proposer +/// (`GroupStateTransitionInfoProposer`): the group action pins the digest of the actions and the +/// sighash binds the proposer as the minter, so every other signer submits the proposer's bundle +/// unchanged through `new_token_mint_to_pool_transition` with +/// `GroupStateTransitionInfoOtherSigner`. Asking this builder for a fresh bundle on behalf of +/// another signer is refused, since consensus would reject it as a modification of the group +/// action. #[allow(clippy::too_many_arguments)] pub async fn build_token_mint_to_pool_transition, P: OrchardProver>( token_id: Identifier, @@ -56,6 +59,14 @@ pub async fn build_token_mint_to_pool_transition, P i64::MAX as u64 ))); } + if matches!( + using_group_info, + Some(GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner(_)) + ) { + return Err(ProtocolError::ShieldedBuildError( + "a group action mint to pool is proven once by the proposer; another signer submits the proposer's bundle unchanged instead of building its own".to_string(), + )); + } let extra_sighash_data = token_pool_output_only_extra_sighash_data( TokenTransitionActionType::MintToPool, @@ -108,6 +119,7 @@ pub async fn build_token_mint_to_pool_transition, P #[cfg(test)] mod tests { use super::*; + use crate::group::GroupStateTransitionInfo; use crate::shielded::builder::test_helpers::{ test_identity_key, test_orchard_address, DummyIdentitySigner, TestProver, }; @@ -138,4 +150,42 @@ mod tests { .to_string(); assert!(err.contains("greater than zero"), "unexpected error: {err}"); } + + #[tokio::test] + async fn rejects_a_fresh_bundle_for_another_group_signer() { + let key = test_identity_key(); + let err = build_token_mint_to_pool_transition( + Identifier::from([1u8; 32]), + Identifier::from([2u8; 32]), + Identifier::from([3u8; 32]), + 0, + &test_orchard_address(), + 100, + [0u8; 36], + None, + None, + Some( + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: 0, + action_id: Identifier::from([4u8; 32]), + action_is_proposer: false, + }, + ), + ), + &key, + 1, + 0, + &DummyIdentitySigner, + &TestProver, + PlatformVersion::latest(), + ) + .await + .expect_err("another signer must reuse the proposer's bundle") + .to_string(); + assert!( + err.contains("proven once by the proposer"), + "unexpected error: {err}" + ); + } } From 0396ed9d994a4291610ad6323c72b57bad6188c8 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 27 Sep 2026 23:47:16 +0700 Subject: [PATCH 79/94] test(dpp): decode the whole StateError discriminant, not its first byte MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `discriminant_of` read `bytes[0]`, which is the entire leading `u32` only while it stays below 251. `StateError` already carries 156 variants and the test pins up to 155. The variant after next crosses the boundary, and past it the helper would read the varint's marker byte for every error alike: every assertion would compare the same constant against itself, the test would keep passing, and the frozen-discriminant guard would be silently vacuous — no longer able to catch the mid-enum insertion it exists to catch. Decode a `u32` the way `BasicError`'s equivalent helper already does. No expected value changes: the highest pinned discriminant is 155, so both forms agree on every variant pinned today. Proof the guard still binds — 155 flipped to 156: test ...state_error::tests::state_error_discriminants_are_frozen ... FAILED assertion `left == right` failed left: 155 right: 156 test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 5005 filtered out and restored: test ...state_error::tests::state_error_discriminants_are_frozen ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 5005 filtered out Co-Authored-By: Claude Opus 5 (1M context) --- packages/rs-dpp/src/errors/consensus/state/state_error.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/rs-dpp/src/errors/consensus/state/state_error.rs b/packages/rs-dpp/src/errors/consensus/state/state_error.rs index da6baea7bba..46d4ff22784 100644 --- a/packages/rs-dpp/src/errors/consensus/state/state_error.rs +++ b/packages/rs-dpp/src/errors/consensus/state/state_error.rs @@ -686,11 +686,13 @@ mod tests { /// that follows the document contest block (the one an insertion there /// would shift first), and of the variants appended since, down to the /// last one, which the test's final assertion pins. - fn discriminant_of(error: StateError) -> u8 { + fn discriminant_of(error: StateError) -> u32 { let bytes = bincode::encode_to_vec(error, bincode::config::standard()) .expect("expected to encode the state error"); - // Discriminants below 251 are a single byte under bincode's varint. - bytes[0] + let (discriminant, _): (u32, usize) = + bincode::decode_from_slice(&bytes, bincode::config::standard()) + .expect("expected to decode the discriminant"); + discriminant } /// A reference error for an id reference encodes exactly as it did before From 1b17e200dc251923b8dcff61cc4f605941ab8445 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Mon, 28 Sep 2026 00:00:42 +0700 Subject: [PATCH 80/94] fix(drive): make the token example contract fixture decodable again `token-example-contract.json` has not been loadable for some time. Nothing references it, so no test failed and the rot accumulated silently. Four independent defects, each confirmed by decoding the file through `json_document_to_contract_with_ids`, the path `setup_contract` uses, and each fixed only once the one before it stopped masking it: 1. `localizations.en` carried no `$formatVersion`. "missing field `$formatVersion`" 2. `transferable: true` names no token-configuration field, in any generation. It is a document-type keyword (`DocumentType::TRANSFERABLE`, 0 or 1), and it is the last survivor of this fixture's original pre-token-configuration draft shape, which also carried `initialSupply`, `decimals`, `roles` and `maintainer`. The others were migrated or dropped when the real `TokenConfiguration` fields arrived; this one was left behind, and `deny_unknown_fields` later turned it from dead weight into a hard failure. There is no counterpart to point it at, so it goes. "unknown field `transferable`, expected one of `conventions`, ..., `description`" 3. `keepsHistory: false` predates `TokenKeepsHistoryRules`, which is an object, not a bool. Written out with every flag false, since the field defaults to keeping all history and dropping the key would have quietly inverted what the fixture asked for. "invalid type: boolean `false`, expected internally tagged enum TokenKeepsHistoryRules" 4. The action takers used `{"type": ...}`; the wire shape is `{"$type": ...}`. "missing field `$type`" Verified with a throwaway test, removed again afterwards, that decoded the fixture and its four intermediate repairs. Before, and after each step in turn: token-example-contract.json: ERR missing field `$formatVersion` +localization format version: ERR unknown field `transferable` -transferable: ERR invalid type: boolean `false` +keepsHistory object: ERR missing field `$type` +$type discriminator: OK, 1 token(s) Only the fixture changes; no code reads it yet. Co-Authored-By: Claude Opus 5 (1M context) --- .../tokens/token-example-contract.json | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/rs-drive/tests/supporting_files/contract/tokens/token-example-contract.json b/packages/rs-drive/tests/supporting_files/contract/tokens/token-example-contract.json index ac5de3630ee..c6d805ee693 100644 --- a/packages/rs-drive/tests/supporting_files/contract/tokens/token-example-contract.json +++ b/packages/rs-drive/tests/supporting_files/contract/tokens/token-example-contract.json @@ -11,6 +11,7 @@ "$formatVersion": "0", "localizations": { "en": { + "$formatVersion": "0", "shouldCapitalize": true, "singularForm": "flurgon", "pluralForm": "flurgons" @@ -20,36 +21,43 @@ }, "baseSupply": 10000000000, "maxSupply": 50000000000, - "transferable": true, - "keepsHistory": false, + "keepsHistory": { + "$formatVersion": "0", + "keepsTransferHistory": false, + "keepsFreezingHistory": false, + "keepsMintingHistory": false, + "keepsBurningHistory": false, + "keepsDirectPricingHistory": false, + "keepsDirectPurchaseHistory": false + }, "freezeRules": { "$formatVersion": "0", - "authorizedToMakeChange": {"type": "contractOwner"}, - "adminActionTakers": {"type": "contractOwner"}, + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "unfreezeRules": { "$formatVersion": "0", - "authorizedToMakeChange": {"type": "contractOwner"}, - "adminActionTakers": {"type": "contractOwner"}, + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "destroyFrozenFundsRules": { "$formatVersion": "0", - "authorizedToMakeChange": {"type": "contractOwner"}, - "adminActionTakers": {"type": "contractOwner"}, + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false }, "emergencyActionRules": { "$formatVersion": "0", - "authorizedToMakeChange": {"type": "contractOwner"}, - "adminActionTakers": {"type": "contractOwner"}, + "authorizedToMakeChange": {"$type": "contractOwner"}, + "adminActionTakers": {"$type": "contractOwner"}, "changingAuthorizedActionTakersToNoOneAllowed": false, "changingAdminActionTakersToNoOneAllowed": false, "selfChangingAdminActionTakersAllowed": false From 9f640057652d984a9c59288b506b589c003d9338 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Mon, 28 Sep 2026 00:05:15 +0700 Subject: [PATCH 81/94] test(drive): cover the evonode claim's epoch cap at the drive method `Drive::evonode_participation_rewards` had no test in either generation. Its version 1, new at protocol version 14, is what stops an evonode further behind than `max_evonode_reward_claim_epochs` from losing a token for good: version 0 evaluated the whole range up to `max_cycle_moment` whatever it read and returned that cap as the moment paid through, so past the read limit a non-fixed distribution failed as an internal error, the last paid moment never advanced, and the claim was unrepeatable. Three tests claim through the dispatcher against real epoch state seeded with finalized epoch infos, and pin the rule rather than the current return value: * a claim spanning 150 epochs with a read limit of 100 pays through cycle 33, ending at epoch 99 inside the cycle the read stopped in, and reports epoch 99 rather than the cap at 150 * a second claim from epoch 99 pays the remaining cycles 34 to 50, and the two together pay what one unbounded claim would have * a cycle reaching an epoch the fee distribution has not finalized yet is left whole for a later claim, neither paid early on the epochs of it that are finalized nor failing the claim The scenario picks a three-epoch cycle so the limit falls inside a cycle, a stepwise emission that changes at the cycle the two claims split on so the amount says which cycles were paid, and a share of nine blocks in twelve that divides exactly, so every expectation is a cycle count times a declared per-cycle payout rather than a copy of the production expression. Each test was checked against the defect it guards, since there is no bug here to reproduce first. Reintroducing version 0's unconditional `max_cycle_moment` as the moment paid through, and separately paying through the cap regardless of what was read, each failed all three on the moment; crediting every proposer's blocks to the claimant failed all three on the amount. All three pass against the code as it stands. Also corrects the transformer test module's claim that the reward ratio is computed in a closure in that file: the branch delegates to `Drive::evonode_participation_rewards`, and the tests reproduce its arithmetic. Co-Authored-By: Claude Opus 5 (1M context) --- .../evonode_participation_rewards/v1/mod.rs | 255 ++++++++++++++++++ .../v0/transformer.rs | 3 +- 2 files changed, 257 insertions(+), 1 deletion(-) diff --git a/packages/rs-drive/src/drive/tokens/distribution/evonode_participation_rewards/v1/mod.rs b/packages/rs-drive/src/drive/tokens/distribution/evonode_participation_rewards/v1/mod.rs index f89d399726c..b6f0eda8831 100644 --- a/packages/rs-drive/src/drive/tokens/distribution/evonode_participation_rewards/v1/mod.rs +++ b/packages/rs-drive/src/drive/tokens/distribution/evonode_participation_rewards/v1/mod.rs @@ -164,3 +164,258 @@ impl Drive { Ok((rewards, paid_through)) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; + use dpp::block::block_info::BlockInfo; + use dpp::block::epoch::Epoch; + use dpp::block::finalized_epoch_info::v0::FinalizedEpochInfoV0; + use dpp::data_contract::associated_token::token_perpetual_distribution::distribution_function::DistributionFunction; + + /// The epoch the distribution counts its cycles from. + const DISTRIBUTION_START: EpochIndex = 0; + + /// Epochs in one cycle of the distribution claimed below. More than one, so that the cycle a + /// claim pays through is not simply the last epoch it read, and not a divisor of the read + /// limit, so that the limit falls inside a cycle rather than on its boundary. + const INTERVAL: EpochIndex = 3; + + /// Blocks produced in each finalized epoch, and how many of them the claimant proposed. Its + /// share of a whole cycle is therefore nine blocks of twelve; the rest go to another evonode, + /// so a claim that counted every proposer's blocks as the claimant's would pay visibly more. + const BLOCKS_PER_EPOCH: u64 = 4; + const BLOCKS_PROPOSED_BY_CLAIMANT: u64 = 3; + + /// The distribution emits this much in each cycle up to cycle 33, and this much from cycle + /// 34, so that the amount a claim pays says which cycles it paid and not merely how many. + const EMISSION_UP_TO_CYCLE_33: TokenAmount = 400; + const EMISSION_FROM_CYCLE_34: TokenAmount = 1_200; + + /// What one cycle pays the claimant on either side of that boundary: the cycle's emission at + /// nine blocks of twelve, three quarters, which divides exactly for both emissions. + const PAID_PER_CYCLE_UP_TO_33: TokenAmount = 300; + const PAID_PER_CYCLE_FROM_34: TokenAmount = 900; + + /// What the fifty cycles spanning epochs 1 to 150 pay the claimant in all: thirty-three + /// cycles at 300 and seventeen at 900. A single claim would pay this if it could read every + /// epoch at once, so a claim split in two must still add up to it. + const PAID_OVER_EPOCHS_1_TO_150: TokenAmount = 25_200; + + fn claimant() -> Identifier { + Identifier::from([1; 32]) + } + + fn other_evonode() -> Identifier { + Identifier::from([2; 32]) + } + + fn distribution() -> RewardDistributionType { + RewardDistributionType::EpochBasedDistribution { + interval: INTERVAL, + function: DistributionFunction::Stepwise(BTreeMap::from([ + (0, EMISSION_UP_TO_CYCLE_33), + (34, EMISSION_FROM_CYCLE_34), + ])), + } + } + + /// Records `BLOCKS_PER_EPOCH` blocks in each of `epochs`, the claimant proposing + /// `BLOCKS_PROPOSED_BY_CLAIMANT` of them and another evonode the rest, as the fee + /// distribution of the first block of the following epoch does once an epoch is over. + fn seed_finalized_epochs( + drive: &Drive, + epochs: impl IntoIterator, + platform_version: &PlatformVersion, + ) { + let transaction = drive.grove.start_transaction(); + for epoch_index in epochs { + let finalized_epoch_info: FinalizedEpochInfo = FinalizedEpochInfoV0 { + first_block_time: u64::from(epoch_index) * 1_000, + first_block_height: u64::from(epoch_index) * BLOCKS_PER_EPOCH, + total_blocks_in_epoch: BLOCKS_PER_EPOCH, + first_core_block_height: 1, + next_epoch_start_core_block_height: 1, + total_processing_fees: 0, + total_distributed_storage_fees: 0, + total_created_storage_fees: 0, + core_block_rewards: 0, + block_proposers: BTreeMap::from([ + (claimant(), BLOCKS_PROPOSED_BY_CLAIMANT), + ( + other_evonode(), + BLOCKS_PER_EPOCH - BLOCKS_PROPOSED_BY_CLAIMANT, + ), + ]), + fee_multiplier_permille: 1_000, + protocol_version: platform_version.protocol_version, + } + .into(); + let operation = drive + .add_epoch_final_info_operation( + &Epoch::new(epoch_index).expect("expected a valid epoch"), + finalized_epoch_info, + platform_version, + ) + .expect("expected a finalized epoch info operation"); + drive + .grove_apply_operation( + operation, + false, + Some(&transaction), + &platform_version.drive, + ) + .expect("expected to store the finalized epoch info"); + } + drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit the finalized epoch infos"); + } + + /// Claims through the dispatcher, so that the version tables choose the generation the same + /// way a block does, and panics if the claim fails rather than reporting what it paid. + fn claim( + drive: &Drive, + last_paid_epoch: EpochIndex, + max_cycle_epoch: EpochIndex, + platform_version: &PlatformVersion, + ) -> (TokenAmount, RewardDistributionMoment) { + // The claim runs while the cycle after the capped one is still going, as a real claim + // does: the cap is the last completed cycle moment. + let block_info = BlockInfo { + epoch: Epoch::new(max_cycle_epoch + INTERVAL).expect("expected a valid epoch"), + ..Default::default() + }; + drive + .evonode_participation_rewards( + claimant(), + &distribution(), + RewardDistributionMoment::EpochBasedMoment(DISTRIBUTION_START), + last_paid_epoch, + RewardDistributionMoment::EpochBasedMoment(max_cycle_epoch), + &block_info, + None, + platform_version, + ) + .expect("expected the claim to compute its rewards") + } + + /// The epochs seeded below and the cycles they fall in are laid out around the read limit, so + /// a change to the limit must be reflected here rather than quietly moving what is covered. + fn assert_scenario_matches_read_limit(platform_version: &PlatformVersion) { + assert_eq!( + platform_version + .system_limits + .max_evonode_reward_claim_epochs, + 100, + "the seeded epochs and cycle boundaries are chosen around a read limit of 100" + ); + } + + /// A claim reaching further back than the read limit allows pays through the last whole cycle + /// inside the epochs it read, and reports that cycle as the moment it paid through. Reporting + /// the cap instead would drop the cycles the claim never weighed; reporting no advance at all + /// would leave the claim unrepeatable, which is how an evonode lost the token for good. + #[test] + fn should_pay_through_the_last_whole_cycle_read_when_a_claim_outruns_the_read_limit() { + let platform_version = PlatformVersion::latest(); + assert_scenario_matches_read_limit(platform_version); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + seed_finalized_epochs(&drive, 1..=150, platform_version); + + let (rewards, paid_through) = claim(&drive, 0, 150, platform_version); + + // The read stops at the hundredth epoch after the last paid one, epoch 100, which sits + // inside the cycle spanning epochs 100 to 102. The last cycle read whole is therefore + // cycle 33, ending at epoch 99, well short of the cap at epoch 150. + assert_eq!( + paid_through, + RewardDistributionMoment::EpochBasedMoment(99), + "a capped claim pays through the last whole cycle it read, not the cap it was given" + ); + // Cycles 1 to 33, each paying the claimant three quarters of its 400 emission. + assert_eq!(rewards, 33 * PAID_PER_CYCLE_UP_TO_33); + } + + /// The moment a capped claim reports is where the next claim starts, and the two together pay + /// exactly what one claim over the whole range would have: the seam neither pays a cycle + /// twice nor skips one, and a third claim finds the range exhausted. + #[test] + fn should_continue_a_capped_claim_from_the_moment_the_previous_one_paid_through() { + let platform_version = PlatformVersion::latest(); + assert_scenario_matches_read_limit(platform_version); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + seed_finalized_epochs(&drive, 1..=150, platform_version); + + let (first_rewards, first_paid_through) = claim(&drive, 0, 150, platform_version); + assert_eq!( + first_paid_through, + RewardDistributionMoment::EpochBasedMoment(99) + ); + assert_eq!(first_rewards, 33 * PAID_PER_CYCLE_UP_TO_33); + + let RewardDistributionMoment::EpochBasedMoment(continue_from) = first_paid_through else { + panic!("an epoch based distribution pays through an epoch based moment"); + }; + let (second_rewards, second_paid_through) = + claim(&drive, continue_from, 150, platform_version); + + // Continuing from epoch 99 the read covers epochs 100 to 150, whose last whole cycle ends + // at the cap: cycles 34 to 50, the seventeen the first claim left, each paying three + // quarters of the larger 1,200 emission. + assert_eq!( + second_paid_through, + RewardDistributionMoment::EpochBasedMoment(150) + ); + assert_eq!(second_rewards, 17 * PAID_PER_CYCLE_FROM_34); + + assert_eq!( + first_rewards + second_rewards, + PAID_OVER_EPOCHS_1_TO_150, + "two claims over a capped range must pay what one unbounded claim would have" + ); + + let (third_rewards, third_paid_through) = claim(&drive, 150, 150, platform_version); + assert_eq!(third_rewards, 0, "every cycle is paid once"); + assert_eq!( + third_paid_through, + RewardDistributionMoment::EpochBasedMoment(150), + "a claim that earns nothing leaves the last paid moment where it was" + ); + } + + /// An epoch whose finalized info the fee distribution has not written yet ends the epochs a + /// claim can read. A cycle reaching into it is left whole for a later claim, rather than paid + /// early on the epochs of it that are finalized, or failing the claim. + #[test] + fn should_leave_a_cycle_reaching_an_epoch_not_finalized_yet_to_a_later_claim() { + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + // Epochs 1 to 8 are finalized and epoch 9 is not, so the cycle spanning epochs 7 to 9 is + // the first the claim cannot weigh, while the cap allows it. + seed_finalized_epochs(&drive, 1..=8, platform_version); + + let (rewards, paid_through) = claim(&drive, 0, 9, platform_version); + + // Cycles 1 and 2 span epochs 1 to 6, the epochs before the unfinished cycle. + assert_eq!( + paid_through, + RewardDistributionMoment::EpochBasedMoment(6), + "a cycle reaching an epoch without finalized info is not paid through" + ); + assert_eq!(rewards, 2 * PAID_PER_CYCLE_UP_TO_33); + + // Once that epoch is finalized the cycle is paid whole, from where the claim stopped. + seed_finalized_epochs(&drive, 9..=9, platform_version); + let (later_rewards, later_paid_through) = claim(&drive, 6, 9, platform_version); + + assert_eq!( + later_paid_through, + RewardDistributionMoment::EpochBasedMoment(9) + ); + assert_eq!(later_rewards, PAID_PER_CYCLE_UP_TO_33); + } +} diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs index e0d96af7bb8..2b477bea967 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs @@ -626,7 +626,8 @@ mod tests { //! These cover: //! * the pre-programmed distribution filtering + "distribution after last paid" lookup //! * the `wrong_claimant_error` resolution logic - //! * `RewardRatio` computation used inside the `EvonodesByParticipation` closure + //! * the `RewardRatio` arithmetic that `Drive::evonode_participation_rewards` applies for + //! the `EvonodesByParticipation` branch, reproduced here //! * recipient resolution for each `TokenDistributionInfo` variant //! * the `From for TokenDistributionRecipient` roundtrip //! * `ClaimAction` variant dispatch / clone / enum wrapper accessors From 0f73b27835e6d1cf7dfa92678899ada9c20b83ba Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Mon, 28 Sep 2026 00:11:13 +0700 Subject: [PATCH 82/94] docs(drive-abci): record that the token format predicate is the only defence `has_is_allowed_validation` is conditional for data contract create and update: it returns true only where some token configuration carries a format version above 0. That reads like a cheap pre-filter in front of a check that would happen anyway, and it is not. Verified before writing it down: - Protocol versions 10 and 11 select contract basic structure validation 0 for both create and update, versions 12 and 13 select 1, and version 14 selects 2. - Generations 0 and 1 contain no call to `validate_token_configurations`, directly or otherwise. Generation 0 does iterate the token configurations, but through `TokenConfigurationV0Getters`, which reads a version 1 configuration just as happily; generation 1 adds only a contract config version check. Generation 2 is the first to validate the configurations. - `token_configuration_format` admits only version 0 up to protocol version 13 and versions 0 and 1 from 14. - The processor and check-tx both reach `validate_is_allowed` only when `has_is_allowed_validation` returns true. - A version 1 token configuration flattens the version 0 one and may keep version 0 distribution rules, so the distribution-rules gate in basic structure 0 does not catch it either. So on protocol versions 10 through 13 this predicate is the only thing that brings such a contract to a refusal. Narrowing it would admit one unrefused on four shipped versions, and the comment now says so where someone editing the predicate will read it. Comment only; no behaviour changes. Co-Authored-By: Claude Opus 5 (1M context) --- .../state_transition/processor/traits/is_allowed.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs index a30bede5522..9ff04ea1a77 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/is_allowed.rs @@ -64,6 +64,16 @@ impl StateTransitionIsAllowedValidationV0 for StateTransition { | StateTransition::ContractFeeClaim(_) => Ok(true), // Newly decoded token formats need an unpaid activation check even while the // older contract basic-structure generations remain frozen. + // + // This predicate is load-bearing rather than a shortcut: contract basic structure + // validation 0 and 1, which protocol versions 10 through 13 select, walk the token + // configurations through the version 0 accessors and never ask what format version + // they carry. On those versions the format-version check `validate_is_allowed` runs + // is the only one there is, and it is reached only where this returns true, so a + // token configuration format they do not admit is refused here or nowhere. + // Narrowing the predicate means first teaching both of those frozen generations to + // validate token configurations. Generation 2 does validate them, which makes this + // redundant from protocol version 14 on, and only from there. StateTransition::DataContractCreate(st) => Ok(st .data_contract() .tokens() From 819ac00173f05e3e1044b5a38edd35c4124fe044 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Mon, 28 Sep 2026 00:23:22 +0700 Subject: [PATCH 83/94] fix(dpp): name the discriminator these deserializers actually accept MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both deserializers match `$type` and both serializers write it, while the `expecting` message promised `type` and gave an example using it. So the only hint a caller gets on an ingest failure — which the comment above one of them says in those words — instructed them to write the shape that produces the failure. A token contract fixture in this repository carries exactly that mistake eight times over, which is the likeliest way it got there. Message text only; no accepted input changes. The same wording is wrong in two voting deserializers, `contested_document_vote_poll_winner_info` and `resource_vote_choice`, which also serialize `$type`. Left for a change of their own rather than widened into this one. Co-Authored-By: Claude Opus 5 (1M context) --- .../distribution_recipient.rs | 8 ++++---- .../change_control_rules/authorized_action_takers.rs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_recipient.rs b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_recipient.rs index 728a709d599..7cd924fa8ad 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_recipient.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_recipient.rs @@ -79,8 +79,8 @@ impl<'de> Deserialize<'de> for TokenDistributionRecipient { // 4.0.0-beta.4 used bare strings / externally-tagged maps, and // this message is the only hint users get on ingest failure. f.write_str( - "TokenDistributionRecipient as a map with a `type` discriminator, \ - e.g. {\"type\": \"contractOwner\"} or {\"type\": \"identity\", \"identity\": \"\"} \ + "TokenDistributionRecipient as a map with a `$type` discriminator, \ + e.g. {\"$type\": \"contractOwner\"} or {\"$type\": \"identity\", \"identity\": \"\"} \ (the pre-4.0.0-beta.4 shapes \"ContractOwner\" / {\"Identity\": \"\"} are no longer accepted)", ) } @@ -250,8 +250,8 @@ impl<'de> Deserialize<'de> for TokenDistributionResolvedRecipient { fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.write_str( - "TokenDistributionResolvedRecipient as a map with a `type` discriminator, \ - e.g. {\"type\": \"identity\", \"identity\": \"\"} \ + "TokenDistributionResolvedRecipient as a map with a `$type` discriminator, \ + e.g. {\"$type\": \"identity\", \"identity\": \"\"} \ (the pre-4.0.0-beta.4 externally-tagged {\"Identity\": \"\"} shape is no longer accepted)", ) } diff --git a/packages/rs-dpp/src/data_contract/change_control_rules/authorized_action_takers.rs b/packages/rs-dpp/src/data_contract/change_control_rules/authorized_action_takers.rs index 3f8f363a6f1..3ac67bf9098 100644 --- a/packages/rs-dpp/src/data_contract/change_control_rules/authorized_action_takers.rs +++ b/packages/rs-dpp/src/data_contract/change_control_rules/authorized_action_takers.rs @@ -83,8 +83,8 @@ impl<'de> Deserialize<'de> for AuthorizedActionTakers { // 4.0.0-beta.4 used bare strings / externally-tagged maps, and // this message is the only hint users get on ingest failure. f.write_str( - "AuthorizedActionTakers as a map with a `type` discriminator, \ - e.g. {\"type\": \"contractOwner\"} or {\"type\": \"identity\", \"identity\": \"\"} \ + "AuthorizedActionTakers as a map with a `$type` discriminator, \ + e.g. {\"$type\": \"contractOwner\"} or {\"$type\": \"identity\", \"identity\": \"\"} \ (the pre-4.0.0-beta.4 shapes \"ContractOwner\" / {\"Identity\": \"\"} are no longer accepted)", ) } From a0c5e400c8b639b2b6e4e2934c967ee5a6773e2b Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Mon, 28 Sep 2026 00:51:02 +0700 Subject: [PATCH 84/94] fix(drive): refuse a token nullifier spend status for a pool the chain lacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `verify_pool_nullifiers_v0` maps a key the absence proof did not carry to `is_spent = false`. A token shielded pool that was never created carries none of the queried keys, so a nullifier that is unspent in a real pool and a pool that does not exist verify to the same rows, against the same root hash. The caller receives nothing that separates them. The unproved read of this question already refuses a missing pool, on the grounds that `is_spent: false` is the answer a wallet leans on to treat a note as still spendable. The proved read was left answering an absence proof instead. That is the path every first-party client takes: each shielded `Query` impl in rs-sdk rejects `prove == false` outright, so the unproved refusal is unreachable from the SDK while the collapsed answer is all it can get. `verify_token_shielded_pool_nullifiers` now reads the pools tree's own Merk proof for the pool's key out of the same proof bytes — a strict subset of what the nullifier proof proves, bound to it by root-hash equality, the mechanism `verify_pool_encrypted_notes_v0` already uses for its parent element — and refuses when that key is proven absent. No prover change: the datum was always in the proof, only discarded. The credit pool entry point is untouched. Its generation is selected by released protocol versions 12 and 13; the check lives in the token entry point, which no released version can reach, and the credit pool exists on every chain that serves the read. Test would have caught this in CI: `proved_nullifier_read_tells_a_missing_ pool_apart_from_an_unspent_nullifier` fails before the fix with a pool the chain does not have was answered with the spend status [([17, ...], false)], the same datum an unspent nullifier in a pool it has returns ([([17, ...], false)]), and against the same root hash (equal: true), so nothing the caller receives separates the two and passes after, in both verification modes. Co-Authored-By: Claude Opus 5 (1M context) --- .../rs-drive-abci/src/query/shielded/mod.rs | 17 +- .../verify_shielded_nullifiers/mod.rs | 30 ++- .../verify_shielded_nullifiers/v0/mod.rs | 196 +++++++++++++++++- 3 files changed, 229 insertions(+), 14 deletions(-) diff --git a/packages/rs-drive-abci/src/query/shielded/mod.rs b/packages/rs-drive-abci/src/query/shielded/mod.rs index b06fbfbad6b..df7bb5ccf74 100644 --- a/packages/rs-drive-abci/src/query/shielded/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/mod.rs @@ -75,9 +75,12 @@ impl ShieldedPoolSelector { /// /// Only the unproved reads need this. A proof over a pool that does not exist is a proof of /// its absence, which is an answer the client can check against the root hash rather than has - /// to trust. That a verifier walks across the missing pool layer and reports `None` is the - /// intent; the proved path is covered only as far as returning a proof, so treat the - /// verifier's side of it as untested. + /// to trust, so the proved reads keep handing one out. Separating the two facts is then the + /// verifier's job, and a proof that descends through the pool carries what it needs: the pools + /// tree's own Merk proof for the pool's key. `Drive::verify_token_shielded_pool_nullifiers` + /// reads that key and refuses a spend status for a pool the chain does not hold, because + /// `is_spent: false` is the one absence here that a caller acts on. The other proved reads + /// answer a missing pool with the same value an empty pool gives. pub(super) fn validate_pool_exists( &self, drive: &Drive, @@ -603,9 +606,11 @@ mod tests { /// hash rather than has to trust, so it is worth more than the `NotFound` the unproved reads /// give. Whoever changes this must change the verifiers with it. /// - /// This test asserts only that a proof comes back. Whether - /// `Drive::verify_pool_notes_count_v0` and its siblings can walk across the absent pool layer - /// and report `None` is not covered here and is worth its own test. + /// This test asserts only that a proof comes back; what a verifier makes of it is covered on + /// the verifier's own side. `Drive::verify_token_shielded_pool_nullifiers` reads the pool's key + /// out of the same proof and refuses rather than reporting every nullifier unspent; + /// `Drive::verify_pool_notes_count_v0` and the other siblings walk across the absent pool layer + /// and report the value an empty pool would give. #[test] fn proved_queries_answer_a_pool_the_chain_does_not_have_with_an_absence_proof() { let (platform, state, version) = setup_platform_with_a_token_pool(); diff --git a/packages/rs-drive/src/verify/shielded/verify_shielded_nullifiers/mod.rs b/packages/rs-drive/src/verify/shielded/verify_shielded_nullifiers/mod.rs index 53bbf97a8f2..addb14b696b 100644 --- a/packages/rs-drive/src/verify/shielded/verify_shielded_nullifiers/mod.rs +++ b/packages/rs-drive/src/verify/shielded/verify_shielded_nullifiers/mod.rs @@ -41,6 +41,11 @@ impl Drive { impl Drive { /// Verifies a proof of nullifier spent statuses in a TOKEN shielded pool. Same versioning /// as [`Drive::verify_shielded_nullifiers`]. + /// + /// Unlike the credit pool, which every chain that serves these reads already holds, a token + /// pool exists only for a token that opted into one, and any 32 bytes a caller supplies name + /// a pool that may never have been created. A spend status is only answered for a pool the + /// same proof shows the chain holds. #[allow(clippy::type_complexity)] pub fn verify_token_shielded_pool_nullifiers( proof: &[u8], @@ -56,13 +61,24 @@ impl Drive { .shielded .verify_shielded_nullifiers { - 0 => Self::verify_pool_nullifiers_v0( - proof, - token_shielded_pool_nullifiers_path_vec(token_id), - nullifiers, - verify_subset_of_proof, - platform_version, - ), + 0 => { + let (root_hash, statuses) = Self::verify_pool_nullifiers_v0( + proof, + token_shielded_pool_nullifiers_path_vec(token_id), + nullifiers, + verify_subset_of_proof, + platform_version, + )?; + + Self::verify_token_shielded_pool_exists_v0( + proof, + token_id, + root_hash, + platform_version, + )?; + + Ok((root_hash, statuses)) + } version => Err(Error::Drive(DriveError::UnknownVersionMismatch { method: "verify_token_shielded_pool_nullifiers".to_string(), known_versions: vec![0], diff --git a/packages/rs-drive/src/verify/shielded/verify_shielded_nullifiers/v0/mod.rs b/packages/rs-drive/src/verify/shielded/verify_shielded_nullifiers/v0/mod.rs index 511eb7840bd..9641ae0efb2 100644 --- a/packages/rs-drive/src/verify/shielded/verify_shielded_nullifiers/v0/mod.rs +++ b/packages/rs-drive/src/verify/shielded/verify_shielded_nullifiers/v0/mod.rs @@ -1,7 +1,10 @@ use crate::drive::shielded::paths::shielded_credit_pool_nullifiers_path_vec; +use crate::drive::tokens::paths::token_shielded_pools_root_path_vec; use crate::drive::Drive; +use crate::error::proof::ProofError; use crate::error::Error; use crate::verify::RootHash; +use dpp::prelude::Identifier; use grovedb::{GroveDb, PathQuery, Query, SizedQuery}; use platform_version::version::PlatformVersion; @@ -30,6 +33,11 @@ impl Drive { /// Verifies nullifier spent statuses against the nullifiers tree at `nullifiers_path`, /// whichever shielded pool (credit or token) it belongs to. + /// + /// Reports a nullifier the proof did not carry as unspent, which is also what a pool that does + /// not exist gives, so a caller whose pool may be missing has to establish that separately, + /// with `verify_token_shielded_pool_exists_v0`. The credit pool needs nothing of the kind: + /// every chain that serves this read holds it. #[allow(clippy::type_complexity)] pub(super) fn verify_pool_nullifiers_v0( proof: &[u8], @@ -79,12 +87,78 @@ impl Drive { Ok((root_hash, statuses)) } + + /// Reads out of the same nullifier proof whether the token's shielded pool exists at all, + /// and refuses the read when it does not. + /// + /// A nullifier proof reports a key it did not carry as absent, and a pool that was never + /// created carries none of them, so an unspent nullifier and a pool the chain does not have + /// project to the same rows. `is_spent: false` is what a wallet leans on to treat a note as + /// still spendable, and a pool that does not exist holds no spendable note, so the two facts + /// must not reach the caller as one answer. The unproved read of the same question refuses a + /// pool the chain does not have for this reason; attaching a proof does not make the collapsed + /// answer safe to act on, it only makes it verifiable. + /// + /// The nullifiers query descends through the pool, so the proof already carries the pools + /// tree's own Merk proof for the pool's key — the pool element, or that key proven absent. + /// Reading it is a strict subset of what the nullifier proof proves, so a subset verification + /// against the same bytes succeeds whichever way the outer verification ran, and the root hash + /// both derive ties the two readings to one state. + pub(super) fn verify_token_shielded_pool_exists_v0( + proof: &[u8], + token_id: [u8; 32], + nullifiers_root_hash: RootHash, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + let pool_path_query = PathQuery { + path: token_shielded_pools_root_path_vec(), + query: SizedQuery { + query: Query::new_single_key(token_id.to_vec()), + limit: Some(1), + offset: None, + }, + }; + + let (pool_root_hash, mut proved_key_values) = GroveDb::verify_subset_query( + proof, + &pool_path_query, + &platform_version.drive.grove_version, + )?; + + // Both readings run against the same bytes, so they derive the same root by construction. + // A mismatch means they came from different state and neither answer can be trusted. + if pool_root_hash != nullifiers_root_hash { + return Err(Error::Proof(ProofError::IncorrectProof( + "token shielded pool existence sub-proof root mismatch".to_string(), + ))); + } + + match proved_key_values.pop() { + // Creating a pool installs its five children, so a pool that exists is a tree the + // proof carries rather than a key it omits. + Some((_, _, Some(element))) if element.is_any_tree() => Ok(()), + Some((_, _, Some(_))) => Err(Error::Proof(ProofError::CorruptedProof(format!( + "the token shielded pools tree holds something other than a pool for token {}", + Identifier::new(token_id) + )))), + Some((_, _, None)) | None => { + Err(Error::Proof(ProofError::UnexpectedResultProof(format!( + "the chain has no shielded pool for token {}, so none of its nullifiers has a \ + spend status", + Identifier::new(token_id) + )))) + } + } + } } #[cfg(test)] mod tests { use super::*; - use crate::drive::shielded::paths::shielded_credit_pool_nullifiers_path_vec; + use crate::drive::shielded::paths::{ + shielded_credit_pool_nullifiers_path_vec, token_shielded_pool_nullifiers_path_vec, + }; + use crate::error::proof::ProofError; use crate::util::batch::grovedb_op_batch::GroveDbOpBatchV0Methods; use crate::util::batch::GroveDbOpBatch; use crate::util::test_helpers::setup::setup_drive_with_initial_state_structure; @@ -92,6 +166,126 @@ mod tests { use grovedb::Element; use platform_version::version::PlatformVersion; + /// A token id the chain holds no shielded pool for. Any 32 bytes an unauthenticated caller + /// makes up land here. + const POOL_LESS_TOKEN_ID: [u8; 32] = [0xAB; 32]; + + /// The token id of the pool these tests create. + const POOLED_TOKEN_ID: [u8; 32] = [3u8; 32]; + + /// A proved nullifier read of a token pool the chain does not have carries the same datum as + /// an unspent nullifier in a pool it does have. + /// + /// The query handler builds a key-selection query at the pool's nullifiers path and hands out + /// whatever GroveDB proves. When the pool subtree is missing, GroveDB proves its absence + /// honestly, and the verifier's absence projection reports every queried key as `None` — + /// exactly what it reports for a key that is simply not in a nullifiers tree that does exist. + /// `is_spent: false` is then the answer a wallet leans on to treat a note as still spendable, + /// and it is anchored to the real root hash, so the client has no way to discount it. + #[test] + fn proved_nullifier_read_tells_a_missing_pool_apart_from_an_unspent_nullifier() { + let drive = setup_drive_with_initial_state_structure(None); + let platform_version = PlatformVersion::latest(); + + // One token pool the chain really has, left empty so the nullifier below is unspent in it. + let operations = drive + .create_token_shielded_pool_trees_operations( + POOLED_TOKEN_ID, + false, + &mut None, + None, + platform_version, + ) + .expect("expected the pool tree operations"); + drive + .apply_batch_low_level_drive_operations( + None, + None, + operations, + &mut vec![], + &platform_version.drive, + ) + .expect("expected the token shielded pool to be created"); + + let nullifier = vec![0x11u8; 32]; + let nullifiers = vec![nullifier.clone()]; + + // The query handler's path query, reproduced: the pool's nullifiers path, the requested + // keys, no limit. + let prove = |token_id: [u8; 32]| { + let path_query = PathQuery { + path: token_shielded_pool_nullifiers_path_vec(token_id), + query: SizedQuery { + query: { + let mut query = Query::new(); + query.insert_keys(nullifiers.clone()); + query + }, + limit: None, + offset: None, + }, + }; + + drive.grove_get_proved_path_query( + &path_query, + None, + &mut vec![], + &platform_version.drive, + ) + }; + + let missing_pool_proof = prove(POOL_LESS_TOKEN_ID) + .expect("a pool the chain does not have is answered with an absence proof"); + let empty_pool_proof = + prove(POOLED_TOKEN_ID).expect("a pool the chain has is answered with a proof"); + + // Both verification modes: the standalone nullifiers query is checked strictly, while the + // state transition proof paths check it as a subset of a larger proof. + for verify_subset_of_proof in [false, true] { + let verify = |proof: &[u8], token_id: [u8; 32]| { + Drive::verify_token_shielded_pool_nullifiers( + proof, + token_id, + &nullifiers, + verify_subset_of_proof, + platform_version, + ) + }; + + let (unspent_root_hash, unspent_statuses) = + verify(empty_pool_proof.as_slice(), POOLED_TOKEN_ID) + .expect("an existing pool verifies"); + assert_eq!( + unspent_statuses, + vec![(nullifier.clone(), false)], + "an unspent nullifier in a pool the chain has is reported unspent \ + (verify_subset_of_proof: {verify_subset_of_proof})" + ); + + // A pool the chain does not have must be refused rather than answered with a spend + // status: the caller cannot tell the two apart, so the verifier is the last place the + // facts can still be separated. + match verify(missing_pool_proof.as_slice(), POOL_LESS_TOKEN_ID) { + Err(Error::Proof(ProofError::UnexpectedResultProof(message))) => assert!( + message.contains("no shielded pool"), + "the refusal must say the pool is the thing that is missing, got {message:?}" + ), + Err(other) => panic!( + "expected the missing pool to be named, got {other} \ + (verify_subset_of_proof: {verify_subset_of_proof})" + ), + Ok((root_hash, statuses)) => panic!( + "a pool the chain does not have was answered with the spend status \ + {statuses:?}, the same datum an unspent nullifier in a pool it has returns \ + ({unspent_statuses:?}), and against the same root hash (equal: {}), so \ + nothing the caller receives separates the two (verify_subset_of_proof: \ + {verify_subset_of_proof})", + root_hash == unspent_root_hash + ), + } + } + } + #[test] fn should_prove_and_verify_nullifier_spent_status() { let drive = setup_drive_with_initial_state_structure(None); From 2fcd7096fa234adcacc472bb82e01422a9693463 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Mon, 28 Sep 2026 00:43:43 +0700 Subject: [PATCH 85/94] fix(drive-abci): reserve the shielded compute fee in the batch balance pre-check A batch's identity minimum balance pre-check asked for a flat 100,000 credits per sub-transition, while a one-action token pool bundle is charged 62,000,000 when its action is built: 40,000,000 for the Halo 2 bundle verification plus 22,000,000 for the action. A signer holding anything between the two numbers was admitted. TokenClaimToPool made that gap exploitable. Its proof is deliberately skipped in check tx, because the claimable amount is only known against state, so such a batch cleared the mempool with no verification run at all. Every validator then ran the verification inside block validation, where the concurrency permit that guards admission does not exist, and the batch was refused unpaid: nothing charged, the identity contract nonce untouched, the same bytes replayable. The same holds for the bundle of a group action's non-proposing signer, whose proof check tx also skips. The pre-check now sums compute_shielded_verification_fee over the sub-transitions that carry a bundle -- a token transition against a pool, or a document whose token cost is paid out of one -- and reserves it on top of the flat minimum. A batch without a bundle is asked for the flat minimum, unchanged. A batch that requests gas sponsorship is still asked for its principal alone, because the compute fee is gas; no batch holding a pool token transition is ever sponsored anyway, since requests_gas_sponsorship gives up on the first non-document member. relies_on_gas_sponsor_to_pay asks with the same floor, so its mempool answer names the shortfall the pre-check would find. The higher floor refuses nothing that could have executed. All seven pool action transformers and the document path add the same fee before anything can fail, and a batch is always ExecutionEvent::Paid from the signer's identity balance, so everyone the floor refuses is already refused by validate_fees_of_event -- only later, and after every validator has paid for the proof. That argument rests on a number: the floor asks flat_minimum + compute_fee where fee validation asks metered_fee + compute_fee, so it over-refuses exactly when metered_fee < flat_minimum, and a bundle-carrying sub-transition's metered storage alone is two orders of magnitude above 100,000. The reasoning is recorded on the floor and on SystemLimits::max_transitions_in_documents_batch, where the shape of a batch is decided. Versioning: the fix lands inside batch_state_transition's identity_minimum_balance_pre_check generation 1, which DRIVE_ABCI_VALIDATION_VERSIONS_V10 selects and keeps selecting -- the table is unchanged but for a comment. That is not an in-place edit of a shipped generation: generation 1 was introduced for PROTOCOL_VERSION_14 and only PLATFORM_V14 has ever selected it, while every released protocol version selects generation 0 through DRIVE_ABCI_VALIDATION_VERSIONS_V1 to V9. Generation 0 is byte-identical. Adding a generation 2 instead would have left generation 1 referenced by no PLATFORM_V* at all -- dead consensus code with no compiler signal, since the dispatch is a runtime match -- which is the outcome the table-version rule exists to prevent. In-place changes to shipped generations: none. Test: would have caught this in CI. With generation 1 asking for the flat minimum alone, which is today's behaviour: running 9 tests test ...::should_refuse_a_batch_claiming_into_a_pool_when_the_balance_cannot_pay_for_the_proof ... FAILED test ...::should_refuse_a_document_paid_from_a_pool_when_the_balance_cannot_pay_for_the_proof ... FAILED test ...::should_reserve_the_fee_of_every_bundle_a_batch_carries ... FAILED panicked at .../processor/traits/identity_balance.rs:522:17: a balance of 1000000 cannot pay the 62000000 a 1-action bundle will be charged panicked at .../processor/traits/identity_balance.rs:577:13: one credit short of the 84000000 the payment's bundle will be charged panicked at .../processor/traits/identity_balance.rs:652:13: one credit short of the 146000000 both bundles will be charged test result: FAILED. 6 passed; 3 failed; 0 ignored; 0 measured; 3741 filtered out With generation 1 reserving the bundles' compute fee: running 9 tests test ...::should_reserve_the_fee_of_every_bundle_a_batch_carries ... ok test ...::should_keep_the_flat_per_sub_transition_minimum_for_a_batch_without_a_pool_bundle ... ok test ...::should_refuse_a_batch_claiming_into_a_pool_when_the_balance_cannot_pay_for_the_proof ... ok test ...::should_refuse_a_document_paid_from_a_pool_when_the_balance_cannot_pay_for_the_proof ... ok test ...::should_not_reserve_the_shielded_fee_against_a_signer_who_asked_for_a_gas_sponsor ... ok test ...::should_return_false_for_transitions_without_balance_pre_check ... ok test ...::should_return_ok_for_transitions_without_balance_check ... ok test ...::should_return_error_when_identity_has_no_balance ... ok test ...::should_return_true_for_transitions_with_balance_pre_check ... ok test result: ok. 9 passed; 0 failed; 0 ignored; 0 measured; 3741 filtered out The three that pass in both states are the point of them: an ordinary document batch keeps the flat minimum whether or not it carries token payment info, and a batch asking for a gas sponsor keeps its principal-only check, so the change cannot raise what the network already pays. test_token_shield_rejected_by_check_tx_when_credits_cannot_cover_the_compute_fee funded its identity with exactly the compute fee, which the new floor is above, so the rejection it asserts would have come from the floor rather than from the full fee estimate it exists to pin -- and it would then have passed even with the transformer's charge removed. It now funds the floor exactly, clears admission, and is refused by the estimate as before. Also green: batch::tests::token::shielded (73 passed), the document gas sponsorship suite (21 passed) and document_shielded_token_payment (4 passed). Co-Authored-By: Claude Opus 5 (1M context) --- ...ate_transition_estimated_fee_validation.rs | 75 ++++ .../processor/traits/identity_balance.rs | 351 +++++++++++++++++- .../batch/tests/token/shielded/mod.rs | 14 +- .../drive_abci_validation_versions/v10.rs | 3 + .../src/version/system_limits/mod.rs | 12 + .../rs-platform-version/src/version/v14.rs | 17 + 6 files changed, 465 insertions(+), 7 deletions(-) diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/state_transition_estimated_fee_validation.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/state_transition_estimated_fee_validation.rs index e3332afe729..2da22192ff2 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/state_transition_estimated_fee_validation.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/state_transition_estimated_fee_validation.rs @@ -3,8 +3,10 @@ use crate::consensus::basic::BasicError; use crate::consensus::state::identity::IdentityInsufficientBalanceError; use crate::consensus::ConsensusError; use crate::fee::Credits; +use crate::shielded::compute_shielded_verification_fee; use crate::state_transition::batch_transition::accessors::DocumentsBatchTransitionAccessorsV0; use crate::state_transition::batch_transition::batched_transition::document_transition::DocumentTransitionV0Methods; +use crate::state_transition::batch_transition::batched_transition::token_transition::TokenTransitionV0Methods; use crate::state_transition::batch_transition::batched_transition::BatchedTransitionRef; use crate::state_transition::batch_transition::document_base_transition::v1::v1_methods::DocumentBaseTransitionV1Methods; use crate::state_transition::batch_transition::methods::v0::DocumentsBatchTransitionMethodsV0; @@ -15,6 +17,7 @@ use crate::state_transition::{ }; use crate::tokens::gas_fees_paid_by::GasFeesPaidBy; use crate::tokens::token_payment_info::v0::v0_accessors::TokenPaymentInfoAccessorsV0; +use crate::tokens::token_payment_info::v1::v1_accessors::TokenPaymentInfoAccessorsV1; use crate::validation::SimpleConsensusValidationResult; use crate::ProtocolError; use platform_version::version::PlatformVersion; @@ -79,6 +82,78 @@ impl BatchTransition { self.validate_estimated_principal_and_fees(identity_known_balance, 0) } + /// The shielded compute fee the batch's Orchard bundles will be charged: one bundle + /// verification plus the per-action work, summed over the sub-transitions that carry a + /// bundle, each priced exactly as the transformer that builds it prices it. + /// + /// A bundle is carried either by a token transition against a shielded pool or by a + /// document whose token cost is paid out of the payment token's pool. + pub fn shielded_bundle_compute_fee( + &self, + platform_version: &PlatformVersion, + ) -> Result { + let mut total: Credits = 0; + for transition in self.transitions_iter() { + let actions = match transition { + BatchedTransitionRef::Token(token_transition) => { + token_transition.shielded_pool_actions() + } + BatchedTransitionRef::Document(document_transition) => document_transition + .base() + .token_payment_info_ref() + .as_ref() + .and_then(|info| info.shielded_payment()) + .map(|payment| payment.actions.as_slice()), + }; + let Some(actions) = actions else { + continue; + }; + total = total.saturating_add(compute_shielded_verification_fee( + actions.len(), + platform_version, + )?); + } + Ok(total) + } + + /// The minimum balance pre-check of a batch, reserving the shielded compute fee of the + /// bundles it carries on top of the flat per-sub-transition minimum. + /// + /// The flat minimum is orders of magnitude below what one bundle costs, and reserving only + /// that leaves the Halo 2 verification unfunded. A claim into a pool makes the gap + /// exploitable: its claimable amount is only known against state, so its proof is skipped + /// in check tx, and a signer holding only the flat minimum passes the mempool without any + /// verification running. Every validator then runs it inside block validation, where the + /// concurrency permit that guards admission does not apply, and the batch is refused + /// unpaid — nothing charged, the identity contract nonce untouched — so the same bytes + /// replay. Reserving the fee here refuses the batch before the proof work is done, and + /// refuses nothing that could have been executed: every bundle-carrying sub-transition is + /// charged this same fee as its action is built, before anything about it can fail, so a + /// signer who cannot cover it is refused by fee validation in any case. + /// + /// That last claim rests on a number, not on the structure. The floor asks for + /// `flat_minimum + compute_fee` where fee validation asks for `metered_fee + compute_fee`, + /// so the floor refuses somebody fee validation would not exactly when + /// `metered_fee < flat_minimum`. A sub-transition carrying a bundle writes notes and + /// nullifiers, whose metered storage alone prices it two orders of magnitude above the + /// flat minimum, so no such batch exists. Anything that lets a bundle ride on a + /// sub-transition cheaper than the flat minimum reopens that band; the same reasoning is + /// recorded on `SystemLimits::max_transitions_in_documents_batch`, which is where the + /// shape of a batch is decided. + /// + /// The caller decides whether to ask for it. A batch that asks the contract owner to pay + /// its gas is not asked, because the compute fee is gas. + pub fn validate_estimated_fee_with_shielded_compute( + &self, + identity_known_balance: Credits, + platform_version: &PlatformVersion, + ) -> Result { + let base_fees = self + .calculate_min_required_fee(platform_version)? + .saturating_add(self.shielded_bundle_compute_fee(platform_version)?); + self.validate_estimated_principal_and_fees(identity_known_balance, base_fees) + } + /// The balance has to cover the principal of the batch (document purchases and the /// collateral of contested creates) plus `base_fees`. fn validate_estimated_principal_and_fees( diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/identity_balance.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/identity_balance.rs index 7791d7473a9..5494b3d544e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/identity_balance.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/processor/traits/identity_balance.rs @@ -78,12 +78,20 @@ impl StateTransitionIdentityBalanceValidationV0 for StateTransition { .map_err(Error::Protocol), // A batch asking the contract owner to pay its gas cannot be judged before its // contracts are loaded: the signer only has to fund the principal here, and fee - // validation judges the gas against whoever ends up paying it. + // validation judges the gas against whoever ends up paying it. This arm wins + // over the shielded floor below, and it should: the compute fee of a bundle a + // sponsored document carries is gas like the rest. 1 if st.requests_gas_sponsorship() => st .validate_estimated_principal(balance) .map_err(Error::Protocol), + // A batch carrying shielded pool bundles also has to hold the compute fee those + // bundles will be charged. The flat per-sub-transition minimum is orders of + // magnitude below one bundle verification, and a claim into a pool has its + // proof skipped in check tx, so a signer in between passes the mempool without + // any verification running and every validator then runs it inside block + // validation, only to refuse the batch unpaid and let the same bytes replay. 1 => st - .validate_estimated_fee(balance, platform_version) + .validate_estimated_fee_with_shielded_compute(balance, platform_version) .map_err(Error::Protocol), version => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { method: "documents batch transition: identity minimum balance pre check" @@ -165,8 +173,10 @@ impl StateTransitionIdentityBalanceValidationV0 for StateTransition { .ok_or(Error::Execution(ExecutionError::CorruptedCodeExecution( "expected to have a balance on identity for the gas sponsorship check", )))?; + // The floor an unsponsored batch would have been held to, shielded bundles included, + // so the answer names the same shortfall the pre-check itself would have found. Ok(!st - .validate_estimated_fee(balance, platform_version) + .validate_estimated_fee_with_shielded_compute(balance, platform_version) .map_err(Error::Protocol)? .is_valid()) } @@ -360,6 +370,341 @@ mod tests { assert!(result.is_err(), "expected error when balance is None"); } + /// `count` distinct Orchard actions. Only how many there are is read here; the pre-check + /// prices a bundle by its action count and never looks inside one. + fn pool_actions(count: usize) -> Vec { + (0..count) + .map(|index| dpp::shielded::SerializedAction { + nullifier: [index as u8; 32], + rk: [2u8; 32], + cmx: [3u8; 32], + encrypted_note: vec![4u8; 216], + cv_net: [5u8; 32], + spend_auth_sig: [6u8; 64], + }) + .collect() + } + + fn batch_of( + transitions: Vec< + dpp::state_transition::batch_transition::batched_transition::BatchedTransition, + >, + ) -> StateTransition { + use dpp::state_transition::batch_transition::BatchTransitionV1; + + StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: Default::default(), + transitions, + user_fee_increase: 0, + signature_public_key_id: 0, + signature: Default::default(), + })) + } + + /// A claim into a token's shielded pool, carrying a bundle of `actions` actions. + fn claim_to_pool( + actions: usize, + ) -> dpp::state_transition::batch_transition::batched_transition::BatchedTransition + { + use dpp::state_transition::batch_transition::batched_transition::token_claim_to_pool_transition::v0::TokenClaimToPoolTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::token_transition::TokenTransition; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::TokenClaimToPoolTransition; + + BatchedTransition::Token(TokenTransition::ClaimToPool( + TokenClaimToPoolTransition::V0(TokenClaimToPoolTransitionV0 { + actions: pool_actions(actions), + ..Default::default() + }), + )) + } + + /// A document deletion carrying `payment_info`, or none at all, on a format 1 base. + fn document_delete( + payment_info: Option, + ) -> dpp::state_transition::batch_transition::batched_transition::BatchedTransition + { + use dpp::state_transition::batch_transition::batched_transition::document_base_transition::v1::DocumentBaseTransitionV1; + use dpp::state_transition::batch_transition::batched_transition::document_base_transition::DocumentBaseTransition; + use dpp::state_transition::batch_transition::batched_transition::document_delete_transition::v0::DocumentDeleteTransitionV0; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::batched_transition::DocumentTransition; + use dpp::state_transition::batch_transition::DocumentDeleteTransition; + + BatchedTransition::Document(DocumentTransition::Delete(DocumentDeleteTransition::V0( + DocumentDeleteTransitionV0 { + base: DocumentBaseTransition::V1(DocumentBaseTransitionV1 { + token_payment_info: payment_info, + ..Default::default() + }), + }, + ))) + } + + /// A document whose token cost is paid out of the payment token's shielded pool. It + /// spends pool notes exactly as a token transition does, so it is priced the same way. + /// `gas_fees_paid_by` is who the document asks to pay its gas. + fn document_paid_from_pool( + actions: usize, + gas_fees_paid_by: dpp::tokens::gas_fees_paid_by::GasFeesPaidBy, + ) -> dpp::state_transition::batch_transition::batched_transition::BatchedTransition + { + use dpp::tokens::token_payment_info::v1::{TokenPaymentInfoV1, TokenShieldedPayment}; + use dpp::tokens::token_payment_info::TokenPaymentInfo; + + document_delete(Some(TokenPaymentInfo::V1(TokenPaymentInfoV1 { + gas_fees_paid_by, + shielded_payment: Box::new(TokenShieldedPayment { + actions: pool_actions(actions), + ..Default::default() + }), + ..Default::default() + }))) + } + + /// A document paying its token cost the ordinary way, out of a token balance. It + /// carries payment info but no bundle, which is the common shape the floor must leave + /// alone. + fn document_paid_from_a_balance( + ) -> dpp::state_transition::batch_transition::batched_transition::BatchedTransition + { + use dpp::tokens::token_payment_info::v0::TokenPaymentInfoV0; + use dpp::tokens::token_payment_info::TokenPaymentInfo; + + document_delete(Some(TokenPaymentInfo::V0(TokenPaymentInfoV0::default()))) + } + + fn identity_with_balance(balance: u64) -> PartialIdentity { + PartialIdentity { + id: Default::default(), + loaded_public_keys: Default::default(), + balance: Some(balance), + revision: None, + not_found_public_keys: Default::default(), + } + } + + /// A claim into a shielded pool costs one Halo 2 bundle verification plus the work of + /// its action, and the flat per-sub-transition minimum is a fraction of that. A signer + /// in between has to be refused here, before the proof runs: the claim's proof is + /// skipped in check tx because the claimable amount is only known against state, so + /// admitting the batch buys every validator the verification inside block validation, + /// which then refuses the batch unpaid and leaves the same bytes replayable. + #[test] + fn should_refuse_a_batch_claiming_into_a_pool_when_the_balance_cannot_pay_for_the_proof() { + let platform_version = PlatformVersion::latest(); + + for actions in [1usize, 3] { + let st = batch_of(vec![claim_to_pool(actions)]); + + let flat_minimum = platform_version + .fee_version + .state_transition_min_fees + .document_batch_sub_transition; + let shielded_fee = + dpp::shielded::compute_shielded_verification_fee(actions, platform_version) + .expect("shielded compute fee"); + assert!( + shielded_fee > flat_minimum, + "the proof costs more than the flat minimum, or there is no gap to close" + ); + + // Comfortably over the flat minimum, nowhere near what the proof costs. + let balance = flat_minimum * 10; + assert!(balance < shielded_fee); + + let result = st + .validate_identity_minimum_balance_pre_check( + &identity_with_balance(balance), + platform_version, + ) + .expect("pre check should not error"); + assert!( + !result.is_valid(), + "a balance of {balance} cannot pay the {shielded_fee} a {actions}-action \ + bundle will be charged" + ); + + // One credit short of the bundle's own fee is still refused, so the floor + // reserves every action's work and not just the bundle's. + let result = st + .validate_identity_minimum_balance_pre_check( + &identity_with_balance(shielded_fee + flat_minimum - 1), + platform_version, + ) + .expect("pre check should not error"); + assert!(!result.is_valid()); + + // A signer who can pay is admitted, so what the floor refuses is only what fee + // validation would have refused later anyway. + let result = st + .validate_identity_minimum_balance_pre_check( + &identity_with_balance(shielded_fee + flat_minimum), + platform_version, + ) + .expect("pre check should not error"); + assert!(result.is_valid()); + } + } + + /// A document paying its token cost out of the pool spends pool notes exactly as a + /// token transition does, and is charged the same compute fee when its action is built, + /// so the floor has to reserve it for a document batch too. + #[test] + fn should_refuse_a_document_paid_from_a_pool_when_the_balance_cannot_pay_for_the_proof() { + use dpp::tokens::gas_fees_paid_by::GasFeesPaidBy; + + let platform_version = PlatformVersion::latest(); + let st = batch_of(vec![document_paid_from_pool( + 2, + GasFeesPaidBy::DocumentOwner, + )]); + + let flat_minimum = platform_version + .fee_version + .state_transition_min_fees + .document_batch_sub_transition; + let shielded_fee = + dpp::shielded::compute_shielded_verification_fee(2, platform_version) + .expect("shielded compute fee"); + + let result = st + .validate_identity_minimum_balance_pre_check( + &identity_with_balance(shielded_fee + flat_minimum - 1), + platform_version, + ) + .expect("pre check should not error"); + assert!( + !result.is_valid(), + "one credit short of the {shielded_fee} the payment's bundle will be charged" + ); + + let result = st + .validate_identity_minimum_balance_pre_check( + &identity_with_balance(shielded_fee + flat_minimum), + platform_version, + ) + .expect("pre check should not error"); + assert!(result.is_valid()); + } + + /// A batch that asks the contract owner for its gas funds only its principal, and the + /// shielded compute fee is gas. Reserving it against the signer would refuse a + /// sponsored document the contract owner was going to pay for. + #[test] + fn should_not_reserve_the_shielded_fee_against_a_signer_who_asked_for_a_gas_sponsor() { + use dpp::tokens::gas_fees_paid_by::GasFeesPaidBy; + + let platform_version = PlatformVersion::latest(); + let st = batch_of(vec![document_paid_from_pool( + 2, + GasFeesPaidBy::ContractOwner, + )]); + + let result = st + .validate_identity_minimum_balance_pre_check( + &identity_with_balance(0), + platform_version, + ) + .expect("pre check should not error"); + assert!( + result.is_valid(), + "a sponsored batch carries no principal, so its signer is asked for nothing" + ); + + // The mempool still has to know the signer could not have paid unsponsored, so a + // batch nobody can be charged for is validated against the state in full. + let identity = identity_with_balance(0); + assert!(st + .relies_on_gas_sponsor_to_pay(&identity, platform_version) + .expect("sponsor reliance should not error")); + } + + /// Every bundle in a batch is priced, not just the first: the floor asks for the sum, + /// so a batch cannot smuggle proof work past it behind one funded bundle. + #[test] + fn should_reserve_the_fee_of_every_bundle_a_batch_carries() { + use dpp::tokens::gas_fees_paid_by::GasFeesPaidBy; + + let platform_version = PlatformVersion::latest(); + let st = batch_of(vec![ + claim_to_pool(1), + document_paid_from_pool(2, GasFeesPaidBy::DocumentOwner), + ]); + + let flat_minimum = platform_version + .fee_version + .state_transition_min_fees + .document_batch_sub_transition; + let both_bundles = + dpp::shielded::compute_shielded_verification_fee(1, platform_version) + .expect("shielded compute fee") + + dpp::shielded::compute_shielded_verification_fee(2, platform_version) + .expect("shielded compute fee"); + let floor = both_bundles + flat_minimum * 2; + + let result = st + .validate_identity_minimum_balance_pre_check( + &identity_with_balance(floor - 1), + platform_version, + ) + .expect("pre check should not error"); + assert!( + !result.is_valid(), + "one credit short of the {both_bundles} both bundles will be charged" + ); + + let result = st + .validate_identity_minimum_balance_pre_check( + &identity_with_balance(floor), + platform_version, + ) + .expect("pre check should not error"); + assert!(result.is_valid()); + } + + /// The shielded floor is scoped to the bundles a batch carries: a batch without one + /// must still be asked for the flat per-sub-transition minimum and no more, or the + /// change would raise the cost of every batch on the network. This holds on both + /// generations of the pre-check; it is what must not change, rather than what did. + #[test] + fn should_keep_the_flat_per_sub_transition_minimum_for_a_batch_without_a_pool_bundle() { + let platform_version = PlatformVersion::latest(); + + let flat_minimum = platform_version + .fee_version + .state_transition_min_fees + .document_batch_sub_transition; + + // A document with no payment info at all, and one paying its token cost the + // ordinary way: neither carries a bundle, so neither is asked for more. + for transition in [document_delete(None), document_paid_from_a_balance()] { + let st = batch_of(vec![transition]); + + let result = st + .validate_identity_minimum_balance_pre_check( + &identity_with_balance(flat_minimum), + platform_version, + ) + .expect("pre check should not error"); + assert!( + result.is_valid(), + "the flat minimum of {flat_minimum} still admits a batch that carries no bundle" + ); + + let result = st + .validate_identity_minimum_balance_pre_check( + &identity_with_balance(flat_minimum - 1), + platform_version, + ) + .expect("pre check should not error"); + assert!( + !result.is_valid(), + "one credit under the flat minimum is still refused" + ); + } + } + #[test] fn should_return_ok_for_transitions_without_balance_check() { let identity = PartialIdentity { diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs index 7f042815bc4..ed21457cfec 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/shielded/mod.rs @@ -855,10 +855,16 @@ mod token_shielded_pool_tests { dpp::shielded::compute_shielded_verification_fee(PADDED_ACTIONS, platform_version) .expect("shielded compute fee"); - // Funded well past the preliminary batch minimum — which has no Orchard component — so - // the rejection has to come from the full fee estimate, and exactly one compute fee - // short of affording the batch. - let (identity, signer, key) = setup_identity(&mut platform, rng.gen(), compute_fee); + // Funded with exactly what the preliminary batch minimum asks of a bundle-carrying + // batch — the flat per-sub-transition minimum plus that same compute fee — so the + // identity clears admission's own floor and the rejection has to come from the full + // fee estimate, one compute fee short of affording the batch. + let batch_minimum = platform_version + .fee_version + .state_transition_min_fees + .document_batch_sub_transition; + let (identity, signer, key) = + setup_identity(&mut platform, rng.gen(), compute_fee + batch_minimum); let (contract, token_id) = create_token_contract_with_owner_identity( &mut platform, identity.id(), diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs index 72c6aafde96..dbf671736af 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs @@ -243,6 +243,9 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = }, // PROTOCOL_VERSION_14: a batch that asks the contract owner to pay its gas // only has to fund its principal (purchases, contest collateral) itself. + // Otherwise, a batch carrying shielded pool bundles has to hold the compute + // fee they will be charged on top of the flat per-sub-transition minimum, + // which is orders of magnitude smaller than one bundle verification. identity_minimum_balance_pre_check: 1, document_create_transition_structure_validation: 1, // changed: v1 also cross-checks the prefunded voting balance against the contested index, leaves its amount to state validation, and refuses a `distinctFrom` identifier property equal to the value it must differ from // Reject deletes on legacy keep-history types as paid consensus errors. diff --git a/packages/rs-platform-version/src/version/system_limits/mod.rs b/packages/rs-platform-version/src/version/system_limits/mod.rs index 3fcf9888a4d..8d8d1137c64 100644 --- a/packages/rs-platform-version/src/version/system_limits/mod.rs +++ b/packages/rs-platform-version/src/version/system_limits/mod.rs @@ -115,6 +115,18 @@ pub struct SystemLimits { /// cap means batch-scoped nullifier deduplication and a delta-based pool balance write, /// not just making the ignored cases above pass. /// + /// * The batch minimum balance pre-check reserves the compute fee of every shielded pool + /// bundle a batch carries, on top of `document_batch_sub_transition` per sub-transition. + /// It refuses nobody who could have paid only because a bundle-carrying sub-transition's + /// metered fee is itself far above that flat minimum: the band the floor newly refuses is + /// `metered_fee < flat_minimum` wide, and one pool action's ~550 metered storage bytes + /// price it two orders of magnitude above the 100,000 flat minimum, so the band is empty. + /// A cap above one does not by itself change that — the floor and the charge are both + /// per sub-transition — but a later change that lets a batch carry a bundle alongside + /// sub-transitions cheaper than the flat minimum would reopen it, and the floor would + /// then start refusing batches that fee validation would have executed. Recheck the + /// inequality rather than assuming it. + /// /// Five cases in `rs-drive`'s `batched_group_drain` suite are `#[ignore]`d for exactly this /// reason; the rest of that suite runs. Anyone raising this cap should un-ignore those five /// first and make them pass. diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index b468c58952a..4b919a84025 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -780,6 +780,23 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// whose anchor is never checked against a pool, binds a per-kind tag, /// the token id and the batch owner (for a group action mint, the /// proposer). +/// A batch carrying any of these bundles, or a document whose token cost +/// is paid out of a pool, has to hold the compute fee the bundles will be +/// charged (`compute_shielded_verification_fee` per bundle-carrying +/// sub-transition): the batch minimum balance pre-check v1 +/// (`identity_minimum_balance_pre_check`) reserves it on top of the flat +/// per-sub-transition minimum, which is orders of magnitude smaller. A +/// batch without a bundle is asked for the flat minimum, unchanged, and +/// one that asks the contract owner to pay its gas is asked for its +/// principal alone as in item 11, the compute fee being gas. The floor +/// refuses only what fee validation would refuse later, but it refuses it +/// before the Halo 2 work: `TokenClaimToPool`'s proof is skipped in check +/// tx, since its claimable amount is only known against state, so without +/// the floor a signer between the two numbers cleared the mempool with no +/// verification run and every validator then did the verification inside +/// block validation, only to refuse the batch unpaid, leaving the same +/// bytes replayable. The same holds for the bundle of a group action's +/// non-proposing signer, whose proof check tx also skips. /// The pool balances are a term of the token conservation check /// (`calculate_total_tokens_balance` v1 in `DRIVE_TOKEN_METHOD_VERSIONS_V2`). /// `record_token_shielded_pool_anchors` From a0837c2525925c6b8efea2f75ba94dfdf80a4220 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Mon, 28 Sep 2026 03:07:04 +0700 Subject: [PATCH 86/94] test: make the token shielded pool tests able to fail Seven tests reported coverage that was not there. Each either restated the implementation it was meant to check, or asserted something weaker than its own failure message claimed. - Credit pool existence gate: drive the six unproved credit reads through the real query handlers instead of re-asserting the selector's Credit arm, and check each for the value an empty pool gives. A credit refusal introduced at a handler was invisible to the helper-only assertion. - Pool total read metering: assert the credits the read books rather than a count of cost operations. Both modes always issue exactly one read, so the count was equal by construction and could not speak to what the quote covers. - Group action shielded nullifiers: assert the JSON wire shape. A serializer that dropped the spend flag and a deserializer that defaulted it back to false agreed with each other and passed the round trip unnoticed. - Batch unique identifiers without a pool bundle: assert the two key strings, written out rather than rebuilt from the batch, so a change to how a mempool key is spelled fails here. It was a strict subset of the mixed-transitions test and never inspected a value despite "unchanged" in its name. - Format version 0 pool refusal, both wires: assert which key the refusal names, and add a control showing the same configuration decodes once the pool request is removed. is_err() alone passed on any fixture breakage. - Flattened version 1 configuration: give the fixture a key no format version carries, which is what the test's stated purpose needs in order to exercise the flatten asymmetry at all. - Events that carry no note: deleted. The match is exhaustive with explicit arms and none of those variants has a note field, so the compiler enforces the property and enforces it first in both places. The test was a second copy of the arm list with no runtime failure mode, and public_note's own doc comment already records the invariant. Every remaining test was verified by breaking the rule it names, watching it fail, and restoring. Two defects these tests now make visible, neither fixed here: - The pool total read is under-metered. The estimate books 7,460 credits while execution charges 14,180, for a shield and an unshield alike, so the difference is charged to nobody. Closing it sets what a pool operation costs and is therefore a fee-schedule decision; the committed assertion is the weaker one the current schedule satisfies, with the gap and the assertion that replaces it recorded at the test. - TokenConfigurationV1 accepts and silently drops any unrecognized key, on both the JSON and Value wires, where V0 refuses it. deny_unknown_fields on V1 does work alongside serde(flatten) and leaves the legitimate flattened round trip passing, so the fix is one attribute, but it changes which configurations decode and is left as a deliberate decision. Co-Authored-By: Claude Opus 5 (1M context) --- .../token_configuration/mod.rs | 116 ++++++++++-- .../src/state_transition/proof_result.rs | 15 ++ .../document/batch_transition/tests.rs | 18 +- packages/rs-dpp/src/tokens/token_event.rs | 20 -- .../rs-drive-abci/src/query/shielded/mod.rs | 174 +++++++++++++++++- .../src/drive/tokens/shielded_pool/mod.rs | 35 +++- 6 files changed, 326 insertions(+), 52 deletions(-) diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs index 03ec11a417b..1e4f6de4f20 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs @@ -740,37 +740,69 @@ mod unknown_configuration_key_tests { json } + /// The same fixture on the Value wire, which is the path a contract is ingested through. + fn v0_value_asking_for_a_pool() -> platform_value::Value { + let mut value = TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()) + .to_object() + .expect("to_object"); + value.as_map_mut().expect("configuration map").push(( + platform_value::Value::Text("hasShieldedPool".to_string()), + platform_value::Value::Bool(true), + )); + value + } + #[test] fn should_refuse_a_pool_asked_for_at_format_version_0_rather_than_drop_it() { let json = v0_json_asking_for_a_pool(); - let decoded = TokenConfiguration::from_json(json); + + // The control. Take the pool request back out and the very same configuration decodes, + // at format version 0 and without a pool. It is what makes the refusal below evidence + // about the pool request rather than about a fixture that stopped building a valid V0. + let mut without_the_request = json.clone(); + without_the_request + .as_object_mut() + .expect("configuration object") + .remove("hasShieldedPool"); + let control = TokenConfiguration::from_json(without_the_request) + .expect("the configuration decodes once the pool request is taken out"); + assert_eq!(control.format_version(), 0); + assert!(!control.has_shielded_pool()); + + // A configuration asking for a pool must not decode as a token that can never have one, + // and the refusal has to name the key it refused: any other message means the decoder + // tripped over something else and the pool request was never the reason. + let error = TokenConfiguration::from_json(json).expect_err("the pool request is refused"); assert!( - decoded.is_err(), - "a configuration asking for a pool must not decode as a token that can never \ - have one, got {:?}", - decoded.map(|configuration| configuration.has_shielded_pool()) + error.to_string().contains("hasShieldedPool"), + "the refusal must name the key it refused, got {error}" ); } #[test] fn should_refuse_a_pool_asked_for_at_format_version_0_on_the_value_wire_too() { - let mut value = TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()) - .to_object() - .expect("to_object"); - value.as_map_mut().expect("configuration map").push(( - platform_value::Value::Text("hasShieldedPool".to_string()), - platform_value::Value::Bool(true), - )); - let decoded = TokenConfiguration::from_object(value); + let value = v0_value_asking_for_a_pool(); + + let mut without_the_request = value.clone(); + without_the_request + .as_map_mut() + .expect("configuration map") + .retain(|(key, _)| key.as_text() != Some("hasShieldedPool")); + let control = TokenConfiguration::from_object(without_the_request) + .expect("the configuration decodes once the pool request is taken out"); + assert_eq!(control.format_version(), 0); + assert!(!control.has_shielded_pool()); + + let error = + TokenConfiguration::from_object(value).expect_err("the pool request is refused"); assert!( - decoded.is_err(), - "the contract ingest path must refuse the same configuration, got {:?}", - decoded.map(|configuration| configuration.has_shielded_pool()) + error.to_string().contains("hasShieldedPool"), + "the refusal must name the key it refused, got {error}" ); } - /// The V0 fields reach a V1 configuration through `serde(flatten)`, which drops the - /// strictness the V0 variant is read with. A V1 configuration must keep decoding. + /// Asking for a pool moves the configuration to format version 1, whose V0 fields sit at the + /// top level of the wire through `serde(flatten)`. Both wires have to read that shape back. #[test] fn should_still_decode_a_pooled_configuration_whose_v0_fields_are_flattened() { let mut configuration = @@ -793,4 +825,52 @@ mod unknown_configuration_key_tests { configuration ); } + + /// Format version 1's field set is open where version 0's is closed: an unrecognized key is + /// dropped instead of refused, on both wires. + /// + /// `deny_unknown_fields` is what closes version 0, and it is declared on + /// `TokenConfigurationV0`. Version 1 reads that struct through `serde(flatten)`, which hands + /// it only the keys left over once the outer struct has claimed its own — so the attribute + /// governs what version 0 does with the leftovers it is given, and says nothing about the + /// outer struct. `TokenConfigurationV1` declares no refusal of its own, so a key neither + /// version names is accepted and dropped, and a caller's typo reaches the chain as a + /// configuration quietly missing whatever they meant to set. + /// + /// Pinned because it is the asymmetry a reader will not expect from the version 0 refusal + /// the tests above assert, and because closing it would change which configurations decode + /// at all — a decision to take deliberately, not as a side effect of tightening a struct. + #[test] + fn should_drop_rather_than_refuse_an_unrecognized_key_at_format_version_1() { + let mut configuration = + TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); + configuration.set_has_shielded_pool(true); + + // A key no format version carries. Beside `$formatVersion: "0"` a key like this is what + // earns the refusal the tests above assert; at version 1 it decodes. + const UNRECOGNIZED_KEY: &str = "thisKeyIsInNoFormatVersion"; + + let mut json = configuration.to_json().expect("to_json"); + json.as_object_mut() + .expect("configuration object") + .insert(UNRECOGNIZED_KEY.to_string(), serde_json::Value::Bool(true)); + let decoded = TokenConfiguration::from_json(json) + .expect("an unrecognized key is dropped at format version 1, not refused"); + // Equal to the configuration without the key: it is dropped rather than kept anywhere, + // and the fields that are recognized are unaffected by its presence. + assert_eq!(decoded, configuration); + assert_eq!(decoded.format_version(), 1); + assert!(decoded.has_shielded_pool()); + + let mut value = configuration.to_object().expect("to_object"); + value.as_map_mut().expect("configuration map").push(( + platform_value::Value::Text(UNRECOGNIZED_KEY.to_string()), + platform_value::Value::Bool(true), + )); + let decoded = + TokenConfiguration::from_object(value).expect("the contract ingest path drops it too"); + assert_eq!(decoded, configuration); + assert_eq!(decoded.format_version(), 1); + assert!(decoded.has_shielded_pool()); + } } diff --git a/packages/rs-dpp/src/state_transition/proof_result.rs b/packages/rs-dpp/src/state_transition/proof_result.rs index 4dcd7746587..1bac0228a7a 100644 --- a/packages/rs-dpp/src/state_transition/proof_result.rs +++ b/packages/rs-dpp/src/state_transition/proof_result.rs @@ -506,6 +506,21 @@ mod json_convertible_tests { vec![(vec![1u8, 2, 3], false), (vec![4u8, 5, 6], false)], ); let json = original.to_json().expect("to_json"); + // Each nullifier is a `[bytes, spent]` pair and the `false` is on the wire. Asserted on + // the wire rather than across a round trip because the round trip cannot see it: a + // serializer that wrote the nullifier alone and a deserializer that filled the flag back + // in as `false` would agree with each other and hand a client a burn with no spend + // status at all. + assert_eq!( + json, + json!({ + "VerifiedTokenGroupActionWithShieldedNullifiers": [ + 1, + "actionActive", + [[[1, 2, 3], false], [[4, 5, 6], false]], + ], + }) + ); let recovered = StateTransitionProofResult::from_json(json).expect("from_json"); assert_eq!(original, recovered); } diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/tests.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/tests.rs index b77b83aabd7..d55399d2319 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/tests.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/tests.rs @@ -700,14 +700,28 @@ mod batch_transition_tests { /// A batch that touches no pool keeps the identifiers it had: its mempool key must not move /// because pool kinds exist. + /// + /// The expected keys are written out rather than rebuilt from the batch, so that a change to + /// how a key is spelled — the field order, the encodings, the separator, the nonce's radix — + /// fails here. Rebuilding them would restate whatever the code does. Each is the batch's + /// owner, the transition's contract and its nonce: owner `[0x02; 32]` from `make_batch_v1`, + /// contract `[0xAA; 32]` at nonce 1 for the document and `[0xBB; 32]` at nonce 2 for the + /// token, base64 for the identifiers and hex for the nonce. #[test] fn v1_unique_identifiers_are_unchanged_for_a_batch_without_a_pool_bundle() { let batch = make_batch_v1(vec![ BatchedTransition::Document(make_delete_transition(1)), BatchedTransition::Token(make_token_burn_transition(2, 100)), ]); - let ids = batch.unique_identifiers(); - assert_eq!(ids.len(), 2, "no nullifier keys to add, got {ids:?}"); + assert_eq!( + batch.unique_identifiers(), + vec![ + "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=\ + -qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqo=-1", + "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=\ + -u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7s=-2", + ] + ); } // ----------------------------------------------------------------------- diff --git a/packages/rs-dpp/src/tokens/token_event.rs b/packages/rs-dpp/src/tokens/token_event.rs index 8bc0967c980..460e0d1795f 100644 --- a/packages/rs-dpp/src/tokens/token_event.rs +++ b/packages/rs-dpp/src/tokens/token_event.rs @@ -1273,24 +1273,4 @@ mod public_note_tests { None ); } - - /// The events that carry no note at all: nothing to show a co-signer. - #[test] - fn should_report_no_note_on_the_events_that_carry_none() { - for event in [ - TokenEvent::DirectPurchase(5, 50), - TokenEvent::Shield(5), - TokenEvent::Unshield(Identifier::from([1u8; 32]), 5), - TokenEvent::ShieldedTransfer, - TokenEvent::ClaimToPool(5), - TokenEvent::DirectPurchaseToPool(5, 50), - ] { - assert_eq!( - event.public_note(), - None, - "{} carries no note", - event.associated_document_type_name() - ); - } - } } diff --git a/packages/rs-drive-abci/src/query/shielded/mod.rs b/packages/rs-drive-abci/src/query/shielded/mod.rs index df7bb5ccf74..e5e8e45f93e 100644 --- a/packages/rs-drive-abci/src/query/shielded/mod.rs +++ b/packages/rs-drive-abci/src/query/shielded/mod.rs @@ -767,16 +767,174 @@ mod tests { } } - /// The credit pool is built with the initial state structure, so a credit-pool read is never - /// gated on the pool's existence — gating it would refuse every shielded credit query. + /// A credit-pool read is never refused for the pool not existing. The client does not name + /// the credit pool, so there is nothing for an existence gate to refuse, and a gate that + /// caught it would refuse every shielded credit query on the network at once. + /// + /// Asserted through the six unproved reads rather than against the gate helper, because the + /// gate is only one of the places a refusal could come from and the helper answering `Ok` is + /// not the same fact as a client getting an answer. Each read here is checked for the value + /// an empty pool gives, which is what a `NotFound` would have replaced. #[test] - fn credit_pool_reads_are_not_gated_on_pool_existence() { - let (platform, _state, version) = setup_platform(None, Network::Testnet, None); + fn unproved_credit_pool_reads_are_never_refused_for_the_pool_not_existing() { + let (platform, state, version) = setup_platform(None, Network::Testnet, None); - assert!(matches!( - ShieldedPoolSelector::Credit.validate_pool_exists(&platform.drive, version), - Ok(Ok(())) - )); + let notes_count = platform + .query_shielded_notes_count( + GetShieldedNotesCountRequest { + version: Some(NotesCountRequestVersion::V0( + GetShieldedNotesCountRequestV0 { + prove: false, + token_id: None, + }, + )), + }, + &state, + version, + ) + .expect("expected the query to complete"); + assert!(notes_count.errors.is_empty(), "{:?}", notes_count.errors); + match notes_count.data.and_then(|response| response.version) { + Some(NotesCountResponseVersion::V0(v0)) => assert!(matches!( + v0.result, + Some(get_shielded_notes_count_response_v0::Result::TotalNotesCount(0)) + )), + other => panic!("expected a notes count, got {other:?}"), + } + + let encrypted_notes = platform + .query_shielded_encrypted_notes( + GetShieldedEncryptedNotesRequest { + version: Some(EncryptedNotesRequestVersion::V0( + GetShieldedEncryptedNotesRequestV0 { + start_index: 0, + count: 1, + prove: false, + token_id: None, + }, + )), + }, + &state, + version, + ) + .expect("expected the query to complete"); + assert!( + encrypted_notes.errors.is_empty(), + "{:?}", + encrypted_notes.errors + ); + match encrypted_notes.data.and_then(|response| response.version) { + Some(EncryptedNotesResponseVersion::V0(v0)) => match v0.result { + Some(get_shielded_encrypted_notes_response_v0::Result::EncryptedNotes(notes)) => { + assert!(notes.entries.is_empty()) + } + other => panic!("expected encrypted notes, got {other:?}"), + }, + other => panic!("expected a v0 response, got {other:?}"), + } + + let anchors = platform + .query_shielded_anchors( + GetShieldedAnchorsRequest { + version: Some(AnchorsRequestVersion::V0(GetShieldedAnchorsRequestV0 { + prove: false, + token_id: None, + })), + }, + &state, + version, + ) + .expect("expected the query to complete"); + assert!(anchors.errors.is_empty(), "{:?}", anchors.errors); + match anchors.data.and_then(|response| response.version) { + Some(AnchorsResponseVersion::V0(v0)) => match v0.result { + Some(get_shielded_anchors_response_v0::Result::Anchors(anchors)) => { + assert!(anchors.anchors.is_empty()) + } + other => panic!("expected anchors, got {other:?}"), + }, + other => panic!("expected a v0 response, got {other:?}"), + } + + let most_recent_anchor = platform + .query_most_recent_shielded_anchor( + GetMostRecentShieldedAnchorRequest { + version: Some(MostRecentAnchorRequestVersion::V0( + GetMostRecentShieldedAnchorRequestV0 { + prove: false, + token_id: None, + }, + )), + }, + &state, + version, + ) + .expect("expected the query to complete"); + assert!( + most_recent_anchor.errors.is_empty(), + "{:?}", + most_recent_anchor.errors + ); + match most_recent_anchor + .data + .and_then(|response| response.version) + { + Some(MostRecentAnchorResponseVersion::V0(v0)) => match v0.result { + // The empty-index sentinel this endpoint documents. + Some(get_most_recent_shielded_anchor_response_v0::Result::Anchor(anchor)) => { + assert_eq!(anchor, vec![0; 32]) + } + other => panic!("expected an anchor, got {other:?}"), + }, + other => panic!("expected a v0 response, got {other:?}"), + } + + let nullifiers = platform + .query_shielded_nullifiers( + GetShieldedNullifiersRequest { + version: Some(NullifiersRequestVersion::V0( + GetShieldedNullifiersRequestV0 { + nullifiers: vec![vec![0x11; 32]], + prove: false, + token_id: None, + }, + )), + }, + &state, + version, + ) + .expect("expected the query to complete"); + assert!(nullifiers.errors.is_empty(), "{:?}", nullifiers.errors); + match nullifiers.data.and_then(|response| response.version) { + Some(NullifiersResponseVersion::V0(v0)) => match v0.result { + Some(get_shielded_nullifiers_response_v0::Result::NullifierStatuses(statuses)) => { + assert!(statuses.entries.iter().all(|status| !status.is_spent)) + } + other => panic!("expected nullifier statuses, got {other:?}"), + }, + other => panic!("expected a v0 response, got {other:?}"), + } + + let pool_state = platform + .query_shielded_pool_state( + GetShieldedPoolStateRequest { + version: Some(PoolStateRequestVersion::V0(GetShieldedPoolStateRequestV0 { + prove: false, + token_id: None, + })), + }, + &state, + version, + ) + .expect("expected the query to complete"); + assert!(pool_state.errors.is_empty(), "{:?}", pool_state.errors); + match pool_state.data.and_then(|response| response.version) { + Some(PoolStateResponseVersion::V0(v0)) => assert!(matches!( + v0.result, + Some(get_shielded_pool_state_response_v0::Result::TotalBalance(0)) + )), + other => panic!("expected a total balance, got {other:?}"), + } } /// A token pool the chain holds passes the existence gate, and one it does not is refused — diff --git a/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs b/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs index b4bf540358f..7a691762a6c 100644 --- a/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs +++ b/packages/rs-drive/src/drive/tokens/shielded_pool/mod.rs @@ -697,6 +697,18 @@ mod tests { /// Only a change of the total reads it. A shielded transfer leaves the total alone and so /// meters no read in either mode, which is what keeps what a transfer costs from revealing /// how much of the token is shielded. + /// + /// What is asserted is that the read is booked, in credits, and not merely that both modes + /// count one read: the count is equal whenever the read happens at all, so it cannot speak + /// to what the quote covers. + /// + /// It does not yet assert that the quote covers the charge, because it does not. The two + /// modes price the same read differently — the estimate from the pool's modelled layer, + /// execution from the tree — and the estimate books less than execution charges: 7,460 + /// credits against 14,180, for a shield and an unshield alike, a shortfall nobody pays. + /// Closing it sets what a pool operation costs, which is a decision about the fee schedule + /// and not one this test can make; `estimated_fee >= actual_fee` is the assertion that + /// belongs here once it is taken. #[test] fn should_meter_the_pool_total_read_when_estimating_as_well_as_when_applying() { let platform_version = PlatformVersion::latest(); @@ -763,11 +775,26 @@ mod tests { ] { let (estimated_reads, estimated_fee) = metered(balance_change, true); let (actual_reads, actual_fee) = metered(balance_change, false); + + // One read of the total per pool update, in both modes: a pool update moves the + // total once, so it reads it once. + assert_eq!( + (estimated_reads, actual_reads), + (1, 1), + "{balance_change:?} must read the total exactly once in either mode" + ); + + // Execution charges for the read, so the quote has to book credits for it. Zero + // would mean the read reaches state on a path nobody was quoted for. + assert!( + actual_fee > 0, + "{balance_change:?} reads the total when applied but that read costs nothing, so \ + there is no charge for the estimate to cover" + ); assert!( - estimated_reads >= actual_reads, - "{balance_change:?} meters {actual_reads} read(s) worth {actual_fee} credits when \ - applied but only {estimated_reads} worth {estimated_fee} when estimated, so the \ - difference is charged to nobody" + estimated_fee > 0, + "{balance_change:?} meters a read worth {actual_fee} credits when applied and \ + books {estimated_fee} when estimated, so the read is charged to nobody" ); } From 586c465f168be24413dbee897d79bda962aeb7ad Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Mon, 28 Sep 2026 03:22:03 +0700 Subject: [PATCH 87/94] docs(book): state the deadline a group burn from a pool inherits A group burn's bundle spends notes, so it names an anchor, and the pool keeps an anchor only for the retention window. The bundle cannot be re-proved against a fresher one to buy time: the action id covers the digest of the actions and the signatures are over a sighash that includes the anchor, so a rebuilt bundle is a different group action. With neither expiry nor cancellation on group actions, a proposal that has not gathered enough signing power before its anchor is pruned can no longer close and can no longer be withdrawn either. The retention window is the real deadline, and an abandoned proposal is permanent. Also drops a restatement of `validate_token_config_update: 0` that the spread of the previous table already supplies. Verified the value is still 0 at every protocol version from 1 to 14. Co-Authored-By: Claude Opus 5 (1M context) --- book/src/data-model/token-shielded-pools.md | 13 +++++++++++++ .../dpp_versions/dpp_validation_versions/v5.rs | 1 - 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/book/src/data-model/token-shielded-pools.md b/book/src/data-model/token-shielded-pools.md index 35a684ce5b0..a3e084e67b7 100644 --- a/book/src/data-model/token-shielded-pools.md +++ b/book/src/data-model/token-shielded-pools.md @@ -101,6 +101,19 @@ Outputs-only bundles (shield, mint, claim, purchase) have no spends, so their an checked against the pool; the client builds them against the empty tree. Spending bundles must name an anchor the pool has recorded. +A group burn from the pool inherits a deadline from that requirement, and it is worth stating +plainly because nothing in the group machinery announces it. The proposer's bundle spends notes, +so it names an anchor, and the pool keeps an anchor only for `shielded_anchor_retention_blocks`. +The bundle cannot be re-proved against a fresher anchor to buy more time: the action id covers +the digest of the actions and the signatures are over a sighash that includes the anchor, so a +bundle rebuilt on a new anchor is a different group action rather than the same one continued. +Group actions have neither an expiry nor a cancellation, so a proposal that has not gathered +enough signing power before its anchor is pruned can no longer close and no longer be withdrawn +either — it simply remains open. A group intending to burn from a pool should therefore gather +its signatures well inside the retention window, and an abandoned proposal should be treated as +permanent. Closing this properly needs group actions to gain an expiry or a cancellation; until +they do, the retention window is the real deadline. + The preimage of an outputs-only bundle binds its owner — the batch owner, or for a group action mint its proposer, whose bundle every other signer submits unchanged — so nobody else's transition can land a bundle lifted out of the mempool ahead of its author. What the preimage diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v5.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v5.rs index 0f8da7df9bb..90ca0d7155b 100644 --- a/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v5.rs +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_validation_versions/v5.rs @@ -23,7 +23,6 @@ pub const DPP_VALIDATION_VERSIONS_V5: DPPValidationVersions = DPPValidationVersi data_contract: DataContractValidationVersions { validate_config_update: 2, validate_once_per_identity_distribution: Some(0), - validate_token_config_update: 0, ..DPP_VALIDATION_VERSIONS_V4.data_contract }, document_type: DocumentTypeValidationVersions { From 3ab258d1d59f6f495c6afc12dd2d10dd873de1a4 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Mon, 28 Sep 2026 07:57:50 +0700 Subject: [PATCH 88/94] fix(dpp)!: refuse an unrecognized token configuration key at format version 1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Format version 0 already refuses a key it does not define, so that a misspelled `hasShieldedPool` cannot leave a token permanently without the pool it was meant to have. That refusal could not reach format version 1, which is the only version carrying the pool fields: version 1 takes version 0's fields through `serde(flatten)`, and an unrecognized key arriving at the outer struct is never offered to the flattened one. So the version that had the refusal had no field to lose, and the version with the fields had no refusal. The attribute belongs on the version that owns the flatten, and it works there — the flattened round trip still decodes while the unknown key is named and refused, on the JSON wire and on the contract ingest wire alike. Test would have caught this in CI: before the change the key was dropped and the configuration decoded as a pooled token; after it, both wires refuse and name the key. The test also decodes the same configuration without the key, so a refusal caused by a fixture that stopped building cannot pass for the refusal under test. No fixture in this repository declares a token at format version 1, and the JavaScript binding deserializes into its own options struct and then builds a format-0 configuration field by field, so neither is affected. Co-Authored-By: Claude Opus 5 (1M context) --- .../token_configuration/mod.rs | 44 ++++++++++++------- .../token_configuration/v1/mod.rs | 7 ++- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs index 1e4f6de4f20..fe55fbb392d 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/mod.rs @@ -840,37 +840,51 @@ mod unknown_configuration_key_tests { /// Pinned because it is the asymmetry a reader will not expect from the version 0 refusal /// the tests above assert, and because closing it would change which configurations decode /// at all — a decision to take deliberately, not as a side effect of tightening a struct. + /// A key no format version carries is refused at format version 1, as it is at 0. + /// + /// The refusal has to hold on both wires. `TokenConfigurationV1` takes version 0's fields + /// through `serde(flatten)`, and an unknown key arriving through a flattened field is not + /// offered to the inner struct at all, so version 0's own refusal cannot reach it — the + /// attribute has to sit on the version that owns the flatten. #[test] - fn should_drop_rather_than_refuse_an_unrecognized_key_at_format_version_1() { + fn should_refuse_an_unrecognized_key_at_format_version_1() { let mut configuration = TokenConfiguration::V0(TokenConfigurationV0::default_most_restrictive()); configuration.set_has_shielded_pool(true); - // A key no format version carries. Beside `$formatVersion: "0"` a key like this is what - // earns the refusal the tests above assert; at version 1 it decodes. const UNRECOGNIZED_KEY: &str = "thisKeyIsInNoFormatVersion"; let mut json = configuration.to_json().expect("to_json"); json.as_object_mut() .expect("configuration object") .insert(UNRECOGNIZED_KEY.to_string(), serde_json::Value::Bool(true)); - let decoded = TokenConfiguration::from_json(json) - .expect("an unrecognized key is dropped at format version 1, not refused"); - // Equal to the configuration without the key: it is dropped rather than kept anywhere, - // and the fields that are recognized are unaffected by its presence. - assert_eq!(decoded, configuration); - assert_eq!(decoded.format_version(), 1); - assert!(decoded.has_shielded_pool()); + let error = TokenConfiguration::from_json(json) + .expect_err("an unrecognized key must be refused, not dropped") + .to_string(); + assert!( + error.contains(UNRECOGNIZED_KEY), + "the refusal must name the key it refused, got {error}" + ); let mut value = configuration.to_object().expect("to_object"); value.as_map_mut().expect("configuration map").push(( platform_value::Value::Text(UNRECOGNIZED_KEY.to_string()), platform_value::Value::Bool(true), )); - let decoded = - TokenConfiguration::from_object(value).expect("the contract ingest path drops it too"); - assert_eq!(decoded, configuration); - assert_eq!(decoded.format_version(), 1); - assert!(decoded.has_shielded_pool()); + let error = TokenConfiguration::from_object(value) + .expect_err("the contract ingest path must refuse it too") + .to_string(); + assert!( + error.contains(UNRECOGNIZED_KEY), + "the refusal must name the key it refused, got {error}" + ); + + // The same configuration without the key still decodes, so the refusal is the key's + // doing and not a fixture that stopped building. + let clean = TokenConfiguration::from_json(configuration.to_json().expect("to_json")) + .expect("the configuration itself still decodes"); + assert_eq!(clean, configuration); + assert_eq!(clean.format_version(), 1); + assert!(clean.has_shielded_pool()); } } diff --git a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v1/mod.rs b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v1/mod.rs index eb5f0d9d4d1..6e84cc0b16a 100644 --- a/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v1/mod.rs +++ b/packages/rs-dpp/src/data_contract/associated_token/token_configuration/v1/mod.rs @@ -25,7 +25,12 @@ use std::fmt; /// Enabling a pool on an existing token is not supported by this version. #[cfg_attr(feature = "json-conversion", json_safe_fields)] #[derive(Serialize, Deserialize, Decode, Encode, Debug, Clone, PartialEq, Eq, DecodeUntrusted)] -#[serde(rename_all = "camelCase")] +// An unknown key is refused rather than dropped: what a token can do is fixed when it is +// created, so a misspelled `hasShieldedPool` would otherwise leave the token permanently +// without the pool it was meant to have, and say nothing. Version 0 carries the same refusal, +// but it cannot reach a key arriving here: `serde(flatten)` never offers an unrecognized key to +// the flattened struct, so the attribute has to sit on the version that owns the flatten. +#[serde(rename_all = "camelCase", deny_unknown_fields)] pub struct TokenConfigurationV1 { /// The V0 configuration this version extends. #[serde(flatten)] From b46f473bba26ce285ba9fd56cc4f4e67bceb9509 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 29 Sep 2026 13:55:42 +0700 Subject: [PATCH 89/94] docs(dpp): say why the payment info accessor clones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The token payment info gained a variant that owns its data, so the accessor can no longer copy. The value is the same either way, which is what makes the edit inert for the released protocol versions that select this generation — the last of this branch's in-place edits to a shipped generation that had not yet said so. Co-Authored-By: Claude Opus 5 (1M context) --- .../document_base_transition/v1/v1_methods.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v1/v1_methods.rs b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v1/v1_methods.rs index 083cbb3f560..4c23c26ad5a 100644 --- a/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v1/v1_methods.rs +++ b/packages/rs-dpp/src/state_transition/state_transitions/document/batch_transition/batched_transition/document_base_transition/v1/v1_methods.rs @@ -21,6 +21,8 @@ pub trait DocumentBaseTransitionV1Methods: DocumentBaseTransitionV0Methods { impl DocumentBaseTransitionV1Methods for DocumentBaseTransitionV1 { fn token_payment_info(&self) -> Option { + // Cloned rather than copied because the payment info gained a variant that owns its + // data. Same value either way, so nothing a released protocol version reads changes. self.token_payment_info.clone() } From 74822b0df4119e296f49d7ec1c100512b586fdb5 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 29 Sep 2026 14:48:20 +0700 Subject: [PATCH 90/94] feat(wasm-dpp2): set a token's shielded pool notes threshold at creation TokenConfigurationV1 carries minimum_pool_notes_for_outgoing, the count of note commitments a token's shielded pool must hold before tokens may leave it for a visible destination. The JavaScript construction path had no way to ask for one: TokenConfigurationOptions did not declare the field, so a caller writing minimumPoolNotesForOutgoing into new TokenConfiguration({...}) had it dropped with no diagnostic, and the only way to get a threshold was a later TokenConfigUpdate, leaving a new pool unguarded until then. The field now takes the route hasShieldedPool already takes: a serde field on the options struct, a documented key on the TypeScript interface, and a getter and setter on the wasm class. Only a format-version-1 configuration has anywhere to keep a threshold, so one asked for without hasShieldedPool is refused rather than stored; the alternative would be a format-version-1 configuration with no pool, which contradicts keeping the two representations canonical and which nothing reads. TokenConfigurationOptions is deliberately left non-strict. deny_unknown_fields there would reject every valid call, because the same JavaScript object also carries the thirteen complex keys the constructor extracts separately and the struct does not declare. Making that path strict, which would close this whole class of silently dropped options, is a larger change and a separate decision. The threshold's 250 bound is unchanged and still enforced where it was, on contract create and update and on the configuration a TokenConfigUpdate proposes; the binding adds no check of its own. Tests would have caught this in CI: TokenConfiguration.spec.ts gains 7 specs, 2 passing / 7 failing before the change, 9 passing after. Co-Authored-By: Claude Opus 5 (1M context) --- .../configuration/token_configuration.rs | 80 ++++++++++ .../tests/unit/TokenConfiguration.spec.ts | 139 ++++++++++++++++++ 2 files changed, 219 insertions(+) diff --git a/packages/wasm-dpp2/src/tokens/configuration/token_configuration.rs b/packages/wasm-dpp2/src/tokens/configuration/token_configuration.rs index 211a3fbbba7..5d4ce2bd42d 100644 --- a/packages/wasm-dpp2/src/tokens/configuration/token_configuration.rs +++ b/packages/wasm-dpp2/src/tokens/configuration/token_configuration.rs @@ -40,6 +40,8 @@ struct TokenConfigurationOptions { description: Option, #[serde(default)] has_shielded_pool: bool, + #[serde(default)] + minimum_pool_notes_for_outgoing: Option, } #[wasm_bindgen(typescript_custom_section)] @@ -71,6 +73,21 @@ export interface TokenConfigurationOptions { * (no action takers and no admins): shielded notes cannot be frozen or destroyed. */ hasShieldedPool?: boolean; + /** + * The fewest notes the token's shielded pool must hold before tokens may leave it for a + * visible destination: unshielding to an identity, burning from the pool, or paying a + * document's token cost out of the pool. Transfers that stay inside the pool are never + * held back. Requires hasShieldedPool. + * + * What is counted is note commitments, not depositors, and one bundle carries several of + * them, so a single depositor can reach a threshold alone: it says how busy the pool + * should be before anyone leaves it, and guarantees no anonymity set. Omitted, or + * undefined, means no threshold, which is the default because any threshold above 0 keeps + * a new pool's first depositors from leaving until enough notes accumulate. A contract + * create refuses a threshold above the protocol's maximum of 250. The issuer changes it + * afterwards with a TokenConfigUpdate, under its own change control rules. + */ + minimumPoolNotesForOutgoing?: bigint; } "#; @@ -98,6 +115,32 @@ impl From for TokenConfiguration { impl_try_from_js_value!(TokenConfigurationWasm, "TokenConfiguration"); +/// Writes the shielded pool's outgoing notes threshold, which only a configuration that has a +/// pool carries at all. +/// +/// A threshold asked for on a token without a pool is refused rather than stored. The only +/// place to keep it would be a format-version-1 configuration with no pool, which contradicts +/// how the two are kept canonical — a token without a pool serializes as format version 0, so +/// that every protocol version accepts it — and which nothing would ever read. Dropping it +/// quietly is worse still: the caller asked for a guard and would get none. +fn write_minimum_pool_notes_for_outgoing( + configuration: &mut TokenConfiguration, + minimum_pool_notes_for_outgoing: Option, +) -> WasmDppResult<()> { + match configuration { + TokenConfiguration::V1(v1) => { + v1.minimum_pool_notes_for_outgoing = minimum_pool_notes_for_outgoing; + Ok(()) + } + // Clearing a threshold a pool-less token never had leaves it as it already is. + TokenConfiguration::V0(_) if minimum_pool_notes_for_outgoing.is_none() => Ok(()), + TokenConfiguration::V0(_) => Err(WasmDppError::invalid_argument( + "'minimumPoolNotesForOutgoing' needs 'hasShieldedPool': a token without a shielded \ + pool holds no notes to count", + )), + } +} + #[wasm_bindgen(js_class = TokenConfiguration)] impl TokenConfigurationWasm { #[wasm_bindgen(constructor)] @@ -159,6 +202,12 @@ impl TokenConfigurationWasm { configuration.set_has_shielded_pool(true); } + // After the pool, since only a pooled configuration has somewhere to keep a threshold. + write_minimum_pool_notes_for_outgoing( + &mut configuration, + opts.minimum_pool_notes_for_outgoing, + )?; + Ok(TokenConfigurationWasm(configuration)) } @@ -224,6 +273,13 @@ impl TokenConfigurationWasm { self.0.format_version() } + /// The fewest notes the shielded pool must hold before tokens may leave it. 0 means no + /// threshold, which is also what every token without a pool reports. + #[wasm_bindgen(getter = "minimumPoolNotesForOutgoing")] + pub fn minimum_pool_notes_for_outgoing(&self) -> u64 { + self.0.minimum_pool_notes_for_outgoing() + } + #[wasm_bindgen(getter = "manualMintingRules")] pub fn manual_minting_rules(&self) -> ChangeControlRulesWasm { self.0.manual_minting_rules().clone().into() @@ -332,11 +388,35 @@ impl TokenConfigurationWasm { } /// Enabling upgrades a format-version-0 configuration to version 1 in place. + /// + /// Disabling downgrades it back to version 0 and takes any + /// `minimumPoolNotesForOutgoing` with it, since a token without a pool keeps no threshold. #[wasm_bindgen(setter = "hasShieldedPool")] pub fn set_has_shielded_pool(&mut self, has_shielded_pool: bool) { self.0.set_has_shielded_pool(has_shielded_pool) } + /// `undefined` or `null` removes the threshold. Throws for a token that has no shielded + /// pool, which has no threshold to hold. + #[wasm_bindgen(setter = "minimumPoolNotesForOutgoing")] + pub fn set_minimum_pool_notes_for_outgoing( + &mut self, + minimum_pool_notes_for_outgoing: JsValue, + ) -> WasmDppResult<()> { + let minimum_pool_notes_for_outgoing = if minimum_pool_notes_for_outgoing.is_undefined() + || minimum_pool_notes_for_outgoing.is_null() + { + None + } else { + Some(try_to_u64( + &minimum_pool_notes_for_outgoing, + "minimumPoolNotesForOutgoing", + )?) + }; + + write_minimum_pool_notes_for_outgoing(&mut self.0, minimum_pool_notes_for_outgoing) + } + #[wasm_bindgen(setter = "manualMintingRules")] pub fn set_manual_minting_rules(&mut self, rules: &ChangeControlRulesWasm) { self.0.set_manual_minting_rules(rules.clone().into()) diff --git a/packages/wasm-dpp2/tests/unit/TokenConfiguration.spec.ts b/packages/wasm-dpp2/tests/unit/TokenConfiguration.spec.ts index 11958e85509..bd985e403d1 100644 --- a/packages/wasm-dpp2/tests/unit/TokenConfiguration.spec.ts +++ b/packages/wasm-dpp2/tests/unit/TokenConfiguration.spec.ts @@ -1,5 +1,6 @@ import { expect } from './helpers/chai.ts'; import { initWasm, wasm } from '../../dist/dpp.compressed.js'; +import { object, ownerId } from './mocks/DataContract/index.js'; before(async () => { await initWasm(); @@ -22,6 +23,11 @@ interface KeepsHistoryRulesOptions { isKeepingEmergencyActionHistory?: boolean; } +interface ShieldedPoolOptions { + hasShieldedPool?: boolean; + minimumPoolNotesForOutgoing?: bigint; +} + describe('TokenConfiguration', () => { function createChangeControlRules(options: ChangeControlRulesOptions = {}) { // AuthorizedActionTakers defaults to NoOne if not specified @@ -213,4 +219,137 @@ describe('TokenConfiguration', () => { expect(config.description).to.equal('note'); }); }); + + describe('minimumPoolNotesForOutgoing', () => { + // A configuration whose shielded pool settings are the only thing under test. Everything + // else is the least interesting valid value, and `hasShieldedPool` defaults to false so a + // test has to ask for a pool to get one. + function createConfiguration(shieldedPool: ShieldedPoolOptions = {}) { + const changeRules = createChangeControlRules(); + + return new wasm.TokenConfiguration({ + conventions: new wasm.TokenConfigurationConvention( + { + en: { + $formatVersion: '0', + shouldCapitalize: true, + singularForm: 'TOKEN', + pluralForm: 'TOKENS', + }, + }, + 8, + ), + conventionsChangeRules: changeRules, + baseSupply: BigInt(1000), + maxSupply: undefined, + keepsHistory: createKeepsHistoryRules(), + isStartedAsPaused: false, + isAllowedTransferToFrozenBalance: false, + maxSupplyChangeRules: changeRules, + distributionRules: new wasm.TokenDistributionRules({ + perpetualDistribution: undefined, + perpetualDistributionRules: changeRules, + preProgrammedDistribution: undefined, + newTokensDestinationIdentityRules: changeRules, + mintingAllowChoosingDestination: true, + mintingAllowChoosingDestinationRules: changeRules, + changeDirectPurchasePricingRules: changeRules, + }), + marketplaceRules: new wasm.TokenMarketplaceRules( + wasm.TokenTradeMode.NotTradeable(), + changeRules, + ), + manualMintingRules: changeRules, + manualBurningRules: changeRules, + freezeRules: changeRules, + unfreezeRules: changeRules, + destroyFrozenFundsRules: changeRules, + emergencyActionRules: changeRules, + mainControlGroup: undefined, + mainControlGroupCanBeModified: wasm.AuthorizedActionTakers.NoOne(), + description: 'note', + ...shieldedPool, + }); + } + + // The configuration a data contract hands back, rebuilt from the one it was given. Reading + // a threshold off it proves the value reached the configuration itself: assigning to a + // property the wasm class does not define lands on the JavaScript object instead, where + // the same object's getter reads it straight back and agrees with anything. + function configurationHeldByContract(config: wasm.TokenConfiguration) { + const contract = new wasm.DataContract({ + ownerId, + identityNonce: BigInt(2), + schemas: object.documentSchemas, + definitions: null, + fullValidation: false, + tokens: { 0: config }, + }); + + return (contract.tokens as Record)[0]; + } + + it('should keep a threshold asked for at creation', () => { + const config = createConfiguration({ + hasShieldedPool: true, + minimumPoolNotesForOutgoing: BigInt(5), + }); + + // A threshold reachable only through a later TokenConfigUpdate would leave every token + // created from JavaScript with an unguarded pool for its first blocks. + expect(config.minimumPoolNotesForOutgoing).to.equal(BigInt(5)); + expect(config.hasShieldedPool).to.equal(true); + expect(config.formatVersion).to.equal(1); + }); + + it('should read no threshold as zero for a pool that asks for none', () => { + const config = createConfiguration({ hasShieldedPool: true }); + + expect(config.minimumPoolNotesForOutgoing).to.equal(BigInt(0)); + }); + + it('should read no threshold as zero for a token without a shielded pool', () => { + const config = createConfiguration(); + + expect(config.hasShieldedPool).to.equal(false); + expect(config.formatVersion).to.equal(0); + expect(config.minimumPoolNotesForOutgoing).to.equal(BigInt(0)); + }); + + it('should round-trip a threshold written through the setter', () => { + const config = createConfiguration({ hasShieldedPool: true }); + + config.minimumPoolNotesForOutgoing = BigInt(7); + + expect(config.minimumPoolNotesForOutgoing).to.equal(BigInt(7)); + expect(configurationHeldByContract(config).minimumPoolNotesForOutgoing) + .to.equal(BigInt(7)); + }); + + it('should drop the threshold when the setter is given undefined', () => { + const config = createConfiguration({ + hasShieldedPool: true, + minimumPoolNotesForOutgoing: BigInt(9), + }); + + config.minimumPoolNotesForOutgoing = undefined; + + expect(config.minimumPoolNotesForOutgoing).to.equal(BigInt(0)); + }); + + it('should refuse a threshold at creation for a token without a shielded pool', () => { + // Silently accepting it is the failure this guards: the only configurations that carry a + // threshold are the ones that carry a pool, so the value would go nowhere unreported. + expect(() => createConfiguration({ minimumPoolNotesForOutgoing: BigInt(5) })) + .to.throw(/hasShieldedPool/); + }); + + it('should refuse a threshold through the setter for a token without a shielded pool', () => { + const config = createConfiguration(); + + expect(() => { + config.minimumPoolNotesForOutgoing = BigInt(5); + }).to.throw(/hasShieldedPool/); + }); + }); }); From 3d52fee08ccade683f3a224a0972ee10d93b0b7f Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 29 Sep 2026 16:55:30 +0700 Subject: [PATCH 91/94] fix(dpp): check the balance allowance ordering when the crate is built A test asserted that the unshield's balance-insert allowance exceeds the replace-only allowance the identity top-up keeps. Both sides are constants, so the assertion had a constant value and clippy refuses it under the workspace's deny-warnings setting, which is why the Rust workspace test job was red. The invariant is real and belongs beside the constants as a compile-time check, where a change to either stops the build rather than waiting for a test to run. Proven to bind: setting the insert allowance below the replace-only one gives `error[E0080]: evaluation panicked: assertion failed` and the crate does not compile. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/shielded/compute_minimum_shielded_fee/v0/mod.rs | 7 ------- packages/rs-dpp/src/shielded/mod.rs | 8 ++++++++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs index 4c99e1ff13e..a67d9f32ce9 100644 --- a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs +++ b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs @@ -616,13 +616,6 @@ mod tests { ); } - assert!( - SHIELDED_TOKEN_BALANCE_INSERT_STORAGE_BYTES - > SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES, - "creating a balance item costs more than rewriting one, so the unshield's allowance \ - ({SHIELDED_TOKEN_BALANCE_INSERT_STORAGE_BYTES} bytes) must exceed the replace-only \ - one ({SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES} bytes)" - ); // The top-up tops up an identity that already exists, so it keeps the replace-only one. assert_eq!( compute_shielded_identity_top_up_fee_v0(2, platform_version).expect("top up fee"), diff --git a/packages/rs-dpp/src/shielded/mod.rs b/packages/rs-dpp/src/shielded/mod.rs index b7c7c0ef3ca..0ae3b0c1b0c 100644 --- a/packages/rs-dpp/src/shielded/mod.rs +++ b/packages/rs-dpp/src/shielded/mod.rs @@ -200,6 +200,14 @@ pub const SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES: u64 = 20; /// [`compute_minimum_shielded_fee::compute_token_unshield_with_shielded_fee_fee`]. pub const SHIELDED_TOKEN_BALANCE_INSERT_STORAGE_BYTES: u64 = 230; +/// Creating a balance item costs more than rewriting one, so the unshield's allowance has to +/// exceed the replace-only allowance the identity top-up keeps. Checked when the crate is built +/// rather than when a test runs, because both sides are constants and a change to either should +/// stop the build rather than wait for a test to notice. +const _: () = assert!( + SHIELDED_TOKEN_BALANCE_INSERT_STORAGE_BYTES > SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES +); + /// Common Orchard bundle parameters shared across all shielded transition types. /// /// Groups the fields that every shielded transition carries identically: From dde09b17d0d3cab94a464d494adc9bac25a3a703 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 29 Sep 2026 17:01:30 +0700 Subject: [PATCH 92/94] chore(dapi-grpc): regenerate the python client for the token shielded pool protos The six shielded pool query requests in protos/platform/v0/platform.proto gained an `optional bytes token_id`, and GetGroupActionsResponse gained the MintToPoolEvent and BurnFromPoolEvent token events, but only the nodejs, web, objective-c and java clients were regenerated for them. The checked-in python descriptor still described the old messages, so the codegen job's `git diff --exit-code -- packages/dapi-grpc/clients` failed on both ubuntu-24.04 and macos-15. Regenerating with the pinned toolchain (protobuf 3.18.1, grpc 1.46.3) rewrites only clients/platform/v0/python/platform_pb2.py. Every other generated file across core, drive and platform is byte-identical, so this carries no generator drift: the whole diff is the six token_id fields with their synthetic oneofs, the two new pool events with their TokenEvent arms, and the descriptor byte offsets those shift. Co-Authored-By: Claude Opus 5 (1M context) --- .../platform/v0/python/platform_pb2.py | 568 +++++++++++++----- 1 file changed, 402 insertions(+), 166 deletions(-) diff --git a/packages/dapi-grpc/clients/platform/v0/python/platform_pb2.py b/packages/dapi-grpc/clients/platform/v0/python/platform_pb2.py index d0d6fedeec8..200b57763aa 100644 --- a/packages/dapi-grpc/clients/platform/v0/python/platform_pb2.py +++ b/packages/dapi-grpc/clients/platform/v0/python/platform_pb2.py @@ -23,7 +23,7 @@ syntax='proto3', serialized_options=None, create_key=_descriptor._internal_create_key, - serialized_pb=b'\n\x0eplatform.proto\x12\x19org.dash.platform.dapi.v0\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x81\x01\n\x05Proof\x12\x15\n\rgrovedb_proof\x18\x01 \x01(\x0c\x12\x13\n\x0bquorum_hash\x18\x02 \x01(\x0c\x12\x11\n\tsignature\x18\x03 \x01(\x0c\x12\r\n\x05round\x18\x04 \x01(\r\x12\x15\n\rblock_id_hash\x18\x05 \x01(\x0c\x12\x13\n\x0bquorum_type\x18\x06 \x01(\r\"\x98\x01\n\x10ResponseMetadata\x12\x12\n\x06height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12 \n\x18\x63ore_chain_locked_height\x18\x02 \x01(\r\x12\r\n\x05\x65poch\x18\x03 \x01(\r\x12\x13\n\x07time_ms\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x18\n\x10protocol_version\x18\x05 \x01(\r\x12\x10\n\x08\x63hain_id\x18\x06 \x01(\t\"L\n\x1dStateTransitionBroadcastError\x12\x0c\n\x04\x63ode\x18\x01 \x01(\r\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\";\n\x1f\x42roadcastStateTransitionRequest\x12\x18\n\x10state_transition\x18\x01 \x01(\x0c\"\"\n BroadcastStateTransitionResponse\"\xa4\x01\n\x12GetIdentityRequest\x12P\n\x02v0\x18\x01 \x01(\x0b\x32\x42.org.dash.platform.dapi.v0.GetIdentityRequest.GetIdentityRequestV0H\x00\x1a\x31\n\x14GetIdentityRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xc1\x01\n\x17GetIdentityNonceRequest\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetIdentityNonceRequest.GetIdentityNonceRequestV0H\x00\x1a?\n\x19GetIdentityNonceRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf6\x01\n\x1fGetIdentityContractNonceRequest\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetIdentityContractNonceRequest.GetIdentityContractNonceRequestV0H\x00\x1a\\\n!GetIdentityContractNonceRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x13\n\x0b\x63ontract_id\x18\x02 \x01(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xc0\x01\n\x19GetIdentityBalanceRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetIdentityBalanceRequest.GetIdentityBalanceRequestV0H\x00\x1a\x38\n\x1bGetIdentityBalanceRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xec\x01\n$GetIdentityBalanceAndRevisionRequest\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionRequest.GetIdentityBalanceAndRevisionRequestV0H\x00\x1a\x43\n&GetIdentityBalanceAndRevisionRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9e\x02\n\x13GetIdentityResponse\x12R\n\x02v0\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetIdentityResponse.GetIdentityResponseV0H\x00\x1a\xa7\x01\n\x15GetIdentityResponseV0\x12\x12\n\x08identity\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xbc\x02\n\x18GetIdentityNonceResponse\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetIdentityNonceResponse.GetIdentityNonceResponseV0H\x00\x1a\xb6\x01\n\x1aGetIdentityNonceResponseV0\x12\x1c\n\x0eidentity_nonce\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xe5\x02\n GetIdentityContractNonceResponse\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetIdentityContractNonceResponse.GetIdentityContractNonceResponseV0H\x00\x1a\xc7\x01\n\"GetIdentityContractNonceResponseV0\x12%\n\x17identity_contract_nonce\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\x8e\x02\n&GetIdentityKeysRemainingBudgetsRequest\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsRequest.GetIdentityKeysRemainingBudgetsRequestV0H\x00\x1a_\n(GetIdentityKeysRemainingBudgetsRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x0f\n\x07key_ids\x18\x02 \x03(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\x96\x06\n\'GetIdentityKeysRemainingBudgetsResponse\x12z\n\x02v0\x18\x01 \x01(\x0b\x32l.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsResponse.GetIdentityKeysRemainingBudgetsResponseV0H\x00\x1a\xe3\x04\n)GetIdentityKeysRemainingBudgetsResponseV0\x12\xa4\x01\n\x16keys_remaining_budgets\x18\x01 \x01(\x0b\x32\x81\x01.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsResponse.GetIdentityKeysRemainingBudgetsResponseV0.KeysRemainingBudgetsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x61\n\x17KeyRemainingBudgetEntry\x12\x0e\n\x06key_id\x18\x01 \x01(\r\x12!\n\x10remaining_budget\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x42\x13\n\x11_remaining_budget\x1a\xaf\x01\n\x14KeysRemainingBudgets\x12\x96\x01\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsResponse.GetIdentityKeysRemainingBudgetsResponseV0.KeyRemainingBudgetEntryB\x08\n\x06resultB\t\n\x07version\"\xbd\x02\n\x1aGetIdentityBalanceResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetIdentityBalanceResponse.GetIdentityBalanceResponseV0H\x00\x1a\xb1\x01\n\x1cGetIdentityBalanceResponseV0\x12\x15\n\x07\x62\x61lance\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xb1\x04\n%GetIdentityBalanceAndRevisionResponse\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionResponse.GetIdentityBalanceAndRevisionResponseV0H\x00\x1a\x84\x03\n\'GetIdentityBalanceAndRevisionResponseV0\x12\x9b\x01\n\x14\x62\x61lance_and_revision\x18\x01 \x01(\x0b\x32{.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionResponse.GetIdentityBalanceAndRevisionResponseV0.BalanceAndRevisionH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a?\n\x12\x42\x61lanceAndRevision\x12\x13\n\x07\x62\x61lance\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x14\n\x08revision\x18\x02 \x01(\x04\x42\x02\x30\x01\x42\x08\n\x06resultB\t\n\x07version\"\xd1\x01\n\x0eKeyRequestType\x12\x36\n\x08\x61ll_keys\x18\x01 \x01(\x0b\x32\".org.dash.platform.dapi.v0.AllKeysH\x00\x12@\n\rspecific_keys\x18\x02 \x01(\x0b\x32\'.org.dash.platform.dapi.v0.SpecificKeysH\x00\x12:\n\nsearch_key\x18\x03 \x01(\x0b\x32$.org.dash.platform.dapi.v0.SearchKeyH\x00\x42\t\n\x07request\"\t\n\x07\x41llKeys\"\x1f\n\x0cSpecificKeys\x12\x0f\n\x07key_ids\x18\x01 \x03(\r\"\xb6\x01\n\tSearchKey\x12I\n\x0bpurpose_map\x18\x01 \x03(\x0b\x32\x34.org.dash.platform.dapi.v0.SearchKey.PurposeMapEntry\x1a^\n\x0fPurposeMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12:\n\x05value\x18\x02 \x01(\x0b\x32+.org.dash.platform.dapi.v0.SecurityLevelMap:\x02\x38\x01\"\xbf\x02\n\x10SecurityLevelMap\x12]\n\x12security_level_map\x18\x01 \x03(\x0b\x32\x41.org.dash.platform.dapi.v0.SecurityLevelMap.SecurityLevelMapEntry\x1aw\n\x15SecurityLevelMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12M\n\x05value\x18\x02 \x01(\x0e\x32>.org.dash.platform.dapi.v0.SecurityLevelMap.KeyKindRequestType:\x02\x38\x01\"S\n\x12KeyKindRequestType\x12\x1f\n\x1b\x43URRENT_KEY_OF_KIND_REQUEST\x10\x00\x12\x1c\n\x18\x41LL_KEYS_OF_KIND_REQUEST\x10\x01\"\xda\x02\n\x16GetIdentityKeysRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetIdentityKeysRequest.GetIdentityKeysRequestV0H\x00\x1a\xda\x01\n\x18GetIdentityKeysRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12?\n\x0crequest_type\x18\x02 \x01(\x0b\x32).org.dash.platform.dapi.v0.KeyRequestType\x12+\n\x05limit\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\t\n\x07version\"\x99\x03\n\x17GetIdentityKeysResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetIdentityKeysResponse.GetIdentityKeysResponseV0H\x00\x1a\x96\x02\n\x19GetIdentityKeysResponseV0\x12\x61\n\x04keys\x18\x01 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetIdentityKeysResponse.GetIdentityKeysResponseV0.KeysH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1a\n\x04Keys\x12\x12\n\nkeys_bytes\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\xef\x02\n GetIdentitiesContractKeysRequest\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetIdentitiesContractKeysRequest.GetIdentitiesContractKeysRequestV0H\x00\x1a\xd1\x01\n\"GetIdentitiesContractKeysRequestV0\x12\x16\n\x0eidentities_ids\x18\x01 \x03(\x0c\x12\x13\n\x0b\x63ontract_id\x18\x02 \x01(\x0c\x12\x1f\n\x12\x64ocument_type_name\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x37\n\x08purposes\x18\x04 \x03(\x0e\x32%.org.dash.platform.dapi.v0.KeyPurpose\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x15\n\x13_document_type_nameB\t\n\x07version\"\xdf\x06\n!GetIdentitiesContractKeysResponse\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0H\x00\x1a\xbe\x05\n#GetIdentitiesContractKeysResponseV0\x12\x8a\x01\n\x0fidentities_keys\x18\x01 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0.IdentitiesKeysH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aY\n\x0bPurposeKeys\x12\x36\n\x07purpose\x18\x01 \x01(\x0e\x32%.org.dash.platform.dapi.v0.KeyPurpose\x12\x12\n\nkeys_bytes\x18\x02 \x03(\x0c\x1a\x9f\x01\n\x0cIdentityKeys\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12z\n\x04keys\x18\x02 \x03(\x0b\x32l.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0.PurposeKeys\x1a\x90\x01\n\x0eIdentitiesKeys\x12~\n\x07\x65ntries\x18\x01 \x03(\x0b\x32m.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0.IdentityKeysB\x08\n\x06resultB\t\n\x07version\"\xa4\x02\n*GetEvonodesProposedEpochBlocksByIdsRequest\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByIdsRequest.GetEvonodesProposedEpochBlocksByIdsRequestV0H\x00\x1ah\n,GetEvonodesProposedEpochBlocksByIdsRequestV0\x12\x12\n\x05\x65poch\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x0b\n\x03ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\x08\n\x06_epochB\t\n\x07version\"\x92\x06\n&GetEvonodesProposedEpochBlocksResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse.GetEvonodesProposedEpochBlocksResponseV0H\x00\x1a\xe2\x04\n(GetEvonodesProposedEpochBlocksResponseV0\x12\xb1\x01\n#evonodes_proposed_block_counts_info\x18\x01 \x01(\x0b\x32\x81\x01.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse.GetEvonodesProposedEpochBlocksResponseV0.EvonodesProposedBlocksH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a?\n\x15\x45vonodeProposedBlocks\x12\x13\n\x0bpro_tx_hash\x18\x01 \x01(\x0c\x12\x11\n\x05\x63ount\x18\x02 \x01(\x04\x42\x02\x30\x01\x1a\xc4\x01\n\x16\x45vonodesProposedBlocks\x12\xa9\x01\n\x1e\x65vonodes_proposed_block_counts\x18\x01 \x03(\x0b\x32\x80\x01.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse.GetEvonodesProposedEpochBlocksResponseV0.EvonodeProposedBlocksB\x08\n\x06resultB\t\n\x07version\"\xf2\x02\n,GetEvonodesProposedEpochBlocksByRangeRequest\x12\x84\x01\n\x02v0\x18\x01 \x01(\x0b\x32v.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByRangeRequest.GetEvonodesProposedEpochBlocksByRangeRequestV0H\x00\x1a\xaf\x01\n.GetEvonodesProposedEpochBlocksByRangeRequestV0\x12\x12\n\x05\x65poch\x18\x01 \x01(\rH\x01\x88\x01\x01\x12\x12\n\x05limit\x18\x02 \x01(\rH\x02\x88\x01\x01\x12\x15\n\x0bstart_after\x18\x03 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x04 \x01(\x0cH\x00\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x07\n\x05startB\x08\n\x06_epochB\x08\n\x06_limitB\t\n\x07version\"\xcd\x01\n\x1cGetIdentitiesBalancesRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetIdentitiesBalancesRequest.GetIdentitiesBalancesRequestV0H\x00\x1a<\n\x1eGetIdentitiesBalancesRequestV0\x12\x0b\n\x03ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9f\x05\n\x1dGetIdentitiesBalancesResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse.GetIdentitiesBalancesResponseV0H\x00\x1a\x8a\x04\n\x1fGetIdentitiesBalancesResponseV0\x12\x8a\x01\n\x13identities_balances\x18\x01 \x01(\x0b\x32k.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse.GetIdentitiesBalancesResponseV0.IdentitiesBalancesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aL\n\x0fIdentityBalance\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x18\n\x07\x62\x61lance\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x42\n\n\x08_balance\x1a\x8f\x01\n\x12IdentitiesBalances\x12y\n\x07\x65ntries\x18\x01 \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse.GetIdentitiesBalancesResponseV0.IdentityBalanceB\x08\n\x06resultB\t\n\x07version\"\xb4\x01\n\x16GetDataContractRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetDataContractRequest.GetDataContractRequestV0H\x00\x1a\x35\n\x18GetDataContractRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xb3\x02\n\x17GetDataContractResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetDataContractResponse.GetDataContractResponseV0H\x00\x1a\xb0\x01\n\x19GetDataContractResponseV0\x12\x17\n\rdata_contract\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\x8c\x02\n%GetDataContractsLatestVersionsRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetDataContractsLatestVersionsRequest.GetDataContractsLatestVersionsRequestV0H\x00\x1a`\n\'GetDataContractsLatestVersionsRequestV0\x12\x0b\n\x03ids\x18\x01 \x03(\x0c\x12\x19\n\x11include_contracts\x18\x02 \x01(\x08\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xfa\x05\n&GetDataContractsLatestVersionsResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetDataContractsLatestVersionsResponse.GetDataContractsLatestVersionsResponseV0H\x00\x1a\x84\x01\n\x1e\x44\x61taContractLatestVersionEntry\x12\x12\n\nidentifier\x18\x01 \x01(\x0c\x12\x14\n\x07version\x18\x02 \x01(\rH\x00\x88\x01\x01\x12\x1a\n\rdata_contract\x18\x03 \x01(\x0cH\x01\x88\x01\x01\x42\n\n\x08_versionB\x10\n\x0e_data_contract\x1a\x90\x01\n\x1b\x44\x61taContractsLatestVersions\x12q\n\x07\x65ntries\x18\x01 \x03(\x0b\x32`.org.dash.platform.dapi.v0.GetDataContractsLatestVersionsResponse.DataContractLatestVersionEntry\x1a\xb0\x02\n(GetDataContractsLatestVersionsResponseV0\x12\x87\x01\n\x1e\x64\x61ta_contracts_latest_versions\x18\x01 \x01(\x0b\x32].org.dash.platform.dapi.v0.GetDataContractsLatestVersionsResponse.DataContractsLatestVersionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"R\n\x1f\x43ontractGroupDocumentTypeMember\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\"G\n\x18\x43ontractGroupTokenMember\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x16\n\x0etoken_position\x18\x02 \x01(\r\"\xd7\x01\n\x1bGetContractGroupInfoRequest\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetContractGroupInfoRequest.GetContractGroupInfoRequestV0H\x00\x1aI\n\x1dGetContractGroupInfoRequestV0\x12\x19\n\x11\x63ontract_group_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x98\x04\n\x1cGetContractGroupInfoResponse\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetContractGroupInfoResponse.GetContractGroupInfoResponseV0H\x00\x1a~\n\x11\x43ontractGroupInfo\x12\x10\n\x08owner_id\x18\x01 \x01(\x0c\x12\x11\n\tadmin_ids\x18\x02 \x03(\x0c\x12\x11\n\x04name\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x01\x88\x01\x01\x42\x07\n\x05_nameB\x0e\n\x0c_description\x1a\x86\x02\n\x1eGetContractGroupInfoResponseV0\x12h\n\x13\x63ontract_group_info\x18\x01 \x01(\x0b\x32I.org.dash.platform.dapi.v0.GetContractGroupInfoResponse.ContractGroupInfoH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xf8\x06\n\x1eGetContractGroupMembersRequest\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetContractGroupMembersRequest.GetContractGroupMembersRequestV0H\x00\x1a@\n\x14\x43ontractMembersQuery\x12\x18\n\x0bstart_after\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x42\x0e\n\x0c_start_after\x1a\x80\x01\n\x18\x44ocumentTypeMembersQuery\x12T\n\x0bstart_after\x18\x01 \x01(\x0b\x32:.org.dash.platform.dapi.v0.ContractGroupDocumentTypeMemberH\x00\x88\x01\x01\x42\x0e\n\x0c_start_after\x1ar\n\x11TokenMembersQuery\x12M\n\x0bstart_after\x18\x01 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractGroupTokenMemberH\x00\x88\x01\x01\x42\x0e\n\x0c_start_after\x1a\xa7\x03\n GetContractGroupMembersRequestV0\x12\x19\n\x11\x63ontract_group_id\x18\x01 \x01(\x0c\x12\x63\n\tcontracts\x18\x02 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetContractGroupMembersRequest.ContractMembersQueryH\x00\x12l\n\x0e\x64ocument_types\x18\x03 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetContractGroupMembersRequest.DocumentTypeMembersQueryH\x00\x12]\n\x06tokens\x18\x04 \x01(\x0b\x32K.org.dash.platform.dapi.v0.GetContractGroupMembersRequest.TokenMembersQueryH\x00\x12\x12\n\x05limit\x18\x05 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x06 \x01(\x08\x42\t\n\x07membersB\x08\n\x06_limitB\t\n\x07version\"\xc9\x06\n\x1fGetContractGroupMembersResponse\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetContractGroupMembersResponse.GetContractGroupMembersResponseV0H\x00\x1a\'\n\x0f\x43ontractMembers\x12\x14\n\x0c\x63ontract_ids\x18\x01 \x03(\x0c\x1ai\n\x13\x44ocumentTypeMembers\x12R\n\x0e\x64ocument_types\x18\x01 \x03(\x0b\x32:.org.dash.platform.dapi.v0.ContractGroupDocumentTypeMember\x1aS\n\x0cTokenMembers\x12\x43\n\x06tokens\x18\x01 \x03(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractGroupTokenMember\x1a\xc5\x03\n!GetContractGroupMembersResponseV0\x12_\n\tcontracts\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetContractGroupMembersResponse.ContractMembersH\x00\x12h\n\x0e\x64ocument_types\x18\x02 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetContractGroupMembersResponse.DocumentTypeMembersH\x00\x12Y\n\x06tokens\x18\x03 \x01(\x0b\x32G.org.dash.platform.dapi.v0.GetContractGroupMembersResponse.TokenMembersH\x00\x12\x31\n\x05proof\x18\x04 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x05 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"M\n\x1a\x43ontractModerationDocument\x12\x1a\n\x12\x64ocument_type_name\x18\x01 \x01(\t\x12\x13\n\x0b\x64ocument_id\x18\x02 \x01(\x0c\"\xc6\x01\n\x18\x43ontractModerationReason\x12\x11\n\x04\x63ode\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x0c\n\x04text\x18\x02 \x01(\t\x12H\n\tdocuments\x18\x03 \x03(\x0b\x32\x35.org.dash.platform.dapi.v0.ContractModerationDocument\x12\x1f\n\x12reason_document_id\x18\x04 \x01(\x0cH\x01\x88\x01\x01\x42\x07\n\x05_codeB\x15\n\x13_reason_document_id\"i\n\x0f\x43ontractWarning\x12\x11\n\twarned_at\x18\x01 \x01(\x04\x12\x43\n\x06reason\x18\x02 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractModerationReason\"\xc5\x02\n\"GetContractModerationStatusRequest\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetContractModerationStatusRequest.GetContractModerationStatusRequestV0H\x00\x1a\xa1\x01\n$GetContractModerationStatusRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x13\n\x0bidentity_id\x18\x02 \x01(\x0c\x12@\n\x05lists\x18\x03 \x03(\x0e\x32\x31.org.dash.platform.dapi.v0.ContractModerationList\x12\r\n\x05prove\x18\x04 \x01(\x08\x42\t\n\x07version\"\xec\x06\n#GetContractModerationStatusResponse\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetContractModerationStatusResponse.GetContractModerationStatusResponseV0H\x00\x1a\xb4\x03\n\x18\x43ontractModerationStatus\x12\x13\n\x06\x62\x61nned\x18\x01 \x01(\x08H\x00\x88\x01\x01\x12\x1c\n\x0fsuspended_until\x18\x02 \x01(\x04H\x01\x88\x01\x01\x12@\n\x05lists\x18\x03 \x03(\x0e\x32\x31.org.dash.platform.dapi.v0.ContractModerationList\x12L\n\nban_reason\x18\x04 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractModerationReasonH\x02\x88\x01\x01\x12S\n\x11suspension_reason\x18\x05 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractModerationReasonH\x03\x88\x01\x01\x12<\n\x08warnings\x18\x06 \x03(\x0b\x32*.org.dash.platform.dapi.v0.ContractWarningB\t\n\x07_bannedB\x12\n\x10_suspended_untilB\r\n\x0b_ban_reasonB\x14\n\x12_suspension_reason\x1a\x8e\x02\n%GetContractModerationStatusResponseV0\x12i\n\x06status\x18\x01 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetContractModerationStatusResponse.ContractModerationStatusH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xfb\x02\n#GetContractModerationEntriesRequest\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetContractModerationEntriesRequest.GetContractModerationEntriesRequestV0H\x00\x1a\xd4\x01\n%GetContractModerationEntriesRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12?\n\x04list\x18\x02 \x01(\x0e\x32\x31.org.dash.platform.dapi.v0.ContractModerationList\x12\x18\n\x0bstart_after\x18\x03 \x01(\x0cH\x00\x88\x01\x01\x12\x12\n\x05limit\x18\x04 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x0e\n\x0c_start_afterB\x08\n\x06_limitB\t\n\x07version\"\x96\x06\n$GetContractModerationEntriesResponse\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetContractModerationEntriesResponse.GetContractModerationEntriesResponseV0H\x00\x1a\xcf\x01\n\x17\x43ontractModerationEntry\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x12\n\x05until\x18\x02 \x01(\x04H\x00\x88\x01\x01\x12\x43\n\x06reason\x18\x03 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractModerationReason\x12<\n\x08warnings\x18\x04 \x03(\x0b\x32*.org.dash.platform.dapi.v0.ContractWarningB\x08\n\x06_until\x1a\x85\x01\n\x19\x43ontractModerationEntries\x12h\n\x07\x65ntries\x18\x01 \x03(\x0b\x32W.org.dash.platform.dapi.v0.GetContractModerationEntriesResponse.ContractModerationEntry\x1a\x92\x02\n&GetContractModerationEntriesResponseV0\x12l\n\x07\x65ntries\x18\x01 \x01(\x0b\x32Y.org.dash.platform.dapi.v0.GetContractModerationEntriesResponse.ContractModerationEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc3\x04\n\"GetContractDocumentRemovalsRequest\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetContractDocumentRemovalsRequest.GetContractDocumentRemovalsRequestV0H\x00\x1a#\n\x0b\x44ocumentIds\x12\x14\n\x0c\x64ocument_ids\x18\x01 \x03(\x0c\x1aN\n\x04Page\x12\x18\n\x0bstart_after\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x12\n\x05limit\x18\x02 \x01(\rH\x01\x88\x01\x01\x42\x0e\n\x0c_start_afterB\x08\n\x06_limit\x1a\xaa\x02\n$GetContractDocumentRemovalsRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x61\n\x0c\x64ocument_ids\x18\x03 \x01(\x0b\x32I.org.dash.platform.dapi.v0.GetContractDocumentRemovalsRequest.DocumentIdsH\x00\x12R\n\x04page\x18\x04 \x01(\x0b\x32\x42.org.dash.platform.dapi.v0.GetContractDocumentRemovalsRequest.PageH\x00\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x0b\n\tselectionB\t\n\x07version\"\xcb\x07\n#GetContractDocumentRemovalsResponse\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetContractDocumentRemovalsResponse.GetContractDocumentRemovalsResponseV0H\x00\x1aH\n\x1b\x43ontractDocumentRestoration\x12\x14\n\x0cmoderator_id\x18\x01 \x01(\x0c\x12\x13\n\x0brestored_at\x18\x02 \x01(\x04\x1a\xc0\x02\n\x17\x43ontractDocumentRemoval\x12\x13\n\x0b\x64ocument_id\x18\x01 \x01(\x0c\x12\x19\n\x11\x64ocument_owner_id\x18\x02 \x01(\x0c\x12\x14\n\x0cmoderator_id\x18\x03 \x01(\x0c\x12\x12\n\nremoved_at\x18\x04 \x01(\x04\x12\x43\n\x06reason\x18\x05 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractModerationReason\x12\x15\n\rdocument_hash\x18\x06 \x01(\x0c\x12o\n\x0brestoration\x18\x07 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetContractDocumentRemovalsResponse.ContractDocumentRestoration\x1a\x84\x01\n\x18\x43ontractDocumentRemovals\x12h\n\x08removals\x18\x01 \x03(\x0b\x32V.org.dash.platform.dapi.v0.GetContractDocumentRemovalsResponse.ContractDocumentRemoval\x1a\x90\x02\n%GetContractDocumentRemovalsResponseV0\x12k\n\x08removals\x18\x01 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetContractDocumentRemovalsResponse.ContractDocumentRemovalsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc9\x01\n\x19GetContractFeePotsRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetContractFeePotsRequest.GetContractFeePotsRequestV0H\x00\x1a\x41\n\x1bGetContractFeePotsRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9f\x06\n\x1aGetContractFeePotsResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetContractFeePotsResponse.GetContractFeePotsResponseV0H\x00\x1aR\n\x17\x43ontractFeePotLastClaim\x12\r\n\x05\x65poch\x18\x01 \x01(\r\x12\x13\n\x07time_ms\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x13\n\x0b\x63laimant_id\x18\x03 \x01(\x0c\x1a\x88\x01\n\x0e\x43ontractFeePot\x12\x13\n\x07\x63redits\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x61\n\nlast_claim\x18\x02 \x01(\x0b\x32M.org.dash.platform.dapi.v0.GetContractFeePotsResponse.ContractFeePotLastClaim\x1a\xc0\x01\n\x0f\x43ontractFeePots\x12S\n\x05owner\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetContractFeePotsResponse.ContractFeePot\x12X\n\nmoderators\x18\x02 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetContractFeePotsResponse.ContractFeePot\x1a\xf1\x01\n\x1cGetContractFeePotsResponseV0\x12U\n\x04pots\x18\x01 \x01(\x0b\x32\x45.org.dash.platform.dapi.v0.GetContractFeePotsResponse.ContractFeePotsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xf1\x01\n#GetContractGroupsForContractRequest\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetContractGroupsForContractRequest.GetContractGroupsForContractRequestV0H\x00\x1aK\n%GetContractGroupsForContractRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf5\x06\n$GetContractGroupsForContractResponse\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetContractGroupsForContractResponse.GetContractGroupsForContractResponseV0H\x00\x1aQ\n\x17\x44ocumentTypeMemberships\x12\x1a\n\x12\x64ocument_type_name\x18\x01 \x01(\t\x12\x1a\n\x12\x63ontract_group_ids\x18\x02 \x03(\x0c\x1a\x46\n\x10TokenMemberships\x12\x16\n\x0etoken_position\x18\x01 \x01(\r\x12\x1a\n\x12\x63ontract_group_ids\x18\x02 \x03(\x0c\x1a\x89\x02\n\x18\x43ontractGroupMemberships\x12\x1a\n\x12\x63ontract_group_ids\x18\x01 \x03(\x0c\x12o\n\x0e\x64ocument_types\x18\x02 \x03(\x0b\x32W.org.dash.platform.dapi.v0.GetContractGroupsForContractResponse.DocumentTypeMemberships\x12`\n\x06tokens\x18\x03 \x03(\x0b\x32P.org.dash.platform.dapi.v0.GetContractGroupsForContractResponse.TokenMemberships\x1a\xa4\x02\n&GetContractGroupsForContractResponseV0\x12~\n\x1a\x63ontract_group_memberships\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetContractGroupsForContractResponse.ContractGroupMembershipsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xb9\x01\n\x17GetDataContractsRequest\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetDataContractsRequest.GetDataContractsRequestV0H\x00\x1a\x37\n\x19GetDataContractsRequestV0\x12\x0b\n\x03ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xad\x02\n\x1eGetDataContractsByRangeRequest\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetDataContractsByRangeRequest.GetDataContractsByRangeRequestV0H\x00\x1a\x95\x01\n GetDataContractsByRangeRequestV0\x12\x12\n\x05limit\x18\x01 \x01(\rH\x01\x88\x01\x01\x12\x15\n\x0bstart_after\x18\x02 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x03 \x01(\x0cH\x00\x12\x10\n\x08ids_only\x18\x04 \x01(\x08\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x07\n\x05startB\x08\n\x06_limitB\t\n\x07version\"\xcf\x04\n\x18GetDataContractsResponse\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetDataContractsResponse.GetDataContractsResponseV0H\x00\x1a[\n\x11\x44\x61taContractEntry\x12\x12\n\nidentifier\x18\x01 \x01(\x0c\x12\x32\n\rdata_contract\x18\x02 \x01(\x0b\x32\x1b.google.protobuf.BytesValue\x1au\n\rDataContracts\x12\x64\n\x15\x64\x61ta_contract_entries\x18\x01 \x03(\x0b\x32\x45.org.dash.platform.dapi.v0.GetDataContractsResponse.DataContractEntry\x1a\xf5\x01\n\x1aGetDataContractsResponseV0\x12[\n\x0e\x64\x61ta_contracts\x18\x01 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDataContractsResponse.DataContractsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc5\x02\n\x1dGetDataContractHistoryRequest\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetDataContractHistoryRequest.GetDataContractHistoryRequestV0H\x00\x1a\xb0\x01\n\x1fGetDataContractHistoryRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12+\n\x05limit\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x17\n\x0bstart_at_ms\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\t\n\x07version\"\xb2\x05\n\x1eGetDataContractHistoryResponse\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetDataContractHistoryResponse.GetDataContractHistoryResponseV0H\x00\x1a\x9a\x04\n GetDataContractHistoryResponseV0\x12\x8f\x01\n\x15\x64\x61ta_contract_history\x18\x01 \x01(\x0b\x32n.org.dash.platform.dapi.v0.GetDataContractHistoryResponse.GetDataContractHistoryResponseV0.DataContractHistoryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a;\n\x18\x44\x61taContractHistoryEntry\x12\x10\n\x04\x64\x61te\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05value\x18\x02 \x01(\x0c\x1a\xaa\x01\n\x13\x44\x61taContractHistory\x12\x92\x01\n\x15\x64\x61ta_contract_entries\x18\x01 \x03(\x0b\x32s.org.dash.platform.dapi.v0.GetDataContractHistoryResponse.GetDataContractHistoryResponseV0.DataContractHistoryEntryB\x08\n\x06resultB\t\n\x07version\"\xd8\"\n\x13GetDocumentsRequest\x12R\n\x02v0\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV0H\x00\x12R\n\x02v1\x18\x02 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1H\x00\x1a\xfe\x02\n\x12\x44ocumentFieldValue\x12\x14\n\nbool_value\x18\x01 \x01(\x08H\x00\x12\x19\n\x0bint64_value\x18\x02 \x01(\x12\x42\x02\x30\x01H\x00\x12\x1a\n\x0cuint64_value\x18\x03 \x01(\x04\x42\x02\x30\x01H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12\x0e\n\x04text\x18\x05 \x01(\tH\x00\x12\x15\n\x0b\x62ytes_value\x18\x06 \x01(\x0cH\x00\x12[\n\x04list\x18\x07 \x01(\x0b\x32K.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue.ValueListH\x00\x12\x14\n\nnull_value\x18\x08 \x01(\x08H\x00\x1a^\n\tValueList\x12Q\n\x06values\x18\x01 \x03(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValueB\t\n\x07variant\x1a\xe2\x02\n\x12TimeRangeSelection\x12\\\n\x08selector\x18\x01 \x01(\x0e\x32J.org.dash.platform.dapi.v0.GetDocumentsRequest.TimeRangeSelection.Selector\x12\x19\n\x08start_ms\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x12T\n\x04grid\x18\x03 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetDocumentsRequest.TimeRangeSelection.Grid\x1a>\n\x04Grid\x12\x11\n\x05range\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x10\n\x04step\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x11\n\x05phase\x18\x03 \x01(\x04\x42\x02\x30\x01\"0\n\x08Selector\x12\n\n\x06NEWEST\x10\x00\x12\n\n\x06OLDEST\x10\x01\x12\x0c\n\x08\x42Y_START\x10\x02\x42\x0b\n\t_start_ms\x1a\x82\x02\n\x15IntegerRangeSelection\x12P\n\x05start\x18\x01 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue\x12W\n\x04grid\x18\x02 \x01(\x0b\x32I.org.dash.platform.dapi.v0.GetDocumentsRequest.IntegerRangeSelection.Grid\x1a>\n\x04Grid\x12\x11\n\x05range\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x10\n\x04step\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x11\n\x05phase\x18\x03 \x01(\x04\x42\x02\x30\x01\x1a\xf2\x02\n\x0bWhereClause\x12\r\n\x05\x66ield\x18\x01 \x01(\t\x12N\n\x08operator\x18\x02 \x01(\x0e\x32<.org.dash.platform.dapi.v0.GetDocumentsRequest.WhereOperator\x12P\n\x05value\x18\x03 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue\x12U\n\ntime_range\x18\x04 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.TimeRangeSelection\x12[\n\rinteger_range\x18\x05 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetDocumentsRequest.IntegerRangeSelection\x1a\xa4\x01\n\x0fHavingAggregate\x12Y\n\x08\x66unction\x18\x01 \x01(\x0e\x32G.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregate.Function\x12\r\n\x05\x66ield\x18\x02 \x01(\t\"\'\n\x08\x46unction\x12\t\n\x05\x43OUNT\x10\x00\x12\x07\n\x03SUM\x10\x01\x12\x07\n\x03\x41VG\x10\x02\x1a\xf9\x03\n\x0cHavingClause\x12Q\n\taggregate\x18\x01 \x01(\x0b\x32>.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregate\x12V\n\x08operator\x18\x02 \x01(\x0e\x32\x44.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.Operator\x12R\n\x05value\x18\x03 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValueH\x00\"\xe0\x01\n\x08Operator\x12\t\n\x05\x45QUAL\x10\x00\x12\r\n\tNOT_EQUAL\x10\x01\x12\x10\n\x0cGREATER_THAN\x10\x02\x12\x1a\n\x16GREATER_THAN_OR_EQUALS\x10\x03\x12\r\n\tLESS_THAN\x10\x04\x12\x17\n\x13LESS_THAN_OR_EQUALS\x10\x05\x12\x0b\n\x07\x42\x45TWEEN\x10\x06\x12\x1a\n\x16\x42\x45TWEEN_EXCLUDE_BOUNDS\x10\x07\x12\x18\n\x14\x42\x45TWEEN_EXCLUDE_LEFT\x10\x08\x12\x19\n\x15\x42\x45TWEEN_EXCLUDE_RIGHT\x10\t\x12\x06\n\x02IN\x10\nB\x07\n\x05right\x1a\x90\x01\n\x0bOrderClause\x12\x0f\n\x05\x66ield\x18\x01 \x01(\tH\x00\x12S\n\taggregate\x18\x03 \x01(\x0b\x32>.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregateH\x00\x12\x11\n\tascending\x18\x02 \x01(\x08\x42\x08\n\x06target\x1a\xbb\x01\n\x15GetDocumentsRequestV0\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x15\n\rdocument_type\x18\x02 \x01(\t\x12\r\n\x05where\x18\x03 \x01(\x0c\x12\x10\n\x08order_by\x18\x04 \x01(\x0c\x12\r\n\x05limit\x18\x05 \x01(\r\x12\x15\n\x0bstart_after\x18\x06 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x07 \x01(\x0cH\x00\x12\r\n\x05prove\x18\x08 \x01(\x08\x42\x07\n\x05start\x1a\xa6\x0c\n\x15GetDocumentsRequestV1\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x15\n\rdocument_type\x18\x02 \x01(\t\x12Q\n\rwhere_clauses\x18\x03 \x03(\x0b\x32:.org.dash.platform.dapi.v0.GetDocumentsRequest.WhereClause\x12L\n\x08order_by\x18\x04 \x03(\x0b\x32:.org.dash.platform.dapi.v0.GetDocumentsRequest.OrderClause\x12\x12\n\x05limit\x18\x05 \x01(\rH\x01\x88\x01\x01\x12\x15\n\x0bstart_after\x18\x06 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x07 \x01(\x0cH\x00\x12\r\n\x05prove\x18\x08 \x01(\x08\x12\\\n\x07selects\x18\t \x03(\x0b\x32K.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.Select\x12\x10\n\x08group_by\x18\n \x03(\t\x12K\n\x06having\x18\x0b \x03(\x0b\x32;.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause\x12\x13\n\x06offset\x18\x0c \x01(\rH\x02\x88\x01\x01\x12\x61\n\x07\x63hained\x18\r \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.ChainedJoin\x12\x62\n\x0bsub_queries\x18\x0e \x03(\x0b\x32M.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.SubQuery\x1a\xc9\x01\n\x06Select\x12\x66\n\x08\x66unction\x18\x01 \x01(\x0e\x32T.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.Select.Function\x12\r\n\x05\x66ield\x18\x02 \x01(\t\"H\n\x08\x46unction\x12\r\n\tDOCUMENTS\x10\x00\x12\t\n\x05\x43OUNT\x10\x01\x12\x07\n\x03SUM\x10\x02\x12\x07\n\x03\x41VG\x10\x03\x12\x07\n\x03MIN\x10\x04\x12\x07\n\x03MAX\x10\x05\x1a\x41\n\x0b\x43hainedJoin\x12\x15\n\rjoin_property\x18\x01 \x01(\t\x12\x1b\n\x13outer_document_type\x18\x02 \x01(\t\x1a\xa6\x04\n\x08SubQuery\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x15\n\rdocument_type\x18\x02 \x01(\t\x12Q\n\rwhere_clauses\x18\x03 \x03(\x0b\x32:.org.dash.platform.dapi.v0.GetDocumentsRequest.WhereClause\x12L\n\x08order_by\x18\x04 \x03(\x0b\x32:.org.dash.platform.dapi.v0.GetDocumentsRequest.OrderClause\x12\x12\n\x05limit\x18\x05 \x01(\rH\x00\x88\x01\x01\x12`\n\x04kind\x18\x06 \x01(\x0e\x32R.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.SubQuery.Kind\x12\x63\n\x04\x62ind\x18\x07 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.SubQuery.Binding\x1a\x41\n\x07\x42inding\x12\x0e\n\x06source\x18\x01 \x01(\r\x12\x17\n\x0fsource_property\x18\x02 \x01(\t\x12\r\n\x05\x66ield\x18\x03 \x01(\t\" \n\x04Kind\x12\r\n\tDOCUMENTS\x10\x00\x12\t\n\x05\x43OUNT\x10\x01\x42\x08\n\x06_limitB\x07\n\x05startB\x08\n\x06_limitB\t\n\x07_offset\"\x90\x02\n\rWhereOperator\x12\t\n\x05\x45QUAL\x10\x00\x12\x10\n\x0cGREATER_THAN\x10\x01\x12\x1a\n\x16GREATER_THAN_OR_EQUALS\x10\x02\x12\r\n\tLESS_THAN\x10\x03\x12\x17\n\x13LESS_THAN_OR_EQUALS\x10\x04\x12\x0b\n\x07\x42\x45TWEEN\x10\x05\x12\x1a\n\x16\x42\x45TWEEN_EXCLUDE_BOUNDS\x10\x06\x12\x18\n\x14\x42\x45TWEEN_EXCLUDE_LEFT\x10\x07\x12\x19\n\x15\x42\x45TWEEN_EXCLUDE_RIGHT\x10\x08\x12\x06\n\x02IN\x10\t\x12\x0f\n\x0bSTARTS_WITH\x10\n\x12\x11\n\rIN_TIME_RANGE\x10\x0b\x12\x14\n\x10IN_INTEGER_RANGE\x10\x0c\x42\t\n\x07version\"\xf2\x1b\n\x14GetDocumentsResponse\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV0H\x00\x12T\n\x02v1\x18\x02 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1H\x00\x1a\x9b\x02\n\x16GetDocumentsResponseV0\x12\x65\n\tdocuments\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV0.DocumentsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1e\n\tDocuments\x12\x11\n\tdocuments\x18\x01 \x03(\x0c\x42\x08\n\x06result\x1a\x84\x18\n\x16GetDocumentsResponseV1\x12\x61\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultDataH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1e\n\tDocuments\x12\x11\n\tdocuments\x18\x01 \x03(\x0c\x1aL\n\nCountEntry\x12\x13\n\x06in_key\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x0b\n\x03key\x18\x02 \x01(\x0c\x12\x11\n\x05\x63ount\x18\x03 \x01(\x04\x42\x02\x30\x01\x42\t\n\x07_in_key\x1ar\n\x0c\x43ountEntries\x12\x62\n\x07\x65ntries\x18\x01 \x03(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountEntry\x1a\xa0\x01\n\x0c\x43ountResults\x12\x1d\n\x0f\x61ggregate_count\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x66\n\x07\x65ntries\x18\x02 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountEntriesH\x00\x42\t\n\x07variant\x1aH\n\x08SumEntry\x12\x13\n\x06in_key\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x0b\n\x03key\x18\x02 \x01(\x0c\x12\x0f\n\x03sum\x18\x03 \x01(\x12\x42\x02\x30\x01\x42\t\n\x07_in_key\x1an\n\nSumEntries\x12`\n\x07\x65ntries\x18\x01 \x03(\x0b\x32O.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumEntry\x1a\x9a\x01\n\nSumResults\x12\x1b\n\raggregate_sum\x18\x01 \x01(\x12\x42\x02\x30\x01H\x00\x12\x64\n\x07\x65ntries\x18\x02 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumEntriesH\x00\x42\t\n\x07variant\x1a_\n\x0c\x41verageEntry\x12\x13\n\x06in_key\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x0b\n\x03key\x18\x02 \x01(\x0c\x12\x11\n\x05\x63ount\x18\x03 \x01(\x04\x42\x02\x30\x01\x12\x0f\n\x03sum\x18\x04 \x01(\x12\x42\x02\x30\x01\x42\t\n\x07_in_key\x1av\n\x0e\x41verageEntries\x12\x64\n\x07\x65ntries\x18\x01 \x03(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageEntry\x1a\x36\n\x10\x41verageAggregate\x12\x11\n\x05\x63ount\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x0f\n\x03sum\x18\x02 \x01(\x12\x42\x02\x30\x01\x1a\xfb\x01\n\x0e\x41verageResults\x12t\n\x11\x61ggregate_average\x18\x01 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageAggregateH\x00\x12h\n\x07\x65ntries\x18\x02 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageEntriesH\x00\x42\t\n\x07variant\x1az\n\x0bRankedEntry\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\x13\n\x05\x63ount\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12\x11\n\x03sum\x18\x03 \x01(\x12\x42\x02\x30\x01H\x00\x12\r\n\x03\x61vg\x18\x04 \x01(\x01H\x00\x12\x13\n\x06in_key\x18\x05 \x01(\x0cH\x01\x88\x01\x01\x42\x07\n\x05valueB\t\n\x07_in_key\x1a\x9a\x01\n\rRankedEntries\x12\x63\n\x07\x65ntries\x18\x01 \x03(\x0b\x32R.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry\x12\x18\n\x07skipped\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x42\n\n\x08_skipped\x1a\xf7\x05\n\nResultData\x12\x65\n\tdocuments\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.DocumentsH\x00\x12\x65\n\x06\x63ounts\x18\x02 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResultsH\x00\x12\x61\n\x04sums\x18\x03 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumResultsH\x00\x12i\n\x08\x61verages\x18\x04 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResultsH\x00\x12\x66\n\x06ranked\x18\x05 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntriesH\x00\x12j\n\x07\x63hained\x18\x06 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ChainedDocumentsH\x00\x12n\n\tcomposite\x18\x07 \x01(\x0b\x32Y.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CompositeDocumentsH\x00\x42\t\n\x07variant\x1a_\n\x10\x43hainedDocuments\x12\x17\n\x0finner_documents\x18\x01 \x03(\x0c\x12\x17\n\x0fouter_documents\x18\x02 \x03(\x0c\x12\x19\n\x11missing_outer_ids\x18\x03 \x03(\x0c\x1a\xab\x03\n\x12\x43ompositeDocuments\x12\x16\n\x0epage_documents\x18\x01 \x03(\x0c\x12}\n\x0bsub_results\x18\x02 \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CompositeDocuments.SubQueryResult\x1a\xfd\x01\n\x0eSubQueryResult\x12\x65\n\tdocuments\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.DocumentsH\x00\x12\x65\n\x06\x63ounts\x18\x02 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountEntriesH\x00\x12\x13\n\x0bmissing_ids\x18\x03 \x03(\x0c\x42\x08\n\x06resultB\x08\n\x06resultB\t\n\x07version\"\xf4\x02\n\x19GetDocumentHistoryRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentHistoryRequest.GetDocumentHistoryRequestV0H\x00\x1a\xeb\x01\n\x1bGetDocumentHistoryRequestV0\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x13\n\x0b\x64ocument_id\x18\x03 \x01(\x0c\x12+\n\x05limit\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x17\n\x0bstart_at_ms\x18\x06 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x07 \x01(\x08\x42\t\n\x07version\"\xf7\x04\n\x1aGetDocumentHistoryResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetDocumentHistoryResponse.GetDocumentHistoryResponseV0H\x00\x1a\xeb\x03\n\x1cGetDocumentHistoryResponseV0\x12~\n\x10\x64ocument_history\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetDocumentHistoryResponse.GetDocumentHistoryResponseV0.DocumentHistoryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x37\n\x14\x44ocumentHistoryEntry\x12\x10\n\x04\x64\x61te\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05value\x18\x02 \x01(\x0c\x1a\x95\x01\n\x0f\x44ocumentHistory\x12\x81\x01\n\x10\x64ocument_entries\x18\x01 \x03(\x0b\x32g.org.dash.platform.dapi.v0.GetDocumentHistoryResponse.GetDocumentHistoryResponseV0.DocumentHistoryEntryB\x08\n\x06resultB\t\n\x07version\"\xed\x01\n!GetIdentityByPublicKeyHashRequest\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashRequest.GetIdentityByPublicKeyHashRequestV0H\x00\x1aM\n#GetIdentityByPublicKeyHashRequestV0\x12\x17\n\x0fpublic_key_hash\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xda\x02\n\"GetIdentityByPublicKeyHashResponse\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashResponse.GetIdentityByPublicKeyHashResponseV0H\x00\x1a\xb6\x01\n$GetIdentityByPublicKeyHashResponseV0\x12\x12\n\x08identity\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xbd\x02\n*GetIdentityByNonUniquePublicKeyHashRequest\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashRequest.GetIdentityByNonUniquePublicKeyHashRequestV0H\x00\x1a\x80\x01\n,GetIdentityByNonUniquePublicKeyHashRequestV0\x12\x17\n\x0fpublic_key_hash\x18\x01 \x01(\x0c\x12\x18\n\x0bstart_after\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\x0e\n\x0c_start_afterB\t\n\x07version\"\xd6\x06\n+GetIdentityByNonUniquePublicKeyHashResponse\x12\x82\x01\n\x02v0\x18\x01 \x01(\x0b\x32t.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse.GetIdentityByNonUniquePublicKeyHashResponseV0H\x00\x1a\x96\x05\n-GetIdentityByNonUniquePublicKeyHashResponseV0\x12\x9a\x01\n\x08identity\x18\x01 \x01(\x0b\x32\x85\x01.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse.GetIdentityByNonUniquePublicKeyHashResponseV0.IdentityResponseH\x00\x12\x9d\x01\n\x05proof\x18\x02 \x01(\x0b\x32\x8b\x01.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse.GetIdentityByNonUniquePublicKeyHashResponseV0.IdentityProvedResponseH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x36\n\x10IdentityResponse\x12\x15\n\x08identity\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x42\x0b\n\t_identity\x1a\xa6\x01\n\x16IdentityProvedResponse\x12P\n&grovedb_identity_public_key_hash_proof\x18\x01 \x01(\x0b\x32 .org.dash.platform.dapi.v0.Proof\x12!\n\x14identity_proof_bytes\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x42\x17\n\x15_identity_proof_bytesB\x08\n\x06resultB\t\n\x07version\"\x99\x02\n#WaitForStateTransitionResultRequest\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.WaitForStateTransitionResultRequest.WaitForStateTransitionResultRequestV0H\x00\x1as\n%WaitForStateTransitionResultRequestV0\x12\x1d\n\x15state_transition_hash\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x12\x1c\n\x14request_user_balance\x18\x03 \x01(\x08\x42\t\n\x07version\"\xce\x04\n$WaitForStateTransitionResultResponse\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.WaitForStateTransitionResultResponse.WaitForStateTransitionResultResponseV0H\x00\x1a\x34\n\x17SuccessWithOwnerBalance\x12\x19\n\rowner_balance\x18\x01 \x01(\x04\x42\x02\x30\x01\x1a\xee\x02\n&WaitForStateTransitionResultResponseV0\x12I\n\x05\x65rror\x18\x01 \x01(\x0b\x32\x38.org.dash.platform.dapi.v0.StateTransitionBroadcastErrorH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12}\n\x1asuccess_with_owner_balance\x18\x04 \x01(\x0b\x32W.org.dash.platform.dapi.v0.WaitForStateTransitionResultResponse.SuccessWithOwnerBalanceH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc4\x01\n\x19GetConsensusParamsRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetConsensusParamsRequest.GetConsensusParamsRequestV0H\x00\x1a<\n\x1bGetConsensusParamsRequestV0\x12\x0e\n\x06height\x18\x01 \x01(\x05\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9c\x04\n\x1aGetConsensusParamsResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetConsensusParamsResponse.GetConsensusParamsResponseV0H\x00\x1aP\n\x14\x43onsensusParamsBlock\x12\x11\n\tmax_bytes\x18\x01 \x01(\t\x12\x0f\n\x07max_gas\x18\x02 \x01(\t\x12\x14\n\x0ctime_iota_ms\x18\x03 \x01(\t\x1a\x62\n\x17\x43onsensusParamsEvidence\x12\x1a\n\x12max_age_num_blocks\x18\x01 \x01(\t\x12\x18\n\x10max_age_duration\x18\x02 \x01(\t\x12\x11\n\tmax_bytes\x18\x03 \x01(\t\x1a\xda\x01\n\x1cGetConsensusParamsResponseV0\x12Y\n\x05\x62lock\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetConsensusParamsResponse.ConsensusParamsBlock\x12_\n\x08\x65vidence\x18\x02 \x01(\x0b\x32M.org.dash.platform.dapi.v0.GetConsensusParamsResponse.ConsensusParamsEvidenceB\t\n\x07version\"\xe4\x01\n%GetProtocolVersionUpgradeStateRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateRequest.GetProtocolVersionUpgradeStateRequestV0H\x00\x1a\x38\n\'GetProtocolVersionUpgradeStateRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xb5\x05\n&GetProtocolVersionUpgradeStateResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse.GetProtocolVersionUpgradeStateResponseV0H\x00\x1a\x85\x04\n(GetProtocolVersionUpgradeStateResponseV0\x12\x87\x01\n\x08versions\x18\x01 \x01(\x0b\x32s.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse.GetProtocolVersionUpgradeStateResponseV0.VersionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x96\x01\n\x08Versions\x12\x89\x01\n\x08versions\x18\x01 \x03(\x0b\x32w.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse.GetProtocolVersionUpgradeStateResponseV0.VersionEntry\x1a:\n\x0cVersionEntry\x12\x16\n\x0eversion_number\x18\x01 \x01(\r\x12\x12\n\nvote_count\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xa3\x02\n*GetProtocolVersionUpgradeVoteStatusRequest\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusRequest.GetProtocolVersionUpgradeVoteStatusRequestV0H\x00\x1ag\n,GetProtocolVersionUpgradeVoteStatusRequestV0\x12\x19\n\x11start_pro_tx_hash\x18\x01 \x01(\x0c\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xef\x05\n+GetProtocolVersionUpgradeVoteStatusResponse\x12\x82\x01\n\x02v0\x18\x01 \x01(\x0b\x32t.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse.GetProtocolVersionUpgradeVoteStatusResponseV0H\x00\x1a\xaf\x04\n-GetProtocolVersionUpgradeVoteStatusResponseV0\x12\x98\x01\n\x08versions\x18\x01 \x01(\x0b\x32\x83\x01.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse.GetProtocolVersionUpgradeVoteStatusResponseV0.VersionSignalsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xaf\x01\n\x0eVersionSignals\x12\x9c\x01\n\x0fversion_signals\x18\x01 \x03(\x0b\x32\x82\x01.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse.GetProtocolVersionUpgradeVoteStatusResponseV0.VersionSignal\x1a\x35\n\rVersionSignal\x12\x13\n\x0bpro_tx_hash\x18\x01 \x01(\x0c\x12\x0f\n\x07version\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xf5\x01\n\x14GetEpochsInfoRequest\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetEpochsInfoRequest.GetEpochsInfoRequestV0H\x00\x1a|\n\x16GetEpochsInfoRequestV0\x12\x31\n\x0bstart_epoch\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\x11\n\tascending\x18\x03 \x01(\x08\x12\r\n\x05prove\x18\x04 \x01(\x08\x42\t\n\x07version\"\x99\x05\n\x15GetEpochsInfoResponse\x12V\n\x02v0\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetEpochsInfoResponse.GetEpochsInfoResponseV0H\x00\x1a\x9c\x04\n\x17GetEpochsInfoResponseV0\x12\x65\n\x06\x65pochs\x18\x01 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetEpochsInfoResponse.GetEpochsInfoResponseV0.EpochInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1au\n\nEpochInfos\x12g\n\x0b\x65poch_infos\x18\x01 \x03(\x0b\x32R.org.dash.platform.dapi.v0.GetEpochsInfoResponse.GetEpochsInfoResponseV0.EpochInfo\x1a\xa6\x01\n\tEpochInfo\x12\x0e\n\x06number\x18\x01 \x01(\r\x12\x1e\n\x12\x66irst_block_height\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x1f\n\x17\x66irst_core_block_height\x18\x03 \x01(\r\x12\x16\n\nstart_time\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x16\n\x0e\x66\x65\x65_multiplier\x18\x05 \x01(\x01\x12\x18\n\x10protocol_version\x18\x06 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xbf\x02\n\x1dGetFinalizedEpochInfosRequest\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetFinalizedEpochInfosRequest.GetFinalizedEpochInfosRequestV0H\x00\x1a\xaa\x01\n\x1fGetFinalizedEpochInfosRequestV0\x12\x19\n\x11start_epoch_index\x18\x01 \x01(\r\x12\"\n\x1astart_epoch_index_included\x18\x02 \x01(\x08\x12\x17\n\x0f\x65nd_epoch_index\x18\x03 \x01(\r\x12 \n\x18\x65nd_epoch_index_included\x18\x04 \x01(\x08\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\t\n\x07version\"\xbd\t\n\x1eGetFinalizedEpochInfosResponse\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0H\x00\x1a\xa5\x08\n GetFinalizedEpochInfosResponseV0\x12\x80\x01\n\x06\x65pochs\x18\x01 \x01(\x0b\x32n.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0.FinalizedEpochInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xa4\x01\n\x13\x46inalizedEpochInfos\x12\x8c\x01\n\x15\x66inalized_epoch_infos\x18\x01 \x03(\x0b\x32m.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0.FinalizedEpochInfo\x1a\x9f\x04\n\x12\x46inalizedEpochInfo\x12\x0e\n\x06number\x18\x01 \x01(\r\x12\x1e\n\x12\x66irst_block_height\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x1f\n\x17\x66irst_core_block_height\x18\x03 \x01(\r\x12\x1c\n\x10\x66irst_block_time\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x16\n\x0e\x66\x65\x65_multiplier\x18\x05 \x01(\x01\x12\x18\n\x10protocol_version\x18\x06 \x01(\r\x12!\n\x15total_blocks_in_epoch\x18\x07 \x01(\x04\x42\x02\x30\x01\x12*\n\"next_epoch_start_core_block_height\x18\x08 \x01(\r\x12!\n\x15total_processing_fees\x18\t \x01(\x04\x42\x02\x30\x01\x12*\n\x1etotal_distributed_storage_fees\x18\n \x01(\x04\x42\x02\x30\x01\x12&\n\x1atotal_created_storage_fees\x18\x0b \x01(\x04\x42\x02\x30\x01\x12\x1e\n\x12\x63ore_block_rewards\x18\x0c \x01(\x04\x42\x02\x30\x01\x12\x81\x01\n\x0f\x62lock_proposers\x18\r \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0.BlockProposer\x1a\x39\n\rBlockProposer\x12\x13\n\x0bproposer_id\x18\x01 \x01(\x0c\x12\x13\n\x0b\x62lock_count\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xde\x04\n\x1cGetContestedResourcesRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetContestedResourcesRequest.GetContestedResourcesRequestV0H\x00\x1a\xcc\x03\n\x1eGetContestedResourcesRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x12\n\nindex_name\x18\x03 \x01(\t\x12\x1a\n\x12start_index_values\x18\x04 \x03(\x0c\x12\x18\n\x10\x65nd_index_values\x18\x05 \x03(\x0c\x12\x89\x01\n\x13start_at_value_info\x18\x06 \x01(\x0b\x32g.org.dash.platform.dapi.v0.GetContestedResourcesRequest.GetContestedResourcesRequestV0.StartAtValueInfoH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x07 \x01(\rH\x01\x88\x01\x01\x12\x17\n\x0forder_ascending\x18\x08 \x01(\x08\x12\r\n\x05prove\x18\t \x01(\x08\x1a\x45\n\x10StartAtValueInfo\x12\x13\n\x0bstart_value\x18\x01 \x01(\x0c\x12\x1c\n\x14start_value_included\x18\x02 \x01(\x08\x42\x16\n\x14_start_at_value_infoB\x08\n\x06_countB\t\n\x07version\"\x88\x04\n\x1dGetContestedResourcesResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetContestedResourcesResponse.GetContestedResourcesResponseV0H\x00\x1a\xf3\x02\n\x1fGetContestedResourcesResponseV0\x12\x95\x01\n\x19\x63ontested_resource_values\x18\x01 \x01(\x0b\x32p.org.dash.platform.dapi.v0.GetContestedResourcesResponse.GetContestedResourcesResponseV0.ContestedResourceValuesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a<\n\x17\x43ontestedResourceValues\x12!\n\x19\x63ontested_resource_values\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\xd2\x05\n\x1cGetVotePollsByEndDateRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest.GetVotePollsByEndDateRequestV0H\x00\x1a\xc0\x04\n\x1eGetVotePollsByEndDateRequestV0\x12\x84\x01\n\x0fstart_time_info\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest.GetVotePollsByEndDateRequestV0.StartAtTimeInfoH\x00\x88\x01\x01\x12\x80\x01\n\rend_time_info\x18\x02 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest.GetVotePollsByEndDateRequestV0.EndAtTimeInfoH\x01\x88\x01\x01\x12\x12\n\x05limit\x18\x03 \x01(\rH\x02\x88\x01\x01\x12\x13\n\x06offset\x18\x04 \x01(\rH\x03\x88\x01\x01\x12\x11\n\tascending\x18\x05 \x01(\x08\x12\r\n\x05prove\x18\x06 \x01(\x08\x1aI\n\x0fStartAtTimeInfo\x12\x19\n\rstart_time_ms\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1b\n\x13start_time_included\x18\x02 \x01(\x08\x1a\x43\n\rEndAtTimeInfo\x12\x17\n\x0b\x65nd_time_ms\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x19\n\x11\x65nd_time_included\x18\x02 \x01(\x08\x42\x12\n\x10_start_time_infoB\x10\n\x0e_end_time_infoB\x08\n\x06_limitB\t\n\x07_offsetB\t\n\x07version\"\x83\x06\n\x1dGetVotePollsByEndDateResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse.GetVotePollsByEndDateResponseV0H\x00\x1a\xee\x04\n\x1fGetVotePollsByEndDateResponseV0\x12\x9c\x01\n\x18vote_polls_by_timestamps\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse.GetVotePollsByEndDateResponseV0.SerializedVotePollsByTimestampsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aV\n\x1eSerializedVotePollsByTimestamp\x12\x15\n\ttimestamp\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1d\n\x15serialized_vote_polls\x18\x02 \x03(\x0c\x1a\xd7\x01\n\x1fSerializedVotePollsByTimestamps\x12\x99\x01\n\x18vote_polls_by_timestamps\x18\x01 \x03(\x0b\x32w.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse.GetVotePollsByEndDateResponseV0.SerializedVotePollsByTimestamp\x12\x18\n\x10\x66inished_results\x18\x02 \x01(\x08\x42\x08\n\x06resultB\t\n\x07version\"\xff\x06\n$GetContestedResourceVoteStateRequest\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest.GetContestedResourceVoteStateRequestV0H\x00\x1a\xd5\x05\n&GetContestedResourceVoteStateRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x12\n\nindex_name\x18\x03 \x01(\t\x12\x14\n\x0cindex_values\x18\x04 \x03(\x0c\x12\x86\x01\n\x0bresult_type\x18\x05 \x01(\x0e\x32q.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest.GetContestedResourceVoteStateRequestV0.ResultType\x12\x36\n.allow_include_locked_and_abstaining_vote_tally\x18\x06 \x01(\x08\x12\xa3\x01\n\x18start_at_identifier_info\x18\x07 \x01(\x0b\x32|.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest.GetContestedResourceVoteStateRequestV0.StartAtIdentifierInfoH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x08 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\t \x01(\x08\x1aT\n\x15StartAtIdentifierInfo\x12\x18\n\x10start_identifier\x18\x01 \x01(\x0c\x12!\n\x19start_identifier_included\x18\x02 \x01(\x08\"I\n\nResultType\x12\r\n\tDOCUMENTS\x10\x00\x12\x0e\n\nVOTE_TALLY\x10\x01\x12\x1c\n\x18\x44OCUMENTS_AND_VOTE_TALLY\x10\x02\x42\x1b\n\x19_start_at_identifier_infoB\x08\n\x06_countB\t\n\x07version\"\x94\x0c\n%GetContestedResourceVoteStateResponse\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0H\x00\x1a\xe7\n\n\'GetContestedResourceVoteStateResponseV0\x12\xae\x01\n\x1d\x63ontested_resource_contenders\x18\x01 \x01(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.ContestedResourceContendersH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xda\x03\n\x10\x46inishedVoteInfo\x12\xad\x01\n\x15\x66inished_vote_outcome\x18\x01 \x01(\x0e\x32\x8d\x01.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.FinishedVoteInfo.FinishedVoteOutcome\x12\x1f\n\x12won_by_identity_id\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x12$\n\x18\x66inished_at_block_height\x18\x03 \x01(\x04\x42\x02\x30\x01\x12%\n\x1d\x66inished_at_core_block_height\x18\x04 \x01(\r\x12%\n\x19\x66inished_at_block_time_ms\x18\x05 \x01(\x04\x42\x02\x30\x01\x12\x19\n\x11\x66inished_at_epoch\x18\x06 \x01(\r\"O\n\x13\x46inishedVoteOutcome\x12\x14\n\x10TOWARDS_IDENTITY\x10\x00\x12\n\n\x06LOCKED\x10\x01\x12\x16\n\x12NO_PREVIOUS_WINNER\x10\x02\x42\x15\n\x13_won_by_identity_id\x1a\xc4\x03\n\x1b\x43ontestedResourceContenders\x12\x86\x01\n\ncontenders\x18\x01 \x03(\x0b\x32r.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.Contender\x12\x1f\n\x12\x61\x62stain_vote_tally\x18\x02 \x01(\rH\x00\x88\x01\x01\x12\x1c\n\x0flock_vote_tally\x18\x03 \x01(\rH\x01\x88\x01\x01\x12\x9a\x01\n\x12\x66inished_vote_info\x18\x04 \x01(\x0b\x32y.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.FinishedVoteInfoH\x02\x88\x01\x01\x42\x15\n\x13_abstain_vote_tallyB\x12\n\x10_lock_vote_tallyB\x15\n\x13_finished_vote_info\x1ak\n\tContender\x12\x12\n\nidentifier\x18\x01 \x01(\x0c\x12\x17\n\nvote_count\x18\x02 \x01(\rH\x00\x88\x01\x01\x12\x15\n\x08\x64ocument\x18\x03 \x01(\x0cH\x01\x88\x01\x01\x42\r\n\x0b_vote_countB\x0b\n\t_documentB\x08\n\x06resultB\t\n\x07version\"\xd5\x05\n,GetContestedResourceVotersForIdentityRequest\x12\x84\x01\n\x02v0\x18\x01 \x01(\x0b\x32v.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityRequest.GetContestedResourceVotersForIdentityRequestV0H\x00\x1a\x92\x04\n.GetContestedResourceVotersForIdentityRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x12\n\nindex_name\x18\x03 \x01(\t\x12\x14\n\x0cindex_values\x18\x04 \x03(\x0c\x12\x15\n\rcontestant_id\x18\x05 \x01(\x0c\x12\xb4\x01\n\x18start_at_identifier_info\x18\x06 \x01(\x0b\x32\x8c\x01.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityRequest.GetContestedResourceVotersForIdentityRequestV0.StartAtIdentifierInfoH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x07 \x01(\rH\x01\x88\x01\x01\x12\x17\n\x0forder_ascending\x18\x08 \x01(\x08\x12\r\n\x05prove\x18\t \x01(\x08\x1aT\n\x15StartAtIdentifierInfo\x12\x18\n\x10start_identifier\x18\x01 \x01(\x0c\x12!\n\x19start_identifier_included\x18\x02 \x01(\x08\x42\x1b\n\x19_start_at_identifier_infoB\x08\n\x06_countB\t\n\x07version\"\xf1\x04\n-GetContestedResourceVotersForIdentityResponse\x12\x86\x01\n\x02v0\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityResponse.GetContestedResourceVotersForIdentityResponseV0H\x00\x1a\xab\x03\n/GetContestedResourceVotersForIdentityResponseV0\x12\xb6\x01\n\x19\x63ontested_resource_voters\x18\x01 \x01(\x0b\x32\x90\x01.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityResponse.GetContestedResourceVotersForIdentityResponseV0.ContestedResourceVotersH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x43\n\x17\x43ontestedResourceVoters\x12\x0e\n\x06voters\x18\x01 \x03(\x0c\x12\x18\n\x10\x66inished_results\x18\x02 \x01(\x08\x42\x08\n\x06resultB\t\n\x07version\"\xad\x05\n(GetContestedResourceIdentityVotesRequest\x12|\n\x02v0\x18\x01 \x01(\x0b\x32n.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesRequest.GetContestedResourceIdentityVotesRequestV0H\x00\x1a\xf7\x03\n*GetContestedResourceIdentityVotesRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12+\n\x05limit\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x17\n\x0forder_ascending\x18\x04 \x01(\x08\x12\xae\x01\n\x1astart_at_vote_poll_id_info\x18\x05 \x01(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesRequest.GetContestedResourceIdentityVotesRequestV0.StartAtVotePollIdInfoH\x00\x88\x01\x01\x12\r\n\x05prove\x18\x06 \x01(\x08\x1a\x61\n\x15StartAtVotePollIdInfo\x12 \n\x18start_at_poll_identifier\x18\x01 \x01(\x0c\x12&\n\x1estart_poll_identifier_included\x18\x02 \x01(\x08\x42\x1d\n\x1b_start_at_vote_poll_id_infoB\t\n\x07version\"\xc8\n\n)GetContestedResourceIdentityVotesResponse\x12~\n\x02v0\x18\x01 \x01(\x0b\x32p.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0H\x00\x1a\x8f\t\n+GetContestedResourceIdentityVotesResponseV0\x12\xa1\x01\n\x05votes\x18\x01 \x01(\x0b\x32\x8f\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ContestedResourceIdentityVotesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xf7\x01\n\x1e\x43ontestedResourceIdentityVotes\x12\xba\x01\n!contested_resource_identity_votes\x18\x01 \x03(\x0b\x32\x8e\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ContestedResourceIdentityVote\x12\x18\n\x10\x66inished_results\x18\x02 \x01(\x08\x1a\xad\x02\n\x12ResourceVoteChoice\x12\xad\x01\n\x10vote_choice_type\x18\x01 \x01(\x0e\x32\x92\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ResourceVoteChoice.VoteChoiceType\x12\x18\n\x0bidentity_id\x18\x02 \x01(\x0cH\x00\x88\x01\x01\"=\n\x0eVoteChoiceType\x12\x14\n\x10TOWARDS_IDENTITY\x10\x00\x12\x0b\n\x07\x41\x42STAIN\x10\x01\x12\x08\n\x04LOCK\x10\x02\x42\x0e\n\x0c_identity_id\x1a\x95\x02\n\x1d\x43ontestedResourceIdentityVote\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\'\n\x1fserialized_index_storage_values\x18\x03 \x03(\x0c\x12\x99\x01\n\x0bvote_choice\x18\x04 \x01(\x0b\x32\x83\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ResourceVoteChoiceB\x08\n\x06resultB\t\n\x07version\"\xf0\x01\n%GetPrefundedSpecializedBalanceRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceRequest.GetPrefundedSpecializedBalanceRequestV0H\x00\x1a\x44\n\'GetPrefundedSpecializedBalanceRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xed\x02\n&GetPrefundedSpecializedBalanceResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceResponse.GetPrefundedSpecializedBalanceResponseV0H\x00\x1a\xbd\x01\n(GetPrefundedSpecializedBalanceResponseV0\x12\x15\n\x07\x62\x61lance\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xd0\x01\n GetTotalCreditsInPlatformRequest\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformRequest.GetTotalCreditsInPlatformRequestV0H\x00\x1a\x33\n\"GetTotalCreditsInPlatformRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xd9\x02\n!GetTotalCreditsInPlatformResponse\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformResponse.GetTotalCreditsInPlatformResponseV0H\x00\x1a\xb8\x01\n#GetTotalCreditsInPlatformResponseV0\x12\x15\n\x07\x63redits\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc4\x01\n\x16GetPathElementsRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetPathElementsRequest.GetPathElementsRequestV0H\x00\x1a\x45\n\x18GetPathElementsRequestV0\x12\x0c\n\x04path\x18\x01 \x03(\x0c\x12\x0c\n\x04keys\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xa3\x03\n\x17GetPathElementsResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetPathElementsResponse.GetPathElementsResponseV0H\x00\x1a\xa0\x02\n\x19GetPathElementsResponseV0\x12i\n\x08\x65lements\x18\x01 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetPathElementsResponse.GetPathElementsResponseV0.ElementsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1c\n\x08\x45lements\x12\x10\n\x08\x65lements\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\x81\x01\n\x10GetStatusRequest\x12L\n\x02v0\x18\x01 \x01(\x0b\x32>.org.dash.platform.dapi.v0.GetStatusRequest.GetStatusRequestV0H\x00\x1a\x14\n\x12GetStatusRequestV0B\t\n\x07version\"\xe4\x10\n\x11GetStatusResponse\x12N\n\x02v0\x18\x01 \x01(\x0b\x32@.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0H\x00\x1a\xf3\x0f\n\x13GetStatusResponseV0\x12Y\n\x07version\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version\x12S\n\x04node\x18\x02 \x01(\x0b\x32\x45.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Node\x12U\n\x05\x63hain\x18\x03 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Chain\x12Y\n\x07network\x18\x04 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Network\x12^\n\nstate_sync\x18\x05 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.StateSync\x12S\n\x04time\x18\x06 \x01(\x0b\x32\x45.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Time\x1a\x82\x05\n\x07Version\x12\x63\n\x08software\x18\x01 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Software\x12\x63\n\x08protocol\x18\x02 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Protocol\x1a^\n\x08Software\x12\x0c\n\x04\x64\x61pi\x18\x01 \x01(\t\x12\x12\n\x05\x64rive\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x17\n\ntenderdash\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\x08\n\x06_driveB\r\n\x0b_tenderdash\x1a\xcc\x02\n\x08Protocol\x12p\n\ntenderdash\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Protocol.Tenderdash\x12\x66\n\x05\x64rive\x18\x02 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Protocol.Drive\x1a(\n\nTenderdash\x12\x0b\n\x03p2p\x18\x01 \x01(\r\x12\r\n\x05\x62lock\x18\x02 \x01(\r\x1a<\n\x05\x44rive\x12\x0e\n\x06latest\x18\x03 \x01(\r\x12\x0f\n\x07\x63urrent\x18\x04 \x01(\r\x12\x12\n\nnext_epoch\x18\x05 \x01(\r\x1a\x7f\n\x04Time\x12\x11\n\x05local\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x16\n\x05\x62lock\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x12\x18\n\x07genesis\x18\x03 \x01(\x04\x42\x02\x30\x01H\x01\x88\x01\x01\x12\x12\n\x05\x65poch\x18\x04 \x01(\rH\x02\x88\x01\x01\x42\x08\n\x06_blockB\n\n\x08_genesisB\x08\n\x06_epoch\x1a<\n\x04Node\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\x18\n\x0bpro_tx_hash\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x42\x0e\n\x0c_pro_tx_hash\x1a\xb3\x02\n\x05\x43hain\x12\x13\n\x0b\x63\x61tching_up\x18\x01 \x01(\x08\x12\x19\n\x11latest_block_hash\x18\x02 \x01(\x0c\x12\x17\n\x0flatest_app_hash\x18\x03 \x01(\x0c\x12\x1f\n\x13latest_block_height\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x1b\n\x13\x65\x61rliest_block_hash\x18\x05 \x01(\x0c\x12\x19\n\x11\x65\x61rliest_app_hash\x18\x06 \x01(\x0c\x12!\n\x15\x65\x61rliest_block_height\x18\x07 \x01(\x04\x42\x02\x30\x01\x12!\n\x15max_peer_block_height\x18\t \x01(\x04\x42\x02\x30\x01\x12%\n\x18\x63ore_chain_locked_height\x18\n \x01(\rH\x00\x88\x01\x01\x42\x1b\n\x19_core_chain_locked_height\x1a\x43\n\x07Network\x12\x10\n\x08\x63hain_id\x18\x01 \x01(\t\x12\x13\n\x0bpeers_count\x18\x02 \x01(\r\x12\x11\n\tlistening\x18\x03 \x01(\x08\x1a\x85\x02\n\tStateSync\x12\x1d\n\x11total_synced_time\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1a\n\x0eremaining_time\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x17\n\x0ftotal_snapshots\x18\x03 \x01(\r\x12\"\n\x16\x63hunk_process_avg_time\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x1b\n\x0fsnapshot_height\x18\x05 \x01(\x04\x42\x02\x30\x01\x12!\n\x15snapshot_chunks_count\x18\x06 \x01(\x04\x42\x02\x30\x01\x12\x1d\n\x11\x62\x61\x63kfilled_blocks\x18\x07 \x01(\x04\x42\x02\x30\x01\x12!\n\x15\x62\x61\x63kfill_blocks_total\x18\x08 \x01(\x04\x42\x02\x30\x01\x42\t\n\x07version\"\xb1\x01\n\x1cGetCurrentQuorumsInfoRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoRequest.GetCurrentQuorumsInfoRequestV0H\x00\x1a \n\x1eGetCurrentQuorumsInfoRequestV0B\t\n\x07version\"\xa1\x05\n\x1dGetCurrentQuorumsInfoResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse.GetCurrentQuorumsInfoResponseV0H\x00\x1a\x46\n\x0bValidatorV0\x12\x13\n\x0bpro_tx_hash\x18\x01 \x01(\x0c\x12\x0f\n\x07node_ip\x18\x02 \x01(\t\x12\x11\n\tis_banned\x18\x03 \x01(\x08\x1a\xaf\x01\n\x0eValidatorSetV0\x12\x13\n\x0bquorum_hash\x18\x01 \x01(\x0c\x12\x13\n\x0b\x63ore_height\x18\x02 \x01(\r\x12U\n\x07members\x18\x03 \x03(\x0b\x32\x44.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse.ValidatorV0\x12\x1c\n\x14threshold_public_key\x18\x04 \x01(\x0c\x1a\x92\x02\n\x1fGetCurrentQuorumsInfoResponseV0\x12\x15\n\rquorum_hashes\x18\x01 \x03(\x0c\x12\x1b\n\x13\x63urrent_quorum_hash\x18\x02 \x01(\x0c\x12_\n\x0evalidator_sets\x18\x03 \x03(\x0b\x32G.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse.ValidatorSetV0\x12\x1b\n\x13last_block_proposer\x18\x04 \x01(\x0c\x12=\n\x08metadata\x18\x05 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\t\n\x07version\"\xf4\x01\n\x1fGetIdentityTokenBalancesRequest\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetIdentityTokenBalancesRequest.GetIdentityTokenBalancesRequestV0H\x00\x1aZ\n!GetIdentityTokenBalancesRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x11\n\ttoken_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xad\x05\n GetIdentityTokenBalancesResponse\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse.GetIdentityTokenBalancesResponseV0H\x00\x1a\x8f\x04\n\"GetIdentityTokenBalancesResponseV0\x12\x86\x01\n\x0etoken_balances\x18\x01 \x01(\x0b\x32l.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse.GetIdentityTokenBalancesResponseV0.TokenBalancesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aG\n\x11TokenBalanceEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x14\n\x07\x62\x61lance\x18\x02 \x01(\x04H\x00\x88\x01\x01\x42\n\n\x08_balance\x1a\x9a\x01\n\rTokenBalances\x12\x88\x01\n\x0etoken_balances\x18\x01 \x03(\x0b\x32p.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse.GetIdentityTokenBalancesResponseV0.TokenBalanceEntryB\x08\n\x06resultB\t\n\x07version\"\xfc\x01\n!GetIdentitiesTokenBalancesRequest\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesRequest.GetIdentitiesTokenBalancesRequestV0H\x00\x1a\\\n#GetIdentitiesTokenBalancesRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x14\n\x0cidentity_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xf2\x05\n\"GetIdentitiesTokenBalancesResponse\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse.GetIdentitiesTokenBalancesResponseV0H\x00\x1a\xce\x04\n$GetIdentitiesTokenBalancesResponseV0\x12\x9b\x01\n\x17identity_token_balances\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse.GetIdentitiesTokenBalancesResponseV0.IdentityTokenBalancesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aR\n\x19IdentityTokenBalanceEntry\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x14\n\x07\x62\x61lance\x18\x02 \x01(\x04H\x00\x88\x01\x01\x42\n\n\x08_balance\x1a\xb7\x01\n\x15IdentityTokenBalances\x12\x9d\x01\n\x17identity_token_balances\x18\x01 \x03(\x0b\x32|.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse.GetIdentitiesTokenBalancesResponseV0.IdentityTokenBalanceEntryB\x08\n\x06resultB\t\n\x07version\"\xe8\x01\n\x1cGetIdentityTokenInfosRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetIdentityTokenInfosRequest.GetIdentityTokenInfosRequestV0H\x00\x1aW\n\x1eGetIdentityTokenInfosRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x11\n\ttoken_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\x98\x06\n\x1dGetIdentityTokenInfosResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0H\x00\x1a\x83\x05\n\x1fGetIdentityTokenInfosResponseV0\x12z\n\x0btoken_infos\x18\x01 \x01(\x0b\x32\x63.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0.TokenInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a(\n\x16TokenIdentityInfoEntry\x12\x0e\n\x06\x66rozen\x18\x01 \x01(\x08\x1a\xb0\x01\n\x0eTokenInfoEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x82\x01\n\x04info\x18\x02 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0.TokenIdentityInfoEntryH\x00\x88\x01\x01\x42\x07\n\x05_info\x1a\x8a\x01\n\nTokenInfos\x12|\n\x0btoken_infos\x18\x01 \x03(\x0b\x32g.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0.TokenInfoEntryB\x08\n\x06resultB\t\n\x07version\"\xf0\x01\n\x1eGetIdentitiesTokenInfosRequest\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosRequest.GetIdentitiesTokenInfosRequestV0H\x00\x1aY\n GetIdentitiesTokenInfosRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x14\n\x0cidentity_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xca\x06\n\x1fGetIdentitiesTokenInfosResponse\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0H\x00\x1a\xaf\x05\n!GetIdentitiesTokenInfosResponseV0\x12\x8f\x01\n\x14identity_token_infos\x18\x01 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0.IdentityTokenInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a(\n\x16TokenIdentityInfoEntry\x12\x0e\n\x06\x66rozen\x18\x01 \x01(\x08\x1a\xb7\x01\n\x0eTokenInfoEntry\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x86\x01\n\x04info\x18\x02 \x01(\x0b\x32s.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0.TokenIdentityInfoEntryH\x00\x88\x01\x01\x42\x07\n\x05_info\x1a\x97\x01\n\x12IdentityTokenInfos\x12\x80\x01\n\x0btoken_infos\x18\x01 \x03(\x0b\x32k.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0.TokenInfoEntryB\x08\n\x06resultB\t\n\x07version\"\xbf\x01\n\x17GetTokenStatusesRequest\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetTokenStatusesRequest.GetTokenStatusesRequestV0H\x00\x1a=\n\x19GetTokenStatusesRequestV0\x12\x11\n\ttoken_ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xe7\x04\n\x18GetTokenStatusesResponse\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetTokenStatusesResponse.GetTokenStatusesResponseV0H\x00\x1a\xe1\x03\n\x1aGetTokenStatusesResponseV0\x12v\n\x0etoken_statuses\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetTokenStatusesResponse.GetTokenStatusesResponseV0.TokenStatusesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x44\n\x10TokenStatusEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x13\n\x06paused\x18\x02 \x01(\x08H\x00\x88\x01\x01\x42\t\n\x07_paused\x1a\x88\x01\n\rTokenStatuses\x12w\n\x0etoken_statuses\x18\x01 \x03(\x0b\x32_.org.dash.platform.dapi.v0.GetTokenStatusesResponse.GetTokenStatusesResponseV0.TokenStatusEntryB\x08\n\x06resultB\t\n\x07version\"\xef\x01\n#GetTokenDirectPurchasePricesRequest\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesRequest.GetTokenDirectPurchasePricesRequestV0H\x00\x1aI\n%GetTokenDirectPurchasePricesRequestV0\x12\x11\n\ttoken_ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x8b\t\n$GetTokenDirectPurchasePricesResponse\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0H\x00\x1a\xe1\x07\n&GetTokenDirectPurchasePricesResponseV0\x12\xa9\x01\n\x1ctoken_direct_purchase_prices\x18\x01 \x01(\x0b\x32\x80\x01.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.TokenDirectPurchasePricesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x33\n\x10PriceForQuantity\x12\x10\n\x08quantity\x18\x01 \x01(\x04\x12\r\n\x05price\x18\x02 \x01(\x04\x1a\xa7\x01\n\x0fPricingSchedule\x12\x93\x01\n\x12price_for_quantity\x18\x01 \x03(\x0b\x32w.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.PriceForQuantity\x1a\xe4\x01\n\x1dTokenDirectPurchasePriceEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x15\n\x0b\x66ixed_price\x18\x02 \x01(\x04H\x00\x12\x90\x01\n\x0evariable_price\x18\x03 \x01(\x0b\x32v.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.PricingScheduleH\x00\x42\x07\n\x05price\x1a\xc8\x01\n\x19TokenDirectPurchasePrices\x12\xaa\x01\n\x1btoken_direct_purchase_price\x18\x01 \x03(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.TokenDirectPurchasePriceEntryB\x08\n\x06resultB\t\n\x07version\"\xce\x01\n\x1bGetTokenContractInfoRequest\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetTokenContractInfoRequest.GetTokenContractInfoRequestV0H\x00\x1a@\n\x1dGetTokenContractInfoRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xfb\x03\n\x1cGetTokenContractInfoResponse\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetTokenContractInfoResponse.GetTokenContractInfoResponseV0H\x00\x1a\xe9\x02\n\x1eGetTokenContractInfoResponseV0\x12|\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32l.org.dash.platform.dapi.v0.GetTokenContractInfoResponse.GetTokenContractInfoResponseV0.TokenContractInfoDataH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aM\n\x15TokenContractInfoData\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17token_contract_position\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xef\x04\n)GetTokenPreProgrammedDistributionsRequest\x12~\n\x02v0\x18\x01 \x01(\x0b\x32p.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsRequest.GetTokenPreProgrammedDistributionsRequestV0H\x00\x1a\xb6\x03\n+GetTokenPreProgrammedDistributionsRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x98\x01\n\rstart_at_info\x18\x02 \x01(\x0b\x32|.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsRequest.GetTokenPreProgrammedDistributionsRequestV0.StartAtInfoH\x00\x88\x01\x01\x12\x12\n\x05limit\x18\x03 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x04 \x01(\x08\x1a\x9a\x01\n\x0bStartAtInfo\x12\x15\n\rstart_time_ms\x18\x01 \x01(\x04\x12\x1c\n\x0fstart_recipient\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x12%\n\x18start_recipient_included\x18\x03 \x01(\x08H\x01\x88\x01\x01\x42\x12\n\x10_start_recipientB\x1b\n\x19_start_recipient_includedB\x10\n\x0e_start_at_infoB\x08\n\x06_limitB\t\n\x07version\"\xec\x07\n*GetTokenPreProgrammedDistributionsResponse\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0H\x00\x1a\xaf\x06\n,GetTokenPreProgrammedDistributionsResponseV0\x12\xa5\x01\n\x13token_distributions\x18\x01 \x01(\x0b\x32\x85\x01.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0.TokenDistributionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a>\n\x16TokenDistributionEntry\x12\x14\n\x0crecipient_id\x18\x01 \x01(\x0c\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x04\x1a\xd4\x01\n\x1bTokenTimedDistributionEntry\x12\x11\n\ttimestamp\x18\x01 \x01(\x04\x12\xa1\x01\n\rdistributions\x18\x02 \x03(\x0b\x32\x89\x01.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0.TokenDistributionEntry\x1a\xc3\x01\n\x12TokenDistributions\x12\xac\x01\n\x13token_distributions\x18\x01 \x03(\x0b\x32\x8e\x01.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0.TokenTimedDistributionEntryB\x08\n\x06resultB\t\n\x07version\"\x82\x04\n-GetTokenPerpetualDistributionLastClaimRequest\x12\x86\x01\n\x02v0\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimRequest.GetTokenPerpetualDistributionLastClaimRequestV0H\x00\x1aI\n\x11\x43ontractTokenInfo\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17token_contract_position\x18\x02 \x01(\r\x1a\xf1\x01\n/GetTokenPerpetualDistributionLastClaimRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12v\n\rcontract_info\x18\x02 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimRequest.ContractTokenInfoH\x00\x88\x01\x01\x12\x13\n\x0bidentity_id\x18\x04 \x01(\x0c\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x10\n\x0e_contract_infoB\t\n\x07version\"\x93\x05\n.GetTokenPerpetualDistributionLastClaimResponse\x12\x88\x01\n\x02v0\x18\x01 \x01(\x0b\x32z.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimResponse.GetTokenPerpetualDistributionLastClaimResponseV0H\x00\x1a\xca\x03\n0GetTokenPerpetualDistributionLastClaimResponseV0\x12\x9f\x01\n\nlast_claim\x18\x01 \x01(\x0b\x32\x88\x01.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimResponse.GetTokenPerpetualDistributionLastClaimResponseV0.LastClaimInfoH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1ax\n\rLastClaimInfo\x12\x1a\n\x0ctimestamp_ms\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x1a\n\x0c\x62lock_height\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12\x0f\n\x05\x65poch\x18\x03 \x01(\rH\x00\x12\x13\n\traw_bytes\x18\x04 \x01(\x0cH\x00\x42\t\n\x07paid_atB\x08\n\x06resultB\t\n\x07version\"\xca\x01\n\x1aGetTokenTotalSupplyRequest\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetTokenTotalSupplyRequest.GetTokenTotalSupplyRequestV0H\x00\x1a?\n\x1cGetTokenTotalSupplyRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xaf\x04\n\x1bGetTokenTotalSupplyResponse\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetTokenTotalSupplyResponse.GetTokenTotalSupplyResponseV0H\x00\x1a\xa0\x03\n\x1dGetTokenTotalSupplyResponseV0\x12\x88\x01\n\x12token_total_supply\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetTokenTotalSupplyResponse.GetTokenTotalSupplyResponseV0.TokenTotalSupplyEntryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1ax\n\x15TokenTotalSupplyEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x30\n(total_aggregated_amount_in_user_accounts\x18\x02 \x01(\x04\x12\x1b\n\x13total_system_amount\x18\x03 \x01(\x04\x42\x08\n\x06resultB\t\n\x07version\"\xd2\x01\n\x13GetGroupInfoRequest\x12R\n\x02v0\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetGroupInfoRequest.GetGroupInfoRequestV0H\x00\x1a\\\n\x15GetGroupInfoRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17group_contract_position\x18\x02 \x01(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xd4\x05\n\x14GetGroupInfoResponse\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0H\x00\x1a\xda\x04\n\x16GetGroupInfoResponseV0\x12\x66\n\ngroup_info\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0.GroupInfoH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x04 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x34\n\x10GroupMemberEntry\x12\x11\n\tmember_id\x18\x01 \x01(\x0c\x12\r\n\x05power\x18\x02 \x01(\r\x1a\x98\x01\n\x0eGroupInfoEntry\x12h\n\x07members\x18\x01 \x03(\x0b\x32W.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0.GroupMemberEntry\x12\x1c\n\x14group_required_power\x18\x02 \x01(\r\x1a\x8a\x01\n\tGroupInfo\x12n\n\ngroup_info\x18\x01 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0.GroupInfoEntryH\x00\x88\x01\x01\x42\r\n\x0b_group_infoB\x08\n\x06resultB\t\n\x07version\"\xed\x03\n\x14GetGroupInfosRequest\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetGroupInfosRequest.GetGroupInfosRequestV0H\x00\x1au\n\x1cStartAtGroupContractPosition\x12%\n\x1dstart_group_contract_position\x18\x01 \x01(\r\x12.\n&start_group_contract_position_included\x18\x02 \x01(\x08\x1a\xfc\x01\n\x16GetGroupInfosRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12{\n start_at_group_contract_position\x18\x02 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetGroupInfosRequest.StartAtGroupContractPositionH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x03 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x04 \x01(\x08\x42#\n!_start_at_group_contract_positionB\x08\n\x06_countB\t\n\x07version\"\xff\x05\n\x15GetGroupInfosResponse\x12V\n\x02v0\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0H\x00\x1a\x82\x05\n\x17GetGroupInfosResponseV0\x12j\n\x0bgroup_infos\x18\x01 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0.GroupInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x04 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x34\n\x10GroupMemberEntry\x12\x11\n\tmember_id\x18\x01 \x01(\x0c\x12\r\n\x05power\x18\x02 \x01(\r\x1a\xc3\x01\n\x16GroupPositionInfoEntry\x12\x1f\n\x17group_contract_position\x18\x01 \x01(\r\x12j\n\x07members\x18\x02 \x03(\x0b\x32Y.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0.GroupMemberEntry\x12\x1c\n\x14group_required_power\x18\x03 \x01(\r\x1a\x82\x01\n\nGroupInfos\x12t\n\x0bgroup_infos\x18\x01 \x03(\x0b\x32_.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0.GroupPositionInfoEntryB\x08\n\x06resultB\t\n\x07version\"\xbe\x04\n\x16GetGroupActionsRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetGroupActionsRequest.GetGroupActionsRequestV0H\x00\x1aL\n\x0fStartAtActionId\x12\x17\n\x0fstart_action_id\x18\x01 \x01(\x0c\x12 \n\x18start_action_id_included\x18\x02 \x01(\x08\x1a\xc8\x02\n\x18GetGroupActionsRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17group_contract_position\x18\x02 \x01(\r\x12N\n\x06status\x18\x03 \x01(\x0e\x32>.org.dash.platform.dapi.v0.GetGroupActionsRequest.ActionStatus\x12\x62\n\x12start_at_action_id\x18\x04 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetGroupActionsRequest.StartAtActionIdH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x05 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x06 \x01(\x08\x42\x15\n\x13_start_at_action_idB\x08\n\x06_count\"&\n\x0c\x41\x63tionStatus\x12\n\n\x06\x41\x43TIVE\x10\x00\x12\n\n\x06\x43LOSED\x10\x01\x42\t\n\x07version\"\xd6\x1e\n\x17GetGroupActionsResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0H\x00\x1a\xd3\x1d\n\x19GetGroupActionsResponseV0\x12r\n\rgroup_actions\x18\x01 \x01(\x0b\x32Y.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a[\n\tMintEvent\x12\x0e\n\x06\x61mount\x18\x01 \x01(\x04\x12\x14\n\x0crecipient_id\x18\x02 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a[\n\tBurnEvent\x12\x0e\n\x06\x61mount\x18\x01 \x01(\x04\x12\x14\n\x0c\x62urn_from_id\x18\x02 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1aJ\n\x0b\x46reezeEvent\x12\x11\n\tfrozen_id\x18\x01 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1aL\n\rUnfreezeEvent\x12\x11\n\tfrozen_id\x18\x01 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\x66\n\x17\x44\x65stroyFrozenFundsEvent\x12\x11\n\tfrozen_id\x18\x01 \x01(\x0c\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x04\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\x64\n\x13SharedEncryptedNote\x12\x18\n\x10sender_key_index\x18\x01 \x01(\r\x12\x1b\n\x13recipient_key_index\x18\x02 \x01(\r\x12\x16\n\x0e\x65ncrypted_data\x18\x03 \x01(\x0c\x1a{\n\x15PersonalEncryptedNote\x12!\n\x19root_encryption_key_index\x18\x01 \x01(\r\x12\'\n\x1f\x64\x65rivation_encryption_key_index\x18\x02 \x01(\r\x12\x16\n\x0e\x65ncrypted_data\x18\x03 \x01(\x0c\x1a\xe9\x01\n\x14\x45mergencyActionEvent\x12\x81\x01\n\x0b\x61\x63tion_type\x18\x01 \x01(\x0e\x32l.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.EmergencyActionEvent.ActionType\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\"#\n\nActionType\x12\t\n\x05PAUSE\x10\x00\x12\n\n\x06RESUME\x10\x01\x42\x0e\n\x0c_public_note\x1a\x64\n\x16TokenConfigUpdateEvent\x12 \n\x18token_config_update_item\x18\x01 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\xe6\x03\n\x1eUpdateDirectPurchasePriceEvent\x12\x15\n\x0b\x66ixed_price\x18\x01 \x01(\x04H\x00\x12\x95\x01\n\x0evariable_price\x18\x02 \x01(\x0b\x32{.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.PricingScheduleH\x00\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x01\x88\x01\x01\x1a\x33\n\x10PriceForQuantity\x12\x10\n\x08quantity\x18\x01 \x01(\x04\x12\r\n\x05price\x18\x02 \x01(\x04\x1a\xac\x01\n\x0fPricingSchedule\x12\x98\x01\n\x12price_for_quantity\x18\x01 \x03(\x0b\x32|.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.PriceForQuantityB\x07\n\x05priceB\x0e\n\x0c_public_note\x1a\xfc\x02\n\x10GroupActionEvent\x12n\n\x0btoken_event\x18\x01 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEventH\x00\x12t\n\x0e\x64ocument_event\x18\x02 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.DocumentEventH\x00\x12t\n\x0e\x63ontract_event\x18\x03 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ContractEventH\x00\x42\x0c\n\nevent_type\x1a\x8b\x01\n\rDocumentEvent\x12r\n\x06\x63reate\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.DocumentCreateEventH\x00\x42\x06\n\x04type\x1a/\n\x13\x44ocumentCreateEvent\x12\x18\n\x10\x63reated_document\x18\x01 \x01(\x0c\x1a/\n\x13\x43ontractUpdateEvent\x12\x18\n\x10updated_contract\x18\x01 \x01(\x0c\x1a\x8b\x01\n\rContractEvent\x12r\n\x06update\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ContractUpdateEventH\x00\x42\x06\n\x04type\x1a\xd1\x07\n\nTokenEvent\x12\x66\n\x04mint\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintEventH\x00\x12\x66\n\x04\x62urn\x18\x02 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnEventH\x00\x12j\n\x06\x66reeze\x18\x03 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.FreezeEventH\x00\x12n\n\x08unfreeze\x18\x04 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UnfreezeEventH\x00\x12\x84\x01\n\x14\x64\x65stroy_frozen_funds\x18\x05 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.DestroyFrozenFundsEventH\x00\x12}\n\x10\x65mergency_action\x18\x06 \x01(\x0b\x32\x61.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.EmergencyActionEventH\x00\x12\x82\x01\n\x13token_config_update\x18\x07 \x01(\x0b\x32\x63.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenConfigUpdateEventH\x00\x12\x83\x01\n\x0cupdate_price\x18\x08 \x01(\x0b\x32k.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEventH\x00\x42\x06\n\x04type\x1a\x93\x01\n\x10GroupActionEntry\x12\x11\n\taction_id\x18\x01 \x01(\x0c\x12l\n\x05\x65vent\x18\x02 \x01(\x0b\x32].org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionEvent\x1a\x84\x01\n\x0cGroupActions\x12t\n\rgroup_actions\x18\x01 \x03(\x0b\x32].org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionEntryB\x08\n\x06resultB\t\n\x07version\"\x88\x03\n\x1cGetGroupActionSignersRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetGroupActionSignersRequest.GetGroupActionSignersRequestV0H\x00\x1a\xce\x01\n\x1eGetGroupActionSignersRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17group_contract_position\x18\x02 \x01(\r\x12T\n\x06status\x18\x03 \x01(\x0e\x32\x44.org.dash.platform.dapi.v0.GetGroupActionSignersRequest.ActionStatus\x12\x11\n\taction_id\x18\x04 \x01(\x0c\x12\r\n\x05prove\x18\x05 \x01(\x08\"&\n\x0c\x41\x63tionStatus\x12\n\n\x06\x41\x43TIVE\x10\x00\x12\n\n\x06\x43LOSED\x10\x01\x42\t\n\x07version\"\x8b\x05\n\x1dGetGroupActionSignersResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetGroupActionSignersResponse.GetGroupActionSignersResponseV0H\x00\x1a\xf6\x03\n\x1fGetGroupActionSignersResponseV0\x12\x8b\x01\n\x14group_action_signers\x18\x01 \x01(\x0b\x32k.org.dash.platform.dapi.v0.GetGroupActionSignersResponse.GetGroupActionSignersResponseV0.GroupActionSignersH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x35\n\x11GroupActionSigner\x12\x11\n\tsigner_id\x18\x01 \x01(\x0c\x12\r\n\x05power\x18\x02 \x01(\r\x1a\x91\x01\n\x12GroupActionSigners\x12{\n\x07signers\x18\x01 \x03(\x0b\x32j.org.dash.platform.dapi.v0.GetGroupActionSignersResponse.GetGroupActionSignersResponseV0.GroupActionSignerB\x08\n\x06resultB\t\n\x07version\"\xb5\x01\n\x15GetAddressInfoRequest\x12V\n\x02v0\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetAddressInfoRequest.GetAddressInfoRequestV0H\x00\x1a\x39\n\x17GetAddressInfoRequestV0\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x85\x01\n\x10\x41\x64\x64ressInfoEntry\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12J\n\x11\x62\x61lance_and_nonce\x18\x02 \x01(\x0b\x32*.org.dash.platform.dapi.v0.BalanceAndNonceH\x00\x88\x01\x01\x42\x14\n\x12_balance_and_nonce\"1\n\x0f\x42\x61lanceAndNonce\x12\x0f\n\x07\x62\x61lance\x18\x01 \x01(\x04\x12\r\n\x05nonce\x18\x02 \x01(\r\"_\n\x12\x41\x64\x64ressInfoEntries\x12I\n\x14\x61\x64\x64ress_info_entries\x18\x01 \x03(\x0b\x32+.org.dash.platform.dapi.v0.AddressInfoEntry\"m\n\x14\x41\x64\x64ressBalanceChange\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12\x19\n\x0bset_balance\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12\x1c\n\x0e\x61\x64\x64_to_balance\x18\x03 \x01(\x04\x42\x02\x30\x01H\x00\x42\x0b\n\toperation\"x\n\x1a\x42lockAddressBalanceChanges\x12\x18\n\x0c\x62lock_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12@\n\x07\x63hanges\x18\x02 \x03(\x0b\x32/.org.dash.platform.dapi.v0.AddressBalanceChange\"k\n\x1b\x41\x64\x64ressBalanceUpdateEntries\x12L\n\rblock_changes\x18\x01 \x03(\x0b\x32\x35.org.dash.platform.dapi.v0.BlockAddressBalanceChanges\"\xe1\x02\n\x16GetAddressInfoResponse\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetAddressInfoResponse.GetAddressInfoResponseV0H\x00\x1a\xe1\x01\n\x18GetAddressInfoResponseV0\x12I\n\x12\x61\x64\x64ress_info_entry\x18\x01 \x01(\x0b\x32+.org.dash.platform.dapi.v0.AddressInfoEntryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc3\x01\n\x18GetAddressesInfosRequest\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetAddressesInfosRequest.GetAddressesInfosRequestV0H\x00\x1a>\n\x1aGetAddressesInfosRequestV0\x12\x11\n\taddresses\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf1\x02\n\x19GetAddressesInfosResponse\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetAddressesInfosResponse.GetAddressesInfosResponseV0H\x00\x1a\xe8\x01\n\x1bGetAddressesInfosResponseV0\x12M\n\x14\x61\x64\x64ress_info_entries\x18\x01 \x01(\x0b\x32-.org.dash.platform.dapi.v0.AddressInfoEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xb5\x01\n\x1dGetAddressesTrunkStateRequest\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetAddressesTrunkStateRequest.GetAddressesTrunkStateRequestV0H\x00\x1a!\n\x1fGetAddressesTrunkStateRequestV0B\t\n\x07version\"\xaa\x02\n\x1eGetAddressesTrunkStateResponse\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetAddressesTrunkStateResponse.GetAddressesTrunkStateResponseV0H\x00\x1a\x92\x01\n GetAddressesTrunkStateResponseV0\x12/\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.Proof\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\t\n\x07version\"\xf0\x01\n\x1eGetAddressesBranchStateRequest\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetAddressesBranchStateRequest.GetAddressesBranchStateRequestV0H\x00\x1aY\n GetAddressesBranchStateRequestV0\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\r\n\x05\x64\x65pth\x18\x02 \x01(\r\x12\x19\n\x11\x63heckpoint_height\x18\x03 \x01(\x04\x42\t\n\x07version\"\xd1\x01\n\x1fGetAddressesBranchStateResponse\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetAddressesBranchStateResponse.GetAddressesBranchStateResponseV0H\x00\x1a\x37\n!GetAddressesBranchStateResponseV0\x12\x12\n\nmerk_proof\x18\x02 \x01(\x0c\x42\t\n\x07version\"\x9e\x02\n%GetRecentAddressBalanceChangesRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesRequest.GetRecentAddressBalanceChangesRequestV0H\x00\x1ar\n\'GetRecentAddressBalanceChangesRequestV0\x12\x18\n\x0cstart_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x02 \x01(\x08\x12\x1e\n\x16start_height_exclusive\x18\x03 \x01(\x08\x42\t\n\x07version\"\xb8\x03\n&GetRecentAddressBalanceChangesResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesResponse.GetRecentAddressBalanceChangesResponseV0H\x00\x1a\x88\x02\n(GetRecentAddressBalanceChangesResponseV0\x12`\n\x1e\x61\x64\x64ress_balance_update_entries\x18\x01 \x01(\x0b\x32\x36.org.dash.platform.dapi.v0.AddressBalanceUpdateEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"G\n\x16\x42lockHeightCreditEntry\x12\x18\n\x0c\x62lock_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x13\n\x07\x63redits\x18\x02 \x01(\x04\x42\x02\x30\x01\"\xb0\x01\n\x1d\x43ompactedAddressBalanceChange\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12\x19\n\x0bset_credits\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12V\n\x19\x61\x64\x64_to_credits_operations\x18\x03 \x01(\x0b\x32\x31.org.dash.platform.dapi.v0.AddToCreditsOperationsH\x00\x42\x0b\n\toperation\"\\\n\x16\x41\x64\x64ToCreditsOperations\x12\x42\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x31.org.dash.platform.dapi.v0.BlockHeightCreditEntry\"\xae\x01\n#CompactedBlockAddressBalanceChanges\x12\x1e\n\x12start_block_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1c\n\x10\x65nd_block_height\x18\x02 \x01(\x04\x42\x02\x30\x01\x12I\n\x07\x63hanges\x18\x03 \x03(\x0b\x32\x38.org.dash.platform.dapi.v0.CompactedAddressBalanceChange\"\x87\x01\n$CompactedAddressBalanceUpdateEntries\x12_\n\x17\x63ompacted_block_changes\x18\x01 \x03(\x0b\x32>.org.dash.platform.dapi.v0.CompactedBlockAddressBalanceChanges\"\xa9\x02\n.GetRecentCompactedAddressBalanceChangesRequest\x12\x88\x01\n\x02v0\x18\x01 \x01(\x0b\x32z.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesRequest.GetRecentCompactedAddressBalanceChangesRequestV0H\x00\x1a\x61\n0GetRecentCompactedAddressBalanceChangesRequestV0\x12\x1e\n\x12start_block_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf0\x03\n/GetRecentCompactedAddressBalanceChangesResponse\x12\x8a\x01\n\x02v0\x18\x01 \x01(\x0b\x32|.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesResponse.GetRecentCompactedAddressBalanceChangesResponseV0H\x00\x1a\xa4\x02\n1GetRecentCompactedAddressBalanceChangesResponseV0\x12s\n(compacted_address_balance_update_entries\x18\x01 \x01(\x0b\x32?.org.dash.platform.dapi.v0.CompactedAddressBalanceUpdateEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xf4\x01\n GetShieldedEncryptedNotesRequest\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0H\x00\x1aW\n\"GetShieldedEncryptedNotesRequestV0\x12\x13\n\x0bstart_index\x18\x01 \x01(\x04\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xbc\x05\n!GetShieldedEncryptedNotesResponse\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse.GetShieldedEncryptedNotesResponseV0H\x00\x1a\x9b\x04\n#GetShieldedEncryptedNotesResponseV0\x12\x8a\x01\n\x0f\x65ncrypted_notes\x18\x01 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse.GetShieldedEncryptedNotesResponseV0.EncryptedNotesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aW\n\rEncryptedNote\x12\x11\n\tnullifier\x18\x01 \x01(\x0c\x12\x0b\n\x03\x63mx\x18\x02 \x01(\x0c\x12\x16\n\x0e\x65ncrypted_note\x18\x03 \x01(\x0c\x12\x0e\n\x06\x63v_net\x18\x04 \x01(\x0c\x1a\x91\x01\n\x0e\x45ncryptedNotes\x12\x7f\n\x07\x65ntries\x18\x01 \x03(\x0b\x32n.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse.GetShieldedEncryptedNotesResponseV0.EncryptedNoteB\x08\n\x06resultB\t\n\x07version\"\xb4\x01\n\x19GetShieldedAnchorsRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0H\x00\x1a,\n\x1bGetShieldedAnchorsRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xb1\x03\n\x1aGetShieldedAnchorsResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetShieldedAnchorsResponse.GetShieldedAnchorsResponseV0H\x00\x1a\xa5\x02\n\x1cGetShieldedAnchorsResponseV0\x12m\n\x07\x61nchors\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetShieldedAnchorsResponse.GetShieldedAnchorsResponseV0.AnchorsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1a\n\x07\x41nchors\x12\x0f\n\x07\x61nchors\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\xd8\x01\n\"GetMostRecentShieldedAnchorRequest\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0H\x00\x1a\x35\n$GetMostRecentShieldedAnchorRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xdc\x02\n#GetMostRecentShieldedAnchorResponse\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorResponse.GetMostRecentShieldedAnchorResponseV0H\x00\x1a\xb5\x01\n%GetMostRecentShieldedAnchorResponseV0\x12\x10\n\x06\x61nchor\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xbc\x01\n\x1bGetShieldedPoolStateRequest\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0H\x00\x1a.\n\x1dGetShieldedPoolStateRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xcb\x02\n\x1cGetShieldedPoolStateResponse\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetShieldedPoolStateResponse.GetShieldedPoolStateResponseV0H\x00\x1a\xb9\x01\n\x1eGetShieldedPoolStateResponseV0\x12\x1b\n\rtotal_balance\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc0\x01\n\x1cGetShieldedNotesCountRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0H\x00\x1a/\n\x1eGetShieldedNotesCountRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xd3\x02\n\x1dGetShieldedNotesCountResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetShieldedNotesCountResponse.GetShieldedNotesCountResponseV0H\x00\x1a\xbe\x01\n\x1fGetShieldedNotesCountResponseV0\x12\x1f\n\x11total_notes_count\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xd4\x01\n\x1cGetShieldedNullifiersRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0H\x00\x1a\x43\n\x1eGetShieldedNullifiersRequestV0\x12\x12\n\nnullifiers\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x86\x05\n\x1dGetShieldedNullifiersResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetShieldedNullifiersResponse.GetShieldedNullifiersResponseV0H\x00\x1a\xf1\x03\n\x1fGetShieldedNullifiersResponseV0\x12\x88\x01\n\x12nullifier_statuses\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetShieldedNullifiersResponse.GetShieldedNullifiersResponseV0.NullifierStatusesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x36\n\x0fNullifierStatus\x12\x11\n\tnullifier\x18\x01 \x01(\x0c\x12\x10\n\x08is_spent\x18\x02 \x01(\x08\x1a\x8e\x01\n\x11NullifierStatuses\x12y\n\x07\x65ntries\x18\x01 \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetShieldedNullifiersResponse.GetShieldedNullifiersResponseV0.NullifierStatusB\x08\n\x06resultB\t\n\x07version*Z\n\nKeyPurpose\x12\x12\n\x0e\x41UTHENTICATION\x10\x00\x12\x0e\n\nENCRYPTION\x10\x01\x12\x0e\n\nDECRYPTION\x10\x02\x12\x0c\n\x08TRANSFER\x10\x03\x12\n\n\x06VOTING\x10\x05*\xb9\x01\n\x16\x43ontractModerationList\x12(\n$CONTRACT_MODERATION_LIST_UNSPECIFIED\x10\x00\x12$\n CONTRACT_MODERATION_LIST_BANLIST\x10\x01\x12(\n$CONTRACT_MODERATION_LIST_SUSPENSIONS\x10\x02\x12%\n!CONTRACT_MODERATION_LIST_WARNINGS\x10\x03\x32\xd0P\n\x08Platform\x12\x93\x01\n\x18\x62roadcastStateTransition\x12:.org.dash.platform.dapi.v0.BroadcastStateTransitionRequest\x1a;.org.dash.platform.dapi.v0.BroadcastStateTransitionResponse\x12l\n\x0bgetIdentity\x12-.org.dash.platform.dapi.v0.GetIdentityRequest\x1a..org.dash.platform.dapi.v0.GetIdentityResponse\x12x\n\x0fgetIdentityKeys\x12\x31.org.dash.platform.dapi.v0.GetIdentityKeysRequest\x1a\x32.org.dash.platform.dapi.v0.GetIdentityKeysResponse\x12\x96\x01\n\x19getIdentitiesContractKeys\x12;.org.dash.platform.dapi.v0.GetIdentitiesContractKeysRequest\x1a<.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse\x12{\n\x10getIdentityNonce\x12\x32.org.dash.platform.dapi.v0.GetIdentityNonceRequest\x1a\x33.org.dash.platform.dapi.v0.GetIdentityNonceResponse\x12\x93\x01\n\x18getIdentityContractNonce\x12:.org.dash.platform.dapi.v0.GetIdentityContractNonceRequest\x1a;.org.dash.platform.dapi.v0.GetIdentityContractNonceResponse\x12\xa8\x01\n\x1fgetIdentityKeysRemainingBudgets\x12\x41.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsRequest\x1a\x42.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsResponse\x12\x81\x01\n\x12getIdentityBalance\x12\x34.org.dash.platform.dapi.v0.GetIdentityBalanceRequest\x1a\x35.org.dash.platform.dapi.v0.GetIdentityBalanceResponse\x12\x8a\x01\n\x15getIdentitiesBalances\x12\x37.org.dash.platform.dapi.v0.GetIdentitiesBalancesRequest\x1a\x38.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse\x12\xa2\x01\n\x1dgetIdentityBalanceAndRevision\x12?.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionRequest\x1a@.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionResponse\x12\xaf\x01\n#getEvonodesProposedEpochBlocksByIds\x12\x45.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByIdsRequest\x1a\x41.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse\x12\xb3\x01\n%getEvonodesProposedEpochBlocksByRange\x12G.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByRangeRequest\x1a\x41.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse\x12x\n\x0fgetDataContract\x12\x31.org.dash.platform.dapi.v0.GetDataContractRequest\x1a\x32.org.dash.platform.dapi.v0.GetDataContractResponse\x12\x8d\x01\n\x16getDataContractHistory\x12\x38.org.dash.platform.dapi.v0.GetDataContractHistoryRequest\x1a\x39.org.dash.platform.dapi.v0.GetDataContractHistoryResponse\x12\xa5\x01\n\x1egetDataContractsLatestVersions\x12@.org.dash.platform.dapi.v0.GetDataContractsLatestVersionsRequest\x1a\x41.org.dash.platform.dapi.v0.GetDataContractsLatestVersionsResponse\x12{\n\x10getDataContracts\x12\x32.org.dash.platform.dapi.v0.GetDataContractsRequest\x1a\x33.org.dash.platform.dapi.v0.GetDataContractsResponse\x12\x89\x01\n\x17getDataContractsByRange\x12\x39.org.dash.platform.dapi.v0.GetDataContractsByRangeRequest\x1a\x33.org.dash.platform.dapi.v0.GetDataContractsResponse\x12\x87\x01\n\x14getContractGroupInfo\x12\x36.org.dash.platform.dapi.v0.GetContractGroupInfoRequest\x1a\x37.org.dash.platform.dapi.v0.GetContractGroupInfoResponse\x12\x90\x01\n\x17getContractGroupMembers\x12\x39.org.dash.platform.dapi.v0.GetContractGroupMembersRequest\x1a:.org.dash.platform.dapi.v0.GetContractGroupMembersResponse\x12\x9f\x01\n\x1cgetContractGroupsForContract\x12>.org.dash.platform.dapi.v0.GetContractGroupsForContractRequest\x1a?.org.dash.platform.dapi.v0.GetContractGroupsForContractResponse\x12\x9c\x01\n\x1bgetContractModerationStatus\x12=.org.dash.platform.dapi.v0.GetContractModerationStatusRequest\x1a>.org.dash.platform.dapi.v0.GetContractModerationStatusResponse\x12\x9f\x01\n\x1cgetContractModerationEntries\x12>.org.dash.platform.dapi.v0.GetContractModerationEntriesRequest\x1a?.org.dash.platform.dapi.v0.GetContractModerationEntriesResponse\x12\x9c\x01\n\x1bgetContractDocumentRemovals\x12=.org.dash.platform.dapi.v0.GetContractDocumentRemovalsRequest\x1a>.org.dash.platform.dapi.v0.GetContractDocumentRemovalsResponse\x12\x81\x01\n\x12getContractFeePots\x12\x34.org.dash.platform.dapi.v0.GetContractFeePotsRequest\x1a\x35.org.dash.platform.dapi.v0.GetContractFeePotsResponse\x12\x81\x01\n\x12getDocumentHistory\x12\x34.org.dash.platform.dapi.v0.GetDocumentHistoryRequest\x1a\x35.org.dash.platform.dapi.v0.GetDocumentHistoryResponse\x12o\n\x0cgetDocuments\x12..org.dash.platform.dapi.v0.GetDocumentsRequest\x1a/.org.dash.platform.dapi.v0.GetDocumentsResponse\x12\x99\x01\n\x1agetIdentityByPublicKeyHash\x12<.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashRequest\x1a=.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashResponse\x12\xb4\x01\n#getIdentityByNonUniquePublicKeyHash\x12\x45.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashRequest\x1a\x46.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse\x12\x9f\x01\n\x1cwaitForStateTransitionResult\x12>.org.dash.platform.dapi.v0.WaitForStateTransitionResultRequest\x1a?.org.dash.platform.dapi.v0.WaitForStateTransitionResultResponse\x12\x81\x01\n\x12getConsensusParams\x12\x34.org.dash.platform.dapi.v0.GetConsensusParamsRequest\x1a\x35.org.dash.platform.dapi.v0.GetConsensusParamsResponse\x12\xa5\x01\n\x1egetProtocolVersionUpgradeState\x12@.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateRequest\x1a\x41.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse\x12\xb4\x01\n#getProtocolVersionUpgradeVoteStatus\x12\x45.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusRequest\x1a\x46.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse\x12r\n\rgetEpochsInfo\x12/.org.dash.platform.dapi.v0.GetEpochsInfoRequest\x1a\x30.org.dash.platform.dapi.v0.GetEpochsInfoResponse\x12\x8d\x01\n\x16getFinalizedEpochInfos\x12\x38.org.dash.platform.dapi.v0.GetFinalizedEpochInfosRequest\x1a\x39.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse\x12\x8a\x01\n\x15getContestedResources\x12\x37.org.dash.platform.dapi.v0.GetContestedResourcesRequest\x1a\x38.org.dash.platform.dapi.v0.GetContestedResourcesResponse\x12\xa2\x01\n\x1dgetContestedResourceVoteState\x12?.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest\x1a@.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse\x12\xba\x01\n%getContestedResourceVotersForIdentity\x12G.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityRequest\x1aH.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityResponse\x12\xae\x01\n!getContestedResourceIdentityVotes\x12\x43.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesRequest\x1a\x44.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse\x12\x8a\x01\n\x15getVotePollsByEndDate\x12\x37.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest\x1a\x38.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse\x12\xa5\x01\n\x1egetPrefundedSpecializedBalance\x12@.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceRequest\x1a\x41.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceResponse\x12\x96\x01\n\x19getTotalCreditsInPlatform\x12;.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformRequest\x1a<.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformResponse\x12x\n\x0fgetPathElements\x12\x31.org.dash.platform.dapi.v0.GetPathElementsRequest\x1a\x32.org.dash.platform.dapi.v0.GetPathElementsResponse\x12\x66\n\tgetStatus\x12+.org.dash.platform.dapi.v0.GetStatusRequest\x1a,.org.dash.platform.dapi.v0.GetStatusResponse\x12\x8a\x01\n\x15getCurrentQuorumsInfo\x12\x37.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoRequest\x1a\x38.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse\x12\x93\x01\n\x18getIdentityTokenBalances\x12:.org.dash.platform.dapi.v0.GetIdentityTokenBalancesRequest\x1a;.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse\x12\x99\x01\n\x1agetIdentitiesTokenBalances\x12<.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesRequest\x1a=.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse\x12\x8a\x01\n\x15getIdentityTokenInfos\x12\x37.org.dash.platform.dapi.v0.GetIdentityTokenInfosRequest\x1a\x38.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse\x12\x90\x01\n\x17getIdentitiesTokenInfos\x12\x39.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosRequest\x1a:.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse\x12{\n\x10getTokenStatuses\x12\x32.org.dash.platform.dapi.v0.GetTokenStatusesRequest\x1a\x33.org.dash.platform.dapi.v0.GetTokenStatusesResponse\x12\x9f\x01\n\x1cgetTokenDirectPurchasePrices\x12>.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesRequest\x1a?.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse\x12\x87\x01\n\x14getTokenContractInfo\x12\x36.org.dash.platform.dapi.v0.GetTokenContractInfoRequest\x1a\x37.org.dash.platform.dapi.v0.GetTokenContractInfoResponse\x12\xb1\x01\n\"getTokenPreProgrammedDistributions\x12\x44.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsRequest\x1a\x45.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse\x12\xbd\x01\n&getTokenPerpetualDistributionLastClaim\x12H.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimRequest\x1aI.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimResponse\x12\x84\x01\n\x13getTokenTotalSupply\x12\x35.org.dash.platform.dapi.v0.GetTokenTotalSupplyRequest\x1a\x36.org.dash.platform.dapi.v0.GetTokenTotalSupplyResponse\x12o\n\x0cgetGroupInfo\x12..org.dash.platform.dapi.v0.GetGroupInfoRequest\x1a/.org.dash.platform.dapi.v0.GetGroupInfoResponse\x12r\n\rgetGroupInfos\x12/.org.dash.platform.dapi.v0.GetGroupInfosRequest\x1a\x30.org.dash.platform.dapi.v0.GetGroupInfosResponse\x12x\n\x0fgetGroupActions\x12\x31.org.dash.platform.dapi.v0.GetGroupActionsRequest\x1a\x32.org.dash.platform.dapi.v0.GetGroupActionsResponse\x12\x8a\x01\n\x15getGroupActionSigners\x12\x37.org.dash.platform.dapi.v0.GetGroupActionSignersRequest\x1a\x38.org.dash.platform.dapi.v0.GetGroupActionSignersResponse\x12u\n\x0egetAddressInfo\x12\x30.org.dash.platform.dapi.v0.GetAddressInfoRequest\x1a\x31.org.dash.platform.dapi.v0.GetAddressInfoResponse\x12~\n\x11getAddressesInfos\x12\x33.org.dash.platform.dapi.v0.GetAddressesInfosRequest\x1a\x34.org.dash.platform.dapi.v0.GetAddressesInfosResponse\x12\x8d\x01\n\x16getAddressesTrunkState\x12\x38.org.dash.platform.dapi.v0.GetAddressesTrunkStateRequest\x1a\x39.org.dash.platform.dapi.v0.GetAddressesTrunkStateResponse\x12\x90\x01\n\x17getAddressesBranchState\x12\x39.org.dash.platform.dapi.v0.GetAddressesBranchStateRequest\x1a:.org.dash.platform.dapi.v0.GetAddressesBranchStateResponse\x12\xa5\x01\n\x1egetRecentAddressBalanceChanges\x12@.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesRequest\x1a\x41.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesResponse\x12\xc0\x01\n\'getRecentCompactedAddressBalanceChanges\x12I.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesRequest\x1aJ.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesResponse\x12\x96\x01\n\x19getShieldedEncryptedNotes\x12;.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest\x1a<.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse\x12\x81\x01\n\x12getShieldedAnchors\x12\x34.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest\x1a\x35.org.dash.platform.dapi.v0.GetShieldedAnchorsResponse\x12\x9c\x01\n\x1bgetMostRecentShieldedAnchor\x12=.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest\x1a>.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorResponse\x12\x87\x01\n\x14getShieldedPoolState\x12\x36.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest\x1a\x37.org.dash.platform.dapi.v0.GetShieldedPoolStateResponse\x12\x8a\x01\n\x15getShieldedNotesCount\x12\x37.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest\x1a\x38.org.dash.platform.dapi.v0.GetShieldedNotesCountResponse\x12\x8a\x01\n\x15getShieldedNullifiers\x12\x37.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest\x1a\x38.org.dash.platform.dapi.v0.GetShieldedNullifiersResponseb\x06proto3' + serialized_pb=b'\n\x0eplatform.proto\x12\x19org.dash.platform.dapi.v0\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x81\x01\n\x05Proof\x12\x15\n\rgrovedb_proof\x18\x01 \x01(\x0c\x12\x13\n\x0bquorum_hash\x18\x02 \x01(\x0c\x12\x11\n\tsignature\x18\x03 \x01(\x0c\x12\r\n\x05round\x18\x04 \x01(\r\x12\x15\n\rblock_id_hash\x18\x05 \x01(\x0c\x12\x13\n\x0bquorum_type\x18\x06 \x01(\r\"\x98\x01\n\x10ResponseMetadata\x12\x12\n\x06height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12 \n\x18\x63ore_chain_locked_height\x18\x02 \x01(\r\x12\r\n\x05\x65poch\x18\x03 \x01(\r\x12\x13\n\x07time_ms\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x18\n\x10protocol_version\x18\x05 \x01(\r\x12\x10\n\x08\x63hain_id\x18\x06 \x01(\t\"L\n\x1dStateTransitionBroadcastError\x12\x0c\n\x04\x63ode\x18\x01 \x01(\r\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\";\n\x1f\x42roadcastStateTransitionRequest\x12\x18\n\x10state_transition\x18\x01 \x01(\x0c\"\"\n BroadcastStateTransitionResponse\"\xa4\x01\n\x12GetIdentityRequest\x12P\n\x02v0\x18\x01 \x01(\x0b\x32\x42.org.dash.platform.dapi.v0.GetIdentityRequest.GetIdentityRequestV0H\x00\x1a\x31\n\x14GetIdentityRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xc1\x01\n\x17GetIdentityNonceRequest\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetIdentityNonceRequest.GetIdentityNonceRequestV0H\x00\x1a?\n\x19GetIdentityNonceRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf6\x01\n\x1fGetIdentityContractNonceRequest\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetIdentityContractNonceRequest.GetIdentityContractNonceRequestV0H\x00\x1a\\\n!GetIdentityContractNonceRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x13\n\x0b\x63ontract_id\x18\x02 \x01(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xc0\x01\n\x19GetIdentityBalanceRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetIdentityBalanceRequest.GetIdentityBalanceRequestV0H\x00\x1a\x38\n\x1bGetIdentityBalanceRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xec\x01\n$GetIdentityBalanceAndRevisionRequest\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionRequest.GetIdentityBalanceAndRevisionRequestV0H\x00\x1a\x43\n&GetIdentityBalanceAndRevisionRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9e\x02\n\x13GetIdentityResponse\x12R\n\x02v0\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetIdentityResponse.GetIdentityResponseV0H\x00\x1a\xa7\x01\n\x15GetIdentityResponseV0\x12\x12\n\x08identity\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xbc\x02\n\x18GetIdentityNonceResponse\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetIdentityNonceResponse.GetIdentityNonceResponseV0H\x00\x1a\xb6\x01\n\x1aGetIdentityNonceResponseV0\x12\x1c\n\x0eidentity_nonce\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xe5\x02\n GetIdentityContractNonceResponse\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetIdentityContractNonceResponse.GetIdentityContractNonceResponseV0H\x00\x1a\xc7\x01\n\"GetIdentityContractNonceResponseV0\x12%\n\x17identity_contract_nonce\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\x8e\x02\n&GetIdentityKeysRemainingBudgetsRequest\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsRequest.GetIdentityKeysRemainingBudgetsRequestV0H\x00\x1a_\n(GetIdentityKeysRemainingBudgetsRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x0f\n\x07key_ids\x18\x02 \x03(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\x96\x06\n\'GetIdentityKeysRemainingBudgetsResponse\x12z\n\x02v0\x18\x01 \x01(\x0b\x32l.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsResponse.GetIdentityKeysRemainingBudgetsResponseV0H\x00\x1a\xe3\x04\n)GetIdentityKeysRemainingBudgetsResponseV0\x12\xa4\x01\n\x16keys_remaining_budgets\x18\x01 \x01(\x0b\x32\x81\x01.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsResponse.GetIdentityKeysRemainingBudgetsResponseV0.KeysRemainingBudgetsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x61\n\x17KeyRemainingBudgetEntry\x12\x0e\n\x06key_id\x18\x01 \x01(\r\x12!\n\x10remaining_budget\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x42\x13\n\x11_remaining_budget\x1a\xaf\x01\n\x14KeysRemainingBudgets\x12\x96\x01\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsResponse.GetIdentityKeysRemainingBudgetsResponseV0.KeyRemainingBudgetEntryB\x08\n\x06resultB\t\n\x07version\"\xbd\x02\n\x1aGetIdentityBalanceResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetIdentityBalanceResponse.GetIdentityBalanceResponseV0H\x00\x1a\xb1\x01\n\x1cGetIdentityBalanceResponseV0\x12\x15\n\x07\x62\x61lance\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xb1\x04\n%GetIdentityBalanceAndRevisionResponse\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionResponse.GetIdentityBalanceAndRevisionResponseV0H\x00\x1a\x84\x03\n\'GetIdentityBalanceAndRevisionResponseV0\x12\x9b\x01\n\x14\x62\x61lance_and_revision\x18\x01 \x01(\x0b\x32{.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionResponse.GetIdentityBalanceAndRevisionResponseV0.BalanceAndRevisionH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a?\n\x12\x42\x61lanceAndRevision\x12\x13\n\x07\x62\x61lance\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x14\n\x08revision\x18\x02 \x01(\x04\x42\x02\x30\x01\x42\x08\n\x06resultB\t\n\x07version\"\xd1\x01\n\x0eKeyRequestType\x12\x36\n\x08\x61ll_keys\x18\x01 \x01(\x0b\x32\".org.dash.platform.dapi.v0.AllKeysH\x00\x12@\n\rspecific_keys\x18\x02 \x01(\x0b\x32\'.org.dash.platform.dapi.v0.SpecificKeysH\x00\x12:\n\nsearch_key\x18\x03 \x01(\x0b\x32$.org.dash.platform.dapi.v0.SearchKeyH\x00\x42\t\n\x07request\"\t\n\x07\x41llKeys\"\x1f\n\x0cSpecificKeys\x12\x0f\n\x07key_ids\x18\x01 \x03(\r\"\xb6\x01\n\tSearchKey\x12I\n\x0bpurpose_map\x18\x01 \x03(\x0b\x32\x34.org.dash.platform.dapi.v0.SearchKey.PurposeMapEntry\x1a^\n\x0fPurposeMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12:\n\x05value\x18\x02 \x01(\x0b\x32+.org.dash.platform.dapi.v0.SecurityLevelMap:\x02\x38\x01\"\xbf\x02\n\x10SecurityLevelMap\x12]\n\x12security_level_map\x18\x01 \x03(\x0b\x32\x41.org.dash.platform.dapi.v0.SecurityLevelMap.SecurityLevelMapEntry\x1aw\n\x15SecurityLevelMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\r\x12M\n\x05value\x18\x02 \x01(\x0e\x32>.org.dash.platform.dapi.v0.SecurityLevelMap.KeyKindRequestType:\x02\x38\x01\"S\n\x12KeyKindRequestType\x12\x1f\n\x1b\x43URRENT_KEY_OF_KIND_REQUEST\x10\x00\x12\x1c\n\x18\x41LL_KEYS_OF_KIND_REQUEST\x10\x01\"\xda\x02\n\x16GetIdentityKeysRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetIdentityKeysRequest.GetIdentityKeysRequestV0H\x00\x1a\xda\x01\n\x18GetIdentityKeysRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12?\n\x0crequest_type\x18\x02 \x01(\x0b\x32).org.dash.platform.dapi.v0.KeyRequestType\x12+\n\x05limit\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\t\n\x07version\"\x99\x03\n\x17GetIdentityKeysResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetIdentityKeysResponse.GetIdentityKeysResponseV0H\x00\x1a\x96\x02\n\x19GetIdentityKeysResponseV0\x12\x61\n\x04keys\x18\x01 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetIdentityKeysResponse.GetIdentityKeysResponseV0.KeysH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1a\n\x04Keys\x12\x12\n\nkeys_bytes\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\xef\x02\n GetIdentitiesContractKeysRequest\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetIdentitiesContractKeysRequest.GetIdentitiesContractKeysRequestV0H\x00\x1a\xd1\x01\n\"GetIdentitiesContractKeysRequestV0\x12\x16\n\x0eidentities_ids\x18\x01 \x03(\x0c\x12\x13\n\x0b\x63ontract_id\x18\x02 \x01(\x0c\x12\x1f\n\x12\x64ocument_type_name\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x37\n\x08purposes\x18\x04 \x03(\x0e\x32%.org.dash.platform.dapi.v0.KeyPurpose\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x15\n\x13_document_type_nameB\t\n\x07version\"\xdf\x06\n!GetIdentitiesContractKeysResponse\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0H\x00\x1a\xbe\x05\n#GetIdentitiesContractKeysResponseV0\x12\x8a\x01\n\x0fidentities_keys\x18\x01 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0.IdentitiesKeysH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aY\n\x0bPurposeKeys\x12\x36\n\x07purpose\x18\x01 \x01(\x0e\x32%.org.dash.platform.dapi.v0.KeyPurpose\x12\x12\n\nkeys_bytes\x18\x02 \x03(\x0c\x1a\x9f\x01\n\x0cIdentityKeys\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12z\n\x04keys\x18\x02 \x03(\x0b\x32l.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0.PurposeKeys\x1a\x90\x01\n\x0eIdentitiesKeys\x12~\n\x07\x65ntries\x18\x01 \x03(\x0b\x32m.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse.GetIdentitiesContractKeysResponseV0.IdentityKeysB\x08\n\x06resultB\t\n\x07version\"\xa4\x02\n*GetEvonodesProposedEpochBlocksByIdsRequest\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByIdsRequest.GetEvonodesProposedEpochBlocksByIdsRequestV0H\x00\x1ah\n,GetEvonodesProposedEpochBlocksByIdsRequestV0\x12\x12\n\x05\x65poch\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x0b\n\x03ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\x08\n\x06_epochB\t\n\x07version\"\x92\x06\n&GetEvonodesProposedEpochBlocksResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse.GetEvonodesProposedEpochBlocksResponseV0H\x00\x1a\xe2\x04\n(GetEvonodesProposedEpochBlocksResponseV0\x12\xb1\x01\n#evonodes_proposed_block_counts_info\x18\x01 \x01(\x0b\x32\x81\x01.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse.GetEvonodesProposedEpochBlocksResponseV0.EvonodesProposedBlocksH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a?\n\x15\x45vonodeProposedBlocks\x12\x13\n\x0bpro_tx_hash\x18\x01 \x01(\x0c\x12\x11\n\x05\x63ount\x18\x02 \x01(\x04\x42\x02\x30\x01\x1a\xc4\x01\n\x16\x45vonodesProposedBlocks\x12\xa9\x01\n\x1e\x65vonodes_proposed_block_counts\x18\x01 \x03(\x0b\x32\x80\x01.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse.GetEvonodesProposedEpochBlocksResponseV0.EvonodeProposedBlocksB\x08\n\x06resultB\t\n\x07version\"\xf2\x02\n,GetEvonodesProposedEpochBlocksByRangeRequest\x12\x84\x01\n\x02v0\x18\x01 \x01(\x0b\x32v.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByRangeRequest.GetEvonodesProposedEpochBlocksByRangeRequestV0H\x00\x1a\xaf\x01\n.GetEvonodesProposedEpochBlocksByRangeRequestV0\x12\x12\n\x05\x65poch\x18\x01 \x01(\rH\x01\x88\x01\x01\x12\x12\n\x05limit\x18\x02 \x01(\rH\x02\x88\x01\x01\x12\x15\n\x0bstart_after\x18\x03 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x04 \x01(\x0cH\x00\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x07\n\x05startB\x08\n\x06_epochB\x08\n\x06_limitB\t\n\x07version\"\xcd\x01\n\x1cGetIdentitiesBalancesRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetIdentitiesBalancesRequest.GetIdentitiesBalancesRequestV0H\x00\x1a<\n\x1eGetIdentitiesBalancesRequestV0\x12\x0b\n\x03ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9f\x05\n\x1dGetIdentitiesBalancesResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse.GetIdentitiesBalancesResponseV0H\x00\x1a\x8a\x04\n\x1fGetIdentitiesBalancesResponseV0\x12\x8a\x01\n\x13identities_balances\x18\x01 \x01(\x0b\x32k.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse.GetIdentitiesBalancesResponseV0.IdentitiesBalancesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aL\n\x0fIdentityBalance\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x18\n\x07\x62\x61lance\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x42\n\n\x08_balance\x1a\x8f\x01\n\x12IdentitiesBalances\x12y\n\x07\x65ntries\x18\x01 \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse.GetIdentitiesBalancesResponseV0.IdentityBalanceB\x08\n\x06resultB\t\n\x07version\"\xb4\x01\n\x16GetDataContractRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetDataContractRequest.GetDataContractRequestV0H\x00\x1a\x35\n\x18GetDataContractRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xb3\x02\n\x17GetDataContractResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetDataContractResponse.GetDataContractResponseV0H\x00\x1a\xb0\x01\n\x19GetDataContractResponseV0\x12\x17\n\rdata_contract\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\x8c\x02\n%GetDataContractsLatestVersionsRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetDataContractsLatestVersionsRequest.GetDataContractsLatestVersionsRequestV0H\x00\x1a`\n\'GetDataContractsLatestVersionsRequestV0\x12\x0b\n\x03ids\x18\x01 \x03(\x0c\x12\x19\n\x11include_contracts\x18\x02 \x01(\x08\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xfa\x05\n&GetDataContractsLatestVersionsResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetDataContractsLatestVersionsResponse.GetDataContractsLatestVersionsResponseV0H\x00\x1a\x84\x01\n\x1e\x44\x61taContractLatestVersionEntry\x12\x12\n\nidentifier\x18\x01 \x01(\x0c\x12\x14\n\x07version\x18\x02 \x01(\rH\x00\x88\x01\x01\x12\x1a\n\rdata_contract\x18\x03 \x01(\x0cH\x01\x88\x01\x01\x42\n\n\x08_versionB\x10\n\x0e_data_contract\x1a\x90\x01\n\x1b\x44\x61taContractsLatestVersions\x12q\n\x07\x65ntries\x18\x01 \x03(\x0b\x32`.org.dash.platform.dapi.v0.GetDataContractsLatestVersionsResponse.DataContractLatestVersionEntry\x1a\xb0\x02\n(GetDataContractsLatestVersionsResponseV0\x12\x87\x01\n\x1e\x64\x61ta_contracts_latest_versions\x18\x01 \x01(\x0b\x32].org.dash.platform.dapi.v0.GetDataContractsLatestVersionsResponse.DataContractsLatestVersionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"R\n\x1f\x43ontractGroupDocumentTypeMember\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\"G\n\x18\x43ontractGroupTokenMember\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x16\n\x0etoken_position\x18\x02 \x01(\r\"\xd7\x01\n\x1bGetContractGroupInfoRequest\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetContractGroupInfoRequest.GetContractGroupInfoRequestV0H\x00\x1aI\n\x1dGetContractGroupInfoRequestV0\x12\x19\n\x11\x63ontract_group_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x98\x04\n\x1cGetContractGroupInfoResponse\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetContractGroupInfoResponse.GetContractGroupInfoResponseV0H\x00\x1a~\n\x11\x43ontractGroupInfo\x12\x10\n\x08owner_id\x18\x01 \x01(\x0c\x12\x11\n\tadmin_ids\x18\x02 \x03(\x0c\x12\x11\n\x04name\x18\x03 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0b\x64\x65scription\x18\x04 \x01(\tH\x01\x88\x01\x01\x42\x07\n\x05_nameB\x0e\n\x0c_description\x1a\x86\x02\n\x1eGetContractGroupInfoResponseV0\x12h\n\x13\x63ontract_group_info\x18\x01 \x01(\x0b\x32I.org.dash.platform.dapi.v0.GetContractGroupInfoResponse.ContractGroupInfoH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xf8\x06\n\x1eGetContractGroupMembersRequest\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetContractGroupMembersRequest.GetContractGroupMembersRequestV0H\x00\x1a@\n\x14\x43ontractMembersQuery\x12\x18\n\x0bstart_after\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x42\x0e\n\x0c_start_after\x1a\x80\x01\n\x18\x44ocumentTypeMembersQuery\x12T\n\x0bstart_after\x18\x01 \x01(\x0b\x32:.org.dash.platform.dapi.v0.ContractGroupDocumentTypeMemberH\x00\x88\x01\x01\x42\x0e\n\x0c_start_after\x1ar\n\x11TokenMembersQuery\x12M\n\x0bstart_after\x18\x01 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractGroupTokenMemberH\x00\x88\x01\x01\x42\x0e\n\x0c_start_after\x1a\xa7\x03\n GetContractGroupMembersRequestV0\x12\x19\n\x11\x63ontract_group_id\x18\x01 \x01(\x0c\x12\x63\n\tcontracts\x18\x02 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetContractGroupMembersRequest.ContractMembersQueryH\x00\x12l\n\x0e\x64ocument_types\x18\x03 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetContractGroupMembersRequest.DocumentTypeMembersQueryH\x00\x12]\n\x06tokens\x18\x04 \x01(\x0b\x32K.org.dash.platform.dapi.v0.GetContractGroupMembersRequest.TokenMembersQueryH\x00\x12\x12\n\x05limit\x18\x05 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x06 \x01(\x08\x42\t\n\x07membersB\x08\n\x06_limitB\t\n\x07version\"\xc9\x06\n\x1fGetContractGroupMembersResponse\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetContractGroupMembersResponse.GetContractGroupMembersResponseV0H\x00\x1a\'\n\x0f\x43ontractMembers\x12\x14\n\x0c\x63ontract_ids\x18\x01 \x03(\x0c\x1ai\n\x13\x44ocumentTypeMembers\x12R\n\x0e\x64ocument_types\x18\x01 \x03(\x0b\x32:.org.dash.platform.dapi.v0.ContractGroupDocumentTypeMember\x1aS\n\x0cTokenMembers\x12\x43\n\x06tokens\x18\x01 \x03(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractGroupTokenMember\x1a\xc5\x03\n!GetContractGroupMembersResponseV0\x12_\n\tcontracts\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetContractGroupMembersResponse.ContractMembersH\x00\x12h\n\x0e\x64ocument_types\x18\x02 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetContractGroupMembersResponse.DocumentTypeMembersH\x00\x12Y\n\x06tokens\x18\x03 \x01(\x0b\x32G.org.dash.platform.dapi.v0.GetContractGroupMembersResponse.TokenMembersH\x00\x12\x31\n\x05proof\x18\x04 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x05 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"M\n\x1a\x43ontractModerationDocument\x12\x1a\n\x12\x64ocument_type_name\x18\x01 \x01(\t\x12\x13\n\x0b\x64ocument_id\x18\x02 \x01(\x0c\"\xc6\x01\n\x18\x43ontractModerationReason\x12\x11\n\x04\x63ode\x18\x01 \x01(\rH\x00\x88\x01\x01\x12\x0c\n\x04text\x18\x02 \x01(\t\x12H\n\tdocuments\x18\x03 \x03(\x0b\x32\x35.org.dash.platform.dapi.v0.ContractModerationDocument\x12\x1f\n\x12reason_document_id\x18\x04 \x01(\x0cH\x01\x88\x01\x01\x42\x07\n\x05_codeB\x15\n\x13_reason_document_id\"i\n\x0f\x43ontractWarning\x12\x11\n\twarned_at\x18\x01 \x01(\x04\x12\x43\n\x06reason\x18\x02 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractModerationReason\"\xc5\x02\n\"GetContractModerationStatusRequest\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetContractModerationStatusRequest.GetContractModerationStatusRequestV0H\x00\x1a\xa1\x01\n$GetContractModerationStatusRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x13\n\x0bidentity_id\x18\x02 \x01(\x0c\x12@\n\x05lists\x18\x03 \x03(\x0e\x32\x31.org.dash.platform.dapi.v0.ContractModerationList\x12\r\n\x05prove\x18\x04 \x01(\x08\x42\t\n\x07version\"\xec\x06\n#GetContractModerationStatusResponse\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetContractModerationStatusResponse.GetContractModerationStatusResponseV0H\x00\x1a\xb4\x03\n\x18\x43ontractModerationStatus\x12\x13\n\x06\x62\x61nned\x18\x01 \x01(\x08H\x00\x88\x01\x01\x12\x1c\n\x0fsuspended_until\x18\x02 \x01(\x04H\x01\x88\x01\x01\x12@\n\x05lists\x18\x03 \x03(\x0e\x32\x31.org.dash.platform.dapi.v0.ContractModerationList\x12L\n\nban_reason\x18\x04 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractModerationReasonH\x02\x88\x01\x01\x12S\n\x11suspension_reason\x18\x05 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractModerationReasonH\x03\x88\x01\x01\x12<\n\x08warnings\x18\x06 \x03(\x0b\x32*.org.dash.platform.dapi.v0.ContractWarningB\t\n\x07_bannedB\x12\n\x10_suspended_untilB\r\n\x0b_ban_reasonB\x14\n\x12_suspension_reason\x1a\x8e\x02\n%GetContractModerationStatusResponseV0\x12i\n\x06status\x18\x01 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetContractModerationStatusResponse.ContractModerationStatusH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xfb\x02\n#GetContractModerationEntriesRequest\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetContractModerationEntriesRequest.GetContractModerationEntriesRequestV0H\x00\x1a\xd4\x01\n%GetContractModerationEntriesRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12?\n\x04list\x18\x02 \x01(\x0e\x32\x31.org.dash.platform.dapi.v0.ContractModerationList\x12\x18\n\x0bstart_after\x18\x03 \x01(\x0cH\x00\x88\x01\x01\x12\x12\n\x05limit\x18\x04 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x0e\n\x0c_start_afterB\x08\n\x06_limitB\t\n\x07version\"\x96\x06\n$GetContractModerationEntriesResponse\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetContractModerationEntriesResponse.GetContractModerationEntriesResponseV0H\x00\x1a\xcf\x01\n\x17\x43ontractModerationEntry\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x12\n\x05until\x18\x02 \x01(\x04H\x00\x88\x01\x01\x12\x43\n\x06reason\x18\x03 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractModerationReason\x12<\n\x08warnings\x18\x04 \x03(\x0b\x32*.org.dash.platform.dapi.v0.ContractWarningB\x08\n\x06_until\x1a\x85\x01\n\x19\x43ontractModerationEntries\x12h\n\x07\x65ntries\x18\x01 \x03(\x0b\x32W.org.dash.platform.dapi.v0.GetContractModerationEntriesResponse.ContractModerationEntry\x1a\x92\x02\n&GetContractModerationEntriesResponseV0\x12l\n\x07\x65ntries\x18\x01 \x01(\x0b\x32Y.org.dash.platform.dapi.v0.GetContractModerationEntriesResponse.ContractModerationEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc3\x04\n\"GetContractDocumentRemovalsRequest\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetContractDocumentRemovalsRequest.GetContractDocumentRemovalsRequestV0H\x00\x1a#\n\x0b\x44ocumentIds\x12\x14\n\x0c\x64ocument_ids\x18\x01 \x03(\x0c\x1aN\n\x04Page\x12\x18\n\x0bstart_after\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x12\n\x05limit\x18\x02 \x01(\rH\x01\x88\x01\x01\x42\x0e\n\x0c_start_afterB\x08\n\x06_limit\x1a\xaa\x02\n$GetContractDocumentRemovalsRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x61\n\x0c\x64ocument_ids\x18\x03 \x01(\x0b\x32I.org.dash.platform.dapi.v0.GetContractDocumentRemovalsRequest.DocumentIdsH\x00\x12R\n\x04page\x18\x04 \x01(\x0b\x32\x42.org.dash.platform.dapi.v0.GetContractDocumentRemovalsRequest.PageH\x00\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x0b\n\tselectionB\t\n\x07version\"\xcb\x07\n#GetContractDocumentRemovalsResponse\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetContractDocumentRemovalsResponse.GetContractDocumentRemovalsResponseV0H\x00\x1aH\n\x1b\x43ontractDocumentRestoration\x12\x14\n\x0cmoderator_id\x18\x01 \x01(\x0c\x12\x13\n\x0brestored_at\x18\x02 \x01(\x04\x1a\xc0\x02\n\x17\x43ontractDocumentRemoval\x12\x13\n\x0b\x64ocument_id\x18\x01 \x01(\x0c\x12\x19\n\x11\x64ocument_owner_id\x18\x02 \x01(\x0c\x12\x14\n\x0cmoderator_id\x18\x03 \x01(\x0c\x12\x12\n\nremoved_at\x18\x04 \x01(\x04\x12\x43\n\x06reason\x18\x05 \x01(\x0b\x32\x33.org.dash.platform.dapi.v0.ContractModerationReason\x12\x15\n\rdocument_hash\x18\x06 \x01(\x0c\x12o\n\x0brestoration\x18\x07 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetContractDocumentRemovalsResponse.ContractDocumentRestoration\x1a\x84\x01\n\x18\x43ontractDocumentRemovals\x12h\n\x08removals\x18\x01 \x03(\x0b\x32V.org.dash.platform.dapi.v0.GetContractDocumentRemovalsResponse.ContractDocumentRemoval\x1a\x90\x02\n%GetContractDocumentRemovalsResponseV0\x12k\n\x08removals\x18\x01 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetContractDocumentRemovalsResponse.ContractDocumentRemovalsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc9\x01\n\x19GetContractFeePotsRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetContractFeePotsRequest.GetContractFeePotsRequestV0H\x00\x1a\x41\n\x1bGetContractFeePotsRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9f\x06\n\x1aGetContractFeePotsResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetContractFeePotsResponse.GetContractFeePotsResponseV0H\x00\x1aR\n\x17\x43ontractFeePotLastClaim\x12\r\n\x05\x65poch\x18\x01 \x01(\r\x12\x13\n\x07time_ms\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x13\n\x0b\x63laimant_id\x18\x03 \x01(\x0c\x1a\x88\x01\n\x0e\x43ontractFeePot\x12\x13\n\x07\x63redits\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x61\n\nlast_claim\x18\x02 \x01(\x0b\x32M.org.dash.platform.dapi.v0.GetContractFeePotsResponse.ContractFeePotLastClaim\x1a\xc0\x01\n\x0f\x43ontractFeePots\x12S\n\x05owner\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetContractFeePotsResponse.ContractFeePot\x12X\n\nmoderators\x18\x02 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetContractFeePotsResponse.ContractFeePot\x1a\xf1\x01\n\x1cGetContractFeePotsResponseV0\x12U\n\x04pots\x18\x01 \x01(\x0b\x32\x45.org.dash.platform.dapi.v0.GetContractFeePotsResponse.ContractFeePotsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xf1\x01\n#GetContractGroupsForContractRequest\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetContractGroupsForContractRequest.GetContractGroupsForContractRequestV0H\x00\x1aK\n%GetContractGroupsForContractRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf5\x06\n$GetContractGroupsForContractResponse\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetContractGroupsForContractResponse.GetContractGroupsForContractResponseV0H\x00\x1aQ\n\x17\x44ocumentTypeMemberships\x12\x1a\n\x12\x64ocument_type_name\x18\x01 \x01(\t\x12\x1a\n\x12\x63ontract_group_ids\x18\x02 \x03(\x0c\x1a\x46\n\x10TokenMemberships\x12\x16\n\x0etoken_position\x18\x01 \x01(\r\x12\x1a\n\x12\x63ontract_group_ids\x18\x02 \x03(\x0c\x1a\x89\x02\n\x18\x43ontractGroupMemberships\x12\x1a\n\x12\x63ontract_group_ids\x18\x01 \x03(\x0c\x12o\n\x0e\x64ocument_types\x18\x02 \x03(\x0b\x32W.org.dash.platform.dapi.v0.GetContractGroupsForContractResponse.DocumentTypeMemberships\x12`\n\x06tokens\x18\x03 \x03(\x0b\x32P.org.dash.platform.dapi.v0.GetContractGroupsForContractResponse.TokenMemberships\x1a\xa4\x02\n&GetContractGroupsForContractResponseV0\x12~\n\x1a\x63ontract_group_memberships\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetContractGroupsForContractResponse.ContractGroupMembershipsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xb9\x01\n\x17GetDataContractsRequest\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetDataContractsRequest.GetDataContractsRequestV0H\x00\x1a\x37\n\x19GetDataContractsRequestV0\x12\x0b\n\x03ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xad\x02\n\x1eGetDataContractsByRangeRequest\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetDataContractsByRangeRequest.GetDataContractsByRangeRequestV0H\x00\x1a\x95\x01\n GetDataContractsByRangeRequestV0\x12\x12\n\x05limit\x18\x01 \x01(\rH\x01\x88\x01\x01\x12\x15\n\x0bstart_after\x18\x02 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x03 \x01(\x0cH\x00\x12\x10\n\x08ids_only\x18\x04 \x01(\x08\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x07\n\x05startB\x08\n\x06_limitB\t\n\x07version\"\xcf\x04\n\x18GetDataContractsResponse\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetDataContractsResponse.GetDataContractsResponseV0H\x00\x1a[\n\x11\x44\x61taContractEntry\x12\x12\n\nidentifier\x18\x01 \x01(\x0c\x12\x32\n\rdata_contract\x18\x02 \x01(\x0b\x32\x1b.google.protobuf.BytesValue\x1au\n\rDataContracts\x12\x64\n\x15\x64\x61ta_contract_entries\x18\x01 \x03(\x0b\x32\x45.org.dash.platform.dapi.v0.GetDataContractsResponse.DataContractEntry\x1a\xf5\x01\n\x1aGetDataContractsResponseV0\x12[\n\x0e\x64\x61ta_contracts\x18\x01 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDataContractsResponse.DataContractsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc5\x02\n\x1dGetDataContractHistoryRequest\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetDataContractHistoryRequest.GetDataContractHistoryRequestV0H\x00\x1a\xb0\x01\n\x1fGetDataContractHistoryRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12+\n\x05limit\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x17\n\x0bstart_at_ms\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\t\n\x07version\"\xb2\x05\n\x1eGetDataContractHistoryResponse\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetDataContractHistoryResponse.GetDataContractHistoryResponseV0H\x00\x1a\x9a\x04\n GetDataContractHistoryResponseV0\x12\x8f\x01\n\x15\x64\x61ta_contract_history\x18\x01 \x01(\x0b\x32n.org.dash.platform.dapi.v0.GetDataContractHistoryResponse.GetDataContractHistoryResponseV0.DataContractHistoryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a;\n\x18\x44\x61taContractHistoryEntry\x12\x10\n\x04\x64\x61te\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05value\x18\x02 \x01(\x0c\x1a\xaa\x01\n\x13\x44\x61taContractHistory\x12\x92\x01\n\x15\x64\x61ta_contract_entries\x18\x01 \x03(\x0b\x32s.org.dash.platform.dapi.v0.GetDataContractHistoryResponse.GetDataContractHistoryResponseV0.DataContractHistoryEntryB\x08\n\x06resultB\t\n\x07version\"\xd8\"\n\x13GetDocumentsRequest\x12R\n\x02v0\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV0H\x00\x12R\n\x02v1\x18\x02 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1H\x00\x1a\xfe\x02\n\x12\x44ocumentFieldValue\x12\x14\n\nbool_value\x18\x01 \x01(\x08H\x00\x12\x19\n\x0bint64_value\x18\x02 \x01(\x12\x42\x02\x30\x01H\x00\x12\x1a\n\x0cuint64_value\x18\x03 \x01(\x04\x42\x02\x30\x01H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12\x0e\n\x04text\x18\x05 \x01(\tH\x00\x12\x15\n\x0b\x62ytes_value\x18\x06 \x01(\x0cH\x00\x12[\n\x04list\x18\x07 \x01(\x0b\x32K.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue.ValueListH\x00\x12\x14\n\nnull_value\x18\x08 \x01(\x08H\x00\x1a^\n\tValueList\x12Q\n\x06values\x18\x01 \x03(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValueB\t\n\x07variant\x1a\xe2\x02\n\x12TimeRangeSelection\x12\\\n\x08selector\x18\x01 \x01(\x0e\x32J.org.dash.platform.dapi.v0.GetDocumentsRequest.TimeRangeSelection.Selector\x12\x19\n\x08start_ms\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x12T\n\x04grid\x18\x03 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetDocumentsRequest.TimeRangeSelection.Grid\x1a>\n\x04Grid\x12\x11\n\x05range\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x10\n\x04step\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x11\n\x05phase\x18\x03 \x01(\x04\x42\x02\x30\x01\"0\n\x08Selector\x12\n\n\x06NEWEST\x10\x00\x12\n\n\x06OLDEST\x10\x01\x12\x0c\n\x08\x42Y_START\x10\x02\x42\x0b\n\t_start_ms\x1a\x82\x02\n\x15IntegerRangeSelection\x12P\n\x05start\x18\x01 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue\x12W\n\x04grid\x18\x02 \x01(\x0b\x32I.org.dash.platform.dapi.v0.GetDocumentsRequest.IntegerRangeSelection.Grid\x1a>\n\x04Grid\x12\x11\n\x05range\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x10\n\x04step\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x11\n\x05phase\x18\x03 \x01(\x04\x42\x02\x30\x01\x1a\xf2\x02\n\x0bWhereClause\x12\r\n\x05\x66ield\x18\x01 \x01(\t\x12N\n\x08operator\x18\x02 \x01(\x0e\x32<.org.dash.platform.dapi.v0.GetDocumentsRequest.WhereOperator\x12P\n\x05value\x18\x03 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValue\x12U\n\ntime_range\x18\x04 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.TimeRangeSelection\x12[\n\rinteger_range\x18\x05 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetDocumentsRequest.IntegerRangeSelection\x1a\xa4\x01\n\x0fHavingAggregate\x12Y\n\x08\x66unction\x18\x01 \x01(\x0e\x32G.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregate.Function\x12\r\n\x05\x66ield\x18\x02 \x01(\t\"\'\n\x08\x46unction\x12\t\n\x05\x43OUNT\x10\x00\x12\x07\n\x03SUM\x10\x01\x12\x07\n\x03\x41VG\x10\x02\x1a\xf9\x03\n\x0cHavingClause\x12Q\n\taggregate\x18\x01 \x01(\x0b\x32>.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregate\x12V\n\x08operator\x18\x02 \x01(\x0e\x32\x44.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause.Operator\x12R\n\x05value\x18\x03 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetDocumentsRequest.DocumentFieldValueH\x00\"\xe0\x01\n\x08Operator\x12\t\n\x05\x45QUAL\x10\x00\x12\r\n\tNOT_EQUAL\x10\x01\x12\x10\n\x0cGREATER_THAN\x10\x02\x12\x1a\n\x16GREATER_THAN_OR_EQUALS\x10\x03\x12\r\n\tLESS_THAN\x10\x04\x12\x17\n\x13LESS_THAN_OR_EQUALS\x10\x05\x12\x0b\n\x07\x42\x45TWEEN\x10\x06\x12\x1a\n\x16\x42\x45TWEEN_EXCLUDE_BOUNDS\x10\x07\x12\x18\n\x14\x42\x45TWEEN_EXCLUDE_LEFT\x10\x08\x12\x19\n\x15\x42\x45TWEEN_EXCLUDE_RIGHT\x10\t\x12\x06\n\x02IN\x10\nB\x07\n\x05right\x1a\x90\x01\n\x0bOrderClause\x12\x0f\n\x05\x66ield\x18\x01 \x01(\tH\x00\x12S\n\taggregate\x18\x03 \x01(\x0b\x32>.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingAggregateH\x00\x12\x11\n\tascending\x18\x02 \x01(\x08\x42\x08\n\x06target\x1a\xbb\x01\n\x15GetDocumentsRequestV0\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x15\n\rdocument_type\x18\x02 \x01(\t\x12\r\n\x05where\x18\x03 \x01(\x0c\x12\x10\n\x08order_by\x18\x04 \x01(\x0c\x12\r\n\x05limit\x18\x05 \x01(\r\x12\x15\n\x0bstart_after\x18\x06 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x07 \x01(\x0cH\x00\x12\r\n\x05prove\x18\x08 \x01(\x08\x42\x07\n\x05start\x1a\xa6\x0c\n\x15GetDocumentsRequestV1\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x15\n\rdocument_type\x18\x02 \x01(\t\x12Q\n\rwhere_clauses\x18\x03 \x03(\x0b\x32:.org.dash.platform.dapi.v0.GetDocumentsRequest.WhereClause\x12L\n\x08order_by\x18\x04 \x03(\x0b\x32:.org.dash.platform.dapi.v0.GetDocumentsRequest.OrderClause\x12\x12\n\x05limit\x18\x05 \x01(\rH\x01\x88\x01\x01\x12\x15\n\x0bstart_after\x18\x06 \x01(\x0cH\x00\x12\x12\n\x08start_at\x18\x07 \x01(\x0cH\x00\x12\r\n\x05prove\x18\x08 \x01(\x08\x12\\\n\x07selects\x18\t \x03(\x0b\x32K.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.Select\x12\x10\n\x08group_by\x18\n \x03(\t\x12K\n\x06having\x18\x0b \x03(\x0b\x32;.org.dash.platform.dapi.v0.GetDocumentsRequest.HavingClause\x12\x13\n\x06offset\x18\x0c \x01(\rH\x02\x88\x01\x01\x12\x61\n\x07\x63hained\x18\r \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.ChainedJoin\x12\x62\n\x0bsub_queries\x18\x0e \x03(\x0b\x32M.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.SubQuery\x1a\xc9\x01\n\x06Select\x12\x66\n\x08\x66unction\x18\x01 \x01(\x0e\x32T.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.Select.Function\x12\r\n\x05\x66ield\x18\x02 \x01(\t\"H\n\x08\x46unction\x12\r\n\tDOCUMENTS\x10\x00\x12\t\n\x05\x43OUNT\x10\x01\x12\x07\n\x03SUM\x10\x02\x12\x07\n\x03\x41VG\x10\x03\x12\x07\n\x03MIN\x10\x04\x12\x07\n\x03MAX\x10\x05\x1a\x41\n\x0b\x43hainedJoin\x12\x15\n\rjoin_property\x18\x01 \x01(\t\x12\x1b\n\x13outer_document_type\x18\x02 \x01(\t\x1a\xa6\x04\n\x08SubQuery\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x15\n\rdocument_type\x18\x02 \x01(\t\x12Q\n\rwhere_clauses\x18\x03 \x03(\x0b\x32:.org.dash.platform.dapi.v0.GetDocumentsRequest.WhereClause\x12L\n\x08order_by\x18\x04 \x03(\x0b\x32:.org.dash.platform.dapi.v0.GetDocumentsRequest.OrderClause\x12\x12\n\x05limit\x18\x05 \x01(\rH\x00\x88\x01\x01\x12`\n\x04kind\x18\x06 \x01(\x0e\x32R.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.SubQuery.Kind\x12\x63\n\x04\x62ind\x18\x07 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetDocumentsRequest.GetDocumentsRequestV1.SubQuery.Binding\x1a\x41\n\x07\x42inding\x12\x0e\n\x06source\x18\x01 \x01(\r\x12\x17\n\x0fsource_property\x18\x02 \x01(\t\x12\r\n\x05\x66ield\x18\x03 \x01(\t\" \n\x04Kind\x12\r\n\tDOCUMENTS\x10\x00\x12\t\n\x05\x43OUNT\x10\x01\x42\x08\n\x06_limitB\x07\n\x05startB\x08\n\x06_limitB\t\n\x07_offset\"\x90\x02\n\rWhereOperator\x12\t\n\x05\x45QUAL\x10\x00\x12\x10\n\x0cGREATER_THAN\x10\x01\x12\x1a\n\x16GREATER_THAN_OR_EQUALS\x10\x02\x12\r\n\tLESS_THAN\x10\x03\x12\x17\n\x13LESS_THAN_OR_EQUALS\x10\x04\x12\x0b\n\x07\x42\x45TWEEN\x10\x05\x12\x1a\n\x16\x42\x45TWEEN_EXCLUDE_BOUNDS\x10\x06\x12\x18\n\x14\x42\x45TWEEN_EXCLUDE_LEFT\x10\x07\x12\x19\n\x15\x42\x45TWEEN_EXCLUDE_RIGHT\x10\x08\x12\x06\n\x02IN\x10\t\x12\x0f\n\x0bSTARTS_WITH\x10\n\x12\x11\n\rIN_TIME_RANGE\x10\x0b\x12\x14\n\x10IN_INTEGER_RANGE\x10\x0c\x42\t\n\x07version\"\xf2\x1b\n\x14GetDocumentsResponse\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV0H\x00\x12T\n\x02v1\x18\x02 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1H\x00\x1a\x9b\x02\n\x16GetDocumentsResponseV0\x12\x65\n\tdocuments\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV0.DocumentsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1e\n\tDocuments\x12\x11\n\tdocuments\x18\x01 \x03(\x0c\x42\x08\n\x06result\x1a\x84\x18\n\x16GetDocumentsResponseV1\x12\x61\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ResultDataH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1e\n\tDocuments\x12\x11\n\tdocuments\x18\x01 \x03(\x0c\x1aL\n\nCountEntry\x12\x13\n\x06in_key\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x0b\n\x03key\x18\x02 \x01(\x0c\x12\x11\n\x05\x63ount\x18\x03 \x01(\x04\x42\x02\x30\x01\x42\t\n\x07_in_key\x1ar\n\x0c\x43ountEntries\x12\x62\n\x07\x65ntries\x18\x01 \x03(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountEntry\x1a\xa0\x01\n\x0c\x43ountResults\x12\x1d\n\x0f\x61ggregate_count\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x66\n\x07\x65ntries\x18\x02 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountEntriesH\x00\x42\t\n\x07variant\x1aH\n\x08SumEntry\x12\x13\n\x06in_key\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x0b\n\x03key\x18\x02 \x01(\x0c\x12\x0f\n\x03sum\x18\x03 \x01(\x12\x42\x02\x30\x01\x42\t\n\x07_in_key\x1an\n\nSumEntries\x12`\n\x07\x65ntries\x18\x01 \x03(\x0b\x32O.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumEntry\x1a\x9a\x01\n\nSumResults\x12\x1b\n\raggregate_sum\x18\x01 \x01(\x12\x42\x02\x30\x01H\x00\x12\x64\n\x07\x65ntries\x18\x02 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumEntriesH\x00\x42\t\n\x07variant\x1a_\n\x0c\x41verageEntry\x12\x13\n\x06in_key\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x12\x0b\n\x03key\x18\x02 \x01(\x0c\x12\x11\n\x05\x63ount\x18\x03 \x01(\x04\x42\x02\x30\x01\x12\x0f\n\x03sum\x18\x04 \x01(\x12\x42\x02\x30\x01\x42\t\n\x07_in_key\x1av\n\x0e\x41verageEntries\x12\x64\n\x07\x65ntries\x18\x01 \x03(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageEntry\x1a\x36\n\x10\x41verageAggregate\x12\x11\n\x05\x63ount\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x0f\n\x03sum\x18\x02 \x01(\x12\x42\x02\x30\x01\x1a\xfb\x01\n\x0e\x41verageResults\x12t\n\x11\x61ggregate_average\x18\x01 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageAggregateH\x00\x12h\n\x07\x65ntries\x18\x02 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageEntriesH\x00\x42\t\n\x07variant\x1az\n\x0bRankedEntry\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\x13\n\x05\x63ount\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12\x11\n\x03sum\x18\x03 \x01(\x12\x42\x02\x30\x01H\x00\x12\r\n\x03\x61vg\x18\x04 \x01(\x01H\x00\x12\x13\n\x06in_key\x18\x05 \x01(\x0cH\x01\x88\x01\x01\x42\x07\n\x05valueB\t\n\x07_in_key\x1a\x9a\x01\n\rRankedEntries\x12\x63\n\x07\x65ntries\x18\x01 \x03(\x0b\x32R.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntry\x12\x18\n\x07skipped\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x42\n\n\x08_skipped\x1a\xf7\x05\n\nResultData\x12\x65\n\tdocuments\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.DocumentsH\x00\x12\x65\n\x06\x63ounts\x18\x02 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountResultsH\x00\x12\x61\n\x04sums\x18\x03 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.SumResultsH\x00\x12i\n\x08\x61verages\x18\x04 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.AverageResultsH\x00\x12\x66\n\x06ranked\x18\x05 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.RankedEntriesH\x00\x12j\n\x07\x63hained\x18\x06 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.ChainedDocumentsH\x00\x12n\n\tcomposite\x18\x07 \x01(\x0b\x32Y.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CompositeDocumentsH\x00\x42\t\n\x07variant\x1a_\n\x10\x43hainedDocuments\x12\x17\n\x0finner_documents\x18\x01 \x03(\x0c\x12\x17\n\x0fouter_documents\x18\x02 \x03(\x0c\x12\x19\n\x11missing_outer_ids\x18\x03 \x03(\x0c\x1a\xab\x03\n\x12\x43ompositeDocuments\x12\x16\n\x0epage_documents\x18\x01 \x03(\x0c\x12}\n\x0bsub_results\x18\x02 \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CompositeDocuments.SubQueryResult\x1a\xfd\x01\n\x0eSubQueryResult\x12\x65\n\tdocuments\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.DocumentsH\x00\x12\x65\n\x06\x63ounts\x18\x02 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetDocumentsResponse.GetDocumentsResponseV1.CountEntriesH\x00\x12\x13\n\x0bmissing_ids\x18\x03 \x03(\x0c\x42\x08\n\x06resultB\x08\n\x06resultB\t\n\x07version\"\xf4\x02\n\x19GetDocumentHistoryRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetDocumentHistoryRequest.GetDocumentHistoryRequestV0H\x00\x1a\xeb\x01\n\x1bGetDocumentHistoryRequestV0\x12\x18\n\x10\x64\x61ta_contract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x13\n\x0b\x64ocument_id\x18\x03 \x01(\x0c\x12+\n\x05limit\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x17\n\x0bstart_at_ms\x18\x06 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x07 \x01(\x08\x42\t\n\x07version\"\xf7\x04\n\x1aGetDocumentHistoryResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetDocumentHistoryResponse.GetDocumentHistoryResponseV0H\x00\x1a\xeb\x03\n\x1cGetDocumentHistoryResponseV0\x12~\n\x10\x64ocument_history\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetDocumentHistoryResponse.GetDocumentHistoryResponseV0.DocumentHistoryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x37\n\x14\x44ocumentHistoryEntry\x12\x10\n\x04\x64\x61te\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05value\x18\x02 \x01(\x0c\x1a\x95\x01\n\x0f\x44ocumentHistory\x12\x81\x01\n\x10\x64ocument_entries\x18\x01 \x03(\x0b\x32g.org.dash.platform.dapi.v0.GetDocumentHistoryResponse.GetDocumentHistoryResponseV0.DocumentHistoryEntryB\x08\n\x06resultB\t\n\x07version\"\xed\x01\n!GetIdentityByPublicKeyHashRequest\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashRequest.GetIdentityByPublicKeyHashRequestV0H\x00\x1aM\n#GetIdentityByPublicKeyHashRequestV0\x12\x17\n\x0fpublic_key_hash\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xda\x02\n\"GetIdentityByPublicKeyHashResponse\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashResponse.GetIdentityByPublicKeyHashResponseV0H\x00\x1a\xb6\x01\n$GetIdentityByPublicKeyHashResponseV0\x12\x12\n\x08identity\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xbd\x02\n*GetIdentityByNonUniquePublicKeyHashRequest\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashRequest.GetIdentityByNonUniquePublicKeyHashRequestV0H\x00\x1a\x80\x01\n,GetIdentityByNonUniquePublicKeyHashRequestV0\x12\x17\n\x0fpublic_key_hash\x18\x01 \x01(\x0c\x12\x18\n\x0bstart_after\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\x0e\n\x0c_start_afterB\t\n\x07version\"\xd6\x06\n+GetIdentityByNonUniquePublicKeyHashResponse\x12\x82\x01\n\x02v0\x18\x01 \x01(\x0b\x32t.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse.GetIdentityByNonUniquePublicKeyHashResponseV0H\x00\x1a\x96\x05\n-GetIdentityByNonUniquePublicKeyHashResponseV0\x12\x9a\x01\n\x08identity\x18\x01 \x01(\x0b\x32\x85\x01.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse.GetIdentityByNonUniquePublicKeyHashResponseV0.IdentityResponseH\x00\x12\x9d\x01\n\x05proof\x18\x02 \x01(\x0b\x32\x8b\x01.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse.GetIdentityByNonUniquePublicKeyHashResponseV0.IdentityProvedResponseH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x36\n\x10IdentityResponse\x12\x15\n\x08identity\x18\x01 \x01(\x0cH\x00\x88\x01\x01\x42\x0b\n\t_identity\x1a\xa6\x01\n\x16IdentityProvedResponse\x12P\n&grovedb_identity_public_key_hash_proof\x18\x01 \x01(\x0b\x32 .org.dash.platform.dapi.v0.Proof\x12!\n\x14identity_proof_bytes\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x42\x17\n\x15_identity_proof_bytesB\x08\n\x06resultB\t\n\x07version\"\x99\x02\n#WaitForStateTransitionResultRequest\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.WaitForStateTransitionResultRequest.WaitForStateTransitionResultRequestV0H\x00\x1as\n%WaitForStateTransitionResultRequestV0\x12\x1d\n\x15state_transition_hash\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x12\x1c\n\x14request_user_balance\x18\x03 \x01(\x08\x42\t\n\x07version\"\xce\x04\n$WaitForStateTransitionResultResponse\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.WaitForStateTransitionResultResponse.WaitForStateTransitionResultResponseV0H\x00\x1a\x34\n\x17SuccessWithOwnerBalance\x12\x19\n\rowner_balance\x18\x01 \x01(\x04\x42\x02\x30\x01\x1a\xee\x02\n&WaitForStateTransitionResultResponseV0\x12I\n\x05\x65rror\x18\x01 \x01(\x0b\x32\x38.org.dash.platform.dapi.v0.StateTransitionBroadcastErrorH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12}\n\x1asuccess_with_owner_balance\x18\x04 \x01(\x0b\x32W.org.dash.platform.dapi.v0.WaitForStateTransitionResultResponse.SuccessWithOwnerBalanceH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc4\x01\n\x19GetConsensusParamsRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetConsensusParamsRequest.GetConsensusParamsRequestV0H\x00\x1a<\n\x1bGetConsensusParamsRequestV0\x12\x0e\n\x06height\x18\x01 \x01(\x05\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x9c\x04\n\x1aGetConsensusParamsResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetConsensusParamsResponse.GetConsensusParamsResponseV0H\x00\x1aP\n\x14\x43onsensusParamsBlock\x12\x11\n\tmax_bytes\x18\x01 \x01(\t\x12\x0f\n\x07max_gas\x18\x02 \x01(\t\x12\x14\n\x0ctime_iota_ms\x18\x03 \x01(\t\x1a\x62\n\x17\x43onsensusParamsEvidence\x12\x1a\n\x12max_age_num_blocks\x18\x01 \x01(\t\x12\x18\n\x10max_age_duration\x18\x02 \x01(\t\x12\x11\n\tmax_bytes\x18\x03 \x01(\t\x1a\xda\x01\n\x1cGetConsensusParamsResponseV0\x12Y\n\x05\x62lock\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetConsensusParamsResponse.ConsensusParamsBlock\x12_\n\x08\x65vidence\x18\x02 \x01(\x0b\x32M.org.dash.platform.dapi.v0.GetConsensusParamsResponse.ConsensusParamsEvidenceB\t\n\x07version\"\xe4\x01\n%GetProtocolVersionUpgradeStateRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateRequest.GetProtocolVersionUpgradeStateRequestV0H\x00\x1a\x38\n\'GetProtocolVersionUpgradeStateRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xb5\x05\n&GetProtocolVersionUpgradeStateResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse.GetProtocolVersionUpgradeStateResponseV0H\x00\x1a\x85\x04\n(GetProtocolVersionUpgradeStateResponseV0\x12\x87\x01\n\x08versions\x18\x01 \x01(\x0b\x32s.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse.GetProtocolVersionUpgradeStateResponseV0.VersionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x96\x01\n\x08Versions\x12\x89\x01\n\x08versions\x18\x01 \x03(\x0b\x32w.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse.GetProtocolVersionUpgradeStateResponseV0.VersionEntry\x1a:\n\x0cVersionEntry\x12\x16\n\x0eversion_number\x18\x01 \x01(\r\x12\x12\n\nvote_count\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xa3\x02\n*GetProtocolVersionUpgradeVoteStatusRequest\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusRequest.GetProtocolVersionUpgradeVoteStatusRequestV0H\x00\x1ag\n,GetProtocolVersionUpgradeVoteStatusRequestV0\x12\x19\n\x11start_pro_tx_hash\x18\x01 \x01(\x0c\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xef\x05\n+GetProtocolVersionUpgradeVoteStatusResponse\x12\x82\x01\n\x02v0\x18\x01 \x01(\x0b\x32t.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse.GetProtocolVersionUpgradeVoteStatusResponseV0H\x00\x1a\xaf\x04\n-GetProtocolVersionUpgradeVoteStatusResponseV0\x12\x98\x01\n\x08versions\x18\x01 \x01(\x0b\x32\x83\x01.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse.GetProtocolVersionUpgradeVoteStatusResponseV0.VersionSignalsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xaf\x01\n\x0eVersionSignals\x12\x9c\x01\n\x0fversion_signals\x18\x01 \x03(\x0b\x32\x82\x01.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse.GetProtocolVersionUpgradeVoteStatusResponseV0.VersionSignal\x1a\x35\n\rVersionSignal\x12\x13\n\x0bpro_tx_hash\x18\x01 \x01(\x0c\x12\x0f\n\x07version\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xf5\x01\n\x14GetEpochsInfoRequest\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetEpochsInfoRequest.GetEpochsInfoRequestV0H\x00\x1a|\n\x16GetEpochsInfoRequestV0\x12\x31\n\x0bstart_epoch\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\x11\n\tascending\x18\x03 \x01(\x08\x12\r\n\x05prove\x18\x04 \x01(\x08\x42\t\n\x07version\"\x99\x05\n\x15GetEpochsInfoResponse\x12V\n\x02v0\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetEpochsInfoResponse.GetEpochsInfoResponseV0H\x00\x1a\x9c\x04\n\x17GetEpochsInfoResponseV0\x12\x65\n\x06\x65pochs\x18\x01 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetEpochsInfoResponse.GetEpochsInfoResponseV0.EpochInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1au\n\nEpochInfos\x12g\n\x0b\x65poch_infos\x18\x01 \x03(\x0b\x32R.org.dash.platform.dapi.v0.GetEpochsInfoResponse.GetEpochsInfoResponseV0.EpochInfo\x1a\xa6\x01\n\tEpochInfo\x12\x0e\n\x06number\x18\x01 \x01(\r\x12\x1e\n\x12\x66irst_block_height\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x1f\n\x17\x66irst_core_block_height\x18\x03 \x01(\r\x12\x16\n\nstart_time\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x16\n\x0e\x66\x65\x65_multiplier\x18\x05 \x01(\x01\x12\x18\n\x10protocol_version\x18\x06 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xbf\x02\n\x1dGetFinalizedEpochInfosRequest\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetFinalizedEpochInfosRequest.GetFinalizedEpochInfosRequestV0H\x00\x1a\xaa\x01\n\x1fGetFinalizedEpochInfosRequestV0\x12\x19\n\x11start_epoch_index\x18\x01 \x01(\r\x12\"\n\x1astart_epoch_index_included\x18\x02 \x01(\x08\x12\x17\n\x0f\x65nd_epoch_index\x18\x03 \x01(\r\x12 \n\x18\x65nd_epoch_index_included\x18\x04 \x01(\x08\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\t\n\x07version\"\xbd\t\n\x1eGetFinalizedEpochInfosResponse\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0H\x00\x1a\xa5\x08\n GetFinalizedEpochInfosResponseV0\x12\x80\x01\n\x06\x65pochs\x18\x01 \x01(\x0b\x32n.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0.FinalizedEpochInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xa4\x01\n\x13\x46inalizedEpochInfos\x12\x8c\x01\n\x15\x66inalized_epoch_infos\x18\x01 \x03(\x0b\x32m.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0.FinalizedEpochInfo\x1a\x9f\x04\n\x12\x46inalizedEpochInfo\x12\x0e\n\x06number\x18\x01 \x01(\r\x12\x1e\n\x12\x66irst_block_height\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x1f\n\x17\x66irst_core_block_height\x18\x03 \x01(\r\x12\x1c\n\x10\x66irst_block_time\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x16\n\x0e\x66\x65\x65_multiplier\x18\x05 \x01(\x01\x12\x18\n\x10protocol_version\x18\x06 \x01(\r\x12!\n\x15total_blocks_in_epoch\x18\x07 \x01(\x04\x42\x02\x30\x01\x12*\n\"next_epoch_start_core_block_height\x18\x08 \x01(\r\x12!\n\x15total_processing_fees\x18\t \x01(\x04\x42\x02\x30\x01\x12*\n\x1etotal_distributed_storage_fees\x18\n \x01(\x04\x42\x02\x30\x01\x12&\n\x1atotal_created_storage_fees\x18\x0b \x01(\x04\x42\x02\x30\x01\x12\x1e\n\x12\x63ore_block_rewards\x18\x0c \x01(\x04\x42\x02\x30\x01\x12\x81\x01\n\x0f\x62lock_proposers\x18\r \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse.GetFinalizedEpochInfosResponseV0.BlockProposer\x1a\x39\n\rBlockProposer\x12\x13\n\x0bproposer_id\x18\x01 \x01(\x0c\x12\x13\n\x0b\x62lock_count\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xde\x04\n\x1cGetContestedResourcesRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetContestedResourcesRequest.GetContestedResourcesRequestV0H\x00\x1a\xcc\x03\n\x1eGetContestedResourcesRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x12\n\nindex_name\x18\x03 \x01(\t\x12\x1a\n\x12start_index_values\x18\x04 \x03(\x0c\x12\x18\n\x10\x65nd_index_values\x18\x05 \x03(\x0c\x12\x89\x01\n\x13start_at_value_info\x18\x06 \x01(\x0b\x32g.org.dash.platform.dapi.v0.GetContestedResourcesRequest.GetContestedResourcesRequestV0.StartAtValueInfoH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x07 \x01(\rH\x01\x88\x01\x01\x12\x17\n\x0forder_ascending\x18\x08 \x01(\x08\x12\r\n\x05prove\x18\t \x01(\x08\x1a\x45\n\x10StartAtValueInfo\x12\x13\n\x0bstart_value\x18\x01 \x01(\x0c\x12\x1c\n\x14start_value_included\x18\x02 \x01(\x08\x42\x16\n\x14_start_at_value_infoB\x08\n\x06_countB\t\n\x07version\"\x88\x04\n\x1dGetContestedResourcesResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetContestedResourcesResponse.GetContestedResourcesResponseV0H\x00\x1a\xf3\x02\n\x1fGetContestedResourcesResponseV0\x12\x95\x01\n\x19\x63ontested_resource_values\x18\x01 \x01(\x0b\x32p.org.dash.platform.dapi.v0.GetContestedResourcesResponse.GetContestedResourcesResponseV0.ContestedResourceValuesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a<\n\x17\x43ontestedResourceValues\x12!\n\x19\x63ontested_resource_values\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\xd2\x05\n\x1cGetVotePollsByEndDateRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest.GetVotePollsByEndDateRequestV0H\x00\x1a\xc0\x04\n\x1eGetVotePollsByEndDateRequestV0\x12\x84\x01\n\x0fstart_time_info\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest.GetVotePollsByEndDateRequestV0.StartAtTimeInfoH\x00\x88\x01\x01\x12\x80\x01\n\rend_time_info\x18\x02 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest.GetVotePollsByEndDateRequestV0.EndAtTimeInfoH\x01\x88\x01\x01\x12\x12\n\x05limit\x18\x03 \x01(\rH\x02\x88\x01\x01\x12\x13\n\x06offset\x18\x04 \x01(\rH\x03\x88\x01\x01\x12\x11\n\tascending\x18\x05 \x01(\x08\x12\r\n\x05prove\x18\x06 \x01(\x08\x1aI\n\x0fStartAtTimeInfo\x12\x19\n\rstart_time_ms\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1b\n\x13start_time_included\x18\x02 \x01(\x08\x1a\x43\n\rEndAtTimeInfo\x12\x17\n\x0b\x65nd_time_ms\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x19\n\x11\x65nd_time_included\x18\x02 \x01(\x08\x42\x12\n\x10_start_time_infoB\x10\n\x0e_end_time_infoB\x08\n\x06_limitB\t\n\x07_offsetB\t\n\x07version\"\x83\x06\n\x1dGetVotePollsByEndDateResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse.GetVotePollsByEndDateResponseV0H\x00\x1a\xee\x04\n\x1fGetVotePollsByEndDateResponseV0\x12\x9c\x01\n\x18vote_polls_by_timestamps\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse.GetVotePollsByEndDateResponseV0.SerializedVotePollsByTimestampsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aV\n\x1eSerializedVotePollsByTimestamp\x12\x15\n\ttimestamp\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1d\n\x15serialized_vote_polls\x18\x02 \x03(\x0c\x1a\xd7\x01\n\x1fSerializedVotePollsByTimestamps\x12\x99\x01\n\x18vote_polls_by_timestamps\x18\x01 \x03(\x0b\x32w.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse.GetVotePollsByEndDateResponseV0.SerializedVotePollsByTimestamp\x12\x18\n\x10\x66inished_results\x18\x02 \x01(\x08\x42\x08\n\x06resultB\t\n\x07version\"\xff\x06\n$GetContestedResourceVoteStateRequest\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest.GetContestedResourceVoteStateRequestV0H\x00\x1a\xd5\x05\n&GetContestedResourceVoteStateRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x12\n\nindex_name\x18\x03 \x01(\t\x12\x14\n\x0cindex_values\x18\x04 \x03(\x0c\x12\x86\x01\n\x0bresult_type\x18\x05 \x01(\x0e\x32q.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest.GetContestedResourceVoteStateRequestV0.ResultType\x12\x36\n.allow_include_locked_and_abstaining_vote_tally\x18\x06 \x01(\x08\x12\xa3\x01\n\x18start_at_identifier_info\x18\x07 \x01(\x0b\x32|.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest.GetContestedResourceVoteStateRequestV0.StartAtIdentifierInfoH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x08 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\t \x01(\x08\x1aT\n\x15StartAtIdentifierInfo\x12\x18\n\x10start_identifier\x18\x01 \x01(\x0c\x12!\n\x19start_identifier_included\x18\x02 \x01(\x08\"I\n\nResultType\x12\r\n\tDOCUMENTS\x10\x00\x12\x0e\n\nVOTE_TALLY\x10\x01\x12\x1c\n\x18\x44OCUMENTS_AND_VOTE_TALLY\x10\x02\x42\x1b\n\x19_start_at_identifier_infoB\x08\n\x06_countB\t\n\x07version\"\x94\x0c\n%GetContestedResourceVoteStateResponse\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0H\x00\x1a\xe7\n\n\'GetContestedResourceVoteStateResponseV0\x12\xae\x01\n\x1d\x63ontested_resource_contenders\x18\x01 \x01(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.ContestedResourceContendersH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xda\x03\n\x10\x46inishedVoteInfo\x12\xad\x01\n\x15\x66inished_vote_outcome\x18\x01 \x01(\x0e\x32\x8d\x01.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.FinishedVoteInfo.FinishedVoteOutcome\x12\x1f\n\x12won_by_identity_id\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x12$\n\x18\x66inished_at_block_height\x18\x03 \x01(\x04\x42\x02\x30\x01\x12%\n\x1d\x66inished_at_core_block_height\x18\x04 \x01(\r\x12%\n\x19\x66inished_at_block_time_ms\x18\x05 \x01(\x04\x42\x02\x30\x01\x12\x19\n\x11\x66inished_at_epoch\x18\x06 \x01(\r\"O\n\x13\x46inishedVoteOutcome\x12\x14\n\x10TOWARDS_IDENTITY\x10\x00\x12\n\n\x06LOCKED\x10\x01\x12\x16\n\x12NO_PREVIOUS_WINNER\x10\x02\x42\x15\n\x13_won_by_identity_id\x1a\xc4\x03\n\x1b\x43ontestedResourceContenders\x12\x86\x01\n\ncontenders\x18\x01 \x03(\x0b\x32r.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.Contender\x12\x1f\n\x12\x61\x62stain_vote_tally\x18\x02 \x01(\rH\x00\x88\x01\x01\x12\x1c\n\x0flock_vote_tally\x18\x03 \x01(\rH\x01\x88\x01\x01\x12\x9a\x01\n\x12\x66inished_vote_info\x18\x04 \x01(\x0b\x32y.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse.GetContestedResourceVoteStateResponseV0.FinishedVoteInfoH\x02\x88\x01\x01\x42\x15\n\x13_abstain_vote_tallyB\x12\n\x10_lock_vote_tallyB\x15\n\x13_finished_vote_info\x1ak\n\tContender\x12\x12\n\nidentifier\x18\x01 \x01(\x0c\x12\x17\n\nvote_count\x18\x02 \x01(\rH\x00\x88\x01\x01\x12\x15\n\x08\x64ocument\x18\x03 \x01(\x0cH\x01\x88\x01\x01\x42\r\n\x0b_vote_countB\x0b\n\t_documentB\x08\n\x06resultB\t\n\x07version\"\xd5\x05\n,GetContestedResourceVotersForIdentityRequest\x12\x84\x01\n\x02v0\x18\x01 \x01(\x0b\x32v.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityRequest.GetContestedResourceVotersForIdentityRequestV0H\x00\x1a\x92\x04\n.GetContestedResourceVotersForIdentityRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\x12\n\nindex_name\x18\x03 \x01(\t\x12\x14\n\x0cindex_values\x18\x04 \x03(\x0c\x12\x15\n\rcontestant_id\x18\x05 \x01(\x0c\x12\xb4\x01\n\x18start_at_identifier_info\x18\x06 \x01(\x0b\x32\x8c\x01.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityRequest.GetContestedResourceVotersForIdentityRequestV0.StartAtIdentifierInfoH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x07 \x01(\rH\x01\x88\x01\x01\x12\x17\n\x0forder_ascending\x18\x08 \x01(\x08\x12\r\n\x05prove\x18\t \x01(\x08\x1aT\n\x15StartAtIdentifierInfo\x12\x18\n\x10start_identifier\x18\x01 \x01(\x0c\x12!\n\x19start_identifier_included\x18\x02 \x01(\x08\x42\x1b\n\x19_start_at_identifier_infoB\x08\n\x06_countB\t\n\x07version\"\xf1\x04\n-GetContestedResourceVotersForIdentityResponse\x12\x86\x01\n\x02v0\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityResponse.GetContestedResourceVotersForIdentityResponseV0H\x00\x1a\xab\x03\n/GetContestedResourceVotersForIdentityResponseV0\x12\xb6\x01\n\x19\x63ontested_resource_voters\x18\x01 \x01(\x0b\x32\x90\x01.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityResponse.GetContestedResourceVotersForIdentityResponseV0.ContestedResourceVotersH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x43\n\x17\x43ontestedResourceVoters\x12\x0e\n\x06voters\x18\x01 \x03(\x0c\x12\x18\n\x10\x66inished_results\x18\x02 \x01(\x08\x42\x08\n\x06resultB\t\n\x07version\"\xad\x05\n(GetContestedResourceIdentityVotesRequest\x12|\n\x02v0\x18\x01 \x01(\x0b\x32n.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesRequest.GetContestedResourceIdentityVotesRequestV0H\x00\x1a\xf7\x03\n*GetContestedResourceIdentityVotesRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12+\n\x05limit\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12,\n\x06offset\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x17\n\x0forder_ascending\x18\x04 \x01(\x08\x12\xae\x01\n\x1astart_at_vote_poll_id_info\x18\x05 \x01(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesRequest.GetContestedResourceIdentityVotesRequestV0.StartAtVotePollIdInfoH\x00\x88\x01\x01\x12\r\n\x05prove\x18\x06 \x01(\x08\x1a\x61\n\x15StartAtVotePollIdInfo\x12 \n\x18start_at_poll_identifier\x18\x01 \x01(\x0c\x12&\n\x1estart_poll_identifier_included\x18\x02 \x01(\x08\x42\x1d\n\x1b_start_at_vote_poll_id_infoB\t\n\x07version\"\xc8\n\n)GetContestedResourceIdentityVotesResponse\x12~\n\x02v0\x18\x01 \x01(\x0b\x32p.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0H\x00\x1a\x8f\t\n+GetContestedResourceIdentityVotesResponseV0\x12\xa1\x01\n\x05votes\x18\x01 \x01(\x0b\x32\x8f\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ContestedResourceIdentityVotesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\xf7\x01\n\x1e\x43ontestedResourceIdentityVotes\x12\xba\x01\n!contested_resource_identity_votes\x18\x01 \x03(\x0b\x32\x8e\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ContestedResourceIdentityVote\x12\x18\n\x10\x66inished_results\x18\x02 \x01(\x08\x1a\xad\x02\n\x12ResourceVoteChoice\x12\xad\x01\n\x10vote_choice_type\x18\x01 \x01(\x0e\x32\x92\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ResourceVoteChoice.VoteChoiceType\x12\x18\n\x0bidentity_id\x18\x02 \x01(\x0cH\x00\x88\x01\x01\"=\n\x0eVoteChoiceType\x12\x14\n\x10TOWARDS_IDENTITY\x10\x00\x12\x0b\n\x07\x41\x42STAIN\x10\x01\x12\x08\n\x04LOCK\x10\x02\x42\x0e\n\x0c_identity_id\x1a\x95\x02\n\x1d\x43ontestedResourceIdentityVote\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1a\n\x12\x64ocument_type_name\x18\x02 \x01(\t\x12\'\n\x1fserialized_index_storage_values\x18\x03 \x03(\x0c\x12\x99\x01\n\x0bvote_choice\x18\x04 \x01(\x0b\x32\x83\x01.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse.GetContestedResourceIdentityVotesResponseV0.ResourceVoteChoiceB\x08\n\x06resultB\t\n\x07version\"\xf0\x01\n%GetPrefundedSpecializedBalanceRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceRequest.GetPrefundedSpecializedBalanceRequestV0H\x00\x1a\x44\n\'GetPrefundedSpecializedBalanceRequestV0\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xed\x02\n&GetPrefundedSpecializedBalanceResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceResponse.GetPrefundedSpecializedBalanceResponseV0H\x00\x1a\xbd\x01\n(GetPrefundedSpecializedBalanceResponseV0\x12\x15\n\x07\x62\x61lance\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xd0\x01\n GetTotalCreditsInPlatformRequest\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformRequest.GetTotalCreditsInPlatformRequestV0H\x00\x1a\x33\n\"GetTotalCreditsInPlatformRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x42\t\n\x07version\"\xd9\x02\n!GetTotalCreditsInPlatformResponse\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformResponse.GetTotalCreditsInPlatformResponseV0H\x00\x1a\xb8\x01\n#GetTotalCreditsInPlatformResponseV0\x12\x15\n\x07\x63redits\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc4\x01\n\x16GetPathElementsRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetPathElementsRequest.GetPathElementsRequestV0H\x00\x1a\x45\n\x18GetPathElementsRequestV0\x12\x0c\n\x04path\x18\x01 \x03(\x0c\x12\x0c\n\x04keys\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xa3\x03\n\x17GetPathElementsResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetPathElementsResponse.GetPathElementsResponseV0H\x00\x1a\xa0\x02\n\x19GetPathElementsResponseV0\x12i\n\x08\x65lements\x18\x01 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetPathElementsResponse.GetPathElementsResponseV0.ElementsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1c\n\x08\x45lements\x12\x10\n\x08\x65lements\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\x81\x01\n\x10GetStatusRequest\x12L\n\x02v0\x18\x01 \x01(\x0b\x32>.org.dash.platform.dapi.v0.GetStatusRequest.GetStatusRequestV0H\x00\x1a\x14\n\x12GetStatusRequestV0B\t\n\x07version\"\xe4\x10\n\x11GetStatusResponse\x12N\n\x02v0\x18\x01 \x01(\x0b\x32@.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0H\x00\x1a\xf3\x0f\n\x13GetStatusResponseV0\x12Y\n\x07version\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version\x12S\n\x04node\x18\x02 \x01(\x0b\x32\x45.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Node\x12U\n\x05\x63hain\x18\x03 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Chain\x12Y\n\x07network\x18\x04 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Network\x12^\n\nstate_sync\x18\x05 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.StateSync\x12S\n\x04time\x18\x06 \x01(\x0b\x32\x45.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Time\x1a\x82\x05\n\x07Version\x12\x63\n\x08software\x18\x01 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Software\x12\x63\n\x08protocol\x18\x02 \x01(\x0b\x32Q.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Protocol\x1a^\n\x08Software\x12\x0c\n\x04\x64\x61pi\x18\x01 \x01(\t\x12\x12\n\x05\x64rive\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x17\n\ntenderdash\x18\x03 \x01(\tH\x01\x88\x01\x01\x42\x08\n\x06_driveB\r\n\x0b_tenderdash\x1a\xcc\x02\n\x08Protocol\x12p\n\ntenderdash\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Protocol.Tenderdash\x12\x66\n\x05\x64rive\x18\x02 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetStatusResponse.GetStatusResponseV0.Version.Protocol.Drive\x1a(\n\nTenderdash\x12\x0b\n\x03p2p\x18\x01 \x01(\r\x12\r\n\x05\x62lock\x18\x02 \x01(\r\x1a<\n\x05\x44rive\x12\x0e\n\x06latest\x18\x03 \x01(\r\x12\x0f\n\x07\x63urrent\x18\x04 \x01(\r\x12\x12\n\nnext_epoch\x18\x05 \x01(\r\x1a\x7f\n\x04Time\x12\x11\n\x05local\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x16\n\x05\x62lock\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x88\x01\x01\x12\x18\n\x07genesis\x18\x03 \x01(\x04\x42\x02\x30\x01H\x01\x88\x01\x01\x12\x12\n\x05\x65poch\x18\x04 \x01(\rH\x02\x88\x01\x01\x42\x08\n\x06_blockB\n\n\x08_genesisB\x08\n\x06_epoch\x1a<\n\x04Node\x12\n\n\x02id\x18\x01 \x01(\x0c\x12\x18\n\x0bpro_tx_hash\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x42\x0e\n\x0c_pro_tx_hash\x1a\xb3\x02\n\x05\x43hain\x12\x13\n\x0b\x63\x61tching_up\x18\x01 \x01(\x08\x12\x19\n\x11latest_block_hash\x18\x02 \x01(\x0c\x12\x17\n\x0flatest_app_hash\x18\x03 \x01(\x0c\x12\x1f\n\x13latest_block_height\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x1b\n\x13\x65\x61rliest_block_hash\x18\x05 \x01(\x0c\x12\x19\n\x11\x65\x61rliest_app_hash\x18\x06 \x01(\x0c\x12!\n\x15\x65\x61rliest_block_height\x18\x07 \x01(\x04\x42\x02\x30\x01\x12!\n\x15max_peer_block_height\x18\t \x01(\x04\x42\x02\x30\x01\x12%\n\x18\x63ore_chain_locked_height\x18\n \x01(\rH\x00\x88\x01\x01\x42\x1b\n\x19_core_chain_locked_height\x1a\x43\n\x07Network\x12\x10\n\x08\x63hain_id\x18\x01 \x01(\t\x12\x13\n\x0bpeers_count\x18\x02 \x01(\r\x12\x11\n\tlistening\x18\x03 \x01(\x08\x1a\x85\x02\n\tStateSync\x12\x1d\n\x11total_synced_time\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1a\n\x0eremaining_time\x18\x02 \x01(\x04\x42\x02\x30\x01\x12\x17\n\x0ftotal_snapshots\x18\x03 \x01(\r\x12\"\n\x16\x63hunk_process_avg_time\x18\x04 \x01(\x04\x42\x02\x30\x01\x12\x1b\n\x0fsnapshot_height\x18\x05 \x01(\x04\x42\x02\x30\x01\x12!\n\x15snapshot_chunks_count\x18\x06 \x01(\x04\x42\x02\x30\x01\x12\x1d\n\x11\x62\x61\x63kfilled_blocks\x18\x07 \x01(\x04\x42\x02\x30\x01\x12!\n\x15\x62\x61\x63kfill_blocks_total\x18\x08 \x01(\x04\x42\x02\x30\x01\x42\t\n\x07version\"\xb1\x01\n\x1cGetCurrentQuorumsInfoRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoRequest.GetCurrentQuorumsInfoRequestV0H\x00\x1a \n\x1eGetCurrentQuorumsInfoRequestV0B\t\n\x07version\"\xa1\x05\n\x1dGetCurrentQuorumsInfoResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse.GetCurrentQuorumsInfoResponseV0H\x00\x1a\x46\n\x0bValidatorV0\x12\x13\n\x0bpro_tx_hash\x18\x01 \x01(\x0c\x12\x0f\n\x07node_ip\x18\x02 \x01(\t\x12\x11\n\tis_banned\x18\x03 \x01(\x08\x1a\xaf\x01\n\x0eValidatorSetV0\x12\x13\n\x0bquorum_hash\x18\x01 \x01(\x0c\x12\x13\n\x0b\x63ore_height\x18\x02 \x01(\r\x12U\n\x07members\x18\x03 \x03(\x0b\x32\x44.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse.ValidatorV0\x12\x1c\n\x14threshold_public_key\x18\x04 \x01(\x0c\x1a\x92\x02\n\x1fGetCurrentQuorumsInfoResponseV0\x12\x15\n\rquorum_hashes\x18\x01 \x03(\x0c\x12\x1b\n\x13\x63urrent_quorum_hash\x18\x02 \x01(\x0c\x12_\n\x0evalidator_sets\x18\x03 \x03(\x0b\x32G.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse.ValidatorSetV0\x12\x1b\n\x13last_block_proposer\x18\x04 \x01(\x0c\x12=\n\x08metadata\x18\x05 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\t\n\x07version\"\xf4\x01\n\x1fGetIdentityTokenBalancesRequest\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetIdentityTokenBalancesRequest.GetIdentityTokenBalancesRequestV0H\x00\x1aZ\n!GetIdentityTokenBalancesRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x11\n\ttoken_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xad\x05\n GetIdentityTokenBalancesResponse\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse.GetIdentityTokenBalancesResponseV0H\x00\x1a\x8f\x04\n\"GetIdentityTokenBalancesResponseV0\x12\x86\x01\n\x0etoken_balances\x18\x01 \x01(\x0b\x32l.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse.GetIdentityTokenBalancesResponseV0.TokenBalancesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aG\n\x11TokenBalanceEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x14\n\x07\x62\x61lance\x18\x02 \x01(\x04H\x00\x88\x01\x01\x42\n\n\x08_balance\x1a\x9a\x01\n\rTokenBalances\x12\x88\x01\n\x0etoken_balances\x18\x01 \x03(\x0b\x32p.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse.GetIdentityTokenBalancesResponseV0.TokenBalanceEntryB\x08\n\x06resultB\t\n\x07version\"\xfc\x01\n!GetIdentitiesTokenBalancesRequest\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesRequest.GetIdentitiesTokenBalancesRequestV0H\x00\x1a\\\n#GetIdentitiesTokenBalancesRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x14\n\x0cidentity_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xf2\x05\n\"GetIdentitiesTokenBalancesResponse\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse.GetIdentitiesTokenBalancesResponseV0H\x00\x1a\xce\x04\n$GetIdentitiesTokenBalancesResponseV0\x12\x9b\x01\n\x17identity_token_balances\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse.GetIdentitiesTokenBalancesResponseV0.IdentityTokenBalancesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aR\n\x19IdentityTokenBalanceEntry\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x14\n\x07\x62\x61lance\x18\x02 \x01(\x04H\x00\x88\x01\x01\x42\n\n\x08_balance\x1a\xb7\x01\n\x15IdentityTokenBalances\x12\x9d\x01\n\x17identity_token_balances\x18\x01 \x03(\x0b\x32|.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse.GetIdentitiesTokenBalancesResponseV0.IdentityTokenBalanceEntryB\x08\n\x06resultB\t\n\x07version\"\xe8\x01\n\x1cGetIdentityTokenInfosRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetIdentityTokenInfosRequest.GetIdentityTokenInfosRequestV0H\x00\x1aW\n\x1eGetIdentityTokenInfosRequestV0\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x11\n\ttoken_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\x98\x06\n\x1dGetIdentityTokenInfosResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0H\x00\x1a\x83\x05\n\x1fGetIdentityTokenInfosResponseV0\x12z\n\x0btoken_infos\x18\x01 \x01(\x0b\x32\x63.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0.TokenInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a(\n\x16TokenIdentityInfoEntry\x12\x0e\n\x06\x66rozen\x18\x01 \x01(\x08\x1a\xb0\x01\n\x0eTokenInfoEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x82\x01\n\x04info\x18\x02 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0.TokenIdentityInfoEntryH\x00\x88\x01\x01\x42\x07\n\x05_info\x1a\x8a\x01\n\nTokenInfos\x12|\n\x0btoken_infos\x18\x01 \x03(\x0b\x32g.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse.GetIdentityTokenInfosResponseV0.TokenInfoEntryB\x08\n\x06resultB\t\n\x07version\"\xf0\x01\n\x1eGetIdentitiesTokenInfosRequest\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosRequest.GetIdentitiesTokenInfosRequestV0H\x00\x1aY\n GetIdentitiesTokenInfosRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x14\n\x0cidentity_ids\x18\x02 \x03(\x0c\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xca\x06\n\x1fGetIdentitiesTokenInfosResponse\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0H\x00\x1a\xaf\x05\n!GetIdentitiesTokenInfosResponseV0\x12\x8f\x01\n\x14identity_token_infos\x18\x01 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0.IdentityTokenInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a(\n\x16TokenIdentityInfoEntry\x12\x0e\n\x06\x66rozen\x18\x01 \x01(\x08\x1a\xb7\x01\n\x0eTokenInfoEntry\x12\x13\n\x0bidentity_id\x18\x01 \x01(\x0c\x12\x86\x01\n\x04info\x18\x02 \x01(\x0b\x32s.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0.TokenIdentityInfoEntryH\x00\x88\x01\x01\x42\x07\n\x05_info\x1a\x97\x01\n\x12IdentityTokenInfos\x12\x80\x01\n\x0btoken_infos\x18\x01 \x03(\x0b\x32k.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse.GetIdentitiesTokenInfosResponseV0.TokenInfoEntryB\x08\n\x06resultB\t\n\x07version\"\xbf\x01\n\x17GetTokenStatusesRequest\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetTokenStatusesRequest.GetTokenStatusesRequestV0H\x00\x1a=\n\x19GetTokenStatusesRequestV0\x12\x11\n\ttoken_ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xe7\x04\n\x18GetTokenStatusesResponse\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetTokenStatusesResponse.GetTokenStatusesResponseV0H\x00\x1a\xe1\x03\n\x1aGetTokenStatusesResponseV0\x12v\n\x0etoken_statuses\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetTokenStatusesResponse.GetTokenStatusesResponseV0.TokenStatusesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x44\n\x10TokenStatusEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x13\n\x06paused\x18\x02 \x01(\x08H\x00\x88\x01\x01\x42\t\n\x07_paused\x1a\x88\x01\n\rTokenStatuses\x12w\n\x0etoken_statuses\x18\x01 \x03(\x0b\x32_.org.dash.platform.dapi.v0.GetTokenStatusesResponse.GetTokenStatusesResponseV0.TokenStatusEntryB\x08\n\x06resultB\t\n\x07version\"\xef\x01\n#GetTokenDirectPurchasePricesRequest\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesRequest.GetTokenDirectPurchasePricesRequestV0H\x00\x1aI\n%GetTokenDirectPurchasePricesRequestV0\x12\x11\n\ttoken_ids\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x8b\t\n$GetTokenDirectPurchasePricesResponse\x12t\n\x02v0\x18\x01 \x01(\x0b\x32\x66.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0H\x00\x1a\xe1\x07\n&GetTokenDirectPurchasePricesResponseV0\x12\xa9\x01\n\x1ctoken_direct_purchase_prices\x18\x01 \x01(\x0b\x32\x80\x01.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.TokenDirectPurchasePricesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x33\n\x10PriceForQuantity\x12\x10\n\x08quantity\x18\x01 \x01(\x04\x12\r\n\x05price\x18\x02 \x01(\x04\x1a\xa7\x01\n\x0fPricingSchedule\x12\x93\x01\n\x12price_for_quantity\x18\x01 \x03(\x0b\x32w.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.PriceForQuantity\x1a\xe4\x01\n\x1dTokenDirectPurchasePriceEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x15\n\x0b\x66ixed_price\x18\x02 \x01(\x04H\x00\x12\x90\x01\n\x0evariable_price\x18\x03 \x01(\x0b\x32v.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.PricingScheduleH\x00\x42\x07\n\x05price\x1a\xc8\x01\n\x19TokenDirectPurchasePrices\x12\xaa\x01\n\x1btoken_direct_purchase_price\x18\x01 \x03(\x0b\x32\x84\x01.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse.GetTokenDirectPurchasePricesResponseV0.TokenDirectPurchasePriceEntryB\x08\n\x06resultB\t\n\x07version\"\xce\x01\n\x1bGetTokenContractInfoRequest\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetTokenContractInfoRequest.GetTokenContractInfoRequestV0H\x00\x1a@\n\x1dGetTokenContractInfoRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xfb\x03\n\x1cGetTokenContractInfoResponse\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetTokenContractInfoResponse.GetTokenContractInfoResponseV0H\x00\x1a\xe9\x02\n\x1eGetTokenContractInfoResponseV0\x12|\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32l.org.dash.platform.dapi.v0.GetTokenContractInfoResponse.GetTokenContractInfoResponseV0.TokenContractInfoDataH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aM\n\x15TokenContractInfoData\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17token_contract_position\x18\x02 \x01(\rB\x08\n\x06resultB\t\n\x07version\"\xef\x04\n)GetTokenPreProgrammedDistributionsRequest\x12~\n\x02v0\x18\x01 \x01(\x0b\x32p.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsRequest.GetTokenPreProgrammedDistributionsRequestV0H\x00\x1a\xb6\x03\n+GetTokenPreProgrammedDistributionsRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x98\x01\n\rstart_at_info\x18\x02 \x01(\x0b\x32|.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsRequest.GetTokenPreProgrammedDistributionsRequestV0.StartAtInfoH\x00\x88\x01\x01\x12\x12\n\x05limit\x18\x03 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x04 \x01(\x08\x1a\x9a\x01\n\x0bStartAtInfo\x12\x15\n\rstart_time_ms\x18\x01 \x01(\x04\x12\x1c\n\x0fstart_recipient\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x12%\n\x18start_recipient_included\x18\x03 \x01(\x08H\x01\x88\x01\x01\x42\x12\n\x10_start_recipientB\x1b\n\x19_start_recipient_includedB\x10\n\x0e_start_at_infoB\x08\n\x06_limitB\t\n\x07version\"\xec\x07\n*GetTokenPreProgrammedDistributionsResponse\x12\x80\x01\n\x02v0\x18\x01 \x01(\x0b\x32r.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0H\x00\x1a\xaf\x06\n,GetTokenPreProgrammedDistributionsResponseV0\x12\xa5\x01\n\x13token_distributions\x18\x01 \x01(\x0b\x32\x85\x01.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0.TokenDistributionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a>\n\x16TokenDistributionEntry\x12\x14\n\x0crecipient_id\x18\x01 \x01(\x0c\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x04\x1a\xd4\x01\n\x1bTokenTimedDistributionEntry\x12\x11\n\ttimestamp\x18\x01 \x01(\x04\x12\xa1\x01\n\rdistributions\x18\x02 \x03(\x0b\x32\x89\x01.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0.TokenDistributionEntry\x1a\xc3\x01\n\x12TokenDistributions\x12\xac\x01\n\x13token_distributions\x18\x01 \x03(\x0b\x32\x8e\x01.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse.GetTokenPreProgrammedDistributionsResponseV0.TokenTimedDistributionEntryB\x08\n\x06resultB\t\n\x07version\"\x82\x04\n-GetTokenPerpetualDistributionLastClaimRequest\x12\x86\x01\n\x02v0\x18\x01 \x01(\x0b\x32x.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimRequest.GetTokenPerpetualDistributionLastClaimRequestV0H\x00\x1aI\n\x11\x43ontractTokenInfo\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17token_contract_position\x18\x02 \x01(\r\x1a\xf1\x01\n/GetTokenPerpetualDistributionLastClaimRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12v\n\rcontract_info\x18\x02 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimRequest.ContractTokenInfoH\x00\x88\x01\x01\x12\x13\n\x0bidentity_id\x18\x04 \x01(\x0c\x12\r\n\x05prove\x18\x05 \x01(\x08\x42\x10\n\x0e_contract_infoB\t\n\x07version\"\x93\x05\n.GetTokenPerpetualDistributionLastClaimResponse\x12\x88\x01\n\x02v0\x18\x01 \x01(\x0b\x32z.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimResponse.GetTokenPerpetualDistributionLastClaimResponseV0H\x00\x1a\xca\x03\n0GetTokenPerpetualDistributionLastClaimResponseV0\x12\x9f\x01\n\nlast_claim\x18\x01 \x01(\x0b\x32\x88\x01.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimResponse.GetTokenPerpetualDistributionLastClaimResponseV0.LastClaimInfoH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1ax\n\rLastClaimInfo\x12\x1a\n\x0ctimestamp_ms\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x1a\n\x0c\x62lock_height\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12\x0f\n\x05\x65poch\x18\x03 \x01(\rH\x00\x12\x13\n\traw_bytes\x18\x04 \x01(\x0cH\x00\x42\t\n\x07paid_atB\x08\n\x06resultB\t\n\x07version\"\xca\x01\n\x1aGetTokenTotalSupplyRequest\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetTokenTotalSupplyRequest.GetTokenTotalSupplyRequestV0H\x00\x1a?\n\x1cGetTokenTotalSupplyRequestV0\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xaf\x04\n\x1bGetTokenTotalSupplyResponse\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetTokenTotalSupplyResponse.GetTokenTotalSupplyResponseV0H\x00\x1a\xa0\x03\n\x1dGetTokenTotalSupplyResponseV0\x12\x88\x01\n\x12token_total_supply\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetTokenTotalSupplyResponse.GetTokenTotalSupplyResponseV0.TokenTotalSupplyEntryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1ax\n\x15TokenTotalSupplyEntry\x12\x10\n\x08token_id\x18\x01 \x01(\x0c\x12\x30\n(total_aggregated_amount_in_user_accounts\x18\x02 \x01(\x04\x12\x1b\n\x13total_system_amount\x18\x03 \x01(\x04\x42\x08\n\x06resultB\t\n\x07version\"\xd2\x01\n\x13GetGroupInfoRequest\x12R\n\x02v0\x18\x01 \x01(\x0b\x32\x44.org.dash.platform.dapi.v0.GetGroupInfoRequest.GetGroupInfoRequestV0H\x00\x1a\\\n\x15GetGroupInfoRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17group_contract_position\x18\x02 \x01(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x42\t\n\x07version\"\xd4\x05\n\x14GetGroupInfoResponse\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0H\x00\x1a\xda\x04\n\x16GetGroupInfoResponseV0\x12\x66\n\ngroup_info\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0.GroupInfoH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x04 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x34\n\x10GroupMemberEntry\x12\x11\n\tmember_id\x18\x01 \x01(\x0c\x12\r\n\x05power\x18\x02 \x01(\r\x1a\x98\x01\n\x0eGroupInfoEntry\x12h\n\x07members\x18\x01 \x03(\x0b\x32W.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0.GroupMemberEntry\x12\x1c\n\x14group_required_power\x18\x02 \x01(\r\x1a\x8a\x01\n\tGroupInfo\x12n\n\ngroup_info\x18\x01 \x01(\x0b\x32U.org.dash.platform.dapi.v0.GetGroupInfoResponse.GetGroupInfoResponseV0.GroupInfoEntryH\x00\x88\x01\x01\x42\r\n\x0b_group_infoB\x08\n\x06resultB\t\n\x07version\"\xed\x03\n\x14GetGroupInfosRequest\x12T\n\x02v0\x18\x01 \x01(\x0b\x32\x46.org.dash.platform.dapi.v0.GetGroupInfosRequest.GetGroupInfosRequestV0H\x00\x1au\n\x1cStartAtGroupContractPosition\x12%\n\x1dstart_group_contract_position\x18\x01 \x01(\r\x12.\n&start_group_contract_position_included\x18\x02 \x01(\x08\x1a\xfc\x01\n\x16GetGroupInfosRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12{\n start_at_group_contract_position\x18\x02 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetGroupInfosRequest.StartAtGroupContractPositionH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x03 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x04 \x01(\x08\x42#\n!_start_at_group_contract_positionB\x08\n\x06_countB\t\n\x07version\"\xff\x05\n\x15GetGroupInfosResponse\x12V\n\x02v0\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0H\x00\x1a\x82\x05\n\x17GetGroupInfosResponseV0\x12j\n\x0bgroup_infos\x18\x01 \x01(\x0b\x32S.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0.GroupInfosH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x04 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x34\n\x10GroupMemberEntry\x12\x11\n\tmember_id\x18\x01 \x01(\x0c\x12\r\n\x05power\x18\x02 \x01(\r\x1a\xc3\x01\n\x16GroupPositionInfoEntry\x12\x1f\n\x17group_contract_position\x18\x01 \x01(\r\x12j\n\x07members\x18\x02 \x03(\x0b\x32Y.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0.GroupMemberEntry\x12\x1c\n\x14group_required_power\x18\x03 \x01(\r\x1a\x82\x01\n\nGroupInfos\x12t\n\x0bgroup_infos\x18\x01 \x03(\x0b\x32_.org.dash.platform.dapi.v0.GetGroupInfosResponse.GetGroupInfosResponseV0.GroupPositionInfoEntryB\x08\n\x06resultB\t\n\x07version\"\xbe\x04\n\x16GetGroupActionsRequest\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetGroupActionsRequest.GetGroupActionsRequestV0H\x00\x1aL\n\x0fStartAtActionId\x12\x17\n\x0fstart_action_id\x18\x01 \x01(\x0c\x12 \n\x18start_action_id_included\x18\x02 \x01(\x08\x1a\xc8\x02\n\x18GetGroupActionsRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17group_contract_position\x18\x02 \x01(\r\x12N\n\x06status\x18\x03 \x01(\x0e\x32>.org.dash.platform.dapi.v0.GetGroupActionsRequest.ActionStatus\x12\x62\n\x12start_at_action_id\x18\x04 \x01(\x0b\x32\x41.org.dash.platform.dapi.v0.GetGroupActionsRequest.StartAtActionIdH\x00\x88\x01\x01\x12\x12\n\x05\x63ount\x18\x05 \x01(\rH\x01\x88\x01\x01\x12\r\n\x05prove\x18\x06 \x01(\x08\x42\x15\n\x13_start_at_action_idB\x08\n\x06_count\"&\n\x0c\x41\x63tionStatus\x12\n\n\x06\x41\x43TIVE\x10\x00\x12\n\n\x06\x43LOSED\x10\x01\x42\t\n\x07version\"\x92\"\n\x17GetGroupActionsResponse\x12Z\n\x02v0\x18\x01 \x01(\x0b\x32L.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0H\x00\x1a\x8f!\n\x19GetGroupActionsResponseV0\x12r\n\rgroup_actions\x18\x01 \x01(\x0b\x32Y.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a[\n\tMintEvent\x12\x0e\n\x06\x61mount\x18\x01 \x01(\x04\x12\x14\n\x0crecipient_id\x18\x02 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a[\n\tBurnEvent\x12\x0e\n\x06\x61mount\x18\x01 \x01(\x04\x12\x14\n\x0c\x62urn_from_id\x18\x02 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1aJ\n\x0b\x46reezeEvent\x12\x11\n\tfrozen_id\x18\x01 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1aL\n\rUnfreezeEvent\x12\x11\n\tfrozen_id\x18\x01 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\x66\n\x17\x44\x65stroyFrozenFundsEvent\x12\x11\n\tfrozen_id\x18\x01 \x01(\x0c\x12\x0e\n\x06\x61mount\x18\x02 \x01(\x04\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\x64\n\x13SharedEncryptedNote\x12\x18\n\x10sender_key_index\x18\x01 \x01(\r\x12\x1b\n\x13recipient_key_index\x18\x02 \x01(\r\x12\x16\n\x0e\x65ncrypted_data\x18\x03 \x01(\x0c\x1a{\n\x15PersonalEncryptedNote\x12!\n\x19root_encryption_key_index\x18\x01 \x01(\r\x12\'\n\x1f\x64\x65rivation_encryption_key_index\x18\x02 \x01(\r\x12\x16\n\x0e\x65ncrypted_data\x18\x03 \x01(\x0c\x1a\xe9\x01\n\x14\x45mergencyActionEvent\x12\x81\x01\n\x0b\x61\x63tion_type\x18\x01 \x01(\x0e\x32l.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.EmergencyActionEvent.ActionType\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\"#\n\nActionType\x12\t\n\x05PAUSE\x10\x00\x12\n\n\x06RESUME\x10\x01\x42\x0e\n\x0c_public_note\x1a\x64\n\x16TokenConfigUpdateEvent\x12 \n\x18token_config_update_item\x18\x01 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x02 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\xe6\x03\n\x1eUpdateDirectPurchasePriceEvent\x12\x15\n\x0b\x66ixed_price\x18\x01 \x01(\x04H\x00\x12\x95\x01\n\x0evariable_price\x18\x02 \x01(\x0b\x32{.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.PricingScheduleH\x00\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x01\x88\x01\x01\x1a\x33\n\x10PriceForQuantity\x12\x10\n\x08quantity\x18\x01 \x01(\x04\x12\r\n\x05price\x18\x02 \x01(\x04\x1a\xac\x01\n\x0fPricingSchedule\x12\x98\x01\n\x12price_for_quantity\x18\x01 \x03(\x0b\x32|.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.PriceForQuantityB\x07\n\x05priceB\x0e\n\x0c_public_note\x1a\x63\n\x0fMintToPoolEvent\x12\x0e\n\x06\x61mount\x18\x01 \x01(\x04\x12\x16\n\x0e\x61\x63tions_digest\x18\x02 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\x65\n\x11\x42urnFromPoolEvent\x12\x0e\n\x06\x61mount\x18\x01 \x01(\x04\x12\x16\n\x0e\x61\x63tions_digest\x18\x02 \x01(\x0c\x12\x18\n\x0bpublic_note\x18\x03 \x01(\tH\x00\x88\x01\x01\x42\x0e\n\x0c_public_note\x1a\xfc\x02\n\x10GroupActionEvent\x12n\n\x0btoken_event\x18\x01 \x01(\x0b\x32W.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEventH\x00\x12t\n\x0e\x64ocument_event\x18\x02 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.DocumentEventH\x00\x12t\n\x0e\x63ontract_event\x18\x03 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ContractEventH\x00\x42\x0c\n\nevent_type\x1a\x8b\x01\n\rDocumentEvent\x12r\n\x06\x63reate\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.DocumentCreateEventH\x00\x42\x06\n\x04type\x1a/\n\x13\x44ocumentCreateEvent\x12\x18\n\x10\x63reated_document\x18\x01 \x01(\x0c\x1a/\n\x13\x43ontractUpdateEvent\x12\x18\n\x10updated_contract\x18\x01 \x01(\x0c\x1a\x8b\x01\n\rContractEvent\x12r\n\x06update\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.ContractUpdateEventH\x00\x42\x06\n\x04type\x1a\xc1\t\n\nTokenEvent\x12\x66\n\x04mint\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintEventH\x00\x12\x66\n\x04\x62urn\x18\x02 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnEventH\x00\x12j\n\x06\x66reeze\x18\x03 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.FreezeEventH\x00\x12n\n\x08unfreeze\x18\x04 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UnfreezeEventH\x00\x12\x84\x01\n\x14\x64\x65stroy_frozen_funds\x18\x05 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.DestroyFrozenFundsEventH\x00\x12}\n\x10\x65mergency_action\x18\x06 \x01(\x0b\x32\x61.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.EmergencyActionEventH\x00\x12\x82\x01\n\x13token_config_update\x18\x07 \x01(\x0b\x32\x63.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenConfigUpdateEventH\x00\x12\x83\x01\n\x0cupdate_price\x18\x08 \x01(\x0b\x32k.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEventH\x00\x12t\n\x0cmint_to_pool\x18\t \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEventH\x00\x12x\n\x0e\x62urn_from_pool\x18\n \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEventH\x00\x42\x06\n\x04type\x1a\x93\x01\n\x10GroupActionEntry\x12\x11\n\taction_id\x18\x01 \x01(\x0c\x12l\n\x05\x65vent\x18\x02 \x01(\x0b\x32].org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionEvent\x1a\x84\x01\n\x0cGroupActions\x12t\n\rgroup_actions\x18\x01 \x03(\x0b\x32].org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionEntryB\x08\n\x06resultB\t\n\x07version\"\x88\x03\n\x1cGetGroupActionSignersRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetGroupActionSignersRequest.GetGroupActionSignersRequestV0H\x00\x1a\xce\x01\n\x1eGetGroupActionSignersRequestV0\x12\x13\n\x0b\x63ontract_id\x18\x01 \x01(\x0c\x12\x1f\n\x17group_contract_position\x18\x02 \x01(\r\x12T\n\x06status\x18\x03 \x01(\x0e\x32\x44.org.dash.platform.dapi.v0.GetGroupActionSignersRequest.ActionStatus\x12\x11\n\taction_id\x18\x04 \x01(\x0c\x12\r\n\x05prove\x18\x05 \x01(\x08\"&\n\x0c\x41\x63tionStatus\x12\n\n\x06\x41\x43TIVE\x10\x00\x12\n\n\x06\x43LOSED\x10\x01\x42\t\n\x07version\"\x8b\x05\n\x1dGetGroupActionSignersResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetGroupActionSignersResponse.GetGroupActionSignersResponseV0H\x00\x1a\xf6\x03\n\x1fGetGroupActionSignersResponseV0\x12\x8b\x01\n\x14group_action_signers\x18\x01 \x01(\x0b\x32k.org.dash.platform.dapi.v0.GetGroupActionSignersResponse.GetGroupActionSignersResponseV0.GroupActionSignersH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x35\n\x11GroupActionSigner\x12\x11\n\tsigner_id\x18\x01 \x01(\x0c\x12\r\n\x05power\x18\x02 \x01(\r\x1a\x91\x01\n\x12GroupActionSigners\x12{\n\x07signers\x18\x01 \x03(\x0b\x32j.org.dash.platform.dapi.v0.GetGroupActionSignersResponse.GetGroupActionSignersResponseV0.GroupActionSignerB\x08\n\x06resultB\t\n\x07version\"\xb5\x01\n\x15GetAddressInfoRequest\x12V\n\x02v0\x18\x01 \x01(\x0b\x32H.org.dash.platform.dapi.v0.GetAddressInfoRequest.GetAddressInfoRequestV0H\x00\x1a\x39\n\x17GetAddressInfoRequestV0\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\x85\x01\n\x10\x41\x64\x64ressInfoEntry\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12J\n\x11\x62\x61lance_and_nonce\x18\x02 \x01(\x0b\x32*.org.dash.platform.dapi.v0.BalanceAndNonceH\x00\x88\x01\x01\x42\x14\n\x12_balance_and_nonce\"1\n\x0f\x42\x61lanceAndNonce\x12\x0f\n\x07\x62\x61lance\x18\x01 \x01(\x04\x12\r\n\x05nonce\x18\x02 \x01(\r\"_\n\x12\x41\x64\x64ressInfoEntries\x12I\n\x14\x61\x64\x64ress_info_entries\x18\x01 \x03(\x0b\x32+.org.dash.platform.dapi.v0.AddressInfoEntry\"m\n\x14\x41\x64\x64ressBalanceChange\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12\x19\n\x0bset_balance\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12\x1c\n\x0e\x61\x64\x64_to_balance\x18\x03 \x01(\x04\x42\x02\x30\x01H\x00\x42\x0b\n\toperation\"x\n\x1a\x42lockAddressBalanceChanges\x12\x18\n\x0c\x62lock_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12@\n\x07\x63hanges\x18\x02 \x03(\x0b\x32/.org.dash.platform.dapi.v0.AddressBalanceChange\"k\n\x1b\x41\x64\x64ressBalanceUpdateEntries\x12L\n\rblock_changes\x18\x01 \x03(\x0b\x32\x35.org.dash.platform.dapi.v0.BlockAddressBalanceChanges\"\xe1\x02\n\x16GetAddressInfoResponse\x12X\n\x02v0\x18\x01 \x01(\x0b\x32J.org.dash.platform.dapi.v0.GetAddressInfoResponse.GetAddressInfoResponseV0H\x00\x1a\xe1\x01\n\x18GetAddressInfoResponseV0\x12I\n\x12\x61\x64\x64ress_info_entry\x18\x01 \x01(\x0b\x32+.org.dash.platform.dapi.v0.AddressInfoEntryH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xc3\x01\n\x18GetAddressesInfosRequest\x12\\\n\x02v0\x18\x01 \x01(\x0b\x32N.org.dash.platform.dapi.v0.GetAddressesInfosRequest.GetAddressesInfosRequestV0H\x00\x1a>\n\x1aGetAddressesInfosRequestV0\x12\x11\n\taddresses\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf1\x02\n\x19GetAddressesInfosResponse\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetAddressesInfosResponse.GetAddressesInfosResponseV0H\x00\x1a\xe8\x01\n\x1bGetAddressesInfosResponseV0\x12M\n\x14\x61\x64\x64ress_info_entries\x18\x01 \x01(\x0b\x32-.org.dash.platform.dapi.v0.AddressInfoEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xb5\x01\n\x1dGetAddressesTrunkStateRequest\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetAddressesTrunkStateRequest.GetAddressesTrunkStateRequestV0H\x00\x1a!\n\x1fGetAddressesTrunkStateRequestV0B\t\n\x07version\"\xaa\x02\n\x1eGetAddressesTrunkStateResponse\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetAddressesTrunkStateResponse.GetAddressesTrunkStateResponseV0H\x00\x1a\x92\x01\n GetAddressesTrunkStateResponseV0\x12/\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.Proof\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\t\n\x07version\"\xf0\x01\n\x1eGetAddressesBranchStateRequest\x12h\n\x02v0\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetAddressesBranchStateRequest.GetAddressesBranchStateRequestV0H\x00\x1aY\n GetAddressesBranchStateRequestV0\x12\x0b\n\x03key\x18\x01 \x01(\x0c\x12\r\n\x05\x64\x65pth\x18\x02 \x01(\r\x12\x19\n\x11\x63heckpoint_height\x18\x03 \x01(\x04\x42\t\n\x07version\"\xd1\x01\n\x1fGetAddressesBranchStateResponse\x12j\n\x02v0\x18\x01 \x01(\x0b\x32\\.org.dash.platform.dapi.v0.GetAddressesBranchStateResponse.GetAddressesBranchStateResponseV0H\x00\x1a\x37\n!GetAddressesBranchStateResponseV0\x12\x12\n\nmerk_proof\x18\x02 \x01(\x0c\x42\t\n\x07version\"\x9e\x02\n%GetRecentAddressBalanceChangesRequest\x12v\n\x02v0\x18\x01 \x01(\x0b\x32h.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesRequest.GetRecentAddressBalanceChangesRequestV0H\x00\x1ar\n\'GetRecentAddressBalanceChangesRequestV0\x12\x18\n\x0cstart_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x02 \x01(\x08\x12\x1e\n\x16start_height_exclusive\x18\x03 \x01(\x08\x42\t\n\x07version\"\xb8\x03\n&GetRecentAddressBalanceChangesResponse\x12x\n\x02v0\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesResponse.GetRecentAddressBalanceChangesResponseV0H\x00\x1a\x88\x02\n(GetRecentAddressBalanceChangesResponseV0\x12`\n\x1e\x61\x64\x64ress_balance_update_entries\x18\x01 \x01(\x0b\x32\x36.org.dash.platform.dapi.v0.AddressBalanceUpdateEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"G\n\x16\x42lockHeightCreditEntry\x12\x18\n\x0c\x62lock_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x13\n\x07\x63redits\x18\x02 \x01(\x04\x42\x02\x30\x01\"\xb0\x01\n\x1d\x43ompactedAddressBalanceChange\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\x0c\x12\x19\n\x0bset_credits\x18\x02 \x01(\x04\x42\x02\x30\x01H\x00\x12V\n\x19\x61\x64\x64_to_credits_operations\x18\x03 \x01(\x0b\x32\x31.org.dash.platform.dapi.v0.AddToCreditsOperationsH\x00\x42\x0b\n\toperation\"\\\n\x16\x41\x64\x64ToCreditsOperations\x12\x42\n\x07\x65ntries\x18\x01 \x03(\x0b\x32\x31.org.dash.platform.dapi.v0.BlockHeightCreditEntry\"\xae\x01\n#CompactedBlockAddressBalanceChanges\x12\x1e\n\x12start_block_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\x1c\n\x10\x65nd_block_height\x18\x02 \x01(\x04\x42\x02\x30\x01\x12I\n\x07\x63hanges\x18\x03 \x03(\x0b\x32\x38.org.dash.platform.dapi.v0.CompactedAddressBalanceChange\"\x87\x01\n$CompactedAddressBalanceUpdateEntries\x12_\n\x17\x63ompacted_block_changes\x18\x01 \x03(\x0b\x32>.org.dash.platform.dapi.v0.CompactedBlockAddressBalanceChanges\"\xa9\x02\n.GetRecentCompactedAddressBalanceChangesRequest\x12\x88\x01\n\x02v0\x18\x01 \x01(\x0b\x32z.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesRequest.GetRecentCompactedAddressBalanceChangesRequestV0H\x00\x1a\x61\n0GetRecentCompactedAddressBalanceChangesRequestV0\x12\x1e\n\x12start_block_height\x18\x01 \x01(\x04\x42\x02\x30\x01\x12\r\n\x05prove\x18\x02 \x01(\x08\x42\t\n\x07version\"\xf0\x03\n/GetRecentCompactedAddressBalanceChangesResponse\x12\x8a\x01\n\x02v0\x18\x01 \x01(\x0b\x32|.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesResponse.GetRecentCompactedAddressBalanceChangesResponseV0H\x00\x1a\xa4\x02\n1GetRecentCompactedAddressBalanceChangesResponseV0\x12s\n(compacted_address_balance_update_entries\x18\x01 \x01(\x0b\x32?.org.dash.platform.dapi.v0.CompactedAddressBalanceUpdateEntriesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\x98\x02\n GetShieldedEncryptedNotesRequest\x12l\n\x02v0\x18\x01 \x01(\x0b\x32^.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0H\x00\x1a{\n\"GetShieldedEncryptedNotesRequestV0\x12\x13\n\x0bstart_index\x18\x01 \x01(\x04\x12\r\n\x05\x63ount\x18\x02 \x01(\r\x12\r\n\x05prove\x18\x03 \x01(\x08\x12\x15\n\x08token_id\x18\x04 \x01(\x0cH\x00\x88\x01\x01\x42\x0b\n\t_token_idB\t\n\x07version\"\xbc\x05\n!GetShieldedEncryptedNotesResponse\x12n\n\x02v0\x18\x01 \x01(\x0b\x32`.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse.GetShieldedEncryptedNotesResponseV0H\x00\x1a\x9b\x04\n#GetShieldedEncryptedNotesResponseV0\x12\x8a\x01\n\x0f\x65ncrypted_notes\x18\x01 \x01(\x0b\x32o.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse.GetShieldedEncryptedNotesResponseV0.EncryptedNotesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1aW\n\rEncryptedNote\x12\x11\n\tnullifier\x18\x01 \x01(\x0c\x12\x0b\n\x03\x63mx\x18\x02 \x01(\x0c\x12\x16\n\x0e\x65ncrypted_note\x18\x03 \x01(\x0c\x12\x0e\n\x06\x63v_net\x18\x04 \x01(\x0c\x1a\x91\x01\n\x0e\x45ncryptedNotes\x12\x7f\n\x07\x65ntries\x18\x01 \x03(\x0b\x32n.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse.GetShieldedEncryptedNotesResponseV0.EncryptedNoteB\x08\n\x06resultB\t\n\x07version\"\xd8\x01\n\x19GetShieldedAnchorsRequest\x12^\n\x02v0\x18\x01 \x01(\x0b\x32P.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0H\x00\x1aP\n\x1bGetShieldedAnchorsRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x12\x15\n\x08token_id\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x42\x0b\n\t_token_idB\t\n\x07version\"\xb1\x03\n\x1aGetShieldedAnchorsResponse\x12`\n\x02v0\x18\x01 \x01(\x0b\x32R.org.dash.platform.dapi.v0.GetShieldedAnchorsResponse.GetShieldedAnchorsResponseV0H\x00\x1a\xa5\x02\n\x1cGetShieldedAnchorsResponseV0\x12m\n\x07\x61nchors\x18\x01 \x01(\x0b\x32Z.org.dash.platform.dapi.v0.GetShieldedAnchorsResponse.GetShieldedAnchorsResponseV0.AnchorsH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x1a\n\x07\x41nchors\x12\x0f\n\x07\x61nchors\x18\x01 \x03(\x0c\x42\x08\n\x06resultB\t\n\x07version\"\xfc\x01\n\"GetMostRecentShieldedAnchorRequest\x12p\n\x02v0\x18\x01 \x01(\x0b\x32\x62.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0H\x00\x1aY\n$GetMostRecentShieldedAnchorRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x12\x15\n\x08token_id\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x42\x0b\n\t_token_idB\t\n\x07version\"\xdc\x02\n#GetMostRecentShieldedAnchorResponse\x12r\n\x02v0\x18\x01 \x01(\x0b\x32\x64.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorResponse.GetMostRecentShieldedAnchorResponseV0H\x00\x1a\xb5\x01\n%GetMostRecentShieldedAnchorResponseV0\x12\x10\n\x06\x61nchor\x18\x01 \x01(\x0cH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xe0\x01\n\x1bGetShieldedPoolStateRequest\x12\x62\n\x02v0\x18\x01 \x01(\x0b\x32T.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0H\x00\x1aR\n\x1dGetShieldedPoolStateRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x12\x15\n\x08token_id\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x42\x0b\n\t_token_idB\t\n\x07version\"\xcb\x02\n\x1cGetShieldedPoolStateResponse\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetShieldedPoolStateResponse.GetShieldedPoolStateResponseV0H\x00\x1a\xb9\x01\n\x1eGetShieldedPoolStateResponseV0\x12\x1b\n\rtotal_balance\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xe4\x01\n\x1cGetShieldedNotesCountRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0H\x00\x1aS\n\x1eGetShieldedNotesCountRequestV0\x12\r\n\x05prove\x18\x01 \x01(\x08\x12\x15\n\x08token_id\x18\x02 \x01(\x0cH\x00\x88\x01\x01\x42\x0b\n\t_token_idB\t\n\x07version\"\xd3\x02\n\x1dGetShieldedNotesCountResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetShieldedNotesCountResponse.GetShieldedNotesCountResponseV0H\x00\x1a\xbe\x01\n\x1fGetShieldedNotesCountResponseV0\x12\x1f\n\x11total_notes_count\x18\x01 \x01(\x04\x42\x02\x30\x01H\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadataB\x08\n\x06resultB\t\n\x07version\"\xf8\x01\n\x1cGetShieldedNullifiersRequest\x12\x64\n\x02v0\x18\x01 \x01(\x0b\x32V.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0H\x00\x1ag\n\x1eGetShieldedNullifiersRequestV0\x12\x12\n\nnullifiers\x18\x01 \x03(\x0c\x12\r\n\x05prove\x18\x02 \x01(\x08\x12\x15\n\x08token_id\x18\x03 \x01(\x0cH\x00\x88\x01\x01\x42\x0b\n\t_token_idB\t\n\x07version\"\x86\x05\n\x1dGetShieldedNullifiersResponse\x12\x66\n\x02v0\x18\x01 \x01(\x0b\x32X.org.dash.platform.dapi.v0.GetShieldedNullifiersResponse.GetShieldedNullifiersResponseV0H\x00\x1a\xf1\x03\n\x1fGetShieldedNullifiersResponseV0\x12\x88\x01\n\x12nullifier_statuses\x18\x01 \x01(\x0b\x32j.org.dash.platform.dapi.v0.GetShieldedNullifiersResponse.GetShieldedNullifiersResponseV0.NullifierStatusesH\x00\x12\x31\n\x05proof\x18\x02 \x01(\x0b\x32 .org.dash.platform.dapi.v0.ProofH\x00\x12=\n\x08metadata\x18\x03 \x01(\x0b\x32+.org.dash.platform.dapi.v0.ResponseMetadata\x1a\x36\n\x0fNullifierStatus\x12\x11\n\tnullifier\x18\x01 \x01(\x0c\x12\x10\n\x08is_spent\x18\x02 \x01(\x08\x1a\x8e\x01\n\x11NullifierStatuses\x12y\n\x07\x65ntries\x18\x01 \x03(\x0b\x32h.org.dash.platform.dapi.v0.GetShieldedNullifiersResponse.GetShieldedNullifiersResponseV0.NullifierStatusB\x08\n\x06resultB\t\n\x07version*Z\n\nKeyPurpose\x12\x12\n\x0e\x41UTHENTICATION\x10\x00\x12\x0e\n\nENCRYPTION\x10\x01\x12\x0e\n\nDECRYPTION\x10\x02\x12\x0c\n\x08TRANSFER\x10\x03\x12\n\n\x06VOTING\x10\x05*\xb9\x01\n\x16\x43ontractModerationList\x12(\n$CONTRACT_MODERATION_LIST_UNSPECIFIED\x10\x00\x12$\n CONTRACT_MODERATION_LIST_BANLIST\x10\x01\x12(\n$CONTRACT_MODERATION_LIST_SUSPENSIONS\x10\x02\x12%\n!CONTRACT_MODERATION_LIST_WARNINGS\x10\x03\x32\xd0P\n\x08Platform\x12\x93\x01\n\x18\x62roadcastStateTransition\x12:.org.dash.platform.dapi.v0.BroadcastStateTransitionRequest\x1a;.org.dash.platform.dapi.v0.BroadcastStateTransitionResponse\x12l\n\x0bgetIdentity\x12-.org.dash.platform.dapi.v0.GetIdentityRequest\x1a..org.dash.platform.dapi.v0.GetIdentityResponse\x12x\n\x0fgetIdentityKeys\x12\x31.org.dash.platform.dapi.v0.GetIdentityKeysRequest\x1a\x32.org.dash.platform.dapi.v0.GetIdentityKeysResponse\x12\x96\x01\n\x19getIdentitiesContractKeys\x12;.org.dash.platform.dapi.v0.GetIdentitiesContractKeysRequest\x1a<.org.dash.platform.dapi.v0.GetIdentitiesContractKeysResponse\x12{\n\x10getIdentityNonce\x12\x32.org.dash.platform.dapi.v0.GetIdentityNonceRequest\x1a\x33.org.dash.platform.dapi.v0.GetIdentityNonceResponse\x12\x93\x01\n\x18getIdentityContractNonce\x12:.org.dash.platform.dapi.v0.GetIdentityContractNonceRequest\x1a;.org.dash.platform.dapi.v0.GetIdentityContractNonceResponse\x12\xa8\x01\n\x1fgetIdentityKeysRemainingBudgets\x12\x41.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsRequest\x1a\x42.org.dash.platform.dapi.v0.GetIdentityKeysRemainingBudgetsResponse\x12\x81\x01\n\x12getIdentityBalance\x12\x34.org.dash.platform.dapi.v0.GetIdentityBalanceRequest\x1a\x35.org.dash.platform.dapi.v0.GetIdentityBalanceResponse\x12\x8a\x01\n\x15getIdentitiesBalances\x12\x37.org.dash.platform.dapi.v0.GetIdentitiesBalancesRequest\x1a\x38.org.dash.platform.dapi.v0.GetIdentitiesBalancesResponse\x12\xa2\x01\n\x1dgetIdentityBalanceAndRevision\x12?.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionRequest\x1a@.org.dash.platform.dapi.v0.GetIdentityBalanceAndRevisionResponse\x12\xaf\x01\n#getEvonodesProposedEpochBlocksByIds\x12\x45.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByIdsRequest\x1a\x41.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse\x12\xb3\x01\n%getEvonodesProposedEpochBlocksByRange\x12G.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksByRangeRequest\x1a\x41.org.dash.platform.dapi.v0.GetEvonodesProposedEpochBlocksResponse\x12x\n\x0fgetDataContract\x12\x31.org.dash.platform.dapi.v0.GetDataContractRequest\x1a\x32.org.dash.platform.dapi.v0.GetDataContractResponse\x12\x8d\x01\n\x16getDataContractHistory\x12\x38.org.dash.platform.dapi.v0.GetDataContractHistoryRequest\x1a\x39.org.dash.platform.dapi.v0.GetDataContractHistoryResponse\x12\xa5\x01\n\x1egetDataContractsLatestVersions\x12@.org.dash.platform.dapi.v0.GetDataContractsLatestVersionsRequest\x1a\x41.org.dash.platform.dapi.v0.GetDataContractsLatestVersionsResponse\x12{\n\x10getDataContracts\x12\x32.org.dash.platform.dapi.v0.GetDataContractsRequest\x1a\x33.org.dash.platform.dapi.v0.GetDataContractsResponse\x12\x89\x01\n\x17getDataContractsByRange\x12\x39.org.dash.platform.dapi.v0.GetDataContractsByRangeRequest\x1a\x33.org.dash.platform.dapi.v0.GetDataContractsResponse\x12\x87\x01\n\x14getContractGroupInfo\x12\x36.org.dash.platform.dapi.v0.GetContractGroupInfoRequest\x1a\x37.org.dash.platform.dapi.v0.GetContractGroupInfoResponse\x12\x90\x01\n\x17getContractGroupMembers\x12\x39.org.dash.platform.dapi.v0.GetContractGroupMembersRequest\x1a:.org.dash.platform.dapi.v0.GetContractGroupMembersResponse\x12\x9f\x01\n\x1cgetContractGroupsForContract\x12>.org.dash.platform.dapi.v0.GetContractGroupsForContractRequest\x1a?.org.dash.platform.dapi.v0.GetContractGroupsForContractResponse\x12\x9c\x01\n\x1bgetContractModerationStatus\x12=.org.dash.platform.dapi.v0.GetContractModerationStatusRequest\x1a>.org.dash.platform.dapi.v0.GetContractModerationStatusResponse\x12\x9f\x01\n\x1cgetContractModerationEntries\x12>.org.dash.platform.dapi.v0.GetContractModerationEntriesRequest\x1a?.org.dash.platform.dapi.v0.GetContractModerationEntriesResponse\x12\x9c\x01\n\x1bgetContractDocumentRemovals\x12=.org.dash.platform.dapi.v0.GetContractDocumentRemovalsRequest\x1a>.org.dash.platform.dapi.v0.GetContractDocumentRemovalsResponse\x12\x81\x01\n\x12getContractFeePots\x12\x34.org.dash.platform.dapi.v0.GetContractFeePotsRequest\x1a\x35.org.dash.platform.dapi.v0.GetContractFeePotsResponse\x12\x81\x01\n\x12getDocumentHistory\x12\x34.org.dash.platform.dapi.v0.GetDocumentHistoryRequest\x1a\x35.org.dash.platform.dapi.v0.GetDocumentHistoryResponse\x12o\n\x0cgetDocuments\x12..org.dash.platform.dapi.v0.GetDocumentsRequest\x1a/.org.dash.platform.dapi.v0.GetDocumentsResponse\x12\x99\x01\n\x1agetIdentityByPublicKeyHash\x12<.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashRequest\x1a=.org.dash.platform.dapi.v0.GetIdentityByPublicKeyHashResponse\x12\xb4\x01\n#getIdentityByNonUniquePublicKeyHash\x12\x45.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashRequest\x1a\x46.org.dash.platform.dapi.v0.GetIdentityByNonUniquePublicKeyHashResponse\x12\x9f\x01\n\x1cwaitForStateTransitionResult\x12>.org.dash.platform.dapi.v0.WaitForStateTransitionResultRequest\x1a?.org.dash.platform.dapi.v0.WaitForStateTransitionResultResponse\x12\x81\x01\n\x12getConsensusParams\x12\x34.org.dash.platform.dapi.v0.GetConsensusParamsRequest\x1a\x35.org.dash.platform.dapi.v0.GetConsensusParamsResponse\x12\xa5\x01\n\x1egetProtocolVersionUpgradeState\x12@.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateRequest\x1a\x41.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeStateResponse\x12\xb4\x01\n#getProtocolVersionUpgradeVoteStatus\x12\x45.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusRequest\x1a\x46.org.dash.platform.dapi.v0.GetProtocolVersionUpgradeVoteStatusResponse\x12r\n\rgetEpochsInfo\x12/.org.dash.platform.dapi.v0.GetEpochsInfoRequest\x1a\x30.org.dash.platform.dapi.v0.GetEpochsInfoResponse\x12\x8d\x01\n\x16getFinalizedEpochInfos\x12\x38.org.dash.platform.dapi.v0.GetFinalizedEpochInfosRequest\x1a\x39.org.dash.platform.dapi.v0.GetFinalizedEpochInfosResponse\x12\x8a\x01\n\x15getContestedResources\x12\x37.org.dash.platform.dapi.v0.GetContestedResourcesRequest\x1a\x38.org.dash.platform.dapi.v0.GetContestedResourcesResponse\x12\xa2\x01\n\x1dgetContestedResourceVoteState\x12?.org.dash.platform.dapi.v0.GetContestedResourceVoteStateRequest\x1a@.org.dash.platform.dapi.v0.GetContestedResourceVoteStateResponse\x12\xba\x01\n%getContestedResourceVotersForIdentity\x12G.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityRequest\x1aH.org.dash.platform.dapi.v0.GetContestedResourceVotersForIdentityResponse\x12\xae\x01\n!getContestedResourceIdentityVotes\x12\x43.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesRequest\x1a\x44.org.dash.platform.dapi.v0.GetContestedResourceIdentityVotesResponse\x12\x8a\x01\n\x15getVotePollsByEndDate\x12\x37.org.dash.platform.dapi.v0.GetVotePollsByEndDateRequest\x1a\x38.org.dash.platform.dapi.v0.GetVotePollsByEndDateResponse\x12\xa5\x01\n\x1egetPrefundedSpecializedBalance\x12@.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceRequest\x1a\x41.org.dash.platform.dapi.v0.GetPrefundedSpecializedBalanceResponse\x12\x96\x01\n\x19getTotalCreditsInPlatform\x12;.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformRequest\x1a<.org.dash.platform.dapi.v0.GetTotalCreditsInPlatformResponse\x12x\n\x0fgetPathElements\x12\x31.org.dash.platform.dapi.v0.GetPathElementsRequest\x1a\x32.org.dash.platform.dapi.v0.GetPathElementsResponse\x12\x66\n\tgetStatus\x12+.org.dash.platform.dapi.v0.GetStatusRequest\x1a,.org.dash.platform.dapi.v0.GetStatusResponse\x12\x8a\x01\n\x15getCurrentQuorumsInfo\x12\x37.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoRequest\x1a\x38.org.dash.platform.dapi.v0.GetCurrentQuorumsInfoResponse\x12\x93\x01\n\x18getIdentityTokenBalances\x12:.org.dash.platform.dapi.v0.GetIdentityTokenBalancesRequest\x1a;.org.dash.platform.dapi.v0.GetIdentityTokenBalancesResponse\x12\x99\x01\n\x1agetIdentitiesTokenBalances\x12<.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesRequest\x1a=.org.dash.platform.dapi.v0.GetIdentitiesTokenBalancesResponse\x12\x8a\x01\n\x15getIdentityTokenInfos\x12\x37.org.dash.platform.dapi.v0.GetIdentityTokenInfosRequest\x1a\x38.org.dash.platform.dapi.v0.GetIdentityTokenInfosResponse\x12\x90\x01\n\x17getIdentitiesTokenInfos\x12\x39.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosRequest\x1a:.org.dash.platform.dapi.v0.GetIdentitiesTokenInfosResponse\x12{\n\x10getTokenStatuses\x12\x32.org.dash.platform.dapi.v0.GetTokenStatusesRequest\x1a\x33.org.dash.platform.dapi.v0.GetTokenStatusesResponse\x12\x9f\x01\n\x1cgetTokenDirectPurchasePrices\x12>.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesRequest\x1a?.org.dash.platform.dapi.v0.GetTokenDirectPurchasePricesResponse\x12\x87\x01\n\x14getTokenContractInfo\x12\x36.org.dash.platform.dapi.v0.GetTokenContractInfoRequest\x1a\x37.org.dash.platform.dapi.v0.GetTokenContractInfoResponse\x12\xb1\x01\n\"getTokenPreProgrammedDistributions\x12\x44.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsRequest\x1a\x45.org.dash.platform.dapi.v0.GetTokenPreProgrammedDistributionsResponse\x12\xbd\x01\n&getTokenPerpetualDistributionLastClaim\x12H.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimRequest\x1aI.org.dash.platform.dapi.v0.GetTokenPerpetualDistributionLastClaimResponse\x12\x84\x01\n\x13getTokenTotalSupply\x12\x35.org.dash.platform.dapi.v0.GetTokenTotalSupplyRequest\x1a\x36.org.dash.platform.dapi.v0.GetTokenTotalSupplyResponse\x12o\n\x0cgetGroupInfo\x12..org.dash.platform.dapi.v0.GetGroupInfoRequest\x1a/.org.dash.platform.dapi.v0.GetGroupInfoResponse\x12r\n\rgetGroupInfos\x12/.org.dash.platform.dapi.v0.GetGroupInfosRequest\x1a\x30.org.dash.platform.dapi.v0.GetGroupInfosResponse\x12x\n\x0fgetGroupActions\x12\x31.org.dash.platform.dapi.v0.GetGroupActionsRequest\x1a\x32.org.dash.platform.dapi.v0.GetGroupActionsResponse\x12\x8a\x01\n\x15getGroupActionSigners\x12\x37.org.dash.platform.dapi.v0.GetGroupActionSignersRequest\x1a\x38.org.dash.platform.dapi.v0.GetGroupActionSignersResponse\x12u\n\x0egetAddressInfo\x12\x30.org.dash.platform.dapi.v0.GetAddressInfoRequest\x1a\x31.org.dash.platform.dapi.v0.GetAddressInfoResponse\x12~\n\x11getAddressesInfos\x12\x33.org.dash.platform.dapi.v0.GetAddressesInfosRequest\x1a\x34.org.dash.platform.dapi.v0.GetAddressesInfosResponse\x12\x8d\x01\n\x16getAddressesTrunkState\x12\x38.org.dash.platform.dapi.v0.GetAddressesTrunkStateRequest\x1a\x39.org.dash.platform.dapi.v0.GetAddressesTrunkStateResponse\x12\x90\x01\n\x17getAddressesBranchState\x12\x39.org.dash.platform.dapi.v0.GetAddressesBranchStateRequest\x1a:.org.dash.platform.dapi.v0.GetAddressesBranchStateResponse\x12\xa5\x01\n\x1egetRecentAddressBalanceChanges\x12@.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesRequest\x1a\x41.org.dash.platform.dapi.v0.GetRecentAddressBalanceChangesResponse\x12\xc0\x01\n\'getRecentCompactedAddressBalanceChanges\x12I.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesRequest\x1aJ.org.dash.platform.dapi.v0.GetRecentCompactedAddressBalanceChangesResponse\x12\x96\x01\n\x19getShieldedEncryptedNotes\x12;.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest\x1a<.org.dash.platform.dapi.v0.GetShieldedEncryptedNotesResponse\x12\x81\x01\n\x12getShieldedAnchors\x12\x34.org.dash.platform.dapi.v0.GetShieldedAnchorsRequest\x1a\x35.org.dash.platform.dapi.v0.GetShieldedAnchorsResponse\x12\x9c\x01\n\x1bgetMostRecentShieldedAnchor\x12=.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest\x1a>.org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorResponse\x12\x87\x01\n\x14getShieldedPoolState\x12\x36.org.dash.platform.dapi.v0.GetShieldedPoolStateRequest\x1a\x37.org.dash.platform.dapi.v0.GetShieldedPoolStateResponse\x12\x8a\x01\n\x15getShieldedNotesCount\x12\x37.org.dash.platform.dapi.v0.GetShieldedNotesCountRequest\x1a\x38.org.dash.platform.dapi.v0.GetShieldedNotesCountResponse\x12\x8a\x01\n\x15getShieldedNullifiers\x12\x37.org.dash.platform.dapi.v0.GetShieldedNullifiersRequest\x1a\x38.org.dash.platform.dapi.v0.GetShieldedNullifiersResponseb\x06proto3' , dependencies=[google_dot_protobuf_dot_wrappers__pb2.DESCRIPTOR,google_dot_protobuf_dot_struct__pb2.DESCRIPTOR,google_dot_protobuf_dot_timestamp__pb2.DESCRIPTOR,]) @@ -62,8 +62,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=79946, - serialized_end=80036, + serialized_start=80606, + serialized_end=80696, ) _sym_db.RegisterEnumDescriptor(_KEYPURPOSE) @@ -98,8 +98,8 @@ ], containing_type=None, serialized_options=None, - serialized_start=80039, - serialized_end=80224, + serialized_start=80699, + serialized_end=80884, ) _sym_db.RegisterEnumDescriptor(_CONTRACTMODERATIONLIST) @@ -17956,6 +17956,106 @@ serialized_end=67911, ) +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTTOPOOLEVENT = _descriptor.Descriptor( + name='MintToPoolEvent', + full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='amount', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.amount', index=0, + number=1, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='actions_digest', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.actions_digest', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='public_note', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent.public_note', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='_public_note', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent._public_note', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), + ], + serialized_start=67913, + serialized_end=68012, +) + +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNFROMPOOLEVENT = _descriptor.Descriptor( + name='BurnFromPoolEvent', + full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='amount', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.amount', index=0, + number=1, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='actions_digest', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.actions_digest', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='public_note', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent.public_note', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + _descriptor.OneofDescriptor( + name='_public_note', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent._public_note', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), + ], + serialized_start=68014, + serialized_end=68115, +) + _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONEVENT = _descriptor.Descriptor( name='GroupActionEvent', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionEvent', @@ -18002,8 +18102,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=67914, - serialized_end=68294, + serialized_start=68118, + serialized_end=68498, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DOCUMENTEVENT = _descriptor.Descriptor( @@ -18038,8 +18138,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=68297, - serialized_end=68436, + serialized_start=68501, + serialized_end=68640, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DOCUMENTCREATEEVENT = _descriptor.Descriptor( @@ -18069,8 +18169,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=68438, - serialized_end=68485, + serialized_start=68642, + serialized_end=68689, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_CONTRACTUPDATEEVENT = _descriptor.Descriptor( @@ -18100,8 +18200,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=68487, - serialized_end=68534, + serialized_start=68691, + serialized_end=68738, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_CONTRACTEVENT = _descriptor.Descriptor( @@ -18136,8 +18236,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=68537, - serialized_end=68676, + serialized_start=68741, + serialized_end=68880, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT = _descriptor.Descriptor( @@ -18204,6 +18304,20 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='mint_to_pool', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.mint_to_pool', index=8, + number=9, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='burn_from_pool', full_name='org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.TokenEvent.burn_from_pool', index=9, + number=10, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -18221,8 +18335,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=68679, - serialized_end=69656, + serialized_start=68883, + serialized_end=70100, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONENTRY = _descriptor.Descriptor( @@ -18259,8 +18373,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=69659, - serialized_end=69806, + serialized_start=70103, + serialized_end=70250, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONS = _descriptor.Descriptor( @@ -18290,8 +18404,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=69809, - serialized_end=69941, + serialized_start=70253, + serialized_end=70385, ) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0 = _descriptor.Descriptor( @@ -18326,7 +18440,7 @@ ], extensions=[ ], - nested_types=[_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_FREEZEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UNFREEZEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DESTROYFROZENFUNDSEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_SHAREDENCRYPTEDNOTE, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_PERSONALENCRYPTEDNOTE, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_EMERGENCYACTIONEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENCONFIGUPDATEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UPDATEDIRECTPURCHASEPRICEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DOCUMENTEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DOCUMENTCREATEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_CONTRACTUPDATEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_CONTRACTEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONENTRY, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONS, ], + nested_types=[_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_FREEZEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UNFREEZEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DESTROYFROZENFUNDSEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_SHAREDENCRYPTEDNOTE, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_PERSONALENCRYPTEDNOTE, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_EMERGENCYACTIONEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENCONFIGUPDATEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UPDATEDIRECTPURCHASEPRICEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTTOPOOLEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNFROMPOOLEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DOCUMENTEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DOCUMENTCREATEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_CONTRACTUPDATEEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_CONTRACTEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONENTRY, _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONS, ], enum_types=[ ], serialized_options=None, @@ -18341,7 +18455,7 @@ fields=[]), ], serialized_start=66156, - serialized_end=69951, + serialized_end=70395, ) _GETGROUPACTIONSRESPONSE = _descriptor.Descriptor( @@ -18377,7 +18491,7 @@ fields=[]), ], serialized_start=66036, - serialized_end=69962, + serialized_end=70406, ) @@ -18436,8 +18550,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=70100, - serialized_end=70306, + serialized_start=70544, + serialized_end=70750, ) _GETGROUPACTIONSIGNERSREQUEST = _descriptor.Descriptor( @@ -18473,8 +18587,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=69965, - serialized_end=70357, + serialized_start=70409, + serialized_end=70801, ) @@ -18512,8 +18626,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=70789, - serialized_end=70842, + serialized_start=71233, + serialized_end=71286, ) _GETGROUPACTIONSIGNERSRESPONSE_GETGROUPACTIONSIGNERSRESPONSEV0_GROUPACTIONSIGNERS = _descriptor.Descriptor( @@ -18543,8 +18657,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=70845, - serialized_end=70990, + serialized_start=71289, + serialized_end=71434, ) _GETGROUPACTIONSIGNERSRESPONSE_GETGROUPACTIONSIGNERSRESPONSEV0 = _descriptor.Descriptor( @@ -18593,8 +18707,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=70498, - serialized_end=71000, + serialized_start=70942, + serialized_end=71444, ) _GETGROUPACTIONSIGNERSRESPONSE = _descriptor.Descriptor( @@ -18629,8 +18743,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=70360, - serialized_end=71011, + serialized_start=70804, + serialized_end=71455, ) @@ -18668,8 +18782,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=71127, - serialized_end=71184, + serialized_start=71571, + serialized_end=71628, ) _GETADDRESSINFOREQUEST = _descriptor.Descriptor( @@ -18704,8 +18818,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=71014, - serialized_end=71195, + serialized_start=71458, + serialized_end=71639, ) @@ -18748,8 +18862,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=71198, - serialized_end=71331, + serialized_start=71642, + serialized_end=71775, ) @@ -18787,8 +18901,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=71333, - serialized_end=71382, + serialized_start=71777, + serialized_end=71826, ) @@ -18819,8 +18933,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=71384, - serialized_end=71479, + serialized_start=71828, + serialized_end=71923, ) @@ -18870,8 +18984,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=71481, - serialized_end=71590, + serialized_start=71925, + serialized_end=72034, ) @@ -18909,8 +19023,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=71592, - serialized_end=71712, + serialized_start=72036, + serialized_end=72156, ) @@ -18941,8 +19055,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=71714, - serialized_end=71821, + serialized_start=72158, + serialized_end=72265, ) @@ -18992,8 +19106,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=71941, - serialized_end=72166, + serialized_start=72385, + serialized_end=72610, ) _GETADDRESSINFORESPONSE = _descriptor.Descriptor( @@ -19028,8 +19142,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=71824, - serialized_end=72177, + serialized_start=72268, + serialized_end=72621, ) @@ -19067,8 +19181,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=72302, - serialized_end=72364, + serialized_start=72746, + serialized_end=72808, ) _GETADDRESSESINFOSREQUEST = _descriptor.Descriptor( @@ -19103,8 +19217,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=72180, - serialized_end=72375, + serialized_start=72624, + serialized_end=72819, ) @@ -19154,8 +19268,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=72504, - serialized_end=72736, + serialized_start=72948, + serialized_end=73180, ) _GETADDRESSESINFOSRESPONSE = _descriptor.Descriptor( @@ -19190,8 +19304,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=72378, - serialized_end=72747, + serialized_start=72822, + serialized_end=73191, ) @@ -19215,8 +19329,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=72887, - serialized_end=72920, + serialized_start=73331, + serialized_end=73364, ) _GETADDRESSESTRUNKSTATEREQUEST = _descriptor.Descriptor( @@ -19251,8 +19365,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=72750, - serialized_end=72931, + serialized_start=73194, + serialized_end=73375, ) @@ -19290,8 +19404,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=73075, - serialized_end=73221, + serialized_start=73519, + serialized_end=73665, ) _GETADDRESSESTRUNKSTATERESPONSE = _descriptor.Descriptor( @@ -19326,8 +19440,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=72934, - serialized_end=73232, + serialized_start=73378, + serialized_end=73676, ) @@ -19372,8 +19486,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=73375, - serialized_end=73464, + serialized_start=73819, + serialized_end=73908, ) _GETADDRESSESBRANCHSTATEREQUEST = _descriptor.Descriptor( @@ -19408,8 +19522,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=73235, - serialized_end=73475, + serialized_start=73679, + serialized_end=73919, ) @@ -19440,8 +19554,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=73621, - serialized_end=73676, + serialized_start=74065, + serialized_end=74120, ) _GETADDRESSESBRANCHSTATERESPONSE = _descriptor.Descriptor( @@ -19476,8 +19590,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=73478, - serialized_end=73687, + serialized_start=73922, + serialized_end=74131, ) @@ -19522,8 +19636,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=73851, - serialized_end=73965, + serialized_start=74295, + serialized_end=74409, ) _GETRECENTADDRESSBALANCECHANGESREQUEST = _descriptor.Descriptor( @@ -19558,8 +19672,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=73690, - serialized_end=73976, + serialized_start=74134, + serialized_end=74420, ) @@ -19609,8 +19723,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=74144, - serialized_end=74408, + serialized_start=74588, + serialized_end=74852, ) _GETRECENTADDRESSBALANCECHANGESRESPONSE = _descriptor.Descriptor( @@ -19645,8 +19759,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=73979, - serialized_end=74419, + serialized_start=74423, + serialized_end=74863, ) @@ -19684,8 +19798,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=74421, - serialized_end=74492, + serialized_start=74865, + serialized_end=74936, ) @@ -19735,8 +19849,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=74495, - serialized_end=74671, + serialized_start=74939, + serialized_end=75115, ) @@ -19767,8 +19881,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=74673, - serialized_end=74765, + serialized_start=75117, + serialized_end=75209, ) @@ -19813,8 +19927,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=74768, - serialized_end=74942, + serialized_start=75212, + serialized_end=75386, ) @@ -19845,8 +19959,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=74945, - serialized_end=75080, + serialized_start=75389, + serialized_end=75524, ) @@ -19884,8 +19998,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=75272, - serialized_end=75369, + serialized_start=75716, + serialized_end=75813, ) _GETRECENTCOMPACTEDADDRESSBALANCECHANGESREQUEST = _descriptor.Descriptor( @@ -19920,8 +20034,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=75083, - serialized_end=75380, + serialized_start=75527, + serialized_end=75824, ) @@ -19971,8 +20085,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=75576, - serialized_end=75868, + serialized_start=76020, + serialized_end=76312, ) _GETRECENTCOMPACTEDADDRESSBALANCECHANGESRESPONSE = _descriptor.Descriptor( @@ -20007,8 +20121,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=75383, - serialized_end=75879, + serialized_start=75827, + serialized_end=76323, ) @@ -20041,6 +20155,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='token_id', full_name='org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0.token_id', index=3, + number=4, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -20052,9 +20173,14 @@ syntax='proto3', extension_ranges=[], oneofs=[ + _descriptor.OneofDescriptor( + name='_token_id', full_name='org.dash.platform.dapi.v0.GetShieldedEncryptedNotesRequest.GetShieldedEncryptedNotesRequestV0._token_id', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), ], - serialized_start=76028, - serialized_end=76115, + serialized_start=76472, + serialized_end=76595, ) _GETSHIELDEDENCRYPTEDNOTESREQUEST = _descriptor.Descriptor( @@ -20089,8 +20215,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=75882, - serialized_end=76126, + serialized_start=76326, + serialized_end=76606, ) @@ -20142,8 +20268,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=76573, - serialized_end=76660, + serialized_start=77053, + serialized_end=77140, ) _GETSHIELDEDENCRYPTEDNOTESRESPONSE_GETSHIELDEDENCRYPTEDNOTESRESPONSEV0_ENCRYPTEDNOTES = _descriptor.Descriptor( @@ -20173,8 +20299,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=76663, - serialized_end=76808, + serialized_start=77143, + serialized_end=77288, ) _GETSHIELDEDENCRYPTEDNOTESRESPONSE_GETSHIELDEDENCRYPTEDNOTESRESPONSEV0 = _descriptor.Descriptor( @@ -20223,8 +20349,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=76279, - serialized_end=76818, + serialized_start=76759, + serialized_end=77298, ) _GETSHIELDEDENCRYPTEDNOTESRESPONSE = _descriptor.Descriptor( @@ -20259,8 +20385,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=76129, - serialized_end=76829, + serialized_start=76609, + serialized_end=77309, ) @@ -20279,6 +20405,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='token_id', full_name='org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0.token_id', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -20290,9 +20423,14 @@ syntax='proto3', extension_ranges=[], oneofs=[ + _descriptor.OneofDescriptor( + name='_token_id', full_name='org.dash.platform.dapi.v0.GetShieldedAnchorsRequest.GetShieldedAnchorsRequestV0._token_id', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), ], - serialized_start=76957, - serialized_end=77001, + serialized_start=77437, + serialized_end=77517, ) _GETSHIELDEDANCHORSREQUEST = _descriptor.Descriptor( @@ -20327,8 +20465,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=76832, - serialized_end=77012, + serialized_start=77312, + serialized_end=77528, ) @@ -20359,8 +20497,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=77401, - serialized_end=77427, + serialized_start=77917, + serialized_end=77943, ) _GETSHIELDEDANCHORSRESPONSE_GETSHIELDEDANCHORSRESPONSEV0 = _descriptor.Descriptor( @@ -20409,8 +20547,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=77144, - serialized_end=77437, + serialized_start=77660, + serialized_end=77953, ) _GETSHIELDEDANCHORSRESPONSE = _descriptor.Descriptor( @@ -20445,8 +20583,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=77015, - serialized_end=77448, + serialized_start=77531, + serialized_end=77964, ) @@ -20465,6 +20603,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='token_id', full_name='org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0.token_id', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -20476,9 +20621,14 @@ syntax='proto3', extension_ranges=[], oneofs=[ + _descriptor.OneofDescriptor( + name='_token_id', full_name='org.dash.platform.dapi.v0.GetMostRecentShieldedAnchorRequest.GetMostRecentShieldedAnchorRequestV0._token_id', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), ], - serialized_start=77603, - serialized_end=77656, + serialized_start=78119, + serialized_end=78208, ) _GETMOSTRECENTSHIELDEDANCHORREQUEST = _descriptor.Descriptor( @@ -20513,8 +20663,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=77451, - serialized_end=77667, + serialized_start=77967, + serialized_end=78219, ) @@ -20564,8 +20714,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=77826, - serialized_end=78007, + serialized_start=78378, + serialized_end=78559, ) _GETMOSTRECENTSHIELDEDANCHORRESPONSE = _descriptor.Descriptor( @@ -20600,8 +20750,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=77670, - serialized_end=78018, + serialized_start=78222, + serialized_end=78570, ) @@ -20620,6 +20770,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='token_id', full_name='org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0.token_id', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -20631,9 +20788,14 @@ syntax='proto3', extension_ranges=[], oneofs=[ + _descriptor.OneofDescriptor( + name='_token_id', full_name='org.dash.platform.dapi.v0.GetShieldedPoolStateRequest.GetShieldedPoolStateRequestV0._token_id', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), ], - serialized_start=78152, - serialized_end=78198, + serialized_start=78704, + serialized_end=78786, ) _GETSHIELDEDPOOLSTATEREQUEST = _descriptor.Descriptor( @@ -20668,8 +20830,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=78021, - serialized_end=78209, + serialized_start=78573, + serialized_end=78797, ) @@ -20719,8 +20881,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=78347, - serialized_end=78532, + serialized_start=78935, + serialized_end=79120, ) _GETSHIELDEDPOOLSTATERESPONSE = _descriptor.Descriptor( @@ -20755,8 +20917,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=78212, - serialized_end=78543, + serialized_start=78800, + serialized_end=79131, ) @@ -20775,6 +20937,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='token_id', full_name='org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0.token_id', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -20786,9 +20955,14 @@ syntax='proto3', extension_ranges=[], oneofs=[ + _descriptor.OneofDescriptor( + name='_token_id', full_name='org.dash.platform.dapi.v0.GetShieldedNotesCountRequest.GetShieldedNotesCountRequestV0._token_id', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), ], - serialized_start=78680, - serialized_end=78727, + serialized_start=79268, + serialized_end=79351, ) _GETSHIELDEDNOTESCOUNTREQUEST = _descriptor.Descriptor( @@ -20823,8 +20997,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=78546, - serialized_end=78738, + serialized_start=79134, + serialized_end=79362, ) @@ -20874,8 +21048,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=78879, - serialized_end=79069, + serialized_start=79503, + serialized_end=79693, ) _GETSHIELDEDNOTESCOUNTRESPONSE = _descriptor.Descriptor( @@ -20910,8 +21084,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=78741, - serialized_end=79080, + serialized_start=79365, + serialized_end=79704, ) @@ -20937,6 +21111,13 @@ message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='token_id', full_name='org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0.token_id', index=2, + number=3, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], @@ -20948,9 +21129,14 @@ syntax='proto3', extension_ranges=[], oneofs=[ + _descriptor.OneofDescriptor( + name='_token_id', full_name='org.dash.platform.dapi.v0.GetShieldedNullifiersRequest.GetShieldedNullifiersRequestV0._token_id', + index=0, containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[]), ], - serialized_start=79217, - serialized_end=79284, + serialized_start=79841, + serialized_end=79944, ) _GETSHIELDEDNULLIFIERSREQUEST = _descriptor.Descriptor( @@ -20985,8 +21171,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=79083, - serialized_end=79295, + serialized_start=79707, + serialized_end=79955, ) @@ -21024,8 +21210,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=79724, - serialized_end=79778, + serialized_start=80384, + serialized_end=80438, ) _GETSHIELDEDNULLIFIERSRESPONSE_GETSHIELDEDNULLIFIERSRESPONSEV0_NULLIFIERSTATUSES = _descriptor.Descriptor( @@ -21055,8 +21241,8 @@ extension_ranges=[], oneofs=[ ], - serialized_start=79781, - serialized_end=79923, + serialized_start=80441, + serialized_end=80583, ) _GETSHIELDEDNULLIFIERSRESPONSE_GETSHIELDEDNULLIFIERSRESPONSEV0 = _descriptor.Descriptor( @@ -21105,8 +21291,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=79436, - serialized_end=79933, + serialized_start=80096, + serialized_end=80593, ) _GETSHIELDEDNULLIFIERSRESPONSE = _descriptor.Descriptor( @@ -21141,8 +21327,8 @@ create_key=_descriptor._internal_create_key, fields=[]), ], - serialized_start=79298, - serialized_end=79944, + serialized_start=79958, + serialized_end=80604, ) _GETIDENTITYREQUEST_GETIDENTITYREQUESTV0.containing_type = _GETIDENTITYREQUEST @@ -22913,6 +23099,14 @@ _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UPDATEDIRECTPURCHASEPRICEEVENT.oneofs_by_name['_public_note'].fields.append( _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UPDATEDIRECTPURCHASEPRICEEVENT.fields_by_name['public_note']) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UPDATEDIRECTPURCHASEPRICEEVENT.fields_by_name['public_note'].containing_oneof = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UPDATEDIRECTPURCHASEPRICEEVENT.oneofs_by_name['_public_note'] +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTTOPOOLEVENT.containing_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0 +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTTOPOOLEVENT.oneofs_by_name['_public_note'].fields.append( + _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTTOPOOLEVENT.fields_by_name['public_note']) +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTTOPOOLEVENT.fields_by_name['public_note'].containing_oneof = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTTOPOOLEVENT.oneofs_by_name['_public_note'] +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNFROMPOOLEVENT.containing_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0 +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNFROMPOOLEVENT.oneofs_by_name['_public_note'].fields.append( + _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNFROMPOOLEVENT.fields_by_name['public_note']) +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNFROMPOOLEVENT.fields_by_name['public_note'].containing_oneof = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNFROMPOOLEVENT.oneofs_by_name['_public_note'] _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONEVENT.fields_by_name['token_event'].message_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONEVENT.fields_by_name['document_event'].message_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_DOCUMENTEVENT _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONEVENT.fields_by_name['contract_event'].message_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_CONTRACTEVENT @@ -22946,6 +23140,8 @@ _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['emergency_action'].message_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_EMERGENCYACTIONEVENT _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['token_config_update'].message_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENCONFIGUPDATEEVENT _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['update_price'].message_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_UPDATEDIRECTPURCHASEPRICEEVENT +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['mint_to_pool'].message_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTTOPOOLEVENT +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['burn_from_pool'].message_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNFROMPOOLEVENT _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.containing_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0 _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.oneofs_by_name['type'].fields.append( _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['mint']) @@ -22971,6 +23167,12 @@ _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.oneofs_by_name['type'].fields.append( _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['update_price']) _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['update_price'].containing_oneof = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.oneofs_by_name['type'] +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.oneofs_by_name['type'].fields.append( + _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['mint_to_pool']) +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['mint_to_pool'].containing_oneof = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.oneofs_by_name['type'] +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.oneofs_by_name['type'].fields.append( + _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['burn_from_pool']) +_GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.fields_by_name['burn_from_pool'].containing_oneof = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_TOKENEVENT.oneofs_by_name['type'] _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONENTRY.fields_by_name['event'].message_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONEVENT _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONENTRY.containing_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0 _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONS.fields_by_name['group_actions'].message_type = _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONENTRY @@ -23135,6 +23337,9 @@ _GETRECENTCOMPACTEDADDRESSBALANCECHANGESRESPONSE.fields_by_name['v0']) _GETRECENTCOMPACTEDADDRESSBALANCECHANGESRESPONSE.fields_by_name['v0'].containing_oneof = _GETRECENTCOMPACTEDADDRESSBALANCECHANGESRESPONSE.oneofs_by_name['version'] _GETSHIELDEDENCRYPTEDNOTESREQUEST_GETSHIELDEDENCRYPTEDNOTESREQUESTV0.containing_type = _GETSHIELDEDENCRYPTEDNOTESREQUEST +_GETSHIELDEDENCRYPTEDNOTESREQUEST_GETSHIELDEDENCRYPTEDNOTESREQUESTV0.oneofs_by_name['_token_id'].fields.append( + _GETSHIELDEDENCRYPTEDNOTESREQUEST_GETSHIELDEDENCRYPTEDNOTESREQUESTV0.fields_by_name['token_id']) +_GETSHIELDEDENCRYPTEDNOTESREQUEST_GETSHIELDEDENCRYPTEDNOTESREQUESTV0.fields_by_name['token_id'].containing_oneof = _GETSHIELDEDENCRYPTEDNOTESREQUEST_GETSHIELDEDENCRYPTEDNOTESREQUESTV0.oneofs_by_name['_token_id'] _GETSHIELDEDENCRYPTEDNOTESREQUEST.fields_by_name['v0'].message_type = _GETSHIELDEDENCRYPTEDNOTESREQUEST_GETSHIELDEDENCRYPTEDNOTESREQUESTV0 _GETSHIELDEDENCRYPTEDNOTESREQUEST.oneofs_by_name['version'].fields.append( _GETSHIELDEDENCRYPTEDNOTESREQUEST.fields_by_name['v0']) @@ -23157,6 +23362,9 @@ _GETSHIELDEDENCRYPTEDNOTESRESPONSE.fields_by_name['v0']) _GETSHIELDEDENCRYPTEDNOTESRESPONSE.fields_by_name['v0'].containing_oneof = _GETSHIELDEDENCRYPTEDNOTESRESPONSE.oneofs_by_name['version'] _GETSHIELDEDANCHORSREQUEST_GETSHIELDEDANCHORSREQUESTV0.containing_type = _GETSHIELDEDANCHORSREQUEST +_GETSHIELDEDANCHORSREQUEST_GETSHIELDEDANCHORSREQUESTV0.oneofs_by_name['_token_id'].fields.append( + _GETSHIELDEDANCHORSREQUEST_GETSHIELDEDANCHORSREQUESTV0.fields_by_name['token_id']) +_GETSHIELDEDANCHORSREQUEST_GETSHIELDEDANCHORSREQUESTV0.fields_by_name['token_id'].containing_oneof = _GETSHIELDEDANCHORSREQUEST_GETSHIELDEDANCHORSREQUESTV0.oneofs_by_name['_token_id'] _GETSHIELDEDANCHORSREQUEST.fields_by_name['v0'].message_type = _GETSHIELDEDANCHORSREQUEST_GETSHIELDEDANCHORSREQUESTV0 _GETSHIELDEDANCHORSREQUEST.oneofs_by_name['version'].fields.append( _GETSHIELDEDANCHORSREQUEST.fields_by_name['v0']) @@ -23177,6 +23385,9 @@ _GETSHIELDEDANCHORSRESPONSE.fields_by_name['v0']) _GETSHIELDEDANCHORSRESPONSE.fields_by_name['v0'].containing_oneof = _GETSHIELDEDANCHORSRESPONSE.oneofs_by_name['version'] _GETMOSTRECENTSHIELDEDANCHORREQUEST_GETMOSTRECENTSHIELDEDANCHORREQUESTV0.containing_type = _GETMOSTRECENTSHIELDEDANCHORREQUEST +_GETMOSTRECENTSHIELDEDANCHORREQUEST_GETMOSTRECENTSHIELDEDANCHORREQUESTV0.oneofs_by_name['_token_id'].fields.append( + _GETMOSTRECENTSHIELDEDANCHORREQUEST_GETMOSTRECENTSHIELDEDANCHORREQUESTV0.fields_by_name['token_id']) +_GETMOSTRECENTSHIELDEDANCHORREQUEST_GETMOSTRECENTSHIELDEDANCHORREQUESTV0.fields_by_name['token_id'].containing_oneof = _GETMOSTRECENTSHIELDEDANCHORREQUEST_GETMOSTRECENTSHIELDEDANCHORREQUESTV0.oneofs_by_name['_token_id'] _GETMOSTRECENTSHIELDEDANCHORREQUEST.fields_by_name['v0'].message_type = _GETMOSTRECENTSHIELDEDANCHORREQUEST_GETMOSTRECENTSHIELDEDANCHORREQUESTV0 _GETMOSTRECENTSHIELDEDANCHORREQUEST.oneofs_by_name['version'].fields.append( _GETMOSTRECENTSHIELDEDANCHORREQUEST.fields_by_name['v0']) @@ -23195,6 +23406,9 @@ _GETMOSTRECENTSHIELDEDANCHORRESPONSE.fields_by_name['v0']) _GETMOSTRECENTSHIELDEDANCHORRESPONSE.fields_by_name['v0'].containing_oneof = _GETMOSTRECENTSHIELDEDANCHORRESPONSE.oneofs_by_name['version'] _GETSHIELDEDPOOLSTATEREQUEST_GETSHIELDEDPOOLSTATEREQUESTV0.containing_type = _GETSHIELDEDPOOLSTATEREQUEST +_GETSHIELDEDPOOLSTATEREQUEST_GETSHIELDEDPOOLSTATEREQUESTV0.oneofs_by_name['_token_id'].fields.append( + _GETSHIELDEDPOOLSTATEREQUEST_GETSHIELDEDPOOLSTATEREQUESTV0.fields_by_name['token_id']) +_GETSHIELDEDPOOLSTATEREQUEST_GETSHIELDEDPOOLSTATEREQUESTV0.fields_by_name['token_id'].containing_oneof = _GETSHIELDEDPOOLSTATEREQUEST_GETSHIELDEDPOOLSTATEREQUESTV0.oneofs_by_name['_token_id'] _GETSHIELDEDPOOLSTATEREQUEST.fields_by_name['v0'].message_type = _GETSHIELDEDPOOLSTATEREQUEST_GETSHIELDEDPOOLSTATEREQUESTV0 _GETSHIELDEDPOOLSTATEREQUEST.oneofs_by_name['version'].fields.append( _GETSHIELDEDPOOLSTATEREQUEST.fields_by_name['v0']) @@ -23213,6 +23427,9 @@ _GETSHIELDEDPOOLSTATERESPONSE.fields_by_name['v0']) _GETSHIELDEDPOOLSTATERESPONSE.fields_by_name['v0'].containing_oneof = _GETSHIELDEDPOOLSTATERESPONSE.oneofs_by_name['version'] _GETSHIELDEDNOTESCOUNTREQUEST_GETSHIELDEDNOTESCOUNTREQUESTV0.containing_type = _GETSHIELDEDNOTESCOUNTREQUEST +_GETSHIELDEDNOTESCOUNTREQUEST_GETSHIELDEDNOTESCOUNTREQUESTV0.oneofs_by_name['_token_id'].fields.append( + _GETSHIELDEDNOTESCOUNTREQUEST_GETSHIELDEDNOTESCOUNTREQUESTV0.fields_by_name['token_id']) +_GETSHIELDEDNOTESCOUNTREQUEST_GETSHIELDEDNOTESCOUNTREQUESTV0.fields_by_name['token_id'].containing_oneof = _GETSHIELDEDNOTESCOUNTREQUEST_GETSHIELDEDNOTESCOUNTREQUESTV0.oneofs_by_name['_token_id'] _GETSHIELDEDNOTESCOUNTREQUEST.fields_by_name['v0'].message_type = _GETSHIELDEDNOTESCOUNTREQUEST_GETSHIELDEDNOTESCOUNTREQUESTV0 _GETSHIELDEDNOTESCOUNTREQUEST.oneofs_by_name['version'].fields.append( _GETSHIELDEDNOTESCOUNTREQUEST.fields_by_name['v0']) @@ -23231,6 +23448,9 @@ _GETSHIELDEDNOTESCOUNTRESPONSE.fields_by_name['v0']) _GETSHIELDEDNOTESCOUNTRESPONSE.fields_by_name['v0'].containing_oneof = _GETSHIELDEDNOTESCOUNTRESPONSE.oneofs_by_name['version'] _GETSHIELDEDNULLIFIERSREQUEST_GETSHIELDEDNULLIFIERSREQUESTV0.containing_type = _GETSHIELDEDNULLIFIERSREQUEST +_GETSHIELDEDNULLIFIERSREQUEST_GETSHIELDEDNULLIFIERSREQUESTV0.oneofs_by_name['_token_id'].fields.append( + _GETSHIELDEDNULLIFIERSREQUEST_GETSHIELDEDNULLIFIERSREQUESTV0.fields_by_name['token_id']) +_GETSHIELDEDNULLIFIERSREQUEST_GETSHIELDEDNULLIFIERSREQUESTV0.fields_by_name['token_id'].containing_oneof = _GETSHIELDEDNULLIFIERSREQUEST_GETSHIELDEDNULLIFIERSREQUESTV0.oneofs_by_name['_token_id'] _GETSHIELDEDNULLIFIERSREQUEST.fields_by_name['v0'].message_type = _GETSHIELDEDNULLIFIERSREQUEST_GETSHIELDEDNULLIFIERSREQUESTV0 _GETSHIELDEDNULLIFIERSREQUEST.oneofs_by_name['version'].fields.append( _GETSHIELDEDNULLIFIERSREQUEST.fields_by_name['v0']) @@ -26454,6 +26674,20 @@ }) , + 'MintToPoolEvent' : _reflection.GeneratedProtocolMessageType('MintToPoolEvent', (_message.Message,), { + 'DESCRIPTOR' : _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_MINTTOPOOLEVENT, + '__module__' : 'platform_pb2' + # @@protoc_insertion_point(class_scope:org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent) + }) + , + + 'BurnFromPoolEvent' : _reflection.GeneratedProtocolMessageType('BurnFromPoolEvent', (_message.Message,), { + 'DESCRIPTOR' : _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_BURNFROMPOOLEVENT, + '__module__' : 'platform_pb2' + # @@protoc_insertion_point(class_scope:org.dash.platform.dapi.v0.GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent) + }) + , + 'GroupActionEvent' : _reflection.GeneratedProtocolMessageType('GroupActionEvent', (_message.Message,), { 'DESCRIPTOR' : _GETGROUPACTIONSRESPONSE_GETGROUPACTIONSRESPONSEV0_GROUPACTIONEVENT, '__module__' : 'platform_pb2' @@ -26532,6 +26766,8 @@ _sym_db.RegisterMessage(GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent) _sym_db.RegisterMessage(GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.PriceForQuantity) _sym_db.RegisterMessage(GetGroupActionsResponse.GetGroupActionsResponseV0.UpdateDirectPurchasePriceEvent.PricingSchedule) +_sym_db.RegisterMessage(GetGroupActionsResponse.GetGroupActionsResponseV0.MintToPoolEvent) +_sym_db.RegisterMessage(GetGroupActionsResponse.GetGroupActionsResponseV0.BurnFromPoolEvent) _sym_db.RegisterMessage(GetGroupActionsResponse.GetGroupActionsResponseV0.GroupActionEvent) _sym_db.RegisterMessage(GetGroupActionsResponse.GetGroupActionsResponseV0.DocumentEvent) _sym_db.RegisterMessage(GetGroupActionsResponse.GetGroupActionsResponseV0.DocumentCreateEvent) @@ -27155,8 +27391,8 @@ index=0, serialized_options=None, create_key=_descriptor._internal_create_key, - serialized_start=80227, - serialized_end=90547, + serialized_start=80887, + serialized_end=91207, methods=[ _descriptor.MethodDescriptor( name='broadcastStateTransition', From 9b176c571679b4361d1a7aeecadcf309d0e7dcd7 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 4 Oct 2026 15:13:51 +0700 Subject: [PATCH 93/94] fix(dpp): preserve shielded token purchase fees Keep the contract-owner balance allowance at 20 bytes independently of ShieldFromIdentity calibration, restoring the token purchase flat allowance to its historical 28 bytes. Make shared outputs-only fixtures reproducible per value, output count and funder binding, and test nullifier replay inside one uncommitted block. Test would have caught this in CI: RED before the fee fix (1,096,000 excess credits), GREEN after for three bundle sizes. Verified 35 nullifier tests and all-feature all-target Clippy on an isolated Cargo target. The separate identity admission-floor regression is still RED and is intentionally outside this commit. Correct the fee guide against PV14 values and arithmetic. Documentation has no behavior delta, so no separate behavior test was added. --- book/src/fees/shielded-fees.md | 56 +++++++++---------- .../compute_minimum_shielded_fee/mod.rs | 35 +++++++++++- packages/rs-dpp/src/shielded/mod.rs | 4 ++ .../state_transitions/shield/tests.rs | 54 ++++++++++++++++++ .../state_transitions/test_helpers.rs | 46 +++++++++------ 5 files changed, 146 insertions(+), 49 deletions(-) diff --git a/book/src/fees/shielded-fees.md b/book/src/fees/shielded-fees.md index 1cc658d9b3d..0cd6e2921cd 100644 --- a/book/src/fees/shielded-fees.md +++ b/book/src/fees/shielded-fees.md @@ -236,11 +236,10 @@ min_fee = proof_verification_fee + num_actions × (processing_fee + storage_fee) ### 1. Proof Verification Fee (per bundle) A single Halo 2 ZK proof covers the entire bundle regardless of action count. -Verifying it is the most expensive operation — benchmarked at approximately -30× the cost of a per-action signature verification. This is a fixed cost per -bundle. +The bundle's base verification work is benchmarked at approximately 5 ms. +This is a fixed cost per bundle. -**Current value:** `100,000,000` credits (100M) +**Protocol version 14 value:** `40,000,000` credits (40M) ### 2. Per-Action Processing Fee @@ -258,12 +257,8 @@ the same per-action processing charge — this fee tracks the marginal verificat work, not a fixed per-action checklist. From protocol version 14 it also prices the check of the nullifier each of their actions reveals. -The fee is calibrated at roughly a 4.5:1 ratio against the fixed -proof-verification fee (100M : 22M) rather than the looser ratio used before the -recalibration. (Note the two ratios on this page use different baselines: the -“30×” in §1 is the proof fee relative to a single RedPallas signature -verification, whereas this 4.5:1 is the proof fee relative to the per-action -processing fee.) +At protocol version 14, the proof and per-action fees are versioned independently. +Their numerical ratio is about 1.8:1 (40M : 22M). **Current value:** `22,000,000` credits (22M) @@ -273,33 +268,33 @@ Each action permanently stores data in two places: | Storage | Bytes | Contents | |---|---|---| -| BulkAppendTree (commitment tree) | 280 | 32 cmx + 32 rho + 216 encrypted note | +| BulkAppendTree (commitment tree) | 312 | 32 cmx + 32 rho + 32 cv_net + 216 encrypted note | | Nullifier tree | 32 | nullifier key (value is empty) | -| **Total** | **312** | | +| **Total physical payload** | **344** | | -The storage fee is derived from the platform's existing per-byte storage rates: +Protocol version 14 prices a 550-byte allowance per action, covering the physical +payload and database framing, at the platform's per-byte storage rates: ``` -storage_fee_per_action = 312 × (storage_disk_usage_credit_per_byte +storage_fee_per_action = 550 × (storage_disk_usage_credit_per_byte + storage_processing_credit_per_byte) - = 312 × (27,000 + 400) - = 312 × 27,400 - = 8,548,800 + = 550 × (27,000 + 400) + = 550 × 27,400 + = 15,070,000 ``` -This is not a separate constant — it is computed dynamically from the storage fee -version, ensuring shielded storage costs stay consistent with transparent storage -costs as fee parameters evolve. +The byte allowance is versioned. Its per-byte rates come from the storage fee +version, so the fee tracks changes to those rates. ## Fee Table -Combining all three components: +Combining all three components at protocol version 14: | Actions | Proof Fee | Processing | Storage | Total Minimum Fee | |---|---|---|---|---| -| 2 | 100,000,000 | 44,000,000 | 17,097,600 | **161,097,600** | -| 3 | 100,000,000 | 66,000,000 | 25,646,400 | **191,646,400** | -| 4 | 100,000,000 | 88,000,000 | 34,195,200 | **222,195,200** | +| 2 | 40,000,000 | 44,000,000 | 30,140,000 | **114,140,000** | +| 3 | 40,000,000 | 66,000,000 | 45,210,000 | **151,210,000** | +| 4 | 40,000,000 | 88,000,000 | 60,280,000 | **188,280,000** | Note: The Orchard protocol requires a minimum of 2 actions per bundle for privacy (even a single-input single-output transfer produces 2 actions with a dummy padding @@ -312,13 +307,13 @@ add a flat component on top of this base: - **`Unshield` adds the output-address write cost**: a flat `unshield_address_storage_fee = 222 × per_byte_rate = 222 × 27,400 = 6,082,800` credits, independent of action count. So the 2-action Unshield fee is - `161,097,600 + 6,082,800 = 167,180,400` credits (and likewise `+6,082,800` at every action + `114,140,000 + 6,082,800 = 120,222,800` credits (and likewise `+6,082,800` at every action count). See the [Fee Extraction](#fee-extraction-by-transition-type) Unshield row for why this component exists. - **`ShieldedWithdrawal` adds the Core withdrawal-document storage cost**: a flat `withdrawal_document_storage_fee = 4100 × per_byte_rate = 4100 × 27,400 = 112,340,000` credits, independent of action count. So the 2-action ShieldedWithdrawal fee is - `161,097,600 + 112,340,000 = 273,437,600` credits (and likewise `+112,340,000` at every action + `114,140,000 + 112,340,000 = 226,480,000` credits (and likewise `+112,340,000` at every action count). See the [Fee Extraction](#fee-extraction-by-transition-type) ShieldedWithdrawal row for why this component exists. - **`IdentityTopUpFromShieldedPool` adds the identity balance write cost**: a flat @@ -372,8 +367,9 @@ pub struct DriveAbciValidationConstants { pub minimum_pool_notes_for_outgoing: u64, pub shielded_anchor_retention_blocks: u64, pub shielded_anchor_pruning_interval: u64, - pub shielded_proof_verification_fee: u64, // 100_000_000 + pub shielded_proof_verification_fee: u64, // 40_000_000 at protocol 14 pub shielded_per_action_processing_fee: u64, // 22_000_000 + pub shielded_storage_bytes_per_action: u64, // 550 at protocol 14 pub shielded_implicit_fee_cap: u64, // 20_000_000_000 (0.2 DASH) } ``` @@ -382,15 +378,15 @@ The `shielded_implicit_fee_cap` bounds the surplus that a `ShieldFromAssetLock` implicitly donate to the fee pools when no `surplus_output` is set (see [Entry-Transition Fees](#entry-transition-fees-shield-shieldfromassetlock-and-shieldfromidentity)). -The storage component is not a separate constant — it is derived at runtime from +The storage component is derived at runtime from `fee_version.storage.storage_disk_usage_credit_per_byte` and `fee_version.storage.storage_processing_credit_per_byte`, multiplied by the -constant `SHIELDED_STORAGE_BYTES_PER_ACTION = 312`. +versioned `shielded_storage_bytes_per_action` allowance. This design means: - **Proof and processing fees** can be tuned independently via version bumps - **Storage fees** automatically track changes to the platform-wide storage rates -- No "magic number" for storage cost exists in the version constants +- **Storage allowances** can be calibrated independently of the per-byte rates ## How Fees Flow After Validation diff --git a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/mod.rs b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/mod.rs index a7739ec1b41..1b815acc1fa 100644 --- a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/mod.rs +++ b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/mod.rs @@ -2,8 +2,8 @@ mod v0; use crate::fee::Credits; use crate::shielded::{ - SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES, SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES, - SHIELDED_TOKEN_BALANCE_INSERT_STORAGE_BYTES, + SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES, SHIELDED_TOKEN_BALANCE_INSERT_STORAGE_BYTES, + SHIELDED_TOKEN_PURCHASE_OWNER_BALANCE_STORAGE_BYTES, }; use crate::ProtocolError; use platform_version::version::PlatformVersion; @@ -302,8 +302,37 @@ pub fn compute_token_purchase_from_shielded_pool_fee( compute_token_pool_paid_shielded_fee( token_actions, fee_actions, - SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES + SHIELDED_TOKEN_PURCHASE_OWNER_BALANCE_STORAGE_BYTES .saturating_add(SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES), platform_version, ) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn should_preserve_token_purchase_fee_when_identity_shield_floor_changes() { + let platform_version = PlatformVersion::latest(); + for (token_actions, fee_actions) in [(2, 2), (3, 4), (16, 16)] { + let historical_fee = compute_token_pool_paid_shielded_fee( + token_actions, + fee_actions, + 28, + platform_version, + ) + .expect("historical token purchase fee"); + assert_eq!( + compute_token_purchase_from_shielded_pool_fee( + token_actions, + fee_actions, + platform_version, + ) + .expect("token purchase fee"), + historical_fee, + "identity shielding must not change the token purchase allowance" + ); + } + } +} diff --git a/packages/rs-dpp/src/shielded/mod.rs b/packages/rs-dpp/src/shielded/mod.rs index dee4f470974..dc95059a49b 100644 --- a/packages/rs-dpp/src/shielded/mod.rs +++ b/packages/rs-dpp/src/shielded/mod.rs @@ -181,6 +181,10 @@ pub const SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES: u64 = 8; /// [`compute_minimum_shielded_fee::compute_shielded_identity_balance_write_fee`]. pub const SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES: u64 = 60; +/// Effective storage bytes for crediting the contract owner's existing identity +/// balance when tokens are bought from a shielded pool. +pub const SHIELDED_TOKEN_PURCHASE_OWNER_BALANCE_STORAGE_BYTES: u64 = 20; + /// Per-action component (in effective bytes at the per-byte storage rate) the `ShieldFromIdentity` /// admission floor adds on top of the per-action allowance of [`compute_minimum_shielded_fee`] /// (`shielded_storage_bytes_per_action`). diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs index 4b3463f9c56..b340786fd7a 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield/tests.rs @@ -1629,6 +1629,60 @@ mod tests { ); } + #[tokio::test] + async fn should_refuse_a_shield_repeating_a_nullifier_in_the_same_block() { + let pv = PlatformVersion::latest(); + let mut platform = setup_platform(); + let (signer, address) = funded_address(&mut platform); + let bundle = bound_bundle(address, pv); + let first = signed_shield(&signer, address, 1, &bundle).await; + let repeat = signed_shield(&signer, address, 2, &bundle).await; + let state = platform.state.load(); + let transaction = platform.drive.grove.start_transaction(); + let result = platform + .platform + .process_raw_state_transitions( + &[ + first.serialize_to_bytes().expect("first shield"), + repeat.serialize_to_bytes().expect("repeated shield"), + ], + &state, + &BlockInfo::default(), + &transaction, + pv, + false, + None, + ) + .expect("block processing"); + assert_matches!( + result.execution_results().as_slice(), + [ + StateTransitionExecutionResult::SuccessfulExecution { .. }, + StateTransitionExecutionResult::UnpaidConsensusError( + ConsensusError::StateError(StateError::NullifierAlreadySpentError(error)) + ) + ] if error.nullifier() == bundle.nullifiers()[0] + ); + assert_eq!( + platform + .drive + .read_shielded_pool_total_balance(Some(&transaction), &mut vec![], pv) + .expect("pool balance"), + bundle.amount, + "the repeated shield adds no credits" + ); + assert_eq!( + platform + .drive + .fetch_balance_and_nonce(&address, Some(&transaction), pv) + .expect("address") + .expect("funded address") + .0, + 1, + "the nullifier refusal does not consume another nonce" + ); + } + #[tokio::test] async fn should_refuse_a_spend_revealing_a_nullifier_a_shield_recorded() { let platform_version = PlatformVersion::latest(); diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/test_helpers.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/test_helpers.rs index c33eef4a8c5..983772d50ab 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/test_helpers.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/test_helpers.rs @@ -34,8 +34,15 @@ use drive::drive::shielded::paths::{ SHIELDED_TOTAL_BALANCE_KEY, }; use drive::grovedb::Element; +use grovedb_commitment_tree::{ + Anchor, Authorized as OrchardAuthorized, Builder, Bundle, BundleType, DashMemo, + Flags as OrchardFlags, FullViewingKey, NoteValue, ProvingKey, Scope, SpendingKey, +}; use platform_version::version::PlatformVersion; +use rand::rngs::StdRng; +use rand::SeedableRng; use std::collections::HashMap; +use std::sync::OnceLock; // Re-export commonly used types for convenience pub use dpp::dashcore::blockdata::opcodes::all::{ @@ -543,13 +550,21 @@ pub fn build_outputs_only_bundle(value: u64) -> OutputsOnlyBundle { /// funding it, so it cannot be lifted into a transition funded by anything else. Orchard pads it to /// two actions. pub fn build_outputs_only_bundle_bound(value: u64, extra_sighash_data: &[u8]) -> OutputsOnlyBundle { - use grovedb_commitment_tree::{ - Anchor, Builder, BundleType, Flags as OrchardFlags, FullViewingKey, NoteValue, Scope, - SpendingKey, - }; - use rand::rngs::OsRng; - - let mut rng = OsRng; + build_outputs_only_bundle_bound_with_outputs(value, 1, extra_sighash_data) +} + +/// Builds a reproducible bundle with the requested number of outputs. Different +/// values, output counts and funder bindings produce distinct dummy nullifiers. +pub fn build_outputs_only_bundle_bound_with_outputs( + value: u64, + output_count: usize, + extra_sighash_data: &[u8], +) -> OutputsOnlyBundle { + assert!(output_count > 0 && value >= output_count as u64); + let mut seed_data = value.to_be_bytes().to_vec(); + seed_data.extend_from_slice(&(output_count as u64).to_be_bytes()); + seed_data.extend_from_slice(extra_sighash_data); + let mut rng = StdRng::from_seed(sha256::Hash::hash(&seed_data).to_byte_array()); let sk = SpendingKey::from_bytes([0u8; 32]).unwrap(); let recipient = FullViewingKey::from(&sk).address_at(0u32, Scope::External); @@ -560,9 +575,13 @@ pub fn build_outputs_only_bundle_bound(value: u64, extra_sighash_data: &[u8]) -> }, Anchor::empty_tree(), ); - builder - .add_output(None, recipient, NoteValue::from_raw(value), [0u8; 36]) - .unwrap(); + let per_output = value / output_count as u64; + for output in 0..output_count { + let amount = per_output + u64::from(output == 0) * (value % output_count as u64); + builder + .add_output(None, recipient, NoteValue::from_raw(amount), [0u8; 36]) + .unwrap(); + } let (unauthorized, _) = builder.build::(&mut rng).unwrap().unwrap(); let bundle_commitment: [u8; 32] = unauthorized.commitment().into(); let sighash = compute_platform_sighash(&bundle_commitment, extra_sighash_data); @@ -573,6 +592,7 @@ pub fn build_outputs_only_bundle_bound(value: u64, extra_sighash_data: &[u8]) -> let (actions, _flags, value_balance, anchor, proof, binding_signature) = serialize_authorized_bundle_with_flags(&bundle); + assert_eq!(actions.len(), output_count.max(2)); assert!(value_balance < 0, "value must enter the pool"); OutputsOnlyBundle { actions, @@ -802,12 +822,6 @@ pub fn insert_dummy_encrypted_notes(platform: &TempPlatform, co // Shared Orchard Proving Key & Serialization // ========================================== -use grovedb_commitment_tree::{ - Authorized as OrchardAuthorized, Bundle, DashMemo, FullViewingKey, ProvingKey, Scope, - SpendingKey, -}; -use std::sync::OnceLock; - /// Single process-wide proving key shared by ALL shielded tests. /// /// `ProvingKey::build()` costs ~1-2 s. Previously every test module had its From ee44bba9dbc2725dbe0075a7542026fecf83e8fc Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 4 Oct 2026 22:19:29 +0700 Subject: [PATCH 94/94] fix!: cover identity shield admission estimates Move identity-shield admission allowances into protocol tables: historical versions keep 0/action and 20 flat; PV14 selects 400/action and 500 flat. Calibrate against the complete successful-event estimate, including the most expensive identity signature, rather than only the eventual metered charge. Token purchase retains its independent allowance and fee; charged rates do not change. Update the fee book and PV14 changelog to the final calibration. Test would have caught this in CI: FAIL before fix, PASS after. The complete-event bound failed at all action counts 1..16, and a real Shield funded with exactly amount + wallet estimate failed CheckTx. Both are GREEN; real 2/3/4/6-action bundles pass CheckTx and committed execution on empty and 512-nullifier trees. Tests assert conserved balances, nonces, nullifier writes and actual fee <= wallet floor. Historical wallet estimates remain pinned. Validation: dpp shielded 274; platform-version 25; nullifiers 37; identity Shield 24; all zero ignored. Scoped all-feature/all-target Clippy and workspace all-target check passed. Independent accounting, replay and scope reviews passed. Follow-up corrections for PR #5014. --- book/src/fees/shielded-fees.md | 20 +- .../compute_minimum_shielded_fee/mod.rs | 12 +- .../compute_minimum_shielded_fee/v0/mod.rs | 67 ++++-- packages/rs-dpp/src/shielded/mod.rs | 45 ---- .../shield_from_identity/tests.rs | 209 +++++++++++++++++- .../drive_abci_validation_versions/mod.rs | 7 + .../drive_abci_validation_versions/v1.rs | 2 + .../drive_abci_validation_versions/v10.rs | 6 + .../drive_abci_validation_versions/v2.rs | 2 + .../drive_abci_validation_versions/v3.rs | 2 + .../drive_abci_validation_versions/v4.rs | 2 + .../drive_abci_validation_versions/v5.rs | 2 + .../drive_abci_validation_versions/v6.rs | 2 + .../drive_abci_validation_versions/v7.rs | 2 + .../drive_abci_validation_versions/v8.rs | 2 + .../drive_abci_validation_versions/v9.rs | 2 + .../rs-platform-version/src/version/v14.rs | 7 +- 17 files changed, 305 insertions(+), 86 deletions(-) diff --git a/book/src/fees/shielded-fees.md b/book/src/fees/shielded-fees.md index 0cd6e2921cd..d9c2c4f3be4 100644 --- a/book/src/fees/shielded-fees.md +++ b/book/src/fees/shielded-fees.md @@ -154,15 +154,17 @@ identity_balance_after = identity_balance_before - amount - fee `user_fee_increase` applies to the metered processing portion. The stateless floor requires `identity_balance >= amount + compute_shielded_identity_balance_write_fee`, -the conservative complete fee: `compute_minimum_shielded_fee`, plus -`SHIELDED_IDENTITY_ACTION_WRITE_STORAGE_BYTES` (120 effective bytes) per action for the -metered processing of the note and nullifier writes, which the per-action allowance -(sized for the storage a spend books) does not price, plus the flat -`SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES` identity-side component (60 effective -bytes: the nonce and balance rewrites, which add no storage but replace 883 bytes of -Merk path for a measured 466,760 credits of processing, and the reads and pool-total -update around them, 1,076,280 credits in all), each at the storage rate and with -headroom like the other shielded components. An identity that could not pay the +the conservative complete admission estimate: `compute_minimum_shielded_fee`, plus +PV14's versioned `shielded_identity_action_write_storage_bytes` (400 effective +bytes per action) and `shielded_identity_balance_write_storage_bytes` (500 flat +bytes), each priced at the storage rate. The allowances cover the complete +execution-event estimate: note/nullifier writes at the estimator's depth 16, +identity nonce/balance writes at its maximum-element depth, and the signature +and state-read validation context. They are admission reserves, not physical +payload sizes or changes to the actual metered charge. For two actions the +base wallet estimate is `114,140,000 + (2 × 400 + 500) × 27,400 = 149,760,000` +credits. Historical tables preserve the preceding zero per-action and 20-byte +flat allowance. An identity that could not pay the complete fee is therefore refused before the Orchard proof is verified. The authoritative gate is the identity-paid fee validation of the execution event (`Paid`), which rejects with `IdentityInsufficientBalanceError`. The identity balance and the pool total are diff --git a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/mod.rs b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/mod.rs index 1b815acc1fa..e24db54c77f 100644 --- a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/mod.rs +++ b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/mod.rs @@ -140,13 +140,11 @@ pub fn compute_shielded_unshield_fee( } } -/// Computes the conservative **admission floor** (in credits) of a shielded transition that also -/// writes an identity balance (`ShieldFromIdentity`): [`compute_minimum_shielded_fee`] plus a -/// per-action write component (`SHIELDED_IDENTITY_ACTION_WRITE_STORAGE_BYTES` effective bytes per -/// action: the metered processing of the note and nullifier writes the per-action allowance does -/// not price) and the flat identity-side component (`SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES` -/// effective bytes: the nonce and balance rewrites and the reads around them), both at the -/// per-byte storage rate and folded into flat figures like the other shielded components. +/// Computes the conservative **admission floor** (in credits) for `ShieldFromIdentity`: +/// [`compute_minimum_shielded_fee`] plus the versioned per-action and flat +/// identity-write allowances, priced at the per-byte storage rate. The allowances +/// cover the complete execution-event admission estimate, including the estimated +/// note/nullifier and identity writes and the validation context. /// /// The transition's authoritative fee is metered at execution; this floor stands in for it where /// state is not yet available, so that an identity that could not pay the complete fee is refused diff --git a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs index ff806d91c32..5fad93d87d7 100644 --- a/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs +++ b/packages/rs-dpp/src/shielded/compute_minimum_shielded_fee/v0/mod.rs @@ -1,6 +1,5 @@ use crate::fee::Credits; use crate::shielded::{ - SHIELDED_IDENTITY_ACTION_WRITE_STORAGE_BYTES, SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES, SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES, SHIELDED_UNSHIELD_ADDRESS_STORAGE_BYTES, SHIELDED_WITHDRAWAL_DOCUMENT_STORAGE_BYTES, }; @@ -197,28 +196,26 @@ pub fn compute_shielded_unshield_fee_v0( /// v0 of the `ShieldFromIdentity` **admission floor**: /// /// `floor = compute_minimum_shielded_fee_v0(num_actions) -/// + (num_actions × SHIELDED_IDENTITY_ACTION_WRITE_STORAGE_BYTES -/// + SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES) × (disk + processing) credits/byte` +/// + (num_actions × shielded_identity_action_write_storage_bytes +/// + shielded_identity_balance_write_storage_bytes) × (disk + processing)` /// -/// [`compute_minimum_shielded_fee_v0`] plus a per-action component for the metered processing of -/// the note and nullifier writes that the per-action allowance (sized for a spend's booked -/// storage) does not price, see `SHIELDED_IDENTITY_ACTION_WRITE_STORAGE_BYTES`, plus one flat -/// component for the identity-side work (the nonce and balance rewrites and the reads around -/// them), built the same way as [`compute_shielded_unshield_fee_v0`]'s address-write component, -/// see `SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES`. `ShieldFromIdentity` is refused by -/// `is_allowed` before protocol version 14, so this formula has never been applied at an earlier -/// version, and its components were sized in place. The transition's real fee is metered at -/// execution; this floor is the conservative stand-in the stateless balance pre-check uses so -/// that a short identity is refused before the Orchard proof is verified, and the client-side -/// estimate of the total fee. +/// The versioned allowances cover the complete execution-event admission estimate, +/// including note/nullifier writes, identity nonce/balance writes and validation. +/// They are effective bytes priced at the full storage rate, not physical payload +/// sizes. Execution charges the actual metered fee, which may be lower. /// -/// All arithmetic is checked: an overflow (only reachable via pathological fee constants) -/// surfaces as `ProtocolError::Overflow` instead of silently wrapping. +/// Identity shielding is refused by `is_allowed` before protocol version 14; +/// historical tables retain the preceding wallet formula's zero per-action and +/// 20-byte flat allowance. All arithmetic is checked. pub fn compute_shielded_identity_balance_write_fee_v0( num_actions: usize, platform_version: &PlatformVersion, ) -> Result { let storage = &platform_version.fee_version.storage; + let constants = &platform_version + .drive_abci + .validation_and_processing + .event_constants; let base_fee = compute_minimum_shielded_fee_v0(num_actions, platform_version)?; @@ -229,12 +226,12 @@ pub fn compute_shielded_identity_balance_write_fee_v0( "shielded storage per-byte rate overflow", ))?; let action_write_bytes = (num_actions as u64) - .checked_mul(SHIELDED_IDENTITY_ACTION_WRITE_STORAGE_BYTES) + .checked_mul(constants.shielded_identity_action_write_storage_bytes) .ok_or(ProtocolError::Overflow( "shielded identity action write bytes overflow", ))?; let identity_write_fee = action_write_bytes - .checked_add(SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES) + .checked_add(constants.shielded_identity_balance_write_storage_bytes) .and_then(|bytes| bytes.checked_mul(per_byte_rate)) .ok_or(ProtocolError::Overflow( "shielded identity balance write fee overflow", @@ -396,14 +393,44 @@ mod tests { assert_eq!( floor, minimum - + (num_actions as u64 * SHIELDED_IDENTITY_ACTION_WRITE_STORAGE_BYTES - + SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES) + + (num_actions as u64 + * platform_version + .drive_abci + .validation_and_processing + .event_constants + .shielded_identity_action_write_storage_bytes + + platform_version + .drive_abci + .validation_and_processing + .event_constants + .shielded_identity_balance_write_storage_bytes) * per_byte_rate ); assert!(floor > compute); } } + #[test] + fn should_preserve_historical_identity_shield_wallet_estimates() { + // The transition is unavailable before protocol 14, but clients may still + // ask for the estimate at a historical protocol version. + for protocol in [12, 13] { + let platform_version = PlatformVersion::get(protocol).expect("historical version"); + let storage = &platform_version.fee_version.storage; + let per_byte_rate = storage.storage_disk_usage_credit_per_byte + + storage.storage_processing_credit_per_byte; + for num_actions in [1, 2, 6, 16] { + assert_eq!( + compute_shielded_identity_balance_write_fee_v0(num_actions, platform_version) + .expect("historical estimate"), + compute_minimum_shielded_fee_v0(num_actions, platform_version) + .expect("historical minimum") + + 20 * per_byte_rate, + ); + } + } + } + #[test] fn compute_minimum_shielded_fee_v0_equals_historical_formula() { let platform_version = PlatformVersion::latest(); diff --git a/packages/rs-dpp/src/shielded/mod.rs b/packages/rs-dpp/src/shielded/mod.rs index dc95059a49b..41804cffe55 100644 --- a/packages/rs-dpp/src/shielded/mod.rs +++ b/packages/rs-dpp/src/shielded/mod.rs @@ -152,55 +152,10 @@ pub const SHIELDED_UNSHIELD_ADDRESS_STORAGE_BYTES: u64 = 222; /// [`compute_minimum_shielded_fee::compute_shielded_identity_top_up_fee`]. pub const SHIELDED_IDENTITY_TOP_UP_BALANCE_STORAGE_BYTES: u64 = 8; -/// Flat component (in effective bytes at the per-byte storage rate) for the identity-side work a -/// `ShieldFromIdentity` performs on top of its per-action note and nullifier writes: the -/// `UpdateIdentityNonce` and `RemoveFromIdentityBalance` operations, and the reads and updates -/// around them (the identity fetched for its signature, the pool total read and rewritten). -/// -/// The transition's real fee is metered and only known at execution, so its stateless admission -/// floor needs a conservative stand-in for the metered part: [`compute_minimum_shielded_fee`] -/// (compute plus the per-action allowance), plus -/// [`SHIELDED_IDENTITY_ACTION_WRITE_STORAGE_BYTES`] per action, plus this component. Admission -/// below `amount + floor` is refused BEFORE the Orchard proof is verified, so a short identity -/// never occupies a proof-verification slot. -/// -/// What the two writes do: the identity already exists, so neither adds storage. Each rewrites -/// its element and every Merk node on the path to the root (replaced bytes, charged at the -/// per-byte processing rate), loads the path, seeks, and rehashes the nodes. Measured at protocol -/// version 14: the nonce update replaces 563 bytes and the balance debit 320 bytes (883 in -/// total) for 466,760 credits of processing applied one at a time, 424,400 when batched together -/// (shared path work). Like every other flat shielded component (`shielded_storage_bytes_per_action`, -/// `SHIELDED_UNSHIELD_ADDRESS_STORAGE_BYTES`), that variable tree work is folded into ONE flat -/// effective-byte figure priced at the full storage rate so it tracks the rate as it evolves, -/// rather than modelled per replaced byte: 466,760 credits is 17.0 effective bytes at 27,400 -/// credits/byte. With the reads and the pool-total update, the whole flat part of a -/// `ShieldFromIdentity` measured 1,076,280 credits at protocol version 14 (39.3 effective bytes; -/// the transition's metered fee is exactly linear in the action count on a fresh pool), and 60 -/// leaves headroom for the paths growing by about a node (roughly 0.7 effective bytes per write) -/// each time the identity count doubles. See -/// [`compute_minimum_shielded_fee::compute_shielded_identity_balance_write_fee`]. -pub const SHIELDED_IDENTITY_BALANCE_WRITE_STORAGE_BYTES: u64 = 60; - /// Effective storage bytes for crediting the contract owner's existing identity /// balance when tokens are bought from a shielded pool. pub const SHIELDED_TOKEN_PURCHASE_OWNER_BALANCE_STORAGE_BYTES: u64 = 20; -/// Per-action component (in effective bytes at the per-byte storage rate) the `ShieldFromIdentity` -/// admission floor adds on top of the per-action allowance of [`compute_minimum_shielded_fee`] -/// (`shielded_storage_bytes_per_action`). -/// -/// That allowance is sized for the storage a pool-paid spend books for its note and nullifier; the -/// spends' flat fee absorbs the writes' processing in its compute part. `ShieldFromIdentity` is -/// metered, and from protocol version 14 records the nullifier of each action as the spends do, so -/// its floor has to stand in for that processing as well. Measured at protocol version 14 on an -/// empty pool, each action costs 16,474,000 credits: 8,451,000 of note storage, 5,886,000 of -/// nullifier storage and 2,137,000 of processing, 601 effective bytes against the allowance's 550. -/// The nullifier write's processing grows with the nullifier tree, by about 46,000 credits (1.7 -/// effective bytes) each time the recorded nullifiers double (459,900 credits for the first write, -/// 1,371,280 at 131,072), so 120 covers each action up to about 2^30 recorded nullifiers. See -/// [`compute_minimum_shielded_fee::compute_shielded_identity_balance_write_fee`]. -pub const SHIELDED_IDENTITY_ACTION_WRITE_STORAGE_BYTES: u64 = 120; - /// Flat component (in effective bytes at the per-byte storage rate) for the recipient's token /// balance item a `TokenUnshieldWithShieldedFee` writes on top of its per-action nullifier and /// note writes. diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/tests.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/tests.rs index aadf8777922..c70680d2d21 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/tests.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/shield_from_identity/tests.rs @@ -1382,10 +1382,17 @@ mod tests { mod nullifiers { use super::*; + use crate::execution::types::execution_event::ExecutionEvent; + use crate::execution::types::execution_operation::signature_verification_operation::SignatureVerificationOperation; + use crate::execution::types::execution_operation::ValidationOperation; + use crate::execution::validation::state_transition::check_tx_verification::state_transition_to_execution_event_for_check_tx; use crate::execution::validation::state_transition::state_transitions::test_helpers::{ - build_outputs_only_bundle_bound, has_recorded_nullifier, - shielded_transfer_errors_revealing, OutputsOnlyBundle, + build_outputs_only_bundle_bound, build_outputs_only_bundle_bound_with_outputs, + check_tx_errors, has_recorded_nullifier, shielded_transfer_errors_revealing, + OutputsOnlyBundle, }; + use crate::platform_types::platform_state::PlatformStateV0Methods; + use drive::fees::op::LowLevelDriveOperation; /// A bundle proved against `identity_id`. The sighash binds the identity funding the /// shield, so the bundle verifies for that identity's transitions and for no other's, and @@ -1435,6 +1442,204 @@ mod tests { .expect("fetch pool total") } + fn populate_nullifier_tree( + platform: &TempPlatform, + platform_version: &PlatformVersion, + ) { + let nullifiers: Vec<_> = (0u16..512) + .map(|i| { + let mut nullifier = [0u8; 32]; + nullifier[..2].copy_from_slice(&i.to_le_bytes()); + nullifier + }) + .collect(); + let operations = platform + .drive + .insert_nullifiers(&nullifiers, platform_version) + .expect("nullifier operations"); + platform + .drive + .grove_apply_batch( + LowLevelDriveOperation::grovedb_operations_batch_consume(operations), + false, + None, + &platform_version.drive, + ) + .expect("populate nullifier tree"); + } + + /// Fee admission estimates the successful action before CheckTx verifies its proof. + /// Dummy proof bytes let every permitted action count exercise that complete estimate, + /// including the signature and state-read context, without proving sixteen circuits. + #[tokio::test] + async fn should_cover_complete_identity_shield_admission_estimates() { + let platform_version = PlatformVersion::latest(); + let mut shortfalls = vec![]; + for populated in [false, true] { + let mut platform = setup_platform(); + if populated { + populate_nullifier_tree(&platform, platform_version); + } + let (identity, signer) = funded_identity(&mut platform, 88, platform_version); + let state = platform.state.load(); + let platform_ref = PlatformRef { + drive: &platform.drive, + state: &state, + config: &platform.config, + core_rpc: &platform.core_rpc, + }; + for action_count in 1..=platform_version + .system_limits + .max_shielded_transition_actions + as usize + { + let mut bundle = dummy_bundle(); + bundle.actions = (0..action_count) + .map(|i| { + let mut action = create_dummy_serialized_action(); + action.nullifier[0] = i as u8 + 1; + action + }) + .collect(); + let st = create_signed_transition( + &identity, + &signer, + bundle, + 1_000, + 1, + 0, + platform_version, + ) + .await; + let mut event = state_transition_to_execution_event_for_check_tx( + &platform_ref, + &st, + CheckTxLevel::FirstTimeCheck, + &platform.check_tx_proof_verifier, + platform_version, + ) + .expect("build complete admission event") + .into_data() + .expect("valid admission event") + .expect("first CheckTx produces an event"); + let ExecutionEvent::Paid { + execution_operations, + additional_fixed_fee_cost, + .. + } = &mut event + else { + panic!("identity shield must be identity-paid"); + }; + // BLS is the most expensive identity signature admitted by this transition. + let mut signatures = 0; + for operation in execution_operations { + if let ValidationOperation::SignatureVerification(signature) = operation { + *signature = SignatureVerificationOperation::new(KeyType::BLS12_381); + signatures += 1; + } + } + assert_eq!(signatures, 1); + let compute_fee = additional_fixed_fee_cost.expect("shield compute fee"); + let estimated = platform + .platform + .validate_fees_of_event( + &event, + state.last_block_info(), + None, + platform_version, + &Default::default(), + ) + .expect("estimate complete event") + .into_data() + .expect("funded estimate") + .total_base_fee() + + compute_fee; + let floor = dpp::shielded::compute_shielded_identity_balance_write_fee( + action_count, + platform_version, + ) + .expect("wallet estimate"); + eprintln!( + "actions={action_count} populated={populated} admission={estimated} floor={floor}" + ); + if estimated > floor { + shortfalls.push((action_count, populated, estimated, floor)); + } + } + } + assert!( + shortfalls.is_empty(), + "wallet estimates must cover complete fee admission: {shortfalls:?}" + ); + } + + #[tokio::test] + async fn should_admit_identity_shield_funded_at_wallet_estimate() { + let platform_version = PlatformVersion::latest(); + let extra_sighash_data = + shield_from_identity_extra_sighash_data(&[88; 32], platform_version) + .expect("funding identity binding"); + for action_count in [2, 3, 4, 6] { + let bundle = build_outputs_only_bundle_bound_with_outputs( + 5_000, + action_count, + &extra_sighash_data, + ); + let floor = dpp::shielded::compute_shielded_identity_balance_write_fee( + bundle.actions.len(), + platform_version, + ) + .expect("wallet estimate"); + for populated in [false, true] { + let mut platform = setup_platform(); + if populated { + populate_nullifier_tree(&platform, platform_version); + } + let mut rng = StdRng::seed_from_u64(88); + let (identity, signer) = create_identity_with_transfer_key( + [88; 32], + bundle.amount + floor, + &mut rng, + platform_version, + ); + add_identity_to_drive(&mut platform, &identity); + let st = create_signed_transition( + &identity, + &signer, + proven(&bundle), + bundle.amount, + 1, + 0, + platform_version, + ) + .await; + assert!( + st.serialize_to_bytes().expect("serialize").len() as u64 + <= platform_version.system_limits.max_state_transition_size + ); + let admission = check_tx_errors(&platform, &st); + assert!( + admission.is_empty(), + "exact wallet funding must enter CheckTx: actions={action_count}, populated={populated}: {admission:?}" + ); + let result = process_transition_and_commit(&platform, st, platform_version); + let charged = match result.execution_results().as_slice() { + [StateTransitionExecutionResult::SuccessfulExecution { + fee_result, .. + }] => fee_result.total_base_fee(), + other => panic!("exact wallet funding must enter a block: {other:?}"), + }; + assert!(charged <= floor, "charged fee {charged} exceeds {floor}"); + assert_eq!(identity_balance(&platform, &identity), floor - charged); + assert_eq!(identity_nonce(&platform, &identity), 1); + assert_eq!(pool_total(&platform), bundle.amount); + for nullifier in bundle.nullifiers() { + assert!(has_recorded_nullifier(&platform, &nullifier)); + } + } + } + } + #[tokio::test] async fn should_record_the_nullifiers_a_shield_reveals() { let platform_version = PlatformVersion::latest(); diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs index 9c879945cbb..fc61961a8a5 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/mod.rs @@ -61,6 +61,13 @@ pub struct DriveAbciValidationConstants { /// (Merk node framing, dense path records, the amortized chunk-blob /// framing). pub shielded_storage_bytes_per_action: u64, + /// Effective bytes per action added by `compute_shielded_identity_balance_write_fee_v0` + /// to cover the estimated processing of identity-paid note and nullifier writes. + /// This is an admission allowance, not a physical payload size or a charged fee. + pub shielded_identity_action_write_storage_bytes: u64, + /// Flat effective-byte allowance read by `compute_shielded_identity_balance_write_fee_v0` + /// for estimated identity nonce/balance writes and the complete validation context. + pub shielded_identity_balance_write_storage_bytes: u64, /// Maximum surplus (in credits) that a `ShieldFromAssetLock` may implicitly /// donate to the fee pools when no `surplus_output` address is set. Above this /// cap the transition is rejected so a client cannot accidentally forfeit a diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs index 851fc6651f9..a4d3f23fd29 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v1.rs @@ -373,6 +373,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V1: DriveAbciValidationVersions = // The declared physical payload (312 note bytes + 32 nullifier // bytes); locked — released versions replay what they charged. shielded_storage_bytes_per_action: 344, + shielded_identity_action_write_storage_bytes: 0, + shielded_identity_balance_write_storage_bytes: 20, shielded_implicit_fee_cap: 20_000_000_000, shielded_identity_create_denominations: &[], }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs index 3b262f878a0..74966d12af9 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v10.rs @@ -494,6 +494,12 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V10: DriveAbciValidationVersions = // so the storage component alone pays for the database work and // the compute fees above stay reserved for compute. shielded_storage_bytes_per_action: 550, + // Fee admission estimates note/nullifier paths at depth 16 and identity + // writes at the maximum-element depth. These effective-byte allowances + // cover that estimate and its validation context, including a BLS signature; + // they do not change the metered charge or the pool-paid fee formula. + shielded_identity_action_write_storage_bytes: 400, + shielded_identity_balance_write_storage_bytes: 500, shielded_implicit_fee_cap: 20_000_000_000, // 0.1, 0.3, 0.5, 1.0 DASH in credits (1 DASH = 10^8 duffs, CREDITS_PER_DUFF = 1000). // v13 revises the v8 set: adds 0.03 and 0.25 DASH, retires 0.3 DASH. diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs index f0a8ffc2bee..432bbaa06ec 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v2.rs @@ -373,6 +373,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V2: DriveAbciValidationVersions = // The declared physical payload (312 note bytes + 32 nullifier // bytes); locked — released versions replay what they charged. shielded_storage_bytes_per_action: 344, + shielded_identity_action_write_storage_bytes: 0, + shielded_identity_balance_write_storage_bytes: 20, shielded_implicit_fee_cap: 20_000_000_000, shielded_identity_create_denominations: &[], }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs index 8cd245ceac8..a568addcbc8 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v3.rs @@ -373,6 +373,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V3: DriveAbciValidationVersions = // The declared physical payload (312 note bytes + 32 nullifier // bytes); locked — released versions replay what they charged. shielded_storage_bytes_per_action: 344, + shielded_identity_action_write_storage_bytes: 0, + shielded_identity_balance_write_storage_bytes: 20, shielded_implicit_fee_cap: 20_000_000_000, shielded_identity_create_denominations: &[], }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs index ff193306256..f5c45d3f19c 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v4.rs @@ -376,6 +376,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V4: DriveAbciValidationVersions = // The declared physical payload (312 note bytes + 32 nullifier // bytes); locked — released versions replay what they charged. shielded_storage_bytes_per_action: 344, + shielded_identity_action_write_storage_bytes: 0, + shielded_identity_balance_write_storage_bytes: 20, shielded_implicit_fee_cap: 20_000_000_000, shielded_identity_create_denominations: &[], }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs index 41d9af0c25e..dbd541308a2 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v5.rs @@ -377,6 +377,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V5: DriveAbciValidationVersions = // The declared physical payload (312 note bytes + 32 nullifier // bytes); locked — released versions replay what they charged. shielded_storage_bytes_per_action: 344, + shielded_identity_action_write_storage_bytes: 0, + shielded_identity_balance_write_storage_bytes: 20, shielded_implicit_fee_cap: 20_000_000_000, shielded_identity_create_denominations: &[], }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs index 9750844cb8e..a1b57e3297e 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v6.rs @@ -380,6 +380,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V6: DriveAbciValidationVersions = // The declared physical payload (312 note bytes + 32 nullifier // bytes); locked — released versions replay what they charged. shielded_storage_bytes_per_action: 344, + shielded_identity_action_write_storage_bytes: 0, + shielded_identity_balance_write_storage_bytes: 20, shielded_implicit_fee_cap: 20_000_000_000, shielded_identity_create_denominations: &[], }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs index 3f613268a2f..cf6c289f4ed 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v7.rs @@ -374,6 +374,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V7: DriveAbciValidationVersions = // The declared physical payload (312 note bytes + 32 nullifier // bytes); locked — released versions replay what they charged. shielded_storage_bytes_per_action: 344, + shielded_identity_action_write_storage_bytes: 0, + shielded_identity_balance_write_storage_bytes: 20, shielded_implicit_fee_cap: 20_000_000_000, shielded_identity_create_denominations: &[], }, diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs index 6c7e442a094..633dc8c016f 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v8.rs @@ -427,6 +427,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V8: DriveAbciValidationVersions = // The declared physical payload (312 note bytes + 32 nullifier // bytes); locked — released versions replay what they charged. shielded_storage_bytes_per_action: 344, + shielded_identity_action_write_storage_bytes: 0, + shielded_identity_balance_write_storage_bytes: 20, shielded_implicit_fee_cap: 20_000_000_000, // 0.1, 0.3, 0.5, 1.0 DASH in credits (1 DASH = 10^8 duffs, CREDITS_PER_DUFF = 1000). shielded_identity_create_denominations: &[ diff --git a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs index 00968f69e63..b31dea6dff2 100644 --- a/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs +++ b/packages/rs-platform-version/src/version/drive_abci_versions/drive_abci_validation_versions/v9.rs @@ -423,6 +423,8 @@ pub const DRIVE_ABCI_VALIDATION_VERSIONS_V9: DriveAbciValidationVersions = // The declared physical payload (312 note bytes + 32 nullifier // bytes); locked — released versions replay what they charged. shielded_storage_bytes_per_action: 344, + shielded_identity_action_write_storage_bytes: 0, + shielded_identity_balance_write_storage_bytes: 20, shielded_implicit_fee_cap: 20_000_000_000, // 0.1, 0.3, 0.5, 1.0 DASH in credits (1 DASH = 10^8 duffs, CREDITS_PER_DUFF = 1000). // v13 revises the v8 set: adds 0.03 and 0.25 DASH, retires 0.3 DASH. diff --git a/packages/rs-platform-version/src/version/v14.rs b/packages/rs-platform-version/src/version/v14.rs index ec5d20901ba..4c42b7c724c 100644 --- a/packages/rs-platform-version/src/version/v14.rs +++ b/packages/rs-platform-version/src/version/v14.rs @@ -1962,9 +1962,10 @@ pub const PROTOCOL_VERSION_14: ProtocolVersion = 14; /// asset lock's flat pool fee already prices a note and a nullifier write /// per action. The shield from identity's admission floor /// (`compute_shielded_identity_balance_write_fee` 0, the client's estimate -/// of its complete fee) gains 120 effective bytes per action and its flat -/// identity component grows from 20 to 60, so it still covers the metered -/// fee. Nullifiers revealed by shields before this version are not added. +/// of its complete fee) uses versioned allowances of 400 effective bytes +/// per action and 500 flat bytes, covering the complete execution-event +/// admission estimate. Actual fees remain metered. Nullifiers revealed by +/// shields before this version are not added. /// /// The app-connect system contract (`SystemDataContract::AppConnect`, schema v1) /// carries only the wallet's `loginKeyResponse`: a flat indexOnly entry keyed by