From 231f6ff9da12331f86c8d05e134aa8449bf07447 Mon Sep 17 00:00:00 2001 From: QuantumExplorer Date: Wed, 4 Nov 2026 05:28:48 +0700 Subject: [PATCH 1/3] feat(drive-abci)!: apply block randomness through the ApplyBlockRandomness ABCI method Tenderdash recovers a block's beacon from its polka's prevote signatures and hands it to the application through the new ApplyBlockRandomness method, before the precommit. Drive draws every random document property and random token amount the block's transitions asked for, and returns the app hash after the draw, which the precommits and the commit sign. - abci: ApplyBlockRandomness handler; FinalizeBlock applies the commit's beacon when it did not see the polka, and checks the commit's app hash and state id against the drawn state. - block execution collects the random requests of the block's transitions and runs apply_random_beacon (document draws, token settlements). - activation at protocol version 15: Tenderdash consensus version 2, sys.random.uniform registration and version 1 token transfers and mints. The first block of protocol version 15 still runs consensus version 1, so it refuses random transitions (RandomBeaconUnavailableError, 10604). - mimic and replay call ApplyBlockRandomness; a strategy test draws random documents and token amounts block after block. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 42 +- book/src/contract-keywords/generated-from.md | 8 +- book/src/error-handling/error-codes.md | 2 +- packages/dapi-grpc/Cargo.toml | 2 +- .../class_methods/try_from_schema/mod.rs | 10 +- .../v3/generated_from_random_tests.rs | 53 +- .../src/errors/consensus/basic/basic_error.rs | 17 +- .../consensus/basic/state_transition/mod.rs | 2 + .../random_beacon_unavailable_error.rs | 70 ++ packages/rs-dpp/src/errors/consensus/codes.rs | 1 + packages/rs-dpp/src/state_transition/mod.rs | 108 ++-- packages/rs-drive-abci/Cargo.toml | 2 +- .../rs-drive-abci/src/abci/app/consensus.rs | 7 + packages/rs-drive-abci/src/abci/app/full.rs | 7 + .../abci/handler/apply_block_randomness.rs | 605 ++++++++++++++++++ .../src/abci/handler/extend_vote.rs | 2 + .../src/abci/handler/finalize_block.rs | 287 ++++++++- .../src/abci/handler/init_chain.rs | 2 + .../rs-drive-abci/src/abci/handler/mod.rs | 2 + .../engine/consensus_params_update/mod.rs | 80 ++- .../engine/consensus_params_update/v2/mod.rs | 9 +- .../engine/finalize_block_proposal/v0/mod.rs | 60 +- .../engine/run_block_proposal/mod.rs | 7 + .../engine/run_block_proposal/v0/mod.rs | 9 +- .../block_end/apply_random_beacon/mod.rs | 69 ++ .../block_end/apply_random_beacon/v0/mod.rs | 115 ++++ .../platform_events/block_end/mod.rs | 4 + .../block_end/should_checkpoint/mod.rs | 4 + .../block_end/should_checkpoint/v0/mod.rs | 2 + .../block_end/update_checkpoints/v0/mod.rs | 2 + .../block_end/update_drive_cache/v0/mod.rs | 2 + .../block_end/update_state_cache/v0/mod.rs | 42 ++ .../block_end/validator_set_update/mod.rs | 2 + .../v0/mod.rs | 2 + .../v0/mod.rs | 2 + .../block_processing_end_events/tests.rs | 2 + .../process_raw_state_transitions/v0/mod.rs | 279 +++++++- .../types/block_execution_context/mod.rs | 21 +- .../types/block_execution_context/v0/mod.rs | 44 ++ .../types/block_random_requests/mod.rs | 32 + .../rs-drive-abci/src/execution/types/mod.rs | 2 + .../tests/document/generated_from_random.rs | 486 ++++++++++++++ .../batch/tests/document/mod.rs | 1 + .../batch/tests/token/mod.rs | 1 + .../batch/tests/token/random_amount/mod.rs | 520 +++++++++++++++ .../batch/transformer/v0/mod.rs | 38 ++ .../transformer/v0/random_beacon_gate.rs | 165 +++++ .../v0/mod.rs | 17 +- .../state_transition/state_transitions/mod.rs | 2 + packages/rs-drive-abci/src/mimic/mod.rs | 94 ++- .../cleaned_commit_info/v0/mod.rs | 32 + .../src/platform_types/commit/v0/mod.rs | 11 + .../platform_state/accessors.rs | 69 ++ .../src/platform_types/platform_state/mod.rs | 15 + .../platform_state_for_saving/v0/mod.rs | 2 + .../platform_state_for_saving/v1/mod.rs | 2 + .../platform_state_for_saving/v2/mod.rs | 2 + .../mod.rs | 21 + .../rs-drive-abci/src/replay/log_ingest.rs | 84 ++- packages/rs-drive-abci/src/replay/runner.rs | 71 +- .../src/test/helpers/fast_forward_to_block.rs | 2 + .../test_cases/credit_pool_backfill_tests.rs | 3 + .../tests/strategy_tests/test_cases/mod.rs | 1 + .../test_cases/random_beacon_tests.rs | 213 ++++++ .../test_cases/vote_extension_round_tests.rs | 3 + .../verify_state_transitions.rs | 40 +- packages/rs-drive-proof-verifier/Cargo.toml | 2 +- .../draw_random_document_properties/v0/mod.rs | 39 +- .../v0/transformer.rs | 21 +- .../dpp_versions/dpp_contract_versions/mod.rs | 9 +- .../dpp_versions/dpp_contract_versions/v7.rs | 12 +- .../mod.rs | 10 +- .../v3.rs | 4 +- .../v4.rs | 14 + .../drive_abci_method_versions/mod.rs | 6 + .../drive_abci_method_versions/v1.rs | 1 + .../drive_abci_method_versions/v10.rs | 1 + .../drive_abci_method_versions/v11.rs | 16 +- .../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 + .../src/version/mocks/v3_test.rs | 1 + .../rs-platform-version/src/version/v15.rs | 53 +- .../src/errors/consensus/consensus_error.rs | 5 +- 89 files changed, 3878 insertions(+), 239 deletions(-) create mode 100644 packages/rs-dpp/src/errors/consensus/basic/state_transition/random_beacon_unavailable_error.rs create mode 100644 packages/rs-drive-abci/src/abci/handler/apply_block_randomness.rs create mode 100644 packages/rs-drive-abci/src/execution/platform_events/block_end/apply_random_beacon/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/platform_events/block_end/apply_random_beacon/v0/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/types/block_random_requests/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/generated_from_random.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/random_amount/mod.rs create mode 100644 packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/random_beacon_gate.rs create mode 100644 packages/rs-drive-abci/tests/strategy_tests/test_cases/random_beacon_tests.rs create mode 100644 packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v4.rs diff --git a/Cargo.lock b/Cargo.lock index 0a62fe3a87c..fc41efc6021 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1271,7 +1271,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2729,7 +2729,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2789,7 +2789,7 @@ checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3872,7 +3872,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.4", "system-configuration", "tokio", "tower-service", @@ -4116,7 +4116,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4921,7 +4921,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5793,8 +5793,8 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03da047801ff44bb6a4d407d4860c05fd70bb81714e6b2f3812603d5b145b042" dependencies = [ - "heck 0.4.1", - "itertools 0.14.0", + "heck 0.5.0", + "itertools 0.10.5", "log", "multimap", "petgraph", @@ -5815,7 +5815,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.117", @@ -5828,7 +5828,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.117", @@ -5944,7 +5944,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.2", "rustls", - "socket2 0.5.10", + "socket2 0.6.4", "thiserror 2.0.18", "tokio", "tracing", @@ -5983,9 +5983,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.4", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6783,7 +6783,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6796,7 +6796,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -6855,7 +6855,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -7676,13 +7676,13 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "tenderdash-abci" version = "1.8.1" -source = "git+https://github.com/dashpay/rs-tenderdash-abci?tag=v1.8.1#eebfc46cc64b27022ccbbb3924604dd87a10c340" +source = "git+https://github.com/dashpay/rs-tenderdash-abci?rev=e80c8a7361eda12918d938acd4ca077478419902#e80c8a7361eda12918d938acd4ca077478419902" dependencies = [ "bytes", "futures", @@ -7703,7 +7703,7 @@ dependencies = [ [[package]] name = "tenderdash-proto" version = "1.8.1" -source = "git+https://github.com/dashpay/rs-tenderdash-abci?tag=v1.8.1#eebfc46cc64b27022ccbbb3924604dd87a10c340" +source = "git+https://github.com/dashpay/rs-tenderdash-abci?rev=e80c8a7361eda12918d938acd4ca077478419902#e80c8a7361eda12918d938acd4ca077478419902" dependencies = [ "bytes", "chrono", @@ -7723,7 +7723,7 @@ dependencies = [ [[package]] name = "tenderdash-proto-compiler" version = "1.8.1" -source = "git+https://github.com/dashpay/rs-tenderdash-abci?tag=v1.8.1#eebfc46cc64b27022ccbbb3924604dd87a10c340" +source = "git+https://github.com/dashpay/rs-tenderdash-abci?rev=e80c8a7361eda12918d938acd4ca077478419902#e80c8a7361eda12918d938acd4ca077478419902" dependencies = [ "fs_extra", "prost-build", @@ -9150,7 +9150,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/book/src/contract-keywords/generated-from.md b/book/src/contract-keywords/generated-from.md index 0447be97a23..50ea0661c29 100644 --- a/book/src/contract-keywords/generated-from.md +++ b/book/src/contract-keywords/generated-from.md @@ -2,14 +2,14 @@ `generatedFrom` says that the platform generates a string property's value with a system function of other properties of the same document, its params. The functions change the case of a string (`lowercase`, `uppercase`, `capitalize`, `camelCase`, `snakeCase`), or fold a name so that names differing only by case or by look-alike characters read the same (`homographSafeASCII`): reach for that one when a unique index must treat `Bob`, `BOB` and `B0B` as one name. A client may leave the property out, and the platform generates it when the document arrives; a client that sends it has it checked. The check reads only the document being written, so it costs no state reads. -From protocol version 15 an integer property can be generated too, with `sys.tokens.decimals`: the decimals of the token an identifier property names, read from the token's contract. A value read from state is generated when the document is created and kept after (see [Functions that read state](#functions-that-read-state)). Protocol version 15 also parses `sys.random.uniform`, an integer the platform draws from the block's random beacon, but no protocol version activates it yet: registration refuses it until block execution draws the values (see [Functions drawn at random](#functions-drawn-at-random)). +From protocol version 15 an integer property can be generated too, with `sys.tokens.decimals`: the decimals of the token an identifier property names, read from the token's contract. A value read from state is generated when the document is created and kept after (see [Functions that read state](#functions-that-read-state)). Protocol version 15 also admits `sys.random.uniform`, an integer the platform draws from the block's random beacon once the block's transitions are accepted (see [Functions drawn at random](#functions-drawn-at-random)). | | | |---|---| | **Where** | A string property, at the top level or inside an object; from protocol version 15 also an integer property, for `sys.tokens.decimals` and `sys.random.uniform`. Not on a typed array or its `items`, and not beside `$ref` | | **Value** | `{ "function": , "params": [, ...] }`: a system function, and as many params as it takes, each the dotted path of a property of the same document type (`"profile.display"` for a nested one), 1 to 256 characters (ASCII, as property names are, so as many bytes). `sys.random.uniform` takes none: `params` may be left out, or be `[]` | | **Default** | Absent: no rule | -| **Since** | protocol version 14; `sys.tokens.decimals` protocol version 15; `sys.random.uniform` parsed from protocol version 15, refused at registration until a later protocol version activates it | +| **Since** | protocol version 14; `sys.tokens.decimals` protocol version 15; `sys.random.uniform` protocol version 15 | | **On update** | Fixed: adding, removing or changing it is refused (`IncompatibleDocumentTypeSchemaError`, 10246); a property an update adds may declare it only when one of its params is new too (`DocumentTypeUpdateError`, 40212) | | **Errors** | `DocumentPropertyNotGeneratedError` (10424) on a document, and from protocol version 15 `ReferencedEntityNotFoundError` (40120) for a token that does not exist; at registration `JsonSchemaError` (10101) or `InvalidContractStructure` (10231); on update `IncompatibleDocumentTypeSchemaError` (10246) or `DocumentTypeUpdateError` (40212) | @@ -113,7 +113,7 @@ The `sys.random` functions take no params. The platform draws their value from t |---|---|---| | `sys.random.uniform` | An integer drawn uniformly from the property's `minimum` to its `maximum`, both included | An integer property of at most 64 bits declaring both a `minimum` and a `maximum` inside its type, the `minimum` not above the `maximum`, with no `enum`, `const`, `exclusiveMinimum`, `exclusiveMaximum` or `multipleOf`, so every value drawn passes its schema | -Protocol version 15 parses the function, but it is not active at any protocol version yet: registration refuses it with `InvalidContractStructure` (10231), "not active at this protocol version", until block execution draws the values from the beacon Tenderdash delivers. What follows is how it works once a protocol version activates it. +Tenderdash delivers the beacon from its consensus version 2, which protocol version 15 switches it to. Tenderdash applies that switch from the block after the one that moves to protocol version 15, so that first block receives no beacon: a create of a document whose type declares a `sys.random` property is refused there, unpaid, with `RandomBeaconUnavailableError` (10604), and accepted from the next block on. Before protocol version 15, registration refuses the function. ```json "card": { @@ -167,7 +167,7 @@ For a string transformation, the SDK's transition builders generate the property ## Rules at registration -- The keyword is allowed only on a string property (from protocol version 15 also an integer property, for `sys.tokens.decimals`, whose `minimum` and `maximum` admit 0 to 255 and which declares no `enum`, `const`, `exclusiveMinimum`, `exclusiveMaximum` or `multipleOf`, and for `sys.random.uniform` once a protocol version activates it, as described under [Functions drawn at random](#functions-drawn-at-random)), and not beside `$ref`, whose definition replaces every keyword written next to it (declare it in the definition instead). On any other property, a typed array and its `items` included, the meta-schema refuses it (`JsonSchemaError`, 10101). +- The keyword is allowed only on a string property (from protocol version 15 also an integer property, for `sys.tokens.decimals`, whose `minimum` and `maximum` admit 0 to 255 and which declares no `enum`, `const`, `exclusiveMinimum`, `exclusiveMaximum` or `multipleOf`, and for `sys.random.uniform`, as described under [Functions drawn at random](#functions-drawn-at-random)), and not beside `$ref`, whose definition replaces every keyword written next to it (declare it in the definition instead). On any other property, a typed array and its `items` included, the meta-schema refuses it (`JsonSchemaError`, 10101). - `function` must name a system function, and `params` must list 1 to 16 params, as many as the function takes (none for `sys.random.uniform`, which may leave `params` out). The meta-schema refuses an unknown function or an empty or overlong list (`JsonSchemaError`, 10101). - Every param must name another string property of the same document type (an identifier property for a `sys.tokens` function; not an object, not a system property, not the declaring property), and that property may not be generated itself. - A `sys.tokens` function is refused on an [indexOnly](index-only.md) type, whose deletes send their values again where a kept value could not be checked, on a property in a contested index, since a client, which leaves the property out, could not tell whether its document joins a contest, and on a property inside an object declaring `const` or `enum`, which compare the whole object, the value included (to allow some tokens only, declare `enum` on the token property). The property and every object holding it are declared in place, without a `$ref`: a client checks its document with a value the property admits in place of the one it leaves out, and a `$ref` would bring in schemas, the definition's own references and the keywords beside it included, that registration does not judge. diff --git a/book/src/error-handling/error-codes.md b/book/src/error-handling/error-codes.md index 0544e632bce..4b748144a2b 100644 --- a/book/src/error-handling/error-codes.md +++ b/book/src/error-handling/error-codes.md @@ -56,7 +56,7 @@ Error codes are organized into ranges that correspond to error categories and su | 10400-10425 | Documents | `DataContractNotPresentError` (10400), `DuplicateDocumentTransitionsWithIdsError` (10401), `DocumentPropertyNotDistinctError` (10419), `InvalidEncryptedPropertyShapeError` (10420), `DocumentPropertyMaxBytesExceededError` (10421), `DocumentPropertyConstraintViolatedError` (10422), `DocumentReferencePreimageInvalidError` (10423), `DocumentPropertyNotGeneratedError` (10424), `DocumentTypePrivateError` (10425) | | 10450-10460 | Tokens | `InvalidTokenIdError` (10450), `TokenTransferToOurselfError` (10456) | | 10500-10535 | Identity | `DuplicatedIdentityPublicKeyBasicError` (10500), `InvalidIdentityPublicKeyDataError` (10511) | -| 10600-10603 | State Transition | `InvalidStateTransitionTypeError` (10600), `StateTransitionMaxSizeExceededError` (10602) | +| 10600-10604 | State Transition | `InvalidStateTransitionTypeError` (10600), `StateTransitionMaxSizeExceededError` (10602), `RandomBeaconUnavailableError` (10604) | | 10700-10700 | General | `OverflowError` (10700) | | 10800-10818 | Address | `TransitionOverMaxInputsError` (10800), `WithdrawalBelowMinAmountError` (10818) | | 10819-10827 | Shielded | `ShieldedNoActionsError` (10819), `ShieldedTooManyActionsError` (10825), `ShieldedImplicitFeeCapExceededError` (10826), `ShieldedInvalidDenominationError` (10827 — `IdentityCreateFromShieldedPool` exit amount not a member of the versioned denomination set) | diff --git a/packages/dapi-grpc/Cargo.toml b/packages/dapi-grpc/Cargo.toml index 811ba42861c..a6d1f7bee1a 100644 --- a/packages/dapi-grpc/Cargo.toml +++ b/packages/dapi-grpc/Cargo.toml @@ -57,7 +57,7 @@ serde = ["dep:serde", "dep:serde_bytes", "tenderdash-proto/serde"] mocks = ["serde", "dep:serde_json"] [dependencies] -tenderdash-proto = { git = "https://github.com/dashpay/rs-tenderdash-abci", tag = "v1.8.1", default-features = false } +tenderdash-proto = { git = "https://github.com/dashpay/rs-tenderdash-abci", rev = "e80c8a7361eda12918d938acd4ca077478419902", default-features = false } prost = { version = "0.14" } futures-core = "0.3.30" diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs index f8561b432b6..8568dbf88aa 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/mod.rs @@ -3121,7 +3121,7 @@ fn apply_generated_from_v0( /// A property may also be generated with `sys.random.uniform`, which takes no parameters /// (`params` may be left out) and draws an integer from the property's `minimum` to its /// `maximum`, read into [`GeneratedFrom::range`] ([`uniform_range`]), where the platform -/// version's `random_generated_from` admits it; `None`, in every table for now, refuses it. +/// version's `random_generated_from` admits it (0 at protocol version 15); `None` refuses it. fn apply_generated_from_v1( inner_properties: &BTreeMap, property_type: &DocumentPropertyType, @@ -3229,13 +3229,13 @@ fn apply_generated_from_v1( .schema .random_generated_from { - // Nothing draws the values yet: block execution does not call - // `draw_random_document_properties` until Tenderdash delivers the block's - // random beacon, so a document would keep its placeholder for good + // A table that draws no random values: a document would keep its placeholder + // for good. Unreached by the shipped tables: generation 1 is protocol version + // 15's, whose table admits the function None => { return Err(DataContractError::InvalidContractStructure(format!( "generatedFrom function {function} is not active at this protocol \ - version: the platform does not draw random values yet" + version: the platform draws no random values" ))); } Some(0) => { diff --git a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/generated_from_random_tests.rs b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/generated_from_random_tests.rs index 7be370ff931..56b81e58966 100644 --- a/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/generated_from_random_tests.rs +++ b/packages/rs-dpp/src/data_contract/document_type/class_methods/try_from_schema/v3/generated_from_random_tests.rs @@ -8,9 +8,8 @@ //! (`regenerate_generated_properties`), and a proof verifier takes it from the proved document //! (`adopt_state_generated_properties`). //! -//! No protocol version admits the function yet (`random_generated_from` is `None` in every -//! table, until block execution draws the values), so these tests parse with the latest -//! platform version with it admitted (`drawing_platform_version`). +//! Protocol version 15 admits the function (`random_generated_from` 0), whose blocks draw the +//! values from their random beacon, so these tests parse with the latest platform version. use super::refusal_test_support::{assert_refused, CONTRACT_ID}; use super::typed_array_test_helpers::{ @@ -69,37 +68,25 @@ fn card_schema_with(key: &str, value: Value) -> Value { schema } -/// The latest platform version with `sys.random.uniform` admitted. -fn drawing_platform_version() -> PlatformVersion { - let mut platform_version = PlatformVersion::latest().clone(); - platform_version - .dpp - .contract_versions - .document_type_versions - .schema - .random_generated_from = Some(0); - platform_version -} - fn parse(schema: Value) -> DocumentType { - parse_dispatched(schema, &drawing_platform_version(), true).expect("the schema parses") + parse_dispatched(schema, PlatformVersion::latest(), true).expect("the schema parses") } /// Both parses refuse the declaration: the validating one natively or with the parser's /// structure error, the one that skips the schema check with the parser's. fn expect_refused(schema: Value, needle: &str) { - let platform_version = drawing_platform_version(); + let platform_version = PlatformVersion::latest(); assert!( - parse_dispatched(schema.clone(), &platform_version, true).is_err(), + parse_dispatched(schema.clone(), platform_version, true).is_err(), "a validating parse refuses it ({needle})" ); - expect_structure_error(parse_dispatched(schema, &platform_version, false), needle); + expect_structure_error(parse_dispatched(schema, platform_version, false), needle); } /// Registration refuses the declaration with the parser's structure error. fn expect_refused_at_registration(schema: Value, needle: &str) { expect_structure_error( - parse_dispatched(schema, &drawing_platform_version(), true), + parse_dispatched(schema, PlatformVersion::latest(), true), needle, ); } @@ -108,8 +95,8 @@ fn expect_refused_at_registration(schema: Value, needle: &str) { fn parse_contract( types: Vec<(&str, Value)>, ) -> Result, ProtocolError> { - let platform_version = drawing_platform_version(); - let config = DataContractConfig::default_for_version(&platform_version) + let platform_version = PlatformVersion::latest(); + let config = DataContractConfig::default_for_version(platform_version) .expect("default config available"); DocumentType::create_document_types_from_document_schemas( Identifier::new(CONTRACT_ID), @@ -125,7 +112,7 @@ fn parse_contract( true, false, &mut vec![], - &platform_version, + platform_version, ) } @@ -212,16 +199,22 @@ fn should_parse_sys_random_uniform_onto_a_bounded_integer_property_from_protocol } #[test] -fn should_refuse_sys_random_uniform_while_no_protocol_version_draws_the_values() { - // `random_generated_from` is `None` at the latest protocol version: nothing draws the - // values yet, so a document would keep its placeholder - let platform_version = PlatformVersion::latest(); +fn should_refuse_sys_random_uniform_where_the_table_does_not_admit_it() { + // A table whose `random_generated_from` is `None` draws no values, so a document would + // keep its placeholder + let mut platform_version = PlatformVersion::latest().clone(); + platform_version + .dpp + .contract_versions + .document_type_versions + .schema + .random_generated_from = None; expect_structure_error( - parse_dispatched(card_schema(), platform_version, true), + parse_dispatched(card_schema(), &platform_version, true), "is not active at this protocol version", ); expect_structure_error( - parse_dispatched(card_schema(), platform_version, false), + parse_dispatched(card_schema(), &platform_version, false), "is not active at this protocol version", ); } @@ -386,7 +379,7 @@ fn should_keep_the_stored_value_on_a_replace() { #[test] fn should_leave_the_value_to_the_platform_on_a_client() { let document_type = parse(card_schema()); - let platform_version = &drawing_platform_version(); + let platform_version = PlatformVersion::latest(); // The transition builders remove it, whatever the document holds let mut properties = data(vec![ 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 5d2cdab06a3..19eb52f59f0 100644 --- a/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs +++ b/packages/rs-dpp/src/errors/consensus/basic/basic_error.rs @@ -108,7 +108,7 @@ use crate::consensus::basic::state_transition::{ InputWitnessCountMismatchError, InputsNotLessThanOutputsError, InsufficientFundingAmountError, InvalidRemainderOutputCountError, InvalidStateTransitionTypeError, MissingStateTransitionTypeError, OutputAddressAlsoInputError, OutputBelowMinimumError, - OutputsNotGreaterThanInputsError, ShieldedEmptyProofError, + OutputsNotGreaterThanInputsError, RandomBeaconUnavailableError, ShieldedEmptyProofError, ShieldedEncryptedNoteSizeMismatchError, ShieldedImplicitFeeCapExceededError, ShieldedInvalidDenominationError, ShieldedInvalidValueBalanceError, ShieldedNoActionsError, ShieldedTooManyActionsError, ShieldedZeroAnchorError, StateTransitionMaxSizeExceededError, @@ -896,6 +896,11 @@ pub enum BasicError { // its recipient to the token's configuration (protocol version 15). #[error(transparent)] RandomTokenMintNotAllowedError(RandomTokenMintNotAllowedError), + + // A transition asking for a random value in the block that switches to protocol version 15, + // which receives no random beacon. + #[error(transparent)] + RandomBeaconUnavailableError(RandomBeaconUnavailableError), } impl From for ConsensusError { @@ -1141,7 +1146,7 @@ mod tests { )), 210 ); - // A token mint refusing a random amount (protocol version 15): the tail of the enum. + // A token mint refusing a random amount (protocol version 15). assert_eq!( discriminant_of(BasicError::RandomTokenMintNotAllowedError( RandomTokenMintNotAllowedError::new( @@ -1151,6 +1156,14 @@ mod tests { )), 211 ); + // A random value asked of the block switching to protocol version 15: the tail of the + // enum. + assert_eq!( + discriminant_of(BasicError::RandomBeaconUnavailableError( + RandomBeaconUnavailableError::new("subject", 15) + )), + 212 + ); } /// The variants that shipped in 4.1 keep the discriminants they were released with, so an diff --git a/packages/rs-dpp/src/errors/consensus/basic/state_transition/mod.rs b/packages/rs-dpp/src/errors/consensus/basic/state_transition/mod.rs index b20d1d53f27..c7aa1838c3b 100644 --- a/packages/rs-dpp/src/errors/consensus/basic/state_transition/mod.rs +++ b/packages/rs-dpp/src/errors/consensus/basic/state_transition/mod.rs @@ -13,6 +13,7 @@ mod missing_state_transition_type_error; mod output_address_also_input_error; mod output_below_minimum_error; mod outputs_not_greater_than_inputs_error; +mod random_beacon_unavailable_error; mod shielded_empty_proof_error; mod shielded_encrypted_note_size_mismatch_error; mod shielded_implicit_fee_cap_exceeded_error; @@ -45,6 +46,7 @@ pub use missing_state_transition_type_error::*; pub use output_address_also_input_error::*; pub use output_below_minimum_error::*; pub use outputs_not_greater_than_inputs_error::*; +pub use random_beacon_unavailable_error::*; pub use shielded_empty_proof_error::*; pub use shielded_encrypted_note_size_mismatch_error::*; pub use shielded_implicit_fee_cap_exceeded_error::*; diff --git a/packages/rs-dpp/src/errors/consensus/basic/state_transition/random_beacon_unavailable_error.rs b/packages/rs-dpp/src/errors/consensus/basic/state_transition/random_beacon_unavailable_error.rs new file mode 100644 index 00000000000..dbee46828b4 --- /dev/null +++ b/packages/rs-dpp/src/errors/consensus/basic/state_transition/random_beacon_unavailable_error.rs @@ -0,0 +1,70 @@ +use crate::consensus::basic::BasicError; +use crate::consensus::ConsensusError; +use crate::errors::ProtocolError; +use bincode::{Decode, DecodeUntrusted, Encode}; +use platform_serialization_derive::{ + PlatformDeserializeTrusted, PlatformDeserializeUntrusted, PlatformSerialize, +}; +use platform_version::version::ProtocolVersion; +use thiserror::Error; + +/// A transition asks for a random value (a random token transfer or mint, or a create of a +/// document whose type declares a `sys.random` property) in the block that switches to +/// `protocol_version`, the first whose blocks draw random values (protocol version 15). +/// Tenderdash applies the consensus parameters a block returns from the next height on, so that +/// block still runs under the consensus version of the protocol version before it, which sends +/// no random beacon: nothing would draw the value. The transition is refused without being +/// charged, and is accepted from the next block on. +#[derive( + Error, + Debug, + Clone, + PartialEq, + Eq, + Encode, + Decode, + PlatformSerialize, + PlatformDeserializeTrusted, + PlatformDeserializeUntrusted, + DecodeUntrusted, +)] +#[error( + "{subject} asks for a random value, which the block switching to protocol version \ + {protocol_version} cannot draw: Tenderdash sends that block no random beacon. It is \ + accepted from the next block on" +)] +#[platform_serialize(unversioned)] +pub struct RandomBeaconUnavailableError { + /* + + DO NOT CHANGE ORDER OF FIELDS WITHOUT INTRODUCING OF NEW VERSION + + */ + subject: String, + protocol_version: ProtocolVersion, +} + +impl RandomBeaconUnavailableError { + pub fn new(subject: impl Into, protocol_version: ProtocolVersion) -> Self { + Self { + subject: subject.into(), + protocol_version, + } + } + + /// What asks for the random value: the transition, and the token or document type. + pub fn subject(&self) -> &str { + &self.subject + } + + /// The protocol version the block switches to. + pub fn protocol_version(&self) -> ProtocolVersion { + self.protocol_version + } +} + +impl From for ConsensusError { + fn from(err: RandomBeaconUnavailableError) -> Self { + Self::BasicError(BasicError::RandomBeaconUnavailableError(err)) + } +} diff --git a/packages/rs-dpp/src/errors/consensus/codes.rs b/packages/rs-dpp/src/errors/consensus/codes.rs index 8952e362d6b..352e448244d 100644 --- a/packages/rs-dpp/src/errors/consensus/codes.rs +++ b/packages/rs-dpp/src/errors/consensus/codes.rs @@ -241,6 +241,7 @@ impl ErrorWithCode for BasicError { Self::MissingStateTransitionTypeError { .. } => 10601, Self::StateTransitionMaxSizeExceededError { .. } => 10602, Self::StateTransitionNotActiveError(_) => 10603, + Self::RandomBeaconUnavailableError(_) => 10604, // General Errors 10700-10799 Self::OverflowError(_) => 10700, diff --git a/packages/rs-dpp/src/state_transition/mod.rs b/packages/rs-dpp/src/state_transition/mod.rs index 7b16731c6b5..f27f51813d1 100644 --- a/packages/rs-dpp/src/state_transition/mod.rs +++ b/packages/rs-dpp/src/state_transition/mod.rs @@ -1153,10 +1153,10 @@ fn batch_carries_a_version_1_token_transfer_or_mint(batch_transition: &BatchTran /// The active range of a batch carrying a token transfer or a token mint of format version 1 /// (a random amount), among `versions`, the protocol versions in order: from the first whose -/// table admits a random token amount (`random_token_amount` is `Some`), and `NO_VERSION` while -/// none does. The tables decide it, so the release that settles the amounts activates them by -/// setting the slot in its table alone. Every table of protocol version 14 and earlier keeps it -/// `None` (no binary of theirs decodes the format), so the start is never below 15. +/// table admits a random token amount (`random_token_amount` is `Some`, from protocol version +/// 15, whose blocks settle the amounts), and `NO_VERSION` should none do. The tables decide it. +/// Every table of protocol version 14 and earlier keeps it `None` (no binary of theirs decodes +/// the format), so the start is never below 15. fn random_token_amount_active_version_range( versions: &[PlatformVersion], ) -> RangeInclusive { @@ -1265,11 +1265,11 @@ impl StateTransition { StateTransition::Batch(batch_transition) => { // Version 1 of the token transfer and of the token mint (a random amount) is // active from the first protocol version whose table admits a random token - // amount, and nowhere while none does: block execution does not settle the - // amounts yet, so a reservation would never be paid out. Binaries from before 15 - // cannot decode one, so an earlier version rejects the batch without charging, - // exactly as they do. Checked first: the range starts at 15 at the earliest, the - // later start, so it holds whatever else the batch carries. + // amount, 15, whose blocks settle the amounts from their random beacon. + // Binaries from before 15 cannot decode one, so an earlier version rejects the + // batch without charging, exactly as they do. Checked first: the range starts + // at 15 at the earliest, the later start, so it holds whatever else the batch + // carries. if batch_carries_a_version_1_token_transfer_or_mint(batch_transition) { return random_token_amount_active_version_range(PLATFORM_VERSIONS); } @@ -3649,13 +3649,13 @@ mod tests { assert_eq!(recovered, version_2); } - // Version 1 of the token transfer and of the token mint carries a random amount, which no - // table admits yet: block execution does not settle the amounts, so a reservation would - // never be paid out. New software must refuse the batch at every protocol version, 15 - // included, though the format decodes; version 0 stays active everywhere its batch is. + // Version 1 of the token transfer and of the token mint carries a random amount, which + // exists from protocol version 15, whose blocks settle the amounts. Software from before it + // cannot decode either, so new software must refuse the batch while an earlier version is + // active; version 0 stays active everywhere its batch is. #[test] - fn should_not_activate_a_version_1_token_transfer_or_mint_while_no_table_admits_it() { - use crate::serialization::{PlatformDeserializableUntrusted, PlatformSerializable}; + fn should_activate_a_version_1_token_transfer_or_mint_from_protocol_version_15() { + use crate::serialization::PlatformSerializable; use crate::state_transition::batch_transition::batched_transition::BatchedTransition; use crate::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; use crate::state_transition::batch_transition::token_base_transition::TokenBaseTransition; @@ -3720,43 +3720,48 @@ mod tests { TokenMintTransitionV1::from_v0(mint_v0, Some(10)), ))); for random in [random_transfer, random_mint] { - assert_eq!(random.active_version_range(), NO_VERSION); + assert_eq!(random.active_version_range(), 15..=LATEST_VERSION); let bytes = PlatformSerializable::serialize_to_bytes(&random).expect("serialize succeeds"); for platform_version in PLATFORM_VERSIONS { - assert!( - matches!( - StateTransition::deserialize_from_bytes_untrusted_in_version( - &bytes, - platform_version - ), - Err(ProtocolError::StateTransitionError( - StateTransitionIsNotActiveError { .. } - )) - ), - "expected protocol version {} to refuse a random token amount", - platform_version.protocol_version + let decoded = StateTransition::deserialize_from_bytes_untrusted_in_version( + &bytes, + platform_version, ); + if platform_version.protocol_version < 15 { + assert!( + matches!( + decoded, + Err(ProtocolError::StateTransitionError( + StateTransitionIsNotActiveError { .. } + )) + ), + "expected protocol version {} to refuse a random token amount", + platform_version.protocol_version + ); + } else { + let recovered = decoded.unwrap_or_else(|error| { + panic!( + "expected protocol version {} to decode a random token amount: \ + {error:?}", + platform_version.protocol_version + ) + }); + assert_eq!(recovered, random); + } } - // The format itself decodes: only the gate refuses it - let recovered = StateTransition::deserialize_from_bytes_untrusted(&bytes) - .expect("expected a version 1 token transition to decode"); - assert_eq!(recovered, random); } } - // The release that settles random token amounts activates them by setting - // `random_token_amount` in its table: the active range starts at the first protocol version - // whose table has it, and is empty while none has it. + // `random_token_amount` decides it: the active range starts at the first protocol version + // whose table has it, 15, and is empty should no table have it. #[test] fn should_activate_random_token_amounts_from_the_first_table_admitting_them() { - // No table admits it yet - assert_eq!( - random_token_amount_active_version_range(PLATFORM_VERSIONS), - NO_VERSION - ); + let active = random_token_amount_active_version_range(PLATFORM_VERSIONS); + assert_eq!(active, 15..=LATEST_VERSION); + assert!(!active.contains(&14)); // No binary of protocol version 14 or earlier decodes the format, so none of their - // tables may ever admit it (14 shares its table with 15 for now) + // tables may ever admit it assert!(PLATFORM_VERSIONS .iter() .filter(|platform_version| platform_version.protocol_version < 15) @@ -3767,17 +3772,16 @@ mod tests { .is_none())); let mut versions = PLATFORM_VERSIONS.to_vec(); - let version_15 = versions - .iter_mut() - .find(|platform_version| platform_version.protocol_version == 15) - .expect("platform version 15 exists"); - version_15 - .dpp - .state_transition_serialization_versions - .random_token_amount = Some(0); - let active = random_token_amount_active_version_range(&versions); - assert_eq!(active, 15..=LATEST_VERSION); - assert!(!active.contains(&14)); + for platform_version in versions.iter_mut() { + platform_version + .dpp + .state_transition_serialization_versions + .random_token_amount = None; + } + assert_eq!( + random_token_amount_active_version_range(&versions), + NO_VERSION + ); } fn sample_batch_st_empty() -> StateTransition { diff --git a/packages/rs-drive-abci/Cargo.toml b/packages/rs-drive-abci/Cargo.toml index d95b730e78b..6cf782c91a0 100644 --- a/packages/rs-drive-abci/Cargo.toml +++ b/packages/rs-drive-abci/Cargo.toml @@ -51,7 +51,7 @@ tracing-subscriber = { version = "0.3.22", default-features = false, features = "registry", "tracing-log", ], optional = false } -tenderdash-abci = { git = "https://github.com/dashpay/rs-tenderdash-abci", tag = "v1.8.1", features = [ +tenderdash-abci = { git = "https://github.com/dashpay/rs-tenderdash-abci", rev = "e80c8a7361eda12918d938acd4ca077478419902", features = [ "grpc", ] } time = { version = "0.3", optional = true, features = [ diff --git a/packages/rs-drive-abci/src/abci/app/consensus.rs b/packages/rs-drive-abci/src/abci/app/consensus.rs index ff2d3792619..1a40135d289 100644 --- a/packages/rs-drive-abci/src/abci/app/consensus.rs +++ b/packages/rs-drive-abci/src/abci/app/consensus.rs @@ -203,6 +203,13 @@ where handler::extend_vote(self, request).map_err(error_into_exception) } + fn apply_block_randomness( + &self, + request: proto::RequestApplyBlockRandomness, + ) -> Result { + handler::apply_block_randomness(self, request).map_err(error_into_exception) + } + fn finalize_block( &self, request: proto::RequestFinalizeBlock, diff --git a/packages/rs-drive-abci/src/abci/app/full.rs b/packages/rs-drive-abci/src/abci/app/full.rs index d43ff625d60..c0f1bc726cd 100644 --- a/packages/rs-drive-abci/src/abci/app/full.rs +++ b/packages/rs-drive-abci/src/abci/app/full.rs @@ -222,6 +222,13 @@ where handler::extend_vote(self, request).map_err(error_into_exception) } + fn apply_block_randomness( + &self, + request: proto::RequestApplyBlockRandomness, + ) -> Result { + handler::apply_block_randomness(self, request).map_err(error_into_exception) + } + fn finalize_block( &self, request: proto::RequestFinalizeBlock, diff --git a/packages/rs-drive-abci/src/abci/handler/apply_block_randomness.rs b/packages/rs-drive-abci/src/abci/handler/apply_block_randomness.rs new file mode 100644 index 00000000000..e558db47963 --- /dev/null +++ b/packages/rs-drive-abci/src/abci/handler/apply_block_randomness.rs @@ -0,0 +1,605 @@ +use crate::abci::app::{BlockExecutionApplication, PlatformApplication, TransactionalApplication}; +use crate::abci::AbciError; +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::block_execution_context::v0::{ + AppliedRandomness, BlockExecutionContextV0Getters, BlockExecutionContextV0Setters, +}; +use crate::execution::types::block_execution_context::BlockExecutionContext; +use crate::execution::types::block_state_info::v0::{ + BlockStateInfoV0Getters, BlockStateInfoV0Methods, +}; +use crate::platform_types::epoch_info::v0::EpochInfoV0Getters; +use crate::platform_types::platform::Platform; +use crate::platform_types::platform_state::PlatformStateV0Methods; +use crate::rpc::core::CoreRPCLike; +use dpp::block::epoch::Epoch; +use drive::error::Error as DriveError; +use drive::grovedb::Transaction; +use tenderdash_abci::proto::abci as proto; + +/// Applies the random beacon of a block to the state this node's `ProcessProposal` left for it +/// in the request's round, and returns the app hash the round's precommits sign. +/// +/// Tenderdash calls it from consensus version 2 (protocol version 15): on a validator once the +/// round's prevotes combine into the beacon, before it precommits, and on every path that +/// finalizes a block from its commit (block sync, replay, a node that did not vote) before +/// `FinalizeBlock`, with the commit's beacon and round. Tenderdash processes the block in that +/// round first, so the block execution context must be that block's. It may ask again for the +/// same round, and is answered with the same app hash. +pub fn apply_block_randomness<'a, A, C>( + app: &A, + request: proto::RequestApplyBlockRandomness, +) -> Result +where + A: PlatformApplication + TransactionalApplication<'a> + BlockExecutionApplication, + C: CoreRPCLike, +{ + let _timer = crate::metrics::abci_request_duration("apply_block_randomness"); + + let proto::RequestApplyBlockRandomness { + hash: block_hash, + height, + round, + beacon, + } = request; + + let height = u64::try_from(height).map_err(|_| { + AbciError::BadRequest("height is negative in apply block randomness request".to_string()) + })?; + let round = u32::try_from(round).map_err(|_| { + AbciError::BadRequest("round is negative in apply block randomness request".to_string()) + })?; + + // The transaction first, then the block execution context, the order every handler takes + // them in + let transaction_guard = app + .transaction() + .read() + .expect("poisoned only after a panic, which stops the node"); + let mut block_execution_context_guard = app + .block_execution_context() + .write() + .expect("poisoned only after a panic, which stops the node"); + + let Some(block_execution_context) = block_execution_context_guard.as_mut() else { + return Err(AbciError::RequestForWrongBlockReceived(format!( + "received an apply block randomness request for height: {} round: {}, block: {}, but no block was processed", + height, + round, + hex::encode(&block_hash), + )) + .into()); + }; + + let block_state_info = block_execution_context.block_state_info(); + if !block_state_info.matches_current_block(height, round, block_hash.clone())? { + return Err(AbciError::RequestForWrongBlockReceived(format!( + "received an apply block randomness request for height: {} round: {}, block: {}, but the block processed last is height: {} round: {}, block: {}", + height, + round, + hex::encode(&block_hash), + block_state_info.height(), + block_state_info.round(), + block_state_info + .block_hash() + .map(hex::encode) + .unwrap_or("None".to_string()), + )) + .into()); + } + + let transaction = + transaction_guard + .as_ref() + .ok_or(Error::Execution(ExecutionError::NotInTransaction( + "applying a block's randomness without a current transaction", + )))?; + + let app_hash = apply_randomness( + app.platform(), + block_execution_context, + round, + &beacon, + transaction, + )?; + + Ok(proto::ResponseApplyBlockRandomness { + app_hash: app_hash.to_vec(), + }) +} + +/// Applies `beacon`, the random beacon of `round`, to the block `block_execution_context` +/// holds, in `transaction`, the block's transaction, and returns the app hash after the draws: +/// the one the round's precommits sign and the block commits. Asked again for the same round +/// and beacon, it returns that app hash without drawing again; another beacon is refused, as a +/// round's prevotes combine into exactly one. When the block's protocol version draws nothing +/// (`apply_random_beacon` is `None`, before 15), nothing is applied and the app hash is the +/// block's own. +/// +/// Drive does not verify the beacon's BLS signature: the request carries no part-set header, +/// so the prevote sign bytes the beacon signs cannot be rebuilt here. Tenderdash verified it +/// against the quorum's threshold public key when it recovered it from the prevotes, and +/// verifies it again in every commit it accepts. +pub(super) fn apply_randomness( + platform: &Platform, + block_execution_context: &mut BlockExecutionContext, + round: u32, + beacon: &[u8], + transaction: &Transaction, +) -> Result<[u8; 32], Error> +where + C: CoreRPCLike, +{ + if let Some(applied) = block_execution_context.applied_randomness() { + if applied.round == round && applied.beacon == beacon { + return Ok(applied.app_hash); + } + return Err(AbciError::BadRequest(format!( + "the randomness of round {} was applied to the block at height {} already; the beacon {} of round {} differs", + applied.round, + block_execution_context.block_state_info().height(), + hex::encode(beacon), + round, + )) + .into()); + } + + let platform_version = block_execution_context + .block_platform_state() + .current_platform_version()?; + + if platform_version + .drive_abci + .methods + .block_end + .apply_random_beacon + .is_some() + { + let epoch = Epoch::new(block_execution_context.epoch_info().current_epoch_index()) + .map_err(|_| { + Error::Execution(ExecutionError::CorruptedCodeExecution( + "the epoch of an executed block is in range", + )) + })?; + let block_info = block_execution_context + .block_state_info() + .to_block_info(epoch); + platform.apply_random_beacon( + beacon, + block_execution_context.random_requests(), + &block_info, + transaction, + platform_version, + )?; + } + + let app_hash = platform + .drive + .grove + .root_hash(Some(transaction), &platform_version.drive.grove_version) + .unwrap() + .map_err(|e| Error::Drive(DriveError::from(e)))?; // GroveDb errors are system errors + + block_execution_context.set_applied_randomness(Some(AppliedRandomness { + round, + beacon: beacon.to_vec(), + app_hash, + })); + + Ok(app_hash) +} + +#[cfg(test)] +pub(super) mod tests { + use super::*; + use crate::abci::app::FullAbciApplication; + use crate::execution::types::block_execution_context::v0::BlockExecutionContextV0; + use crate::execution::types::block_random_requests::BlockRandomRequests; + use crate::execution::types::block_state_info::v0::BlockStateInfoV0; + use crate::platform_types::epoch_info::v0::EpochInfoV0; + use crate::platform_types::epoch_info::EpochInfo; + use crate::platform_types::platform_state::PlatformState; + use crate::platform_types::withdrawal::unsigned_withdrawal_txs::v0::UnsignedWithdrawalTxs; + use crate::rpc::core::MockCoreRPCLike; + use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; + use dpp::block::block_info::BlockInfo; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::data_contract::DataContractFactory; + use dpp::document::{Document, DocumentV0, DocumentV0Getters}; + use dpp::platform_value::{platform_value, Identifier, Value}; + use dpp::prelude::DataContract; + use dpp::version::PlatformVersion; + use drive::drive::document::draw_random_document_properties::RandomDocumentRequest; + use drive::drive::document::query::QueryDocumentsOutcomeV0Methods; + use drive::query::DriveDocumentQuery; + use drive::util::object_size_info::DocumentInfo::DocumentRefInfo; + use drive::util::object_size_info::{DocumentAndContractInfo, OwnedDocumentInfo}; + use drive::util::storage_flags::StorageFlags; + use std::borrow::Cow; + use std::collections::BTreeMap; + + const OWNER: [u8; 32] = [42; 32]; + pub(in crate::abci::handler) const BEACON: [u8; 96] = [7; 96]; + const BLOCK_HASH: [u8; 32] = [0xAA; 32]; + const HEIGHT: u64 = 10; + + /// A contract with an immutable `card` type, its `rarity` drawn from 1 to 100 and indexed. + pub(in crate::abci::handler) fn card_contract() -> DataContract { + DataContractFactory::new(PlatformVersion::latest().protocol_version) + .expect("expected a contract factory") + .create_with_value_config( + Identifier::from(OWNER), + 0, + platform_value!({ + "card": { + "type": "object", + "documentsMutable": false, + "properties": { + "name": { "type": "string", "maxLength": 32, "position": 0 }, + "rarity": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "generatedFrom": { "function": "sys.random.uniform" }, + "position": 1 + } + }, + "indices": [{ "name": "byRarity", "properties": [{ "rarity": "asc" }] }], + "required": ["name", "rarity"], + "additionalProperties": false + } + }), + None, + None, + ) + .expect("expected the card contract") + .data_contract_owned() + } + + /// Applies the card contract to committed state. + pub(in crate::abci::handler) fn apply_card_contract( + platform: &TempPlatform, + ) -> DataContract { + let contract = card_contract(); + platform + .drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + PlatformVersion::latest(), + ) + .expect("expected to apply the card contract"); + contract + } + + /// Inserts a `card` holding the placeholder rarity, the way a create transition writes it, + /// in `transaction`, and returns the request the block records for it. + pub(in crate::abci::handler) fn insert_placeholder_card( + platform: &TempPlatform, + contract: &DataContract, + transaction: &Transaction, + ) -> RandomDocumentRequest { + let id = Identifier::from([1; 32]); + let document = Document::V0(DocumentV0 { + id, + owner_id: Identifier::from(OWNER), + properties: BTreeMap::from([ + ("name".to_string(), Value::Text("Ace".to_string())), + ("rarity".to_string(), Value::U8(1)), + ]), + ..Default::default() + }); + platform + .drive + .add_document_for_contract( + DocumentAndContractInfo { + owned_document_info: OwnedDocumentInfo { + document_info: DocumentRefInfo(( + &document, + Some(Cow::Owned(StorageFlags::new_single_epoch(0, Some(OWNER)))), + )), + owner_id: Some(OWNER), + }, + contract, + document_type: contract + .document_type_for_name("card") + .expect("expected the card type"), + }, + false, + BlockInfo::default(), + true, + Some(transaction), + PlatformVersion::latest(), + None, + ) + .expect("expected the card to insert"); + RandomDocumentRequest { + contract_id: contract.id(), + document_type_name: "card".to_string(), + document_id: id, + } + } + + /// The rarity the card holds in `transaction`. + pub(in crate::abci::handler) fn stored_rarity( + platform: &TempPlatform, + contract: &DataContract, + transaction: &Transaction, + ) -> i128 { + let platform_version = PlatformVersion::latest(); + let query = DriveDocumentQuery::from_sql_expr( + "select * from card", + contract, + Some(&platform.config.drive), + platform_version, + ) + .expect("expected a card query"); + let documents = platform + .drive + .query_documents( + query, + None, + false, + Some(transaction), + Some(platform_version.protocol_version), + ) + .expect("expected the card query to run") + .documents() + .to_vec(); + assert_eq!(documents.len(), 1, "one card is stored"); + documents[0] + .get("rarity") + .and_then(|rarity| rarity.to_integer::().ok()) + .expect("expected the card to hold a rarity") + } + + /// The context `ProcessProposal` leaves for the block at [`HEIGHT`] in `round` at + /// `protocol_version`, which created the cards `documents` names. + pub(in crate::abci::handler) fn block_execution_context( + platform: &TempPlatform, + protocol_version: u32, + round: u32, + block_hash: [u8; 32], + documents: Vec, + ) -> BlockExecutionContext { + BlockExecutionContext::V0(BlockExecutionContextV0 { + block_state_info: BlockStateInfoV0 { + height: HEIGHT, + round, + block_time_ms: 1_700_000_000_000, + previous_block_time_ms: None, + proposer_pro_tx_hash: [0u8; 32], + core_chain_locked_height: 1, + block_hash: Some(block_hash), + app_hash: None, + } + .into(), + epoch_info: EpochInfo::V0(EpochInfoV0 { + current_epoch_index: 0, + previous_epoch_index: None, + is_epoch_change: false, + }), + unsigned_withdrawal_transactions: UnsignedWithdrawalTxs::default(), + block_address_balance_changes: BTreeMap::new(), + block_platform_state: PlatformState::default_with_protocol_versions( + protocol_version, + protocol_version, + &platform.config, + ) + .expect("expected a platform state"), + proposer_results: None, + random_requests: BlockRandomRequests { + documents, + tokens: Vec::new(), + }, + applied_randomness: None, + }) + } + + fn request( + round: i32, + block_hash: [u8; 32], + beacon: &[u8], + ) -> proto::RequestApplyBlockRandomness { + proto::RequestApplyBlockRandomness { + hash: block_hash.to_vec(), + height: HEIGHT as i64, + round, + beacon: beacon.to_vec(), + } + } + + fn transaction_root(app: &FullAbciApplication) -> [u8; 32] { + let transaction_guard = app.transaction.read().unwrap(); + let transaction = transaction_guard.as_ref().expect("expected a transaction"); + app.platform + .drive + .grove + .root_hash( + Some(transaction), + &PlatformVersion::latest().drive.grove_version, + ) + .unwrap() + .expect("expected a root hash") + } + + fn rarity_in_block( + platform: &TempPlatform, + app: &FullAbciApplication, + contract: &DataContract, + ) -> i128 { + let transaction_guard = app.transaction.read().unwrap(); + stored_rarity( + platform, + contract, + transaction_guard.as_ref().expect("expected a transaction"), + ) + } + + /// A platform holding the card contract in committed state. + fn setup() -> (TempPlatform, DataContract) { + let platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + let contract = apply_card_contract(&platform); + (platform, contract) + } + + /// An application whose transaction holds a placeholder card, and whose context is the + /// block at `protocol_version` that created it, processed in round 0. + fn start_block<'a>( + platform: &'a TempPlatform, + contract: &DataContract, + protocol_version: u32, + ) -> FullAbciApplication<'a, MockCoreRPCLike> { + let app = FullAbciApplication::::new(&platform.platform); + app.start_transaction(); + let card = { + let transaction_guard = app.transaction.read().unwrap(); + insert_placeholder_card( + platform, + contract, + transaction_guard.as_ref().expect("expected a transaction"), + ) + }; + app.block_execution_context + .write() + .unwrap() + .replace(block_execution_context( + platform, + protocol_version, + 0, + BLOCK_HASH, + vec![card], + )); + app + } + + #[test] + fn should_draw_the_block_and_return_the_root_after_the_draws() { + let latest = PlatformVersion::latest().protocol_version; + let (platform, contract) = setup(); + let app = start_block(&platform, &contract, latest); + let root_before = transaction_root(&app); + assert_eq!(rarity_in_block(&platform, &app, &contract), 1); + + let response = + apply_block_randomness::<_, MockCoreRPCLike>(&app, request(0, BLOCK_HASH, &BEACON)) + .expect("expected the randomness to apply"); + + let root_after = transaction_root(&app); + assert_eq!(response.app_hash, root_after.to_vec()); + let drawn = rarity_in_block(&platform, &app, &contract); + assert!((1..=100).contains(&drawn)); + if drawn != 1 { + assert_ne!(root_before, root_after, "the drawn value moved the root"); + } + assert_eq!( + app.block_execution_context + .read() + .unwrap() + .as_ref() + .expect("expected the context") + .applied_randomness(), + Some(&AppliedRandomness { + round: 0, + beacon: BEACON.to_vec(), + app_hash: root_after, + }) + ); + } + + #[test] + fn should_answer_the_same_round_again_without_drawing_again() { + let latest = PlatformVersion::latest().protocol_version; + let (platform, contract) = setup(); + let app = start_block(&platform, &contract, latest); + + let first = + apply_block_randomness::<_, MockCoreRPCLike>(&app, request(0, BLOCK_HASH, &BEACON)) + .expect("expected the randomness to apply"); + let drawn = rarity_in_block(&platform, &app, &contract); + + let second = + apply_block_randomness::<_, MockCoreRPCLike>(&app, request(0, BLOCK_HASH, &BEACON)) + .expect("expected the same round to be answered again"); + assert_eq!(first, second); + assert_eq!(second.app_hash, transaction_root(&app).to_vec()); + assert_eq!(rarity_in_block(&platform, &app, &contract), drawn); + + // A round's prevotes combine into one beacon: another one for the same round is refused + let result = + apply_block_randomness::<_, MockCoreRPCLike>(&app, request(0, BLOCK_HASH, &[8u8; 96])); + assert!( + matches!(result, Err(Error::Abci(AbciError::BadRequest(_)))), + "{result:?}" + ); + assert_eq!(rarity_in_block(&platform, &app, &contract), drawn); + } + + #[test] + fn should_refuse_a_block_this_node_did_not_process_in_the_round() { + let latest = PlatformVersion::latest().protocol_version; + let (platform, contract) = setup(); + + // No block processed + let app = FullAbciApplication::::new(&platform.platform); + app.start_transaction(); + let result = + apply_block_randomness::<_, MockCoreRPCLike>(&app, request(0, BLOCK_HASH, &BEACON)); + assert!( + matches!( + result, + Err(Error::Abci(AbciError::RequestForWrongBlockReceived(_))) + ), + "{result:?}" + ); + + let app = start_block(&platform, &contract, latest); + let root_before = transaction_root(&app); + for (round, block_hash, height) in [ + (0, [0xBB; 32], HEIGHT), + (1, BLOCK_HASH, HEIGHT), + (0, BLOCK_HASH, HEIGHT + 1), + ] { + let result = apply_block_randomness::<_, MockCoreRPCLike>( + &app, + proto::RequestApplyBlockRandomness { + hash: block_hash.to_vec(), + height: height as i64, + round, + beacon: BEACON.to_vec(), + }, + ); + assert!( + matches!( + result, + Err(Error::Abci(AbciError::RequestForWrongBlockReceived(_))) + ), + "round {round}: {result:?}" + ); + } + assert_eq!(transaction_root(&app), root_before); + assert_eq!(rarity_in_block(&platform, &app, &contract), 1); + } + + #[test] + fn should_draw_nothing_before_protocol_version_15() { + let (platform, contract) = setup(); + let app = start_block(&platform, &contract, 14); + let root_before = transaction_root(&app); + + let response = + apply_block_randomness::<_, MockCoreRPCLike>(&app, request(0, BLOCK_HASH, &BEACON)) + .expect("expected the request to be answered"); + + assert_eq!(response.app_hash, root_before.to_vec()); + assert_eq!(transaction_root(&app), root_before); + assert_eq!(rarity_in_block(&platform, &app, &contract), 1); + } +} diff --git a/packages/rs-drive-abci/src/abci/handler/extend_vote.rs b/packages/rs-drive-abci/src/abci/handler/extend_vote.rs index e2330ef39d6..14a1e0d5802 100644 --- a/packages/rs-drive-abci/src/abci/handler/extend_vote.rs +++ b/packages/rs-drive-abci/src/abci/handler/extend_vote.rs @@ -117,6 +117,8 @@ mod tests { ) .expect("should create default platform state"), proposer_results: None, + random_requests: Default::default(), + applied_randomness: None, }) } diff --git a/packages/rs-drive-abci/src/abci/handler/finalize_block.rs b/packages/rs-drive-abci/src/abci/handler/finalize_block.rs index 48f7dbdbab9..f6275f91af0 100644 --- a/packages/rs-drive-abci/src/abci/handler/finalize_block.rs +++ b/packages/rs-drive-abci/src/abci/handler/finalize_block.rs @@ -1,3 +1,4 @@ +use super::apply_block_randomness::apply_randomness; use super::process_proposal::execute_proposal; use crate::abci::app::{BlockExecutionApplication, PlatformApplication, TransactionalApplication}; use crate::abci::AbciError; @@ -149,7 +150,7 @@ where )))?; // Get current block platform version - let block_execution_context = app + let mut block_execution_context = app .block_execution_context() .write() .unwrap() @@ -169,6 +170,28 @@ where #[cfg(debug_assertions)] phases.end_phase("setup"); + // The commit's random beacon (Tenderdash consensus version 2, protocol version 15), unless + // this node applied it already when it voted in the commit round. A node finalizing a block + // it did not vote on (block sync, replay, a node that was not voting), or that it had to + // execute again above, applies it now in the block's transaction, so it commits the state + // the commit signs; `finalize_block_proposal` checks that state against the commit's app + // hash. Before consensus version 2 the commit carries no beacon and nothing is applied. + if let Some(beacon) = request_finalize_block.commit.beacon { + apply_randomness( + app.platform(), + &mut block_execution_context, + request_finalize_block.commit.round, + &beacon, + transaction, + )?; + } + + #[cfg(debug_assertions)] + phases.end_phase_if( + request_finalize_block.commit.beacon.is_some(), + "apply_randomness", + ); + let block_finalization_outcome = app.platform().finalize_block_proposal( request_finalize_block, block_execution_context, @@ -351,6 +374,9 @@ where mod tests { use super::*; use crate::abci::app::{FullAbciApplication, TransactionalApplication}; + use crate::abci::handler::apply_block_randomness::tests::{ + apply_card_contract, insert_placeholder_card, stored_rarity, BEACON, + }; use crate::config::{CheckpointStep, PlatformConfig}; use crate::execution::types::block_execution_context::v0::BlockExecutionContextV0; use crate::execution::types::block_execution_context::BlockExecutionContext; @@ -364,15 +390,18 @@ mod tests { use crate::rpc::core::MockCoreRPCLike; use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; use dpp::block::block_info::BlockInfo; + use dpp::prelude::DataContract; use dpp::version::PlatformVersion; use drive::grovedb::Transaction; use std::sync::{Arc, Mutex, RwLock}; use tenderdash_abci::proto::abci::CommitInfo; use tenderdash_abci::proto::google::protobuf::Timestamp; use tenderdash_abci::proto::types::{ - Block, BlockId, Data, EvidenceList, Header, PartSetHeader, + Block, BlockId, Data, EvidenceList, Header, PartSetHeader, StateId, }; use tenderdash_abci::proto::version::Consensus; + use tenderdash_abci::proto::ToMillis; + use tenderdash_abci::signatures::Hashable; use tracing_subscriber::layer::SubscriberExt; /// An ABCI application whose `commit_transaction` always fails. @@ -456,6 +485,8 @@ mod tests { block_address_balance_changes: Default::default(), block_platform_state: platform_state, proposer_results: None, + random_requests: Default::default(), + applied_randomness: None, } .into() } @@ -473,6 +504,8 @@ mod tests { quorum_hash: quorum_hash.to_vec(), block_signature: vec![0u8; 96], threshold_vote_extensions: vec![], + beacon: vec![], + app_hash: vec![], }), misbehavior: vec![], hash: BLOCK_HASH.to_vec(), @@ -1269,4 +1302,254 @@ mod tests { .expect("third block after the restart"); assert_eq!(registered_checkpoint_heights(&platform), vec![3]); } + + /// What finalizing a block with a commit's random beacon gives on a node that did not + /// apply it (block sync, replay, a node that did not vote). + struct FinalizedWithTheCommitBeacon { + /// The platform, holding the card contract + platform: TempPlatform, + /// The card contract + contract: DataContract, + /// The committed root before the block + root_before: [u8; 32], + /// The root the block's draws give + drawn_root: [u8; 32], + /// What `finalize_block` returned + result: Result, + } + + /// The state id the precommits of the block `request` finalizes sign over `app_hash`: the + /// hash of the header's state id holding that app hash. + fn signed_state_id(request: &proto::RequestFinalizeBlock, app_hash: &[u8]) -> Vec { + let header = request + .block + .as_ref() + .and_then(|block| block.header.as_ref()) + .expect("expected the block header"); + StateId { + height: header.height as u64, + app_hash: app_hash.to_vec(), + app_version: header + .version + .as_ref() + .expect("expected the header version") + .app, + core_chain_locked_height: header.core_chain_locked_height, + time: header + .time + .as_ref() + .expect("expected the block time") + .to_millis() + .expect("expected a valid block time"), + } + .calculate_msg_hash(&header.chain_id, request.height, request.round) + .expect("expected to hash the state id") + } + + /// Finalizes a block that created one card holding the placeholder rarity, with a commit + /// carrying the random beacon and the app hash `commit_app_hash` makes of the root the + /// draws give, on a node that did not apply the beacon. The block id carries the state id + /// of the app hash `block_id_app_hash` makes of that root, and the zero state id for `None`. + fn finalize_with_the_commit_beacon( + commit_app_hash: impl FnOnce([u8; 32]) -> Vec, + block_id_app_hash: impl FnOnce([u8; 32]) -> Option<[u8; 32]>, + ) -> FinalizedWithTheCommitBeacon { + let grove_version = &PlatformVersion::latest().drive.grove_version; + let mut config = PlatformConfig::default_testnet(); + config.testing_configs.block_commit_signature_verification = false; + let platform: TempPlatform = TestPlatformBuilder::new() + .with_config(config) + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + let contract = apply_card_contract(&platform); + let root_before = platform + .drive + .grove + .root_hash(None, grove_version) + .unwrap() + .expect("expected a root hash"); + + let (drawn_root, result) = { + let app = FullAbciApplication::new(&platform.platform); + app.start_transaction(); + let platform_state = (**platform.state.load()).clone(); + let quorum_hash: [u8; 32] = platform_state.current_validator_set_quorum_hash().into(); + let block_time_ms = 1_700_000_000_000u64; + let mut context = block_execution_context(platform_state, 1, block_time_ms, None); + + let drawn_root = { + let transaction_guard = app.transaction.read().unwrap(); + let transaction = transaction_guard.as_ref().expect("expected a transaction"); + let card = insert_placeholder_card(&platform, &contract, transaction); + let BlockExecutionContext::V0(context_v0) = &mut context; + context_v0.random_requests.documents.push(card); + + // The root the commit signs, from draws this test rolls back, so the block + // reaches `finalize_block` holding the placeholder as on a node that did not vote + transaction.set_savepoint(); + let drawn_root = apply_randomness( + &platform.platform, + &mut context.clone(), + 0, + &BEACON, + transaction, + ) + .expect("expected the draws to apply"); + transaction + .rollback_to_savepoint() + .expect("expected to roll the draws back"); + drawn_root + }; + + app.block_execution_context + .write() + .unwrap() + .replace(context); + + let mut request = finalize_block_request( + platform.config.abci.chain_id.clone(), + quorum_hash, + 1, + block_time_ms, + ); + if let Some(app_hash) = block_id_app_hash(drawn_root) { + let state_id = signed_state_id(&request, &app_hash); + request + .block_id + .as_mut() + .expect("expected the block id") + .state_id = state_id; + } + let commit = request.commit.as_mut().expect("expected the commit info"); + commit.beacon = BEACON.to_vec(); + commit.app_hash = commit_app_hash(drawn_root); + + ( + drawn_root, + finalize_block::<_, MockCoreRPCLike>(&app, request), + ) + }; + + FinalizedWithTheCommitBeacon { + platform, + contract, + root_before, + drawn_root, + result, + } + } + + #[test] + fn should_apply_the_commit_beacon_this_node_did_not_apply() { + let grove_version = &PlatformVersion::latest().drive.grove_version; + let FinalizedWithTheCommitBeacon { + platform, + contract, + drawn_root, + result, + .. + } = finalize_with_the_commit_beacon(|drawn_root| drawn_root.to_vec(), Some); + result.expect("expected the block to finalize"); + + // The block commits the state the commit signs, and the next proposal finds it on disk + let committed_root = platform + .drive + .grove + .root_hash(None, grove_version) + .unwrap() + .expect("expected a root hash"); + assert_eq!(committed_root, drawn_root); + assert_eq!( + platform.state.load().last_committed_block_app_hash(), + Some(drawn_root) + ); + let transaction = platform.drive.grove.start_transaction(); + assert!((1..=100).contains(&stored_rarity(&platform, &contract, &transaction))); + + // The beacon alone is applied too + let FinalizedWithTheCommitBeacon { + platform, + drawn_root, + result, + .. + } = finalize_with_the_commit_beacon(|_| Vec::new(), |_| None); + result.expect("expected the block to finalize"); + assert_eq!( + platform.state.load().last_committed_block_app_hash(), + Some(drawn_root) + ); + } + + #[test] + fn should_refuse_a_commit_signing_another_app_hash() { + let grove_version = &PlatformVersion::latest().drive.grove_version; + let FinalizedWithTheCommitBeacon { + platform, + root_before, + result, + .. + } = finalize_with_the_commit_beacon(|_| vec![9; 32], Some); + assert!( + matches!( + result, + Err(Error::Abci(AbciError::WrongFinalizeBlockReceived(_))) + ), + "{result:?}" + ); + assert_eq!( + platform + .drive + .grove + .root_hash(None, grove_version) + .unwrap() + .expect("expected a root hash"), + root_before, + "nothing is committed" + ); + } + + /// The precommits sign the block id, whose state id binds the app hash after the draws: a + /// commit whose block id carries the state id of another app hash, here the header's, the + /// state before the draws that a consensus version 1 precommit signs, is refused, and + /// nothing is committed. + #[test] + fn should_refuse_a_commit_whose_block_id_carries_another_state_id() { + let grove_version = &PlatformVersion::latest().drive.grove_version; + let FinalizedWithTheCommitBeacon { + platform, + root_before, + result, + .. + } = finalize_with_the_commit_beacon(|drawn_root| drawn_root.to_vec(), |_| Some(APP_HASH)); + assert!( + matches!( + result, + Err(Error::Abci(AbciError::WrongFinalizeBlockReceived(ref message))) + if message.contains("state id") + ), + "{result:?}" + ); + assert_eq!( + platform + .drive + .grove + .root_hash(None, grove_version) + .unwrap() + .expect("expected a root hash"), + root_before, + "nothing is committed" + ); + + // The zero state id of a block id that carries none is refused too + let FinalizedWithTheCommitBeacon { result, .. } = + finalize_with_the_commit_beacon(|drawn_root| drawn_root.to_vec(), |_| None); + assert!( + matches!( + result, + Err(Error::Abci(AbciError::WrongFinalizeBlockReceived(_))) + ), + "{result:?}" + ); + } } diff --git a/packages/rs-drive-abci/src/abci/handler/init_chain.rs b/packages/rs-drive-abci/src/abci/handler/init_chain.rs index 0573b05e6b3..e59f59a98bd 100644 --- a/packages/rs-drive-abci/src/abci/handler/init_chain.rs +++ b/packages/rs-drive-abci/src/abci/handler/init_chain.rs @@ -93,6 +93,8 @@ mod tests { ) .expect("should create default platform state"), proposer_results: None, + random_requests: Default::default(), + applied_randomness: None, }); app.block_execution_context diff --git a/packages/rs-drive-abci/src/abci/handler/mod.rs b/packages/rs-drive-abci/src/abci/handler/mod.rs index 8acd0737ebe..fcc67472f0b 100644 --- a/packages/rs-drive-abci/src/abci/handler/mod.rs +++ b/packages/rs-drive-abci/src/abci/handler/mod.rs @@ -35,6 +35,7 @@ //! can only make changes that are backwards compatible. Otherwise new calls must be made instead. //! +mod apply_block_randomness; mod check_tx; mod echo; pub mod error; @@ -46,6 +47,7 @@ mod prepare_proposal; mod process_proposal; mod verify_vote_extension; +pub use apply_block_randomness::apply_block_randomness; pub use check_tx::check_tx; pub use echo::echo; pub use extend_vote::extend_vote; diff --git a/packages/rs-drive-abci/src/execution/engine/consensus_params_update/mod.rs b/packages/rs-drive-abci/src/execution/engine/consensus_params_update/mod.rs index 8237748708a..68d3bcf542d 100644 --- a/packages/rs-drive-abci/src/execution/engine/consensus_params_update/mod.rs +++ b/packages/rs-drive-abci/src/execution/engine/consensus_params_update/mod.rs @@ -637,11 +637,82 @@ mod evidence_policy_tests { assert_eq!((duration.seconds, duration.nanos), (172_800, 0)); let version = response.version.unwrap(); assert_eq!(version.app_version, latest.protocol_version as u64); - assert_eq!(version.consensus_version, 1); + assert_eq!( + version.consensus_version, + latest.consensus.tenderdash_consensus_version as i32 + ); + } + } + } + + /// The block that switches to protocol version 15 raises Tenderdash to consensus version + /// 2, which recovers each block's random beacon and calls `ApplyBlockRandomness`; every + /// earlier switch keeps sending consensus version 1. + #[test] + fn should_raise_tenderdash_to_consensus_version_2_on_the_switch_to_15() { + for network in [ + Network::Mainnet, + Network::Testnet, + Network::Regtest, + Network::Devnet, + ] { + let params = consensus_params_update( + network, + PlatformVersion::get(14).unwrap(), + PlatformVersion::get(15).unwrap(), + &epoch_info(true, 20), + ) + .unwrap() + .expect("the switch to 15 updates the consensus parameters"); + let version = params.version.expect("version parameters"); + assert_eq!(version.app_version, 15); + assert_eq!(version.consensus_version, 2); + + for new in 3..=14 { + let params = consensus_params_update( + network, + PlatformVersion::get(new - 1).unwrap(), + PlatformVersion::get(new).unwrap(), + &epoch_info(true, 20), + ) + .unwrap() + .expect("a switch of protocol version updates the consensus parameters"); + assert!(params.evidence.is_none()); + let version = params.version.expect("version parameters"); + assert_eq!(version.app_version, new as u64); + assert_eq!(version.consensus_version, 1, "switch to {new}"); } } } + /// The emergency updates at mainnet epoch 3 and testnet epoch 1480 raised Tenderdash to + /// consensus version 1. From protocol version 15 they give the version's own consensus + /// version, so a chain reaching those epochs at 15 keeps its random beacon. + #[test] + fn should_not_lower_the_consensus_version_in_an_emergency_update_from_15() { + for (network, epoch) in [(Network::Mainnet, 3), (Network::Testnet, 1480)] { + let params = consensus_params_update( + network, + PlatformVersion::get(15).unwrap(), + PlatformVersion::get(15).unwrap(), + &epoch_info(true, epoch), + ) + .unwrap() + .expect("emergency update"); + assert_eq!(params.version.unwrap().consensus_version, 2); + + let params = consensus_params_update( + network, + PlatformVersion::get(14).unwrap(), + PlatformVersion::get(14).unwrap(), + &epoch_info(true, epoch), + ) + .unwrap() + .expect("emergency update"); + assert_eq!(params.version.unwrap().consensus_version, 1); + } + } + #[test] fn should_preserve_historical_consensus_responses() { for new in 1..=14 { @@ -734,7 +805,12 @@ mod evidence_policy_tests { .max_bytes, 0 ); - assert_eq!(params.version.unwrap().consensus_version, 1); + assert_eq!( + params.version.unwrap().consensus_version, + PlatformVersion::latest() + .consensus + .tenderdash_consensus_version as i32 + ); let params = consensus_params_update( network, PlatformVersion::latest(), diff --git a/packages/rs-drive-abci/src/execution/engine/consensus_params_update/v2/mod.rs b/packages/rs-drive-abci/src/execution/engine/consensus_params_update/v2/mod.rs index 1a4f1c5e7ae..9d32478558e 100644 --- a/packages/rs-drive-abci/src/execution/engine/consensus_params_update/v2/mod.rs +++ b/packages/rs-drive-abci/src/execution/engine/consensus_params_update/v2/mod.rs @@ -37,7 +37,14 @@ pub(super) fn consensus_params_update_v2( _ => false, }; let consensus_version = if emergency_update { - 1 + // The emergency update raised Tenderdash to consensus version 1; it must not lower a + // protocol version that runs a later one (2 from protocol version 15, which needs + // Tenderdash to send each block's random beacon). Only protocol version 15 and later + // select this generation, and each runs consensus version 1 or more. + new_platform_version + .consensus + .tenderdash_consensus_version + .max(1) as i32 } else { // Update versions or the evidence policy when they change if original_platform_version diff --git a/packages/rs-drive-abci/src/execution/engine/finalize_block_proposal/v0/mod.rs b/packages/rs-drive-abci/src/execution/engine/finalize_block_proposal/v0/mod.rs index 0c07f6ecbe0..1d9514eaefe 100644 --- a/packages/rs-drive-abci/src/execution/engine/finalize_block_proposal/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/engine/finalize_block_proposal/v0/mod.rs @@ -9,7 +9,10 @@ use dpp::block::extended_block_info::v0::ExtendedBlockInfoV0; use dpp::version::PlatformVersion; use tenderdash_abci::{ - proto::{types::BlockId as ProtoBlockId, ToMillis}, + proto::{ + types::{BlockId as ProtoBlockId, StateId}, + ToMillis, + }, signatures::Hashable, }; @@ -129,6 +132,58 @@ where return Ok(validation_result.into()); } + // The state the block commits: after the block's random draws when its random beacon + // was applied (`ApplyBlockRandomness`, or the commit's beacon in `FinalizeBlock`), which + // is the state the precommits sign; the header's, the state before them, otherwise. + // Added in place to this shipped generation: before protocol version 15 Tenderdash runs + // consensus version 1, whose commits carry neither a beacon nor an app hash, so nothing + // is applied, the app hash is the header's as before, and the check below never runs. + let committed_app_hash = block_execution_context + .applied_randomness() + .map(|applied| applied.app_hash) + .unwrap_or(block_header.app_hash); + + if let Some(commit_app_hash) = commit_info.app_hash { + if commit_app_hash != committed_app_hash { + validation_result.add_error(AbciError::WrongFinalizeBlockReceived(format!( + "received a block for h: {} r: {} whose commit signs the app hash {}, but its state after the random draws is {}", + height, + round, + hex::encode(commit_app_hash), + hex::encode(committed_app_hash), + ))); + return Ok(validation_result.into()); + } + + // The precommits sign the block id, whose state id must be the header's with the + // app hash after the draws: what binds that app hash to the commit's signature. + // Same justification as above: only a consensus version 2 commit carries an app + // hash, so this never runs before protocol version 15. + let signed_state_id = StateId { + height: block_header.height, + app_hash: commit_app_hash.to_vec(), + app_version: block_header.version.app, + core_chain_locked_height: block_header.core_chain_locked_height, + time: block_header + .time + .to_millis() + .map_err(|e| AbciError::BadRequest(format!("invalid block time: {}", e)))?, + } + .calculate_msg_hash(&block_header.chain_id, height as i64, round as i32) + .map_err(AbciError::from)?; + if signed_state_id.as_slice() != block_id.state_id.as_slice() { + validation_result.add_error(AbciError::WrongFinalizeBlockReceived(format!( + "received a block for h: {} r: {} whose block id carries the state id {}, but the header's state id with the app hash {} the commit signs hashes to {}", + height, + round, + hex::encode(block_id.state_id), + hex::encode(commit_app_hash), + hex::encode(&signed_state_id), + ))); + return Ok(validation_result.into()); + } + } + let last_committed_state = self.state.load(); let current_quorum_hash = last_committed_state .current_validator_set_quorum_hash() @@ -233,9 +288,10 @@ where // Update platform (drive abci) state + // The committed state's root, so the next proposal finds it on disk let extended_block_info = ExtendedBlockInfoV0 { basic_info: to_commit_block_info, - app_hash: block_header.app_hash, + app_hash: committed_app_hash, quorum_hash: current_quorum_hash, block_id_hash, proposer_pro_tx_hash: block_header.proposer_pro_tx_hash, diff --git a/packages/rs-drive-abci/src/execution/engine/run_block_proposal/mod.rs b/packages/rs-drive-abci/src/execution/engine/run_block_proposal/mod.rs index 2f62b61c6a2..a09ddaa9136 100644 --- a/packages/rs-drive-abci/src/execution/engine/run_block_proposal/mod.rs +++ b/packages/rs-drive-abci/src/execution/engine/run_block_proposal/mod.rs @@ -141,6 +141,13 @@ Your software version: {}, latest supported protocol version: {}."#, // Set current protocol version to the block platform state block_platform_state .set_current_protocol_version_in_consensus(next_protocol_version); + // Tenderdash applies the consensus parameters this block returns from the next + // height on, so this block still runs under the Tenderdash consensus version of + // the version it switches from (`tenderdash_consensus_platform_version`). In + // memory only: nothing saves or hashes it, and `update_state_cache` clears it + // when the block is committed. Only the random beacon gate of the batch + // transformer reads it, which is inert before protocol version 15. + block_platform_state.set_switched_from_protocol_version(Some(old_protocol_version)); let last_block_time_ms = platform_state.last_committed_block_time_ms(); 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 92f8b912a3c..9cbb72133a3 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 @@ -362,7 +362,7 @@ where phases.end_phase("run_dao_platform_events"); // Process transactions - let state_transitions_result = self.process_raw_state_transitions( + let mut state_transitions_result = self.process_raw_state_transitions( raw_state_transitions, &block_platform_state, &block_info, @@ -477,6 +477,13 @@ where block_address_balance_changes: std::collections::BTreeMap::new(), block_platform_state, proposer_results: None, + // Kept for `ApplyBlockRandomness` and `FinalizeBlock`, which draw them from the + // block's random beacon. Added in place to this shipped generation: the list is + // empty at every protocol version before 15, where `process_raw_state_transitions` + // collects nothing (`apply_random_beacon` is `None`), and the context is neither + // serialized nor hashed. + random_requests: state_transitions_result.take_random_requests(), + applied_randomness: None, } .into(); diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_end/apply_random_beacon/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_end/apply_random_beacon/mod.rs new file mode 100644 index 00000000000..fff794ef51f --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/block_end/apply_random_beacon/mod.rs @@ -0,0 +1,69 @@ +mod v0; + +use crate::error::execution::ExecutionError; +use crate::error::Error; +use crate::execution::types::block_random_requests::BlockRandomRequests; +use crate::platform_types::platform::Platform; +use crate::rpc::core::CoreRPCLike; +use dpp::block::block_info::BlockInfo; +use dpp::version::PlatformVersion; +use drive::grovedb::Transaction; + +impl Platform +where + C: CoreRPCLike, +{ + /// Draws a block's random values from its random beacon (protocol version 15), once every + /// transition of the block is accepted: it fills the `sys.random` properties of the + /// documents the block created (`Drive::draw_random_document_properties`), then settles + /// the block's random token transfers and mints (`Drive::settle_random_token_amounts`). + /// The state it leaves is the one the block's precommits sign. Nobody pays: each + /// transition paid for its execution with the placeholder or the reservation. + /// + /// The beacon is the threshold signature the block's prevotes combine into, which + /// Tenderdash recovered and checked against the quorum's public key, and checks again in + /// every commit. + /// + /// # Parameters + /// - `beacon`: the block's random beacon. + /// - `random_requests`: what the block's transitions ask the beacon for, in the order the + /// block executed them. + /// - `block_info`: the block. + /// - `transaction`: the block's transaction, holding the state its execution left. + /// - `platform_version`: selects the method version; `None` before protocol version 15. + /// + /// # Returns + /// `Ok(())` once the values are drawn; the caller reads the root hash. + pub(crate) fn apply_random_beacon( + &self, + beacon: &[u8], + random_requests: &BlockRandomRequests, + block_info: &BlockInfo, + transaction: &Transaction, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + match platform_version + .drive_abci + .methods + .block_end + .apply_random_beacon + { + Some(0) => self.apply_random_beacon_v0( + beacon, + random_requests, + block_info, + transaction, + platform_version, + ), + Some(version) => Err(Error::Execution(ExecutionError::UnknownVersionMismatch { + method: "apply_random_beacon".to_string(), + known_versions: vec![0], + received: version, + })), + None => Err(Error::Execution(ExecutionError::VersionNotActive { + method: "apply_random_beacon".to_string(), + known_versions: vec![0], + })), + } + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_end/apply_random_beacon/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_end/apply_random_beacon/v0/mod.rs new file mode 100644 index 00000000000..5024291b0ef --- /dev/null +++ b/packages/rs-drive-abci/src/execution/platform_events/block_end/apply_random_beacon/v0/mod.rs @@ -0,0 +1,115 @@ +use crate::error::Error; +use crate::execution::types::block_random_requests::BlockRandomRequests; +use crate::platform_types::platform::Platform; +use crate::rpc::core::CoreRPCLike; +use dpp::block::block_info::BlockInfo; +use dpp::version::PlatformVersion; +use drive::grovedb::Transaction; + +impl Platform +where + C: CoreRPCLike, +{ + #[inline(always)] + pub(super) fn apply_random_beacon_v0( + &self, + beacon: &[u8], + random_requests: &BlockRandomRequests, + block_info: &BlockInfo, + transaction: &Transaction, + platform_version: &PlatformVersion, + ) -> Result<(), Error> { + // Nothing is read or written for a block that asked for nothing, which is most of them + if random_requests.is_empty() { + return Ok(()); + } + + let drawn_documents = self.drive.draw_random_document_properties( + beacon, + &random_requests.documents, + block_info, + Some(transaction), + platform_version, + )?; + + let settled_token_amounts = self.drive.settle_random_token_amounts( + beacon, + &random_requests.tokens, + block_info, + Some(transaction), + platform_version, + )?; + + tracing::debug!( + height = block_info.height, + drawn_documents, + settled_token_amounts, + "applied the block's random beacon", + ); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::execution::types::block_random_requests::BlockRandomRequests; + use crate::test::helpers::setup::TestPlatformBuilder; + use dpp::block::block_info::BlockInfo; + use dpp::version::PlatformVersion; + + #[test] + fn should_leave_the_state_alone_when_the_block_asked_for_nothing() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + let transaction = platform.drive.grove.start_transaction(); + let root_before = platform + .drive + .grove + .root_hash(Some(&transaction), &platform_version.drive.grove_version) + .unwrap() + .expect("root hash"); + + platform + .apply_random_beacon( + &[7u8; 96], + &BlockRandomRequests::default(), + &BlockInfo::default(), + &transaction, + platform_version, + ) + .expect("an empty block applies its beacon"); + + let root_after = platform + .drive + .grove + .root_hash(Some(&transaction), &platform_version.drive.grove_version) + .unwrap() + .expect("root hash"); + assert_eq!(root_before, root_after); + } + + #[test] + fn should_refuse_a_protocol_version_before_15() { + let platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + let transaction = platform.drive.grove.start_transaction(); + + let result = platform.apply_random_beacon( + &[7u8; 96], + &BlockRandomRequests::default(), + &BlockInfo::default(), + &transaction, + PlatformVersion::get(14).expect("protocol version 14"), + ); + assert!( + result.is_err(), + "the step is not active before protocol version 15" + ); + } +} diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_end/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_end/mod.rs index 7c1eaaecc6e..44cba7c580f 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_end/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_end/mod.rs @@ -15,5 +15,9 @@ pub(in crate::execution) mod update_checkpoints; /// Deletes documents whose time to live has passed, after the block's state transitions pub(in crate::execution) mod expire_documents; +/// Draws a block's random values from its random beacon (called from `ApplyBlockRandomness` +/// and `FinalizeBlock`) +pub(crate) mod apply_random_beacon; + /// Creates a GroveDB checkpoint (called after transaction commit) pub(crate) mod create_grovedb_checkpoint; diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_end/should_checkpoint/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_end/should_checkpoint/mod.rs index c253ce42ec7..615248a02d4 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_end/should_checkpoint/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_end/should_checkpoint/mod.rs @@ -89,6 +89,8 @@ mod tests { block_address_balance_changes: BTreeMap::new(), block_platform_state, proposer_results: None, + random_requests: Default::default(), + applied_randomness: None, }); // When should_checkpoint version is None, the dispatcher returns Ok(None) directly @@ -143,6 +145,8 @@ mod tests { block_address_balance_changes: BTreeMap::new(), block_platform_state, proposer_results: None, + random_requests: Default::default(), + applied_randomness: None, }); let result = platform.should_checkpoint(&block_execution_context, &modified_version); diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_end/should_checkpoint/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_end/should_checkpoint/v0/mod.rs index 76e7bfd0ee9..b45b93bd6c3 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_end/should_checkpoint/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_end/should_checkpoint/v0/mod.rs @@ -163,6 +163,8 @@ mod tests { block_address_balance_changes: BTreeMap::new(), block_platform_state: platform_state, proposer_results: None, + random_requests: Default::default(), + applied_randomness: None, }) } diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_end/update_checkpoints/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_end/update_checkpoints/v0/mod.rs index f3b297ce7e0..4acf2ddf14e 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_end/update_checkpoints/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_end/update_checkpoints/v0/mod.rs @@ -129,6 +129,8 @@ mod tests { block_address_balance_changes: BTreeMap::new(), block_platform_state, proposer_results: None, + random_requests: Default::default(), + applied_randomness: None, }) } diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_end/update_drive_cache/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_end/update_drive_cache/v0/mod.rs index 1f89660cf2b..2b409932e80 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_end/update_drive_cache/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_end/update_drive_cache/v0/mod.rs @@ -89,6 +89,8 @@ mod tests { block_address_balance_changes: BTreeMap::new(), block_platform_state: platform_state, proposer_results: None, + random_requests: Default::default(), + applied_randomness: None, }) } diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_end/update_state_cache/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_end/update_state_cache/v0/mod.rs index 14f613d8302..fb3a0c34728 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_end/update_state_cache/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_end/update_state_cache/v0/mod.rs @@ -51,6 +51,13 @@ where block_platform_state.set_genesis_block_info(None); + // A committed state runs no block: the next one runs under its protocol version's + // Tenderdash consensus version unless it switches again, which records that afresh. + // Added in place to this shipped generation: the field is in memory only, neither + // saved nor hashed, and only the batch transformer's random beacon gate reads it, + // which is inert before protocol version 15. + block_platform_state.set_switched_from_protocol_version(None); + // Persist block state self.store_platform_state(&block_platform_state, Some(transaction), platform_version)?; @@ -227,6 +234,41 @@ mod tests { ); } + /// The protocol version a block switched from is the block's alone: the committed state, + /// which check_tx reads and the next block starts from, runs under its own. + #[test] + fn should_clear_the_protocol_version_the_committed_block_switched_from() { + let platform_version = PlatformVersion::latest(); + let platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_genesis_state(); + + let loaded = platform.state.load(); + let mut block_platform_state = loaded.as_ref().clone(); + drop(loaded); + block_platform_state.set_switched_from_protocol_version(Some(14)); + + let transaction = platform.drive.grove.start_transaction(); + platform + .update_state_cache_v0( + make_extended_block_info(1), + block_platform_state, + &transaction, + platform_version, + ) + .expect("must succeed"); + + let committed = platform.state.load(); + assert_eq!(committed.switched_from_protocol_version(), None); + assert_eq!( + committed + .tenderdash_consensus_platform_version() + .expect("a known version") + .protocol_version, + platform_version.protocol_version + ); + } + /// While replaying history the full saved record is rewritten only when a /// heavy field changed; the small record carries the block info in between. /// A node restarted from disk must see the newest block info and the heavy diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_end/validator_set_update/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_end/validator_set_update/mod.rs index c611ea2c0a1..25598f85e01 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_end/validator_set_update/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_end/validator_set_update/mod.rs @@ -106,6 +106,8 @@ mod tests { block_address_balance_changes: BTreeMap::new(), block_platform_state, proposer_results: None, + random_requests: Default::default(), + applied_randomness: None, }) } diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/add_process_epoch_change_operations/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/add_process_epoch_change_operations/v0/mod.rs index 9f8304290de..0de824ff4d7 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/add_process_epoch_change_operations/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/add_process_epoch_change_operations/v0/mod.rs @@ -277,6 +277,8 @@ mod tests { unsigned_withdrawal_transactions: Default::default(), block_platform_state, proposer_results: None, + random_requests: Default::default(), + applied_randomness: None, block_address_balance_changes: Default::default(), }; diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/process_block_fees_and_validate_sum_trees/v0/mod.rs b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/process_block_fees_and_validate_sum_trees/v0/mod.rs index 02729d3400b..e805cc3e62a 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/process_block_fees_and_validate_sum_trees/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/process_block_fees_and_validate_sum_trees/v0/mod.rs @@ -273,6 +273,8 @@ mod tests { unsigned_withdrawal_transactions: Default::default(), block_platform_state, proposer_results: None, + random_requests: Default::default(), + applied_randomness: None, block_address_balance_changes: Default::default(), }; diff --git a/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/tests.rs b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/tests.rs index 4ec54f0d698..370d4f5eeed 100644 --- a/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/tests.rs +++ b/packages/rs-drive-abci/src/execution/platform_events/block_processing_end_events/tests.rs @@ -1095,6 +1095,8 @@ mod storage_refund_clawback_tests { block_address_balance_changes: Default::default(), block_platform_state: (**platform_state).clone(), proposer_results: None, + random_requests: Default::default(), + applied_randomness: None, }); // verify_sum_trees is on by default, so this fails if the credits do not balance 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 f42695f2aca..b95ba0f86e1 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 @@ -1,3 +1,4 @@ +use crate::error::execution::ExecutionError; use crate::error::Error; use crate::platform_types::platform::{Platform, PlatformRef}; use crate::platform_types::platform_state::{PlatformState, PlatformStateV0Methods}; @@ -6,18 +7,29 @@ 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::TokenTransitionV0Methods; +use dpp::state_transition::batch_transition::batched_transition::document_transition::{ + DocumentTransition, 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::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::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::state_transition::{StateTransition, StateTransitionOwned}; use dpp::tokens::token_payment_info::methods::v0::TokenPaymentInfoMethodsV0; use dpp::tokens::token_payment_info::v1::v1_accessors::TokenPaymentInfoAccessorsV1; +use drive::drive::document::draw_random_document_properties::RandomDocumentRequest; +use drive::drive::tokens::settle_random_token_amounts::{ + RandomTokenRequest, RandomTokenRequestKind, +}; +use crate::execution::types::block_random_requests::BlockRandomRequests; use crate::execution::types::state_transition_container::v0::{ DecodedStateTransition, InvalidStateTransition, InvalidWithProtocolErrorStateTransition, SuccessfullyDecodedStateTransition, @@ -155,6 +167,23 @@ where // daily withdrawal limit adds to its daily maximum. let mut block_credit_mints: Credits = 0; + // Whether the block collects what its transitions ask its random beacon for: only from + // protocol version 15, whose `apply_random_beacon` draws the values once Tenderdash + // hands the beacon over. Every protocol version selects this generation, and the + // collection is inert before 15 twice over: nothing is collected while the step is + // `None`, which every earlier table sets, and what is collected lives only on the + // in-memory processing result and block execution context, which are neither + // serialized nor hashed. The block that switches to 15 receives no beacon, as Tenderdash + // still runs it under consensus version 1: the batch transformer refuses there whatever + // would ask the beacon for a value (`random_beacon_gate`), so all it collects are the + // creates of types that draw nothing, which no draw would change. + let collect_random_requests = platform_version + .drive_abci + .methods + .block_end + .apply_random_beacon + .is_some(); + for decoded_state_transition in state_transition_container.into_iter() { // If we propose state transitions, we need to check if we have a time limit for processing // set and if we have exceeded it. @@ -214,6 +243,17 @@ where let token_shielded_pools_touched = token_shielded_pools_touched(&state_transition); + // Remembered before the transition is consumed too: what an applied + // transition asks the block's random beacon for (see + // `collect_random_requests` above). Nothing caps how many a block + // collects yet: the draw's cost is bounded, like the block's execution, + // by the block's size and the proposer's processing time limit. + let random_requests = if collect_random_requests { + random_requests_of(&state_transition) + } else { + Ok(BlockRandomRequests::default()) + }; + // Validate state transition and produce an execution event let execution_result = process_state_transition( &platform_ref, @@ -308,6 +348,16 @@ where ) { processing_result .add_token_shielded_pools_touched(token_shielded_pools_touched); + // Likewise only a successful execution writes a placeholder or + // reserves a random amount: a paid failure creates no document and + // moves no token. + processing_result.add_random_requests(random_requests.map_err( + |message| { + Error::Execution(ExecutionError::CorruptedCodeExecution( + message, + )) + }, + )?); } // Store metrics @@ -439,3 +489,226 @@ fn token_shielded_pools_touched(state_transition: &StateTransition) -> Vec<[u8; _ => vec![], } } + +/// What a state transition asks the block's random beacon for (protocol version 15), in the +/// order of its transitions: every document a batch creates, whose `sys.random` properties the +/// draw fills (it leaves alone a document whose type declares none), and every token transfer +/// or mint of a batch with a random minimum amount, whose reservation the settlement pays out. +/// +/// The requests mirror what execution wrote: the reservation of a random amount is keyed by +/// the token, the batch's owner and the transition's identity contract nonce, and holds the +/// transition's amount, the most it moves. A random mint always names its recipient, as +/// structure validation refuses one that does not; the error only reaches the caller, as a +/// corrupted execution, should such a mint ever execute successfully. +fn random_requests_of( + state_transition: &StateTransition, +) -> Result { + let mut requests = BlockRandomRequests::default(); + let StateTransition::Batch(batch) = state_transition else { + return Ok(requests); + }; + let owner_id = batch.owner_id(); + for transition in batch.transitions_iter() { + match transition { + BatchedTransitionRef::Document(document_transition) => { + if matches!(document_transition, DocumentTransition::Create(_)) { + let base = document_transition.base(); + requests.documents.push(RandomDocumentRequest { + contract_id: base.data_contract_id(), + document_type_name: base.document_type_name().clone(), + document_id: base.id(), + }); + } + } + BatchedTransitionRef::Token(token_transition) => { + let random_amount = match token_transition { + TokenTransition::Transfer(transfer) => { + transfer.random_minimum_amount().map(|minimum| { + ( + minimum, + transfer.amount(), + RandomTokenRequestKind::Transfer { + sender_id: owner_id, + recipient_id: transfer.recipient_id(), + }, + ) + }) + } + TokenTransition::Mint(mint) => match mint.random_minimum_amount() { + Some(minimum) => { + let recipient_id = mint + .issued_to_identity_id() + .ok_or("a random token mint names its recipient")?; + Some(( + minimum, + mint.amount(), + RandomTokenRequestKind::Mint { recipient_id }, + )) + } + None => None, + }, + _ => None, + }; + if let Some((minimum, maximum, kind)) = random_amount { + requests.tokens.push(RandomTokenRequest { + token_id: token_transition.token_id(), + owner_id, + identity_contract_nonce: token_transition.identity_contract_nonce(), + minimum, + maximum, + kind, + }); + } + } + } + } + Ok(requests) +} + +#[cfg(test)] +mod tests { + use super::*; + use dpp::identifier::Identifier; + use dpp::platform_value::BinaryData; + use dpp::state_transition::batch_transition::batched_transition::BatchedTransition; + use dpp::state_transition::batch_transition::document_base_transition::v0::DocumentBaseTransitionV0; + use dpp::state_transition::batch_transition::document_create_transition::{ + DocumentCreateTransition, DocumentCreateTransitionV0, + }; + use dpp::state_transition::batch_transition::token_base_transition::v0::TokenBaseTransitionV0; + use dpp::state_transition::batch_transition::token_mint_transition::TokenMintTransitionV0; + use dpp::state_transition::batch_transition::token_transfer_transition::TokenTransferTransitionV0; + use dpp::state_transition::batch_transition::{ + BatchTransition, BatchTransitionV1, TokenMintTransition, TokenTransferTransition, + }; + use std::collections::BTreeMap; + + const OWNER: [u8; 32] = [1; 32]; + const TOKEN: [u8; 32] = [2; 32]; + const RECIPIENT: [u8; 32] = [3; 32]; + const CONTRACT: [u8; 32] = [4; 32]; + + fn batch(transitions: Vec) -> StateTransition { + StateTransition::Batch(BatchTransition::V1(BatchTransitionV1 { + owner_id: Identifier::from(OWNER), + transitions, + user_fee_increase: 0, + signature_public_key_id: 0, + signature: BinaryData::default(), + })) + } + + fn transfer(random_minimum_amount: Option) -> BatchedTransition { + let mut transfer: TokenTransferTransition = TokenTransferTransitionV0 { + base: TokenBaseTransitionV0 { + identity_contract_nonce: 5, + token_contract_position: 0, + data_contract_id: Identifier::from(CONTRACT), + token_id: Identifier::from(TOKEN), + using_group_info: None, + } + .into(), + amount: 900, + recipient_id: Identifier::from(RECIPIENT), + public_note: None, + shared_encrypted_note: None, + private_encrypted_note: None, + } + .into(); + transfer.set_random_minimum_amount(random_minimum_amount); + BatchedTransition::Token(TokenTransition::Transfer(transfer)) + } + + fn mint( + issued_to_identity_id: Option, + random_minimum_amount: Option, + ) -> BatchedTransition { + let mut mint: TokenMintTransition = TokenMintTransitionV0 { + base: TokenBaseTransitionV0 { + identity_contract_nonce: 6, + token_contract_position: 0, + data_contract_id: Identifier::from(CONTRACT), + token_id: Identifier::from(TOKEN), + using_group_info: None, + } + .into(), + issued_to_identity_id, + amount: 700, + public_note: None, + } + .into(); + mint.set_random_minimum_amount(random_minimum_amount); + BatchedTransition::Token(TokenTransition::Mint(mint)) + } + + fn create(document_id: [u8; 32]) -> BatchedTransition { + let create: DocumentCreateTransition = DocumentCreateTransitionV0 { + base: DocumentBaseTransitionV0 { + id: Identifier::from(document_id), + identity_contract_nonce: 7, + document_type_name: "card".to_string(), + data_contract_id: Identifier::from(CONTRACT), + } + .into(), + entropy: [0; 32], + data: BTreeMap::new(), + prefunded_voting_balance: None, + } + .into(); + BatchedTransition::Document(DocumentTransition::Create(create)) + } + + #[test] + fn should_collect_every_created_document_and_random_token_amount_in_order() { + let requests = random_requests_of(&batch(vec![ + create([8; 32]), + transfer(Some(100)), + transfer(None), + mint(Some(Identifier::from(RECIPIENT)), Some(50)), + mint(Some(Identifier::from(RECIPIENT)), None), + create([9; 32]), + ])) + .expect("expected the requests"); + + let card = |document_id: [u8; 32]| RandomDocumentRequest { + contract_id: Identifier::from(CONTRACT), + document_type_name: "card".to_string(), + document_id: Identifier::from(document_id), + }; + assert_eq!(requests.documents, vec![card([8; 32]), card([9; 32])]); + assert_eq!( + requests.tokens, + vec![ + RandomTokenRequest { + token_id: Identifier::from(TOKEN), + owner_id: Identifier::from(OWNER), + identity_contract_nonce: 5, + minimum: 100, + maximum: 900, + kind: RandomTokenRequestKind::Transfer { + sender_id: Identifier::from(OWNER), + recipient_id: Identifier::from(RECIPIENT), + }, + }, + RandomTokenRequest { + token_id: Identifier::from(TOKEN), + owner_id: Identifier::from(OWNER), + identity_contract_nonce: 6, + minimum: 50, + maximum: 700, + kind: RandomTokenRequestKind::Mint { + recipient_id: Identifier::from(RECIPIENT), + }, + }, + ] + ); + } + + #[test] + fn should_refuse_a_random_mint_that_names_no_recipient() { + assert!(random_requests_of(&batch(vec![mint(None, Some(50))])).is_err()); + assert!(random_requests_of(&batch(vec![mint(None, None)])) + .expect("a mint of a fixed amount asks for nothing") + .is_empty()); + } +} diff --git a/packages/rs-drive-abci/src/execution/types/block_execution_context/mod.rs b/packages/rs-drive-abci/src/execution/types/block_execution_context/mod.rs index 42ebb8405e6..0176a011429 100644 --- a/packages/rs-drive-abci/src/execution/types/block_execution_context/mod.rs +++ b/packages/rs-drive-abci/src/execution/types/block_execution_context/mod.rs @@ -3,9 +3,10 @@ pub mod v0; use crate::error::Error; use crate::execution::types::block_execution_context::v0::{ - BlockExecutionContextV0Getters, BlockExecutionContextV0MutableGetters, + AppliedRandomness, BlockExecutionContextV0Getters, BlockExecutionContextV0MutableGetters, BlockExecutionContextV0OwnedGetters, BlockExecutionContextV0Setters, ProposerResults, }; +use crate::execution::types::block_random_requests::BlockRandomRequests; use crate::execution::types::block_state_info::BlockStateInfo; use crate::platform_types::epoch_info::EpochInfo; use crate::platform_types::platform_state::PlatformState; @@ -58,6 +59,18 @@ impl BlockExecutionContextV0Getters for BlockExecutionContext { BlockExecutionContext::V0(v0) => v0.proposer_results.as_ref(), } } + + fn random_requests(&self) -> &BlockRandomRequests { + match self { + BlockExecutionContext::V0(v0) => &v0.random_requests, + } + } + + fn applied_randomness(&self) -> Option<&AppliedRandomness> { + match self { + BlockExecutionContext::V0(v0) => v0.applied_randomness.as_ref(), + } + } } impl BlockExecutionContextV0Setters for BlockExecutionContext { @@ -90,6 +103,12 @@ impl BlockExecutionContextV0Setters for BlockExecutionContext { BlockExecutionContext::V0(v0) => v0.proposer_results = results, } } + + fn set_applied_randomness(&mut self, applied_randomness: Option) { + match self { + BlockExecutionContext::V0(v0) => v0.applied_randomness = applied_randomness, + } + } } impl BlockExecutionContextV0MutableGetters for BlockExecutionContext { diff --git a/packages/rs-drive-abci/src/execution/types/block_execution_context/v0/mod.rs b/packages/rs-drive-abci/src/execution/types/block_execution_context/v0/mod.rs index d166a0a96df..99e6acccf9e 100644 --- a/packages/rs-drive-abci/src/execution/types/block_execution_context/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/types/block_execution_context/v0/mod.rs @@ -1,3 +1,4 @@ +use crate::execution::types::block_random_requests::BlockRandomRequests; use crate::execution::types::block_state_info::BlockStateInfo; use std::collections::BTreeMap; @@ -30,6 +31,21 @@ pub struct ProposerResults { pub validator_set_quorum_hash: [u8; 32], } +/// The random beacon applied to a block's state in the block's transaction, and what it gave. +/// +/// A round's prevotes combine into exactly one beacon, so `ApplyBlockRandomness` may be asked +/// again for the same round only with the same beacon, and is answered with the same app hash +/// without drawing twice. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AppliedRandomness { + /// The round whose prevotes produced the beacon + pub round: u32, + /// The beacon: the threshold signature the round's prevotes combine into + pub beacon: Vec, + /// The app hash after the draws, which the round's precommits sign and the block commits + pub app_hash: [u8; 32], +} + /// V0 of the Block execution context #[derive(Debug, Clone)] pub struct BlockExecutionContextV0 { @@ -45,6 +61,11 @@ pub struct BlockExecutionContextV0 { pub block_platform_state: PlatformState, /// The prepare proposal results if proposed by us pub proposer_results: Option, + /// What the block's transitions ask its random beacon for (protocol version 15; empty + /// before), drawn when `ApplyBlockRandomness` or `FinalizeBlock` hands the beacon over + pub random_requests: BlockRandomRequests, + /// The random beacon applied to the block's state in its transaction, if one was + pub applied_randomness: Option, } /// A trait defining getter methods for interacting with a BlockExecutionContextV0. pub trait BlockExecutionContextV0Getters { @@ -62,6 +83,12 @@ pub trait BlockExecutionContextV0Getters { /// Returns a reference of the proposer_results field. fn proposer_results(&self) -> Option<&ProposerResults>; + + /// Returns a reference of the random_requests field. + fn random_requests(&self) -> &BlockRandomRequests; + + /// Returns a reference of the applied_randomness field. + fn applied_randomness(&self) -> Option<&AppliedRandomness>; } /// A trait defining setter methods for interacting with a BlockExecutionContextV0. @@ -80,6 +107,9 @@ pub trait BlockExecutionContextV0Setters { /// Sets the proposer_results field. fn set_proposer_results(&mut self, results: Option); + + /// Sets the applied_randomness field. + fn set_applied_randomness(&mut self, applied_randomness: Option); } /// A trait defining methods for interacting with a BlockExecutionContextV0. @@ -140,6 +170,16 @@ impl BlockExecutionContextV0Getters for BlockExecutionContextV0 { fn proposer_results(&self) -> Option<&ProposerResults> { self.proposer_results.as_ref() } + + /// Returns a reference to the random_requests field. + fn random_requests(&self) -> &BlockRandomRequests { + &self.random_requests + } + + /// Returns a reference to the applied_randomness field. + fn applied_randomness(&self) -> Option<&AppliedRandomness> { + self.applied_randomness.as_ref() + } } impl BlockExecutionContextV0Setters for BlockExecutionContextV0 { @@ -163,6 +203,10 @@ impl BlockExecutionContextV0Setters for BlockExecutionContextV0 { fn set_proposer_results(&mut self, results: Option) { self.proposer_results = results; } + /// Sets the applied_randomness field. + fn set_applied_randomness(&mut self, applied_randomness: Option) { + self.applied_randomness = applied_randomness; + } } impl BlockExecutionContextV0MutableGetters for BlockExecutionContextV0 { diff --git a/packages/rs-drive-abci/src/execution/types/block_random_requests/mod.rs b/packages/rs-drive-abci/src/execution/types/block_random_requests/mod.rs new file mode 100644 index 00000000000..8c742651d3b --- /dev/null +++ b/packages/rs-drive-abci/src/execution/types/block_random_requests/mod.rs @@ -0,0 +1,32 @@ +use drive::drive::document::draw_random_document_properties::RandomDocumentRequest; +use drive::drive::tokens::settle_random_token_amounts::RandomTokenRequest; + +/// What a block's random beacon fills in once the block's prevotes combine into it (protocol +/// version 15): every document the block created, whose `sys.random` properties are drawn, and +/// every random token transfer and mint, whose amount is drawn and settled. +/// +/// `process_raw_state_transitions` collects them from the transitions the block executed +/// successfully, in the order it executed them, and the block execution context keeps them +/// until `ApplyBlockRandomness`, or `FinalizeBlock` with the commit's beacon, hands the beacon +/// over. Empty before protocol version 15, where nothing is collected. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct BlockRandomRequests { + /// The documents the block created, whose `sys.random` properties are drawn; a document + /// whose type declares none is left alone + pub documents: Vec, + /// The random token transfers and mints of the block, whose amounts are drawn and settled + pub tokens: Vec, +} + +impl BlockRandomRequests { + /// Whether the block asked for no random value. + pub fn is_empty(&self) -> bool { + self.documents.is_empty() && self.tokens.is_empty() + } + + /// Appends the requests of `other` after these, keeping the order of each list. + pub fn append(&mut self, mut other: BlockRandomRequests) { + self.documents.append(&mut other.documents); + self.tokens.append(&mut other.tokens); + } +} diff --git a/packages/rs-drive-abci/src/execution/types/mod.rs b/packages/rs-drive-abci/src/execution/types/mod.rs index 7bc018955de..9424f6a17f2 100644 --- a/packages/rs-drive-abci/src/execution/types/mod.rs +++ b/packages/rs-drive-abci/src/execution/types/mod.rs @@ -2,6 +2,8 @@ pub mod block_execution_context; /// A structure representing block fees pub(crate) mod block_fees; +/// The random values a block asks its random beacon for +pub mod block_random_requests; /// Block state info pub mod block_state_info; /// An execution event diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/generated_from_random.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/generated_from_random.rs new file mode 100644 index 00000000000..c2414f494d7 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/generated_from_random.rs @@ -0,0 +1,486 @@ +//! End-to-end coverage for `generatedFrom` with `sys.random.uniform` (protocol version 15): an +//! integer property the platform writes with a placeholder, its range's minimum, when the +//! document is created, and draws from the block's random beacon once the block's transitions +//! are accepted (`Drive::draw_random_document_properties`). A client leaves the property out +//! and takes the drawn value from the proof of its own transition; one that sends a value is +//! refused, and a replace keeps the drawn value. The block that switches to protocol version 15 +//! receives no random beacon, so it refuses a create of such a type, which the next block +//! accepts. + +use super::*; + +mod generated_from_random_tests { + use super::*; + use crate::platform_types::platform_state::PlatformState; + use crate::rpc::core::MockCoreRPCLike; + use crate::test::helpers::setup::TempPlatform; + use dpp::consensus::basic::BasicError; + use dpp::data_contract::schema::DataContractSchemaMethodsV0; + use dpp::document::Document; + use dpp::identity::{Identity, IdentityPublicKey, SecurityLevel}; + use dpp::platform_value::platform_value; + use dpp::prelude::{DataContract, Identifier, IdentityNonce}; + 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::BatchedTransitionMutRef; + use dpp::state_transition::proof_result::StateTransitionProofResult; + use dpp::state_transition::StateTransition; + use dpp::tests::fixtures::get_data_contract_fixture; + use drive::drive::document::draw_random_document_properties::RandomDocumentRequest; + use drive::drive::Drive; + use drive::util::storage_flags::StorageFlags; + use simple_signer::signer::SimpleSigner; + use std::collections::BTreeMap; + use std::sync::Arc; + + const BEACON: [u8; 96] = [7; 96]; + + /// A mutable `card` type: its `name`, a `rarity` drawn from 1 to 100 and indexed, and a + /// free `note`. + fn card_schema() -> Value { + platform_value!({ + "type": "object", + "documentsMutable": true, + "indices": [{ "name": "byRarity", "properties": [{ "rarity": "asc" }] }], + "properties": { + "name": { "type": "string", "maxLength": 32, "position": 0 }, + "rarity": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "generatedFrom": { "function": "sys.random.uniform" }, + "position": 1 + }, + "note": { "type": "string", "maxLength": 32, "position": 2 } + }, + "required": ["name", "rarity"], + "additionalProperties": false + }) + } + + /// One identity holding a `card` contract. + struct CardFixture { + platform: TempPlatform, + signer: SimpleSigner, + key: IdentityPublicKey, + identity: Identity, + contract: DataContract, + next_nonce: IdentityNonce, + } + + impl CardFixture { + fn new() -> Self { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .build_with_mock_rpc() + .set_initial_state_structure(); + let (identity, signer, key) = setup_identity(&mut platform, 958, dash_to_credits!(0.5)); + + let mut contract = get_data_contract_fixture( + Some(identity.id()), + 0, + platform_version.protocol_version, + ) + .data_contract_owned(); + contract + .set_document_schema( + "card", + card_schema(), + true, + &mut Vec::new(), + platform_version, + ) + .expect("expected to add the card document type"); + platform + .drive + .apply_contract( + &contract, + BlockInfo::default(), + true, + StorageFlags::optional_default_as_cow(), + None, + platform_version, + ) + .expect("expected to apply the contract"); + + Self { + platform, + signer, + key, + identity, + contract, + next_nonce: 1, + } + } + + fn card_data(rarity: Option, note: Option<&str>) -> BTreeMap { + let mut data = BTreeMap::from([("name".to_string(), Value::Text("Ace".to_string()))]); + if let Some(rarity) = rarity { + data.insert("rarity".to_string(), Value::U8(rarity)); + } + if let Some(note) = note { + data.insert("note".to_string(), Value::Text(note.to_string())); + } + data + } + + /// `transition` with its document's data set to exactly `data`, signed again. + async fn send_as_given( + &self, + mut transition: StateTransition, + data: BTreeMap, + ) -> StateTransition { + { + let StateTransition::Batch(batch) = &mut transition else { + panic!("expected a batch transition"); + }; + let Some(BatchedTransitionMutRef::Document(document_transition)) = + batch.first_transition_mut() + else { + panic!("expected a document transition"); + }; + *document_transition + .data_mut() + .expect("the document transition carries data") = data; + } + transition + .sign_external( + &self.key, + &self.signer, + Some(|_, _| Ok(SecurityLevel::HIGH)), + ) + .await + .expect("expected to sign the transition again"); + transition + } + + /// A create transition for a card holding exactly `data`. + async fn create_transition( + &mut self, + data: BTreeMap, + seed: u64, + ) -> StateTransition { + let platform_version = PlatformVersion::latest(); + let document_type = self + .contract + .document_type_for_name("card") + .expect("expected the card document type"); + let mut rng = StdRng::seed_from_u64(seed); + let entropy = Bytes32::random_with_rng(&mut rng); + let mut document = document_type + .random_document_with_identifier_and_entropy( + &mut rng, + self.identity.id(), + entropy, + DocumentFieldFillType::DoNotFillIfNotRequired, + DocumentFieldFillSize::AnyDocumentFillSize, + platform_version, + ) + .expect("expected a random document"); + document + .set_id_for_creation(document_type, &entropy.0, self.next_nonce, platform_version) + .expect("expected to set the document id"); + *document.properties_mut() = data.clone(); + + let transition = BatchTransition::new_document_creation_transition_from_document( + document, + document_type, + entropy.0, + &self.key, + self.next_nonce, + 0, + None, + &self.signer, + platform_version, + None, + ) + .await + .expect("expected the create transition"); + self.next_nonce += 1; + self.send_as_given(transition, data).await + } + + /// A replace transition for `stored`, its revision bumped, holding exactly `data`. + async fn replace_transition( + &mut self, + stored: &Document, + data: BTreeMap, + ) -> StateTransition { + let platform_version = PlatformVersion::latest(); + let mut replacement = stored.clone(); + replacement + .increment_revision() + .expect("expected the revision to increment"); + let document_type = self + .contract + .document_type_for_name("card") + .expect("expected the card document type"); + let transition = BatchTransition::new_document_replacement_transition_from_document( + replacement, + document_type, + &self.key, + self.next_nonce, + 0, + None, + &self.signer, + platform_version, + None, + ) + .await + .expect("expected the replace transition"); + self.next_nonce += 1; + self.send_as_given(transition, data).await + } + + fn process(&self, transition: &StateTransition) -> StateTransitionExecutionResult { + self.process_in_block(transition, &self.platform.state.load()) + } + + /// Processes `transition` in a block whose platform state is `platform_state`. + fn process_in_block( + &self, + transition: &StateTransition, + platform_state: &PlatformState, + ) -> StateTransitionExecutionResult { + let platform_version = PlatformVersion::latest(); + let serialized = transition + .serialize_to_bytes() + .expect("expected the transition to serialize"); + let transaction = self.platform.drive.grove.start_transaction(); + let processing_result = self + .platform + .platform + .process_raw_state_transitions( + &[serialized], + platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process the state transition"); + self.platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit the transaction"); + processing_result.into_execution_results().remove(0) + } + + /// Draws the rarity of `card` from [`BEACON`], as the block that created it does once + /// its transitions are accepted. + fn draw(&self, card: &Document) { + let drawn = self + .platform + .drive + .draw_random_document_properties( + &BEACON, + &[RandomDocumentRequest { + contract_id: self.contract.id(), + document_type_name: "card".to_string(), + document_id: card.id(), + }], + &BlockInfo::default(), + None, + PlatformVersion::latest(), + ) + .expect("expected the draw to run"); + assert_eq!(drawn, 1); + } + + /// Proves the committed state `transition` wrote and verifies the proof as a client + /// holding the card contract does: it cannot know the drawn value. + fn verify_proof(&self, transition: &StateTransition) -> StateTransitionProofResult { + let platform_version = PlatformVersion::latest(); + let proof = self + .platform + .drive + .prove_state_transition(transition, None, platform_version) + .expect("expected to prove the state transition") + .into_data() + .expect("expected proof bytes"); + let known_contracts: BTreeMap = + BTreeMap::from([(self.contract.id(), self.contract.clone())]); + let (_, outcome) = Drive::verify_state_transition_was_executed_with_proof( + transition, + &BlockInfo::default(), + &proof, + &|id| Ok(known_contracts.get(id).cloned().map(Arc::new)), + platform_version, + ) + .expect("expected the proof to verify"); + outcome.into_result() + } + + fn stored_cards(&self) -> Vec { + let platform_version = PlatformVersion::latest(); + let query = DriveDocumentQuery::from_sql_expr( + "select * from card", + &self.contract, + Some(&self.platform.config.drive), + platform_version, + ) + .expect("expected a document query"); + self.platform + .drive + .query_documents(query, None, false, None, None) + .expect("expected a query result") + .documents() + .to_vec() + } + } + + fn rarity_of(document: &Document) -> Option { + document + .properties() + .get("rarity") + .and_then(|value| value.to_integer::().ok()) + } + + /// The rarity of the single document `proof_result` proves, which must be `card_id`'s and + /// present. + fn proved_rarity(proof_result: StateTransitionProofResult, card_id: Identifier) -> Option { + let StateTransitionProofResult::VerifiedDocuments(documents) = proof_result else { + panic!("expected the proof to show the documents, got {proof_result:?}"); + }; + assert_eq!( + documents.len(), + 1, + "the proof shows the transition's document alone: {documents:?}" + ); + let document = documents + .get(&card_id) + .expect("the proof shows the card the transition wrote") + .as_ref() + .expect("the proof shows the card present"); + rarity_of(document) + } + + fn assert_success(result: &StateTransitionExecutionResult) { + assert_matches!( + result, + StateTransitionExecutionResult::SuccessfulExecution { .. }, + "{result:?}" + ); + } + + fn assert_not_generated(result: StateTransitionExecutionResult) { + let StateTransitionExecutionResult::PaidConsensusError { error, .. } = result else { + panic!("expected a paid consensus error, got {result:?}"); + }; + assert_matches!( + error, + ConsensusError::BasicError(BasicError::DocumentPropertyNotGeneratedError(e)) + if e.property() == "rarity" && e.function() == "sys.random.uniform" + ); + } + + #[tokio::test] + async fn should_write_the_placeholder_then_prove_the_drawn_value() { + let mut fixture = CardFixture::new(); + + let transition = fixture + .create_transition(CardFixture::card_data(None, None), 1) + .await; + assert_success(&fixture.process(&transition)); + let stored = fixture.stored_cards(); + assert_eq!(stored.len(), 1); + assert_eq!(rarity_of(&stored[0]), Some(1), "the placeholder"); + let card_id = stored[0].id(); + + fixture.draw(&stored[0]); + let drawn = rarity_of(&fixture.stored_cards()[0]).expect("a drawn rarity"); + assert!((1..=100).contains(&drawn)); + + // The client that sent the create, which left the rarity out, reads the drawn value + // from the proof of its own transition + assert_eq!( + proved_rarity(fixture.verify_proof(&transition), card_id), + Some(drawn) + ); + } + + #[tokio::test] + async fn should_refuse_a_create_that_picks_the_value() { + let mut fixture = CardFixture::new(); + + let transition = fixture + .create_transition(CardFixture::card_data(Some(100), None), 2) + .await; + assert_not_generated(fixture.process(&transition)); + assert!(fixture.stored_cards().is_empty()); + } + + #[tokio::test] + async fn should_keep_the_drawn_value_on_a_replace() { + let mut fixture = CardFixture::new(); + + let transition = fixture + .create_transition(CardFixture::card_data(None, None), 3) + .await; + assert_success(&fixture.process(&transition)); + fixture.draw(&fixture.stored_cards()[0]); + let drawn = fixture.stored_cards().remove(0); + let rarity = rarity_of(&drawn).expect("a drawn rarity"); + + // Left out, the drawn value is kept + let transition = fixture + .replace_transition(&drawn, CardFixture::card_data(None, Some("kept"))) + .await; + assert_success(&fixture.process(&transition)); + let replaced = fixture.stored_cards().remove(0); + assert_eq!(replaced.get("note"), Some(&Value::Text("kept".to_string()))); + assert_eq!(rarity_of(&replaced), Some(rarity)); + assert_eq!( + proved_rarity(fixture.verify_proof(&transition), replaced.id()), + Some(rarity) + ); + + // Another value cannot be picked + let other = if rarity == 100 { 99 } else { rarity + 1 }; + let transition = fixture + .replace_transition(&replaced, CardFixture::card_data(Some(other), None)) + .await; + assert_not_generated(fixture.process(&transition)); + assert_eq!(rarity_of(&fixture.stored_cards()[0]), Some(rarity)); + } + + /// The block that switches to protocol version 15 still runs under Tenderdash consensus + /// version 1, which sends it no random beacon, so nothing would draw the rarity there and + /// the card would keep its placeholder: the create is refused unpaid, before anything is + /// written, and the same create is accepted in the next block, which receives a beacon. + #[tokio::test] + async fn should_refuse_a_random_document_in_the_block_switching_to_protocol_version_15_and_accept_it_in_the_next( + ) { + let mut fixture = CardFixture::new(); + let transition = fixture + .create_transition(CardFixture::card_data(None, None), 4) + .await; + + // The state of the block that switches from protocol version 14 + let mut switching_block_state = PlatformState::clone(&fixture.platform.state.load()); + switching_block_state.set_switched_from_protocol_version(Some(14)); + let result = fixture.process_in_block(&transition, &switching_block_state); + let StateTransitionExecutionResult::UnpaidConsensusError(error) = result else { + panic!("expected the create to be refused unpaid, got {result:?}"); + }; + assert_matches!( + error, + ConsensusError::BasicError(BasicError::RandomBeaconUnavailableError(e)) + if e.protocol_version() == 15 + ); + assert!(fixture.stored_cards().is_empty()); + + // The next block runs under consensus version 2: the same create goes through + assert_success(&fixture.process(&transition)); + let stored = fixture.stored_cards(); + assert_eq!(stored.len(), 1); + assert_eq!( + rarity_of(&stored[0]), + Some(1), + "the placeholder, until the draw" + ); + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/mod.rs index a3750ecbcdb..7132ef7101e 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/document/mod.rs @@ -15,6 +15,7 @@ mod duplicate_keys; mod encrypted_for; mod gas_sponsorship; mod generated_from; +mod generated_from_random; mod generated_from_token; mod id_reuse; mod immutable; 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 aa063c35a84..fa543fbd45e 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 @@ -9,6 +9,7 @@ mod freeze; mod mint; mod mint_many; mod non_transferable; +mod random_amount; mod shielded; mod transfer; mod unfreeze; diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/random_amount/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/random_amount/mod.rs new file mode 100644 index 00000000000..a578e10ffa5 --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/random_amount/mod.rs @@ -0,0 +1,520 @@ +//! End-to-end coverage for random token amounts (protocol version 15): a token transfer or +//! mint naming a random minimum amount reserves the most it moves in the token marketplace +//! escrow when the block executes it, and `Drive::settle_random_token_amounts` pays out the +//! amount drawn from the block's random beacon once the block's transitions are accepted. The +//! token conservation check balances throughout. A client cannot know the amount, so it +//! reads it from the proof of its own transition. The block that switches to protocol version +//! 15 receives no random beacon, so it refuses a random amount, which the next block accepts. + +use super::*; + +mod random_amount_tests { + use super::*; + use crate::platform_types::platform_state::PlatformState; + use crate::rpc::core::MockCoreRPCLike; + use crate::test::helpers::setup::TempPlatform; + use dpp::balances::credits::TokenAmount; + use dpp::prelude::DataContract; + 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::methods::StateTransitionCreationOptions; + use dpp::state_transition::batch_transition::{TokenMintTransition, TokenTransferTransition}; + use dpp::state_transition::proof_result::StateTransitionProofResult; + use dpp::state_transition::StateTransition; + use drive::drive::tokens::settle_random_token_amounts::{ + RandomTokenRequest, RandomTokenRequestKind, + }; + use drive::drive::Drive; + use std::collections::BTreeMap; + use std::sync::Arc; + + const BEACON: [u8; 96] = [7; 96]; + const BASE_SUPPLY: TokenAmount = 100_000; + const MINIMUM: TokenAmount = 100; + const MAXIMUM: TokenAmount = 1_000; + const NONCE: u64 = 2; + + fn random_options() -> Option { + Some(StateTransitionCreationOptions { + random_minimum_amount: Some(MINIMUM), + ..Default::default() + }) + } + + /// The token transition of the single-transition batch `transition`. + fn token_transition(transition: &StateTransition) -> &TokenTransition { + let StateTransition::Batch(BatchTransition::V1(batch)) = transition else { + panic!("expected a version 1 batch"); + }; + let [BatchedTransition::Token(token_transition)] = batch.transitions.as_slice() else { + panic!("expected a single token transition"); + }; + token_transition + } + + fn process( + platform: &TempPlatform, + transition: &StateTransition, + ) -> StateTransitionExecutionResult { + process_in_block(platform, transition, &platform.state.load()) + } + + /// Processes `transition` in a block whose platform state is `platform_state`. + fn process_in_block( + platform: &TempPlatform, + transition: &StateTransition, + platform_state: &PlatformState, + ) -> StateTransitionExecutionResult { + let platform_version = PlatformVersion::latest(); + let serialized = transition + .serialize_to_bytes() + .expect("expected the transition to serialize"); + let transaction = platform.drive.grove.start_transaction(); + let processing_result = platform + .platform + .process_raw_state_transitions( + &[serialized], + platform_state, + &BlockInfo::default(), + &transaction, + platform_version, + false, + None, + ) + .expect("expected to process the state transition"); + platform + .drive + .grove + .commit_transaction(transaction) + .unwrap() + .expect("expected to commit the transaction"); + processing_result.into_execution_results().remove(0) + } + + fn 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("expected to fetch a token balance") + } + + fn supply( + platform: &TempPlatform, + token_id: Identifier, + ) -> Option { + platform + .drive + .fetch_token_total_supply(token_id.to_buffer(), None, PlatformVersion::latest()) + .expect("expected to fetch the total supply") + } + + /// What the reservation of `request` holds in the token marketplace escrow of its token, + /// `None` once settled. + fn reserved( + platform: &TempPlatform, + request: &RandomTokenRequest, + ) -> Option { + platform + .drive + .fetch_token_order_escrow( + request.token_id.to_buffer(), + request.reservation_id(), + None, + &mut Vec::new(), + PlatformVersion::latest(), + ) + .expect("expected to fetch the reservation") + } + + /// The identity balances, the shielded pools and the token marketplace escrow, which holds + /// the reservations, add up to the total supplies. + fn assert_tokens_balanced(platform: &TempPlatform) { + let total = platform + .drive + .calculate_total_tokens_balance(None, PlatformVersion::latest()) + .expect("expected to sum the token balances"); + assert!( + total.ok().expect("expected the sums to compare"), + "tokens are not balanced: {total}" + ); + } + + /// Proves the committed state `transition` wrote and verifies the proof as the client that + /// sent it does, holding the token's contract: it cannot know the drawn amount. + fn verify_proof( + platform: &TempPlatform, + transition: &StateTransition, + contract: &DataContract, + ) -> StateTransitionProofResult { + let platform_version = PlatformVersion::latest(); + let proof = platform + .drive + .prove_state_transition(transition, None, platform_version) + .expect("expected to prove the state transition") + .into_data() + .expect("expected proof bytes"); + let known_contracts: BTreeMap = + BTreeMap::from([(contract.id(), contract.clone())]); + let (_, outcome) = Drive::verify_state_transition_was_executed_with_proof( + transition, + &BlockInfo::default(), + &proof, + &|id| Ok(known_contracts.get(id).cloned().map(Arc::new)), + platform_version, + ) + .expect("expected the proof to verify"); + outcome.into_result() + } + + /// The amount of the history document a proof shows. + fn proved_amount(result: StateTransitionProofResult) -> TokenAmount { + let StateTransitionProofResult::VerifiedTokenActionWithDocument(document) = result else { + panic!("expected the proof to show the history document, got {result:?}"); + }; + document + .properties() + .get_integer("amount") + .expect("expected the history document to hold an amount") + } + + fn settle(platform: &TempPlatform, request: &RandomTokenRequest) { + let settled = platform + .drive + .settle_random_token_amounts( + &BEACON, + std::slice::from_ref(request), + &BlockInfo::default(), + None, + PlatformVersion::latest(), + ) + .expect("expected the settlement to run"); + assert_eq!(settled, 1); + } + + #[tokio::test] + async fn should_reserve_a_random_transfer_then_pay_out_the_drawn_amount() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + let mut rng = StdRng::seed_from_u64(49855); + 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.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + None::, + None, + None, + None, + platform_version, + ); + + let transition = BatchTransition::new_token_transfer_transition( + token_id, + identity.id(), + contract.id(), + 0, + MAXIMUM, + recipient.id(), + None, + None, + None, + &key, + NONCE, + 0, + &signer, + platform_version, + random_options(), + ) + .await + .expect("expected to create the transfer"); + assert!(matches!( + token_transition(&transition), + TokenTransition::Transfer(TokenTransferTransition::V1(_)) + )); + + let request = RandomTokenRequest { + token_id, + owner_id: identity.id(), + identity_contract_nonce: NONCE, + minimum: MINIMUM, + maximum: MAXIMUM, + kind: RandomTokenRequestKind::Transfer { + sender_id: identity.id(), + recipient_id: recipient.id(), + }, + }; + + let result = process(&platform, &transition); + assert_matches!( + &result, + StateTransitionExecutionResult::SuccessfulExecution { .. }, + "{result:?}" + ); + // The block moves the most the transfer moves into a reservation in the token's escrow, + // and writes the recipient's balance at zero, where it waits for the draw + assert_eq!( + balance(&platform, token_id, identity.id()), + Some(BASE_SUPPLY - MAXIMUM) + ); + assert_eq!(reserved(&platform, &request), Some(MAXIMUM)); + assert_eq!(balance(&platform, token_id, recipient.id()), Some(0)); + assert_tokens_balanced(&platform); + // The history holds the range's minimum until the draw + assert_eq!( + proved_amount(verify_proof(&platform, &transition, &contract)), + MINIMUM + ); + + settle(&platform, &request); + + let drawn = balance(&platform, token_id, recipient.id()).expect("the recipient is paid"); + assert!((MINIMUM..=MAXIMUM).contains(&drawn)); + assert_eq!( + balance(&platform, token_id, identity.id()), + Some(BASE_SUPPLY - drawn) + ); + assert_eq!(reserved(&platform, &request), None); + assert_eq!(supply(&platform, token_id), Some(BASE_SUPPLY)); + assert_tokens_balanced(&platform); + // The client that sent the transfer reads the drawn amount from the proof of its own + // transition + assert_eq!( + proved_amount(verify_proof(&platform, &transition, &contract)), + drawn + ); + } + + #[tokio::test] + async fn should_reserve_a_random_mint_then_pay_out_the_drawn_amount() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + let mut rng = StdRng::seed_from_u64(49856); + 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.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + None::, + None, + None, + None, + platform_version, + ); + + let transition = BatchTransition::new_token_mint_transition( + token_id, + identity.id(), + contract.id(), + 0, + MAXIMUM, + Some(recipient.id()), + None, + None, + &key, + NONCE, + 0, + &signer, + platform_version, + random_options(), + ) + .await + .expect("expected to create the mint"); + assert!(matches!( + token_transition(&transition), + TokenTransition::Mint(TokenMintTransition::V1(_)) + )); + + let request = RandomTokenRequest { + token_id, + owner_id: identity.id(), + identity_contract_nonce: NONCE, + minimum: MINIMUM, + maximum: MAXIMUM, + kind: RandomTokenRequestKind::Mint { + recipient_id: recipient.id(), + }, + }; + + let result = process(&platform, &transition); + assert_matches!( + &result, + StateTransitionExecutionResult::SuccessfulExecution { .. }, + "{result:?}" + ); + // The block adds the most the mint issues to the supply and holds it in a reservation in + // the token's escrow, and writes the recipient's balance at zero, where it waits for the + // draw + assert_eq!(supply(&platform, token_id), Some(BASE_SUPPLY + MAXIMUM)); + assert_eq!(reserved(&platform, &request), Some(MAXIMUM)); + assert_eq!(balance(&platform, token_id, recipient.id()), Some(0)); + assert_tokens_balanced(&platform); + + settle(&platform, &request); + + let drawn = balance(&platform, token_id, recipient.id()).expect("the recipient is paid"); + assert!((MINIMUM..=MAXIMUM).contains(&drawn)); + assert_eq!(reserved(&platform, &request), None); + assert_eq!(supply(&platform, token_id), Some(BASE_SUPPLY + drawn)); + assert_eq!( + balance(&platform, token_id, identity.id()), + Some(BASE_SUPPLY) + ); + assert_tokens_balanced(&platform); + assert_eq!( + proved_amount(verify_proof(&platform, &transition, &contract)), + drawn + ); + } + + #[tokio::test] + async fn should_refuse_a_random_mint_without_a_recipient() { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + let mut rng = StdRng::seed_from_u64(49857); + 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| { + token_configuration + .distribution_rules_mut() + .set_new_tokens_destination_identity(Some(identity.id())); + }), + None, + None, + None, + platform_version, + ); + + let transition = BatchTransition::new_token_mint_transition( + token_id, + identity.id(), + contract.id(), + 0, + MAXIMUM, + None, + None, + None, + &key, + NONCE, + 0, + &signer, + platform_version, + random_options(), + ) + .await + .expect("expected to create the mint"); + + let error = match process(&platform, &transition) { + StateTransitionExecutionResult::UnpaidConsensusError(error) => error, + StateTransitionExecutionResult::PaidConsensusError { error, .. } => error, + other => panic!("expected the mint to be refused, got {other:?}"), + }; + assert_matches!( + error, + ConsensusError::BasicError(BasicError::RandomTokenMintNotAllowedError(_)) + ); + assert_eq!(supply(&platform, token_id), Some(BASE_SUPPLY)); + } + + /// The block that switches to protocol version 15 still runs under Tenderdash consensus + /// version 1, which sends it no random beacon, so nothing would settle a random amount + /// there: the transfer is refused unpaid, before anything moves, and the same transfer is + /// accepted in the next block, which receives a beacon. + #[tokio::test] + async fn should_refuse_a_random_transfer_in_the_block_switching_to_protocol_version_15_and_accept_it_in_the_next( + ) { + let platform_version = PlatformVersion::latest(); + let mut platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc() + .set_genesis_state(); + let mut rng = StdRng::seed_from_u64(49858); + 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.5)); + let (contract, token_id) = create_token_contract_with_owner_identity( + &mut platform, + identity.id(), + None::, + None, + None, + None, + platform_version, + ); + + let transition = BatchTransition::new_token_transfer_transition( + token_id, + identity.id(), + contract.id(), + 0, + MAXIMUM, + recipient.id(), + None, + None, + None, + &key, + NONCE, + 0, + &signer, + platform_version, + random_options(), + ) + .await + .expect("expected to create the transfer"); + + // The state of the block that switches from protocol version 14 + let mut switching_block_state = PlatformState::clone(&platform.state.load()); + switching_block_state.set_switched_from_protocol_version(Some(14)); + let result = process_in_block(&platform, &transition, &switching_block_state); + let StateTransitionExecutionResult::UnpaidConsensusError(error) = result else { + panic!("expected the transfer to be refused unpaid, got {result:?}"); + }; + assert_matches!( + error, + ConsensusError::BasicError(BasicError::RandomBeaconUnavailableError(e)) + if e.protocol_version() == 15 + ); + assert_eq!( + balance(&platform, token_id, identity.id()), + Some(BASE_SUPPLY) + ); + assert_eq!(balance(&platform, token_id, recipient.id()), None); + assert_tokens_balanced(&platform); + + // The next block runs under consensus version 2: the same transfer goes through + let result = process(&platform, &transition); + assert_matches!( + &result, + StateTransitionExecutionResult::SuccessfulExecution { .. }, + "{result:?}" + ); + assert_eq!( + balance(&platform, token_id, identity.id()), + Some(BASE_SUPPLY - MAXIMUM) + ); + assert_eq!(balance(&platform, token_id, recipient.id()), Some(0)); + assert_tokens_balanced(&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 7d384fdbb43..05e7315c5e8 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 @@ -25,11 +25,15 @@ mod contract_moderation_gate; mod moderator_fields; pub(crate) mod property_constraint_aggregates; +mod random_beacon_gate; mod retraction; use contract_moderation_gate::{BatchTransitionContractModerationGate, ContractModerationRefusal}; use moderator_fields::judge_moderator_field_write; use property_constraint_aggregates::attach_property_constraint_reads; +use random_beacon_gate::{ + random_beacon_withheld, refuse_random_document, refuse_random_token_amount, +}; use retraction::judge_barred_replace; use std::borrow::Cow; use std::collections::btree_map::Entry; @@ -240,6 +244,24 @@ impl BatchTransitionTransformerV0 for BatchTransition { let owner_id = self.owner_id(); let user_fee_increase = self.user_fee_increase(); let platform_version = platform.state.current_platform_version()?; + + // The block that switches to protocol version 15 receives no random beacon, so nothing + // would settle a random token amount there (`random_beacon_gate`): a batch moving one + // is refused, unpaid, before anything is read. In place: `random_beacon_withheld` is + // false at every protocol version before 15, whose tables draw nothing + if random_beacon_withheld(platform.state, platform_version)? { + if let Some(refusal) = refuse_random_token_amount( + self.transitions_iter() + .filter_map(|transition| match transition { + BatchedTransitionRef::Token(token_transition) => Some(token_transition), + BatchedTransitionRef::Document(_) => None, + }), + platform_version, + ) { + return Ok(ConsensusValidationResult::new_with_error(refusal)); + } + } + let mut document_transitions_by_contracts_and_types: BTreeMap< &Identifier, BTreeMap<&String, Vec<&DocumentTransition>>, @@ -489,6 +511,22 @@ impl BatchTransitionInternalTransformerV0 for BatchTransition { )); } + // The block that switches to protocol version 15 receives no random beacon, so a + // document created there with a `sys.random` property would keep its placeholder for + // good (`random_beacon_gate`): the Batch is refused here, unpaid, before the contract + // moderation gate reads state and before any document is read or written. In place: + // `random_beacon_withheld` is false at every protocol version before 15, whose tables + // draw nothing, and no type parsed there declares such a property + if random_beacon_withheld(platform.state, platform_version)? { + if let Some(refusal) = refuse_random_document( + &data_contract_fetch_info.contract, + document_transitions, + platform_version, + ) { + return Ok(ConsensusValidationResult::new_with_error(refusal)); + } + } + // Contract moderation (protocol version 14): a moderated contract refuses the document // transitions of a banned or suspended signer, its deletions and retractions excepted. // The gate is its own versioned helper, selected by diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/random_beacon_gate.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/random_beacon_gate.rs new file mode 100644 index 00000000000..104bc86872e --- /dev/null +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/transformer/v0/random_beacon_gate.rs @@ -0,0 +1,165 @@ +//! The block that switches to protocol version 15 receives no random beacon. Tenderdash applies +//! the consensus parameters a block returns from the next height on, so that block still runs +//! under Tenderdash consensus version 1, which neither recovers a beacon nor calls +//! `ApplyBlockRandomness`, and its commit carries none. Nothing would then draw what the block's +//! transitions ask for: a document created with a `sys.random` property would keep its +//! placeholder for good, and a random token amount would leave its maximum in escrow. So the +//! batch transformer refuses such a transition in that block, unpaid and before anything is read +//! or written (`RandomBeaconUnavailableError`), and the next block, which runs under consensus +//! version 2, accepts it. +//! +//! Every protocol version selects the transformer this gate sits in, and it is inert before 15: +//! [`random_beacon_withheld`] is false wherever the block's protocol version draws nothing +//! (`apply_random_beacon` is `None`), before it reads anything else. + +use crate::error::Error; +use crate::platform_types::platform_state::{PlatformState, PlatformStateV0Methods}; +use dpp::consensus::basic::state_transition::RandomBeaconUnavailableError; +use dpp::consensus::ConsensusError; +use dpp::data_contract::accessors::v0::DataContractV0Getters; +use dpp::data_contract::document_type::accessors::DocumentTypeV2Getters; +use dpp::data_contract::document_type::{DocumentTypeRef, SystemFunction}; +use dpp::data_contract::DataContract; +use dpp::state_transition::batch_transition::batched_transition::document_transition::DocumentTransition; +use dpp::state_transition::batch_transition::batched_transition::token_transition::{ + TokenTransition, TokenTransitionV0Methods, +}; +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::version::PlatformVersion; +use std::collections::BTreeMap; + +/// Whether the block `platform_state` belongs to draws random values but receives no random +/// beacon to draw them from. Its protocol version, `platform_version`, draws them +/// (`apply_random_beacon` is `Some`), while Tenderdash runs it under the consensus version of +/// the version it switches from +/// ([`PlatformStateV0Methods::tenderdash_consensus_platform_version`]), whose +/// `apply_random_beacon` is `None`: the block that switches to protocol version 15. In +/// check_tx, `platform_state` is the committed state, which runs under its own version, so this +/// is false there. +pub(super) fn random_beacon_withheld( + platform_state: &PlatformState, + platform_version: &PlatformVersion, +) -> Result { + if platform_version + .drive_abci + .methods + .block_end + .apply_random_beacon + .is_none() + { + return Ok(false); + } + Ok(platform_state + .tenderdash_consensus_platform_version()? + .drive_abci + .methods + .block_end + .apply_random_beacon + .is_none()) +} + +/// The refusal of the first of `token_transitions` that asks for a random amount: a token +/// transfer or mint naming a random minimum amount. `None` when none does. +pub(super) fn refuse_random_token_amount<'a>( + token_transitions: impl IntoIterator, + platform_version: &PlatformVersion, +) -> Option { + token_transitions.into_iter().find_map(|token_transition| { + let kind = match token_transition { + TokenTransition::Transfer(transfer) if transfer.random_minimum_amount().is_some() => { + "transfer" + } + TokenTransition::Mint(mint) if mint.random_minimum_amount().is_some() => "mint", + _ => return None, + }; + Some( + RandomBeaconUnavailableError::new( + format!("The token {kind} of token {}", token_transition.token_id()), + platform_version.protocol_version, + ) + .into(), + ) + }) +} + +/// The refusal of the first type of `contract` among `document_transitions` (the transitions of +/// a batch on `contract`, by type name) that creates a document and declares a property drawn at +/// random. `None` when none does. +pub(super) fn refuse_random_document( + contract: &DataContract, + document_transitions: &BTreeMap<&String, Vec<&DocumentTransition>>, + platform_version: &PlatformVersion, +) -> Option { + document_transitions + .iter() + .find(|(document_type_name, transitions)| { + transitions + .iter() + .any(|transition| matches!(transition, DocumentTransition::Create(_))) + && contract + .document_type_optional_for_name(document_type_name) + .is_some_and(declares_a_property_drawn_at_random) + }) + .map(|(document_type_name, _)| { + RandomBeaconUnavailableError::new( + format!( + "The create of a document of type \"{document_type_name}\" of data contract {}", + contract.id() + ), + platform_version.protocol_version, + ) + .into() + }) +} + +/// Whether `document_type` declares a property drawn at random (`generatedFrom` with a +/// `sys.random` function), which no type parsed before protocol version 15 does. +fn declares_a_property_drawn_at_random(document_type: DocumentTypeRef) -> bool { + document_type + .generated_from_fields() + .iter() + .any(|(_, generated_from)| matches!(generated_from.function, SystemFunction::Random(_))) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::PlatformConfig; + use dpp::dashcore::Network; + + /// The state of a block at `protocol_version`, switching from `switched_from` when set. + fn block_state( + protocol_version: u32, + switched_from: Option, + ) -> (PlatformState, &'static PlatformVersion) { + let platform_version = + PlatformVersion::get(protocol_version).expect("expected a known protocol version"); + let mut state = PlatformState::default_with_protocol_versions( + protocol_version, + protocol_version, + &PlatformConfig::default_for_network(Network::Testnet), + ) + .expect("expected a platform state"); + state.set_switched_from_protocol_version(switched_from); + (state, platform_version) + } + + fn withheld(protocol_version: u32, switched_from: Option) -> bool { + let (state, platform_version) = block_state(protocol_version, switched_from); + random_beacon_withheld(&state, platform_version).expect("expected the gate to decide") + } + + #[test] + fn should_withhold_the_beacon_only_from_the_block_switching_to_protocol_version_15() { + let latest = PlatformVersion::latest().protocol_version; + // The block that switches from 14 runs under consensus version 1 + assert!(withheld(latest, Some(14))); + // Any later block, and check_tx on a committed state, run under consensus version 2 + assert!(!withheld(latest, None)); + // Inert at the last shipped protocol version, whatever it switched from: its blocks draw + // nothing + assert!(!withheld(14, Some(13))); + assert!(!withheld(14, None)); + } +} diff --git a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_common/data_contract_reference_validation/v0/mod.rs b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_common/data_contract_reference_validation/v0/mod.rs index 9b227565a74..0c1753c09f3 100644 --- a/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_common/data_contract_reference_validation/v0/mod.rs +++ b/packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/data_contract_common/data_contract_reference_validation/v0/mod.rs @@ -1437,15 +1437,8 @@ mod tests { #[test] fn should_refuse_a_where_entry_comparing_a_property_drawn_at_random() { - // No protocol version admits `sys.random.uniform` yet: the contract parses at the - // latest one with it admitted - let mut platform_version = PlatformVersion::latest().clone(); - platform_version - .dpp - .contract_versions - .document_type_versions - .schema - .random_generated_from = Some(0); + // Protocol version 15 admits `sys.random.uniform` + let platform_version = PlatformVersion::latest(); let contract = DataContract::from_value( platform_value!({ "$formatVersion": "1", @@ -1473,20 +1466,20 @@ mod tests { }, }), false, - &platform_version, + platform_version, ) .expect("the contract parses"); let card = contract .document_type_for_name("card") .expect("the card type"); - let error = referenced_side_error(&contract, card, "rarity", &platform_version) + let error = referenced_side_error(&contract, card, "rarity", platform_version) .expect("the check runs"); assert!( error.is_some_and(|reason| reason.contains("drawn at random")), "a where entry comparing the drawn rarity is refused" ); assert_eq!( - referenced_side_error(&contract, card, "name", &platform_version) + referenced_side_error(&contract, card, "name", platform_version) .expect("the check runs"), None ); 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 0d8c5b3d6eb..8e4be94a770 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 @@ -643,6 +643,8 @@ pub(in crate::execution) mod tests { block_address_balance_changes: Default::default(), block_platform_state: platform_state.clone(), proposer_results: None, + random_requests: Default::default(), + applied_randomness: None, }); // Process fees diff --git a/packages/rs-drive-abci/src/mimic/mod.rs b/packages/rs-drive-abci/src/mimic/mod.rs index b814088a0c7..512360cb892 100644 --- a/packages/rs-drive-abci/src/mimic/mod.rs +++ b/packages/rs-drive-abci/src/mimic/mod.rs @@ -7,6 +7,7 @@ use crate::abci::app::FullAbciApplication; use crate::execution::types::block_execution_context::v0::BlockExecutionContextV0Getters; use crate::execution::types::block_state_info::v0::BlockStateInfoV0Getters; use crate::mimic::test_quorum::TestQuorumInfo; +use crate::platform_types::platform_state::PlatformStateV0Methods; use crate::platform_types::withdrawal::unsigned_withdrawal_txs::v0::{ tx_to_extend_vote_extension, UnsignedWithdrawalTxs, }; @@ -22,6 +23,7 @@ use dpp::platform_value::Value; use dpp::serialization::{PlatformDeserializableUntrusted, PlatformSerializable}; use dpp::state_transition::StateTransition; use dpp::util::deserializer::ProtocolVersion; +use dpp::util::hash::hash_single; use dpp::version::PlatformVersion; use dpp::ProtocolError; use rand::rngs::StdRng; @@ -30,9 +32,9 @@ use tenderdash_abci::proto::abci::response_process_proposal::ProposalStatus; use tenderdash_abci::proto::abci::response_verify_vote_extension::VerifyStatus; use tenderdash_abci::proto::abci::tx_record::TxAction; use tenderdash_abci::proto::abci::{ - CommitInfo, ExecTxResult, ExtendVoteExtension, RequestExtendVote, RequestFinalizeBlock, - RequestPrepareProposal, RequestProcessProposal, RequestVerifyVoteExtension, - ResponsePrepareProposal, ValidatorSetUpdate, + CommitInfo, ExecTxResult, ExtendVoteExtension, RequestApplyBlockRandomness, RequestExtendVote, + RequestFinalizeBlock, RequestPrepareProposal, RequestProcessProposal, + RequestVerifyVoteExtension, ResponsePrepareProposal, ValidatorSetUpdate, }; use tenderdash_abci::proto::google::protobuf::Timestamp; use tenderdash_abci::proto::types::ConsensusParams; @@ -62,9 +64,11 @@ pub struct MimicExecuteBlockOutcome { pub validator_set_update: Option, /// The next validators hash pub next_validator_set_hash: Vec, - /// Root App hash + /// Root App hash: the state the block commits, after its random draws from Tenderdash + /// consensus version 2 pub root_app_hash: [u8; 32], - /// State ID needed to verify the block, for example height, app version, etc. + /// State ID the commit signs, needed to verify the block, for example height, app version, + /// etc. pub state_id: StateId, /// Hash of CanonicalBlockId pub block_id_hash: [u8; 32], @@ -104,6 +108,17 @@ impl FullAbciApplication<'_, C> { ) -> Result { let platform_version = PlatformVersion::get(proposed_version)?; + // The Tenderdash consensus version in force for this block: the one the last committed + // block's protocol version set, since a protocol version a block switches to takes + // effect in Tenderdash at the next height (after init chain, the genesis version's) + let tenderdash_consensus_version = self + .platform + .state + .load() + .current_platform_version()? + .consensus + .tenderdash_consensus_version; + // This will be NONE, except on init chain let original_block_execution_context = self .block_execution_context @@ -496,6 +511,34 @@ impl FullAbciApplication<'_, C> { } } + // From Tenderdash consensus version 2 (protocol version 15) the round's prevotes + // combine into the block's random beacon, which Tenderdash hands to Drive before the + // precommit, and the precommits sign the app hash Drive returns: the state after the + // block's random draws. The commit carries both. + let (beacon, final_app_hash) = if tenderdash_consensus_version >= 2 { + let beacon = stand_in_beacon(&block_header_hash, round); + let response = self + .apply_block_randomness(RequestApplyBlockRandomness { + hash: block_header_hash.to_vec(), + height: height as i64, + round: round as i32, + beacon: beacon.clone(), + }) + .unwrap_or_else(|e| { + panic!( + "should apply the randomness of block #{} round #{} at time #{} : {:?}", + block_info.height, round, block_info.time_ms, e + ) + }); + (beacon, response.app_hash) + } else { + (Vec::new(), app_hash.clone()) + }; + let final_state_id = StateId { + app_hash: final_app_hash.clone(), + ..state_id.clone() + }; + //FixMe: This is not correct for the threshold votes extension (we need to sign and do // things differently @@ -534,13 +577,13 @@ impl FullAbciApplication<'_, C> { // We need to sign the block let quorum_type = self.platform.config.validator_set.quorum_type; - let state_id_hash = state_id + let state_id_hash = final_state_id .calculate_msg_hash(CHAIN_ID, height as i64, round as i32) .expect("cannot calculate state id hash"); let commit = CanonicalVote { block_id: block_id_hash.clone(), - state_id: state_id_hash, + state_id: state_id_hash.clone(), chain_id: CHAIN_ID.to_string(), height: height as i64, round: round as i64, @@ -554,6 +597,12 @@ impl FullAbciApplication<'_, C> { quorum_hash: quorum_hash.clone(), block_signature: Default::default(), threshold_vote_extensions: extensions, + app_hash: if beacon.is_empty() { + Vec::new() + } else { + final_app_hash.clone() + }, + beacon, }; //if not in testing this will default to true #[cfg(not(feature = "testing-config"))] @@ -576,7 +625,7 @@ impl FullAbciApplication<'_, C> { tracing::trace!( digest=hex::encode(&digest), - ?state_id, + ?final_state_id, ?commit, ?quorum_type, ?quorum_hash, @@ -632,6 +681,12 @@ impl FullAbciApplication<'_, C> { core_chain_lock: None, }; + // The commit's block id, whose state id is the one the precommits signed + let block_id = BlockId { + state_id: state_id_hash, + ..block_id + }; + let request_finalize_block = RequestFinalizeBlock { commit: Some(commit_info.clone()), misbehavior: vec![], @@ -656,7 +711,7 @@ impl FullAbciApplication<'_, C> { .root_hash(Some(transaction), &platform_version.drive.grove_version) .unwrap() .unwrap(); - assert_eq!(app_hash, root_hash_before_finalization); + assert_eq!(final_app_hash, root_hash_before_finalization); drop(transaction_guard); if !options.dont_finalize_block @@ -676,7 +731,7 @@ impl FullAbciApplication<'_, C> { .root_hash(None, &platform_version.drive.grove_version) .unwrap() .unwrap(); - assert_eq!(app_hash, root_hash_after_finalization); + assert_eq!(final_app_hash, root_hash_after_finalization); } Ok(MimicExecuteBlockOutcome { @@ -686,10 +741,10 @@ impl FullAbciApplication<'_, C> { withdrawal_transactions, validator_set_update, next_validator_set_hash, - root_app_hash: app_hash + root_app_hash: final_app_hash .try_into() .expect("expected 32 bytes for the root hash"), - state_id, + state_id: final_state_id, block_id_hash: block_id_hash.try_into().expect("invalid block id hash len"), signature: commit_info .block_signature @@ -698,3 +753,18 @@ impl FullAbciApplication<'_, C> { }) } } + +/// A stand-in for the random beacon Tenderdash recovers from a round's prevotes, the threshold +/// signature nobody can compute before the prevotes are cast: 96 bytes expanded from SHA-256 of +/// the block hash and the round, so a strategy test draws the same values on every run. +fn stand_in_beacon(block_hash: &[u8; 32], round: u32) -> Vec { + (0u8..3) + .flat_map(|part| { + let mut preimage = Vec::with_capacity(32 + 4 + 1); + preimage.extend_from_slice(block_hash); + preimage.extend_from_slice(&round.to_be_bytes()); + preimage.push(part); + hash_single(preimage) + }) + .collect() +} diff --git a/packages/rs-drive-abci/src/platform_types/cleaned_abci_messages/cleaned_commit_info/v0/mod.rs b/packages/rs-drive-abci/src/platform_types/cleaned_abci_messages/cleaned_commit_info/v0/mod.rs index 71092b34d4f..1c1500b4612 100644 --- a/packages/rs-drive-abci/src/platform_types/cleaned_abci_messages/cleaned_commit_info/v0/mod.rs +++ b/packages/rs-drive-abci/src/platform_types/cleaned_abci_messages/cleaned_commit_info/v0/mod.rs @@ -17,6 +17,14 @@ pub struct CleanedCommitInfo { pub block_signature: [u8; 96], /// The list of additional votes extensions, if any pub threshold_vote_extensions: Vec, + /// The random beacon of the committed block: the threshold signature the prevotes of the + /// commit round combine into. Sent from Tenderdash consensus version 2 (protocol version + /// 15); `None` before. + pub beacon: Option<[u8; 96]>, + /// The app hash the commit signs: the state after the block's random draws, which + /// `ApplyBlockRandomness` returned for the commit round. Sent from Tenderdash consensus + /// version 2; `None` before. + pub app_hash: Option<[u8; 32]>, } impl TryFrom for CleanedCommitInfo { @@ -28,6 +36,8 @@ impl TryFrom for CleanedCommitInfo { quorum_hash, block_signature, threshold_vote_extensions, + beacon, + app_hash, } = value; if round < 0 { return Err(Error::Abci(AbciError::BadRequest( @@ -47,11 +57,33 @@ impl TryFrom for CleanedCommitInfo { )) })?; + let beacon: Option<[u8; 96]> = if beacon.is_empty() { + None + } else { + Some(beacon.try_into().map_err(|_| { + Error::Abci(AbciError::BadRequestDataSize( + "commit info beacon is not 96 bytes long".to_string(), + )) + })?) + }; + + let app_hash: Option<[u8; 32]> = if app_hash.is_empty() { + None + } else { + Some(app_hash.try_into().map_err(|_| { + Error::Abci(AbciError::BadRequestDataSize( + "commit info app hash is not 32 bytes long".to_string(), + )) + })?) + }; + Ok(CleanedCommitInfo { round: round as u32, quorum_hash, block_signature, threshold_vote_extensions, + beacon, + app_hash, }) } } diff --git a/packages/rs-drive-abci/src/platform_types/commit/v0/mod.rs b/packages/rs-drive-abci/src/platform_types/commit/v0/mod.rs index 7d131cc6be3..f876e997f6d 100644 --- a/packages/rs-drive-abci/src/platform_types/commit/v0/mod.rs +++ b/packages/rs-drive-abci/src/platform_types/commit/v0/mod.rs @@ -45,6 +45,13 @@ impl CommitV0 { quorum_hash: ci.quorum_hash.to_vec(), threshold_block_signature: ci.block_signature.to_vec(), threshold_vote_extensions: ci.threshold_vote_extensions.to_vec(), + // Not signed: the commit's signature covers the block id, whose state id binds + // the app hash, and the beacon is a signature of its own + beacon: ci.beacon.map(|beacon| beacon.to_vec()).unwrap_or_default(), + app_hash: ci + .app_hash + .map(|app_hash| app_hash.to_vec()) + .unwrap_or_default(), }, } } @@ -68,6 +75,8 @@ impl CommitV0 { quorum_hash: ci.quorum_hash, threshold_block_signature: ci.block_signature, threshold_vote_extensions: ci.threshold_vote_extensions, + beacon: ci.beacon, + app_hash: ci.app_hash, }, } } @@ -159,6 +168,8 @@ mod test { quorum_hash: QUORUM_HASH, block_signature: [0u8; 96], threshold_vote_extensions: Vec::new(), + beacon: None, + app_hash: None, }; let app_hash = [1u8, 2, 3, 4].repeat(8); diff --git a/packages/rs-drive-abci/src/platform_types/platform_state/accessors.rs b/packages/rs-drive-abci/src/platform_types/platform_state/accessors.rs index 94fa7759f2a..5591ad07b26 100644 --- a/packages/rs-drive-abci/src/platform_types/platform_state/accessors.rs +++ b/packages/rs-drive-abci/src/platform_types/platform_state/accessors.rs @@ -59,6 +59,24 @@ pub trait PlatformStateV0Methods { /// Returns the upcoming protocol version for the next epoch. fn next_epoch_protocol_version(&self) -> ProtocolVersion; + /// The protocol version of the last committed block, on the state of the block that + /// switches protocol versions; `None` on every other state, a committed one included. + fn switched_from_protocol_version(&self) -> Option; + + /// The platform version whose Tenderdash consensus version + /// (`consensus.tenderdash_consensus_version`) the block of this state runs under. + /// Tenderdash applies the consensus parameters a block returns from the next height on, so + /// the block that switches protocol versions runs under the version it switches from, and + /// any other block under the current one. On a committed state, which check_tx reads, it is + /// the current one: the one the next block runs under, unless that block switches. + fn tenderdash_consensus_platform_version(&self) -> Result<&'static PlatformVersion, Error> { + PlatformVersion::get( + self.switched_from_protocol_version() + .unwrap_or_else(|| self.current_protocol_version_in_consensus()), + ) + .map_err(Error::from) + } + /// Returns the quorum hash of the current validator set. fn current_validator_set_quorum_hash(&self) -> QuorumHash; @@ -124,6 +142,9 @@ pub trait PlatformStateV0Methods { /// Sets the next epoch protocol version. fn set_next_epoch_protocol_version(&mut self, version: ProtocolVersion); + /// Records the protocol version the block of this state switches from, or clears it. + fn set_switched_from_protocol_version(&mut self, version: Option); + /// Sets the current validator set quorum hash. fn set_current_validator_set_quorum_hash(&mut self, hash: QuorumHash); @@ -375,6 +396,10 @@ impl PlatformStateV0Methods for PlatformState { self.next_epoch_protocol_version } + fn switched_from_protocol_version(&self) -> Option { + self.switched_from_protocol_version + } + /// Returns the quorum hash of the current validator set. fn current_validator_set_quorum_hash(&self) -> QuorumHash { self.current_validator_set_quorum_hash @@ -454,6 +479,11 @@ impl PlatformStateV0Methods for PlatformState { self.heavy_fields_dirty = true; } + fn set_switched_from_protocol_version(&mut self, version: Option) { + // Not part of any saved record, so nothing has to be written again + self.switched_from_protocol_version = version; + } + /// Sets the current validator set quorum hash. fn set_current_validator_set_quorum_hash(&mut self, hash: QuorumHash) { self.current_validator_set_quorum_hash = hash; @@ -889,6 +919,45 @@ mod tests { assert!(!leaves_dirty(|s| { s.next_validator_set_quorum_hash_mut(); })); + // Never saved at all + assert!(!leaves_dirty( + |s| s.set_switched_from_protocol_version(Some(14)) + )); + } + + /// The block that switches protocol versions runs under the Tenderdash consensus version + /// of the one it switches from; any other block, and the next block of a committed state, + /// under the current one. + #[test] + fn should_run_the_switching_block_under_the_consensus_version_it_switches_from() { + let mut state = clean_state(); + assert_eq!(state.switched_from_protocol_version(), None); + assert_eq!( + state + .tenderdash_consensus_platform_version() + .expect("a known version") + .protocol_version, + PlatformVersion::latest().protocol_version + ); + + state.set_switched_from_protocol_version(Some(14)); + let consensus_platform_version = state + .tenderdash_consensus_platform_version() + .expect("a known version"); + assert_eq!(consensus_platform_version.protocol_version, 14); + assert_eq!( + consensus_platform_version + .consensus + .tenderdash_consensus_version, + 1 + ); + assert_eq!( + state + .current_platform_version() + .expect("a known version") + .protocol_version, + PlatformVersion::latest().protocol_version + ); } fn masternode(pro_tx_hash: ProTxHash, node_type: MasternodeType) -> MasternodeListItem { diff --git a/packages/rs-drive-abci/src/platform_types/platform_state/mod.rs b/packages/rs-drive-abci/src/platform_types/platform_state/mod.rs index ec35566ab0c..e9b8e07ab69 100644 --- a/packages/rs-drive-abci/src/platform_types/platform_state/mod.rs +++ b/packages/rs-drive-abci/src/platform_types/platform_state/mod.rs @@ -91,6 +91,16 @@ pub struct PlatformState { /// Which validator set entries the next store writes or deletes. Not part /// of the saved record. pub validator_set_changes: EntryChanges, + + /// The protocol version of the last committed block, on the state of the block that + /// switches protocol versions (the first block of an epoch whose version changes, set by + /// `run_block_proposal`), and `None` on every other state. Tenderdash applies the + /// consensus parameters a block returns from the next height on, so that block still runs + /// under the Tenderdash consensus version of the version it switches from + /// ([`PlatformStateV0Methods::tenderdash_consensus_platform_version`]). Not part of the + /// saved record, and cleared when the block's state is committed, so a committed state, + /// which check_tx reads, never holds one. + pub switched_from_protocol_version: Option, } fn hex_encoded_validator_sets(validator_sets: &IndexMap) -> String { @@ -143,6 +153,10 @@ impl Debug for PlatformState { .field("heavy_fields_dirty", &self.heavy_fields_dirty) .field("masternode_changes", &self.masternode_changes) .field("validator_set_changes", &self.validator_set_changes) + .field( + "switched_from_protocol_version", + &self.switched_from_protocol_version, + ) .finish() } } @@ -214,6 +228,7 @@ impl PlatformState { heavy_fields_dirty: true, masternode_changes: EntryChanges::all(), validator_set_changes: EntryChanges::all(), + switched_from_protocol_version: None, }; Ok(state) diff --git a/packages/rs-drive-abci/src/platform_types/platform_state/platform_state_for_saving/v0/mod.rs b/packages/rs-drive-abci/src/platform_types/platform_state/platform_state_for_saving/v0/mod.rs index 6db1974da24..98a1938c4ab 100644 --- a/packages/rs-drive-abci/src/platform_types/platform_state/platform_state_for_saving/v0/mod.rs +++ b/packages/rs-drive-abci/src/platform_types/platform_state/platform_state_for_saving/v0/mod.rs @@ -93,6 +93,8 @@ impl From for PlatformState { // this record carried the collections itself, so no entries back it on disk masternode_changes: EntryChanges::all(), validator_set_changes: EntryChanges::all(), + // Never saved: a state read back is a committed one + switched_from_protocol_version: None, } } } diff --git a/packages/rs-drive-abci/src/platform_types/platform_state/platform_state_for_saving/v1/mod.rs b/packages/rs-drive-abci/src/platform_types/platform_state/platform_state_for_saving/v1/mod.rs index b91987dbe1e..48686986d2b 100644 --- a/packages/rs-drive-abci/src/platform_types/platform_state/platform_state_for_saving/v1/mod.rs +++ b/packages/rs-drive-abci/src/platform_types/platform_state/platform_state_for_saving/v1/mod.rs @@ -166,6 +166,8 @@ impl From for PlatformState { // this record carried the collections itself, so no entries back it on disk masternode_changes: EntryChanges::all(), validator_set_changes: EntryChanges::all(), + // Never saved: a state read back is a committed one + switched_from_protocol_version: None, } } } diff --git a/packages/rs-drive-abci/src/platform_types/platform_state/platform_state_for_saving/v2/mod.rs b/packages/rs-drive-abci/src/platform_types/platform_state/platform_state_for_saving/v2/mod.rs index 4534c9ddebe..1b7aedfa11c 100644 --- a/packages/rs-drive-abci/src/platform_types/platform_state/platform_state_for_saving/v2/mod.rs +++ b/packages/rs-drive-abci/src/platform_types/platform_state/platform_state_for_saving/v2/mod.rs @@ -247,6 +247,8 @@ impl PlatformStateForSavingV2 { heavy_fields_dirty: true, masternode_changes: EntryChanges::default(), validator_set_changes: EntryChanges::default(), + // Never saved: a state read back is a committed one + switched_from_protocol_version: None, }) } } 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 fe3e1283981..1ea3a326a16 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 @@ -5,6 +5,7 @@ use dpp::fee::Credits; use std::collections::{BTreeMap, BTreeSet}; use crate::error::Error; +use crate::execution::types::block_random_requests::BlockRandomRequests; use crate::platform_types::event_execution_result::EstimatedFeeResult; use dpp::fee::fee_result::FeeResult; @@ -67,6 +68,9 @@ pub struct StateTransitionsProcessingResult { /// 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]>, + /// What the block's applied transitions ask its random beacon for (protocol version 15), + /// in the order they were executed, handed to the block execution context. + pub(crate) random_requests: BlockRandomRequests, invalid_paid_count: usize, invalid_unpaid_count: usize, valid_count: usize, @@ -127,6 +131,23 @@ impl StateTransitionsProcessingResult { &self.token_shielded_pools_touched } + /// Records what an applied state transition asks the block's random beacon for, after + /// the requests of the transitions applied before it. + pub fn add_random_requests(&mut self, random_requests: BlockRandomRequests) { + self.random_requests.append(random_requests); + } + + /// What the block's applied state transitions ask its random beacon for. + pub fn random_requests(&self) -> &BlockRandomRequests { + &self.random_requests + } + + /// Takes what the block's applied state transitions ask its random beacon for, leaving + /// none. + pub fn take_random_requests(&mut self) -> BlockRandomRequests { + std::mem::take(&mut self.random_requests) + } + /// 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/replay/log_ingest.rs b/packages/rs-drive-abci/src/replay/log_ingest.rs index 0ff88d5fa9c..db6771af9bc 100644 --- a/packages/rs-drive-abci/src/replay/log_ingest.rs +++ b/packages/rs-drive-abci/src/replay/log_ingest.rs @@ -211,6 +211,9 @@ fn parse_request_variant( "VerifyVoteExtension" => { deserialize_to_loaded(inner, LoadedRequest::VerifyVoteExtension).map(Some) } + "ApplyBlockRandomness" => { + deserialize_to_loaded(inner, LoadedRequest::ApplyBlockRandomness).map(Some) + } "Flush" => Ok(None), other => Err(format!("unsupported request variant {}", other).into()), } @@ -945,14 +948,14 @@ impl<'de> Deserializer<'de> for ParsedValue { fn deserialize_struct( self, - _name: &'static str, - _fields: &'static [&'static str], + name: &'static str, + fields: &'static [&'static str], visitor: V, ) -> Result where V: serde::de::Visitor<'de>, { - self.deserialize_map(visitor) + with_fields_older_logs_omit(name, fields, self).deserialize_map(visitor) } forward_to_deserialize_any! { @@ -961,6 +964,40 @@ impl<'de> Deserializer<'de> for ParsedValue { } } +/// The byte fields Tenderdash added to a struct after earlier requests were logged: the random +/// beacon and the app hash after the block's random draws, which `CommitInfo` and `Commit` +/// carry from consensus version 2. A request logged before prints neither, and both were empty +/// then. +const FIELDS_OLDER_LOGS_OMIT: &[(&str, &[&str])] = &[ + ("CommitInfo", &["beacon", "app_hash"]), + ("Commit", &["beacon", "app_hash"]), +]; + +/// `value`, a logged `name` struct, with each byte field older logs omit and it lacks set +/// empty. +fn with_fields_older_logs_omit(name: &str, fields: &[&str], value: ParsedValue) -> ParsedValue { + let Some((_, omitted)) = FIELDS_OLDER_LOGS_OMIT + .iter() + .find(|(struct_name, _)| *struct_name == name) + else { + return value; + }; + match value { + ParsedValue::Object(mut map) => { + for field in omitted.iter().filter(|field| fields.contains(*field)) { + map.entry(field.to_string()) + .or_insert_with(|| ParsedValue::Array(Vec::new())); + } + ParsedValue::Object(map) + } + ParsedValue::Tagged { tag, value } => ParsedValue::Tagged { + tag, + value: Box::new(with_fields_older_logs_omit(name, fields, *value)), + }, + other => other, + } +} + fn format_timestamp(value: ParsedValue) -> Result { let (seconds, nanos) = parse_seconds_nanos(value, "Timestamp")?; if !(-999_999_999..=999_999_999).contains(&nanos) { @@ -1104,6 +1141,47 @@ mod tests { assert!(matches!(request_two, LoadedRequest::Finalize(_))); } + /// Requests logged before consensus version 2 print `CommitInfo` and `Commit` without the + /// random beacon and the app hash after the draws; they parse as empty. + #[test] + fn should_read_the_commit_beacon_and_app_hash_older_logs_omit_as_empty() { + let LoadedRequest::Finalize(request) = parse_from_log(TESTNET_FINALIZE_BLOCK_TWO) else { + panic!("expected a finalize block request"); + }; + let commit = request.commit.expect("expected the commit info"); + assert!(commit.beacon.is_empty() && commit.app_hash.is_empty()); + let last_commit = request + .block + .and_then(|block| block.last_commit) + .expect("expected the last commit"); + assert!(last_commit.beacon.is_empty() && last_commit.app_hash.is_empty()); + } + + /// From consensus version 2 the commit carries the beacon and the app hash, and Tenderdash + /// sends `ApplyBlockRandomness`. + #[test] + fn should_parse_the_randomness_of_consensus_version_2() { + let request = parse_from_log( + r#"Request { value: Some(ApplyBlockRandomness(RequestApplyBlockRandomness { hash: [1, 2], height: 7, round: 1, beacon: [3, 4] })) }"#, + ); + let LoadedRequest::ApplyBlockRandomness(request) = request else { + panic!("expected an apply block randomness request, got {request:?}"); + }; + assert_eq!( + (request.hash, request.height, request.round, request.beacon), + (vec![1, 2], 7, 1, vec![3, 4]) + ); + + let request = parse_from_log( + r#"Request { value: Some(FinalizeBlock(RequestFinalizeBlock { commit: Some(CommitInfo { round: 1, quorum_hash: [], block_signature: [], threshold_vote_extensions: [], beacon: [5], app_hash: [6] }), misbehavior: [], hash: [], height: 7, round: 1, block: None, block_id: None })) }"#, + ); + let LoadedRequest::Finalize(request) = request else { + panic!("expected a finalize block request, got {request:?}"); + }; + let commit = request.commit.expect("expected the commit info"); + assert_eq!((commit.beacon, commit.app_hash), (vec![5], vec![6])); + } + const TESTNET_INFO_ONE: &str = r#"Request { value: Some(Info(RequestInfo { version: "1.5.1", block_version: 14, p2p_version: 10, abci_version: "1.3.0" })) }"#; const TESTNET_INIT_CHAIN: &str = r#"Request { value: Some(InitChain(RequestInitChain { time: Some(Timestamp { seconds: 1764074708, nanos: 919000000 }), chain_id: "dash-testnet-51", consensus_params: Some(ConsensusParams { block: Some(BlockParams { max_bytes: 2097152, max_gas: 57631392000 }), evidence: Some(EvidenceParams { max_age_num_blocks: 100000, max_age_duration: Some(Duration { seconds: 172800, nanos: 0 }), max_bytes: 512000 }), validator: Some(ValidatorParams { pub_key_types: ["bls12381"], voting_power_threshold: None }), version: Some(VersionParams { app_version: 1, consensus_version: ConsensusVersion0 }), synchrony: Some(SynchronyParams { message_delay: Some(Duration { seconds: 32, nanos: 0 }), precision: Some(Duration { seconds: 0, nanos: 500000000 }) }), timeout: Some(TimeoutParams { propose: Some(Duration { seconds: 30, nanos: 0 }), propose_delta: Some(Duration { seconds: 1, nanos: 0 }), vote: Some(Duration { seconds: 2, nanos: 0 }), vote_delta: Some(Duration { seconds: 0, nanos: 500000000 }) }), abci: Some(AbciParams { recheck_tx: true }) }), validator_set: None, app_state_bytes: [], initial_height: 1, initial_core_height: 0 })) }"#; diff --git a/packages/rs-drive-abci/src/replay/runner.rs b/packages/rs-drive-abci/src/replay/runner.rs index 2e0328c488f..cdb7d6f3a8a 100644 --- a/packages/rs-drive-abci/src/replay/runner.rs +++ b/packages/rs-drive-abci/src/replay/runner.rs @@ -12,8 +12,8 @@ use std::fs; use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; use tenderdash_abci::proto::abci::{ - response_process_proposal, response_verify_vote_extension, RequestExtendVote, - RequestFinalizeBlock, RequestInfo, RequestInitChain, RequestPrepareProposal, + response_process_proposal, response_verify_vote_extension, RequestApplyBlockRandomness, + RequestExtendVote, RequestFinalizeBlock, RequestInfo, RequestInitChain, RequestPrepareProposal, RequestProcessProposal, RequestVerifyVoteExtension, }; use tenderdash_abci::Application; @@ -82,6 +82,7 @@ pub(super) enum LoadedRequest { Finalize(RequestFinalizeBlock), ExtendVote(RequestExtendVote), VerifyVoteExtension(RequestVerifyVoteExtension), + ApplyBlockRandomness(RequestApplyBlockRandomness), } impl LoadedRequest { @@ -98,6 +99,7 @@ impl LoadedRequest { LoadedRequest::Finalize(request) => to_height(request.height), LoadedRequest::ExtendVote(request) => to_height(request.height), LoadedRequest::VerifyVoteExtension(request) => to_height(request.height), + LoadedRequest::ApplyBlockRandomness(request) => to_height(request.height), } } } @@ -363,6 +365,26 @@ where status ); } + LoadedRequest::ApplyBlockRandomness(request) => { + let context = format_height_round(Some(request.height), Some(request.round)); + tracing::debug!( + "executing apply_block_randomness from {} (height={}, round={})", + origin, + request.height, + request.round + ); + let response = app.apply_block_randomness(request).map_err(|err| { + logged_error(format!( + "apply_block_randomness failed for {}{}: {:?}", + origin, context, err + )) + })?; + tracing::debug!( + "apply_block_randomness result ({}): app_hash=0x{}", + origin, + hex::encode(&response.app_hash) + ); + } } Ok(committed_height) @@ -527,12 +549,22 @@ pub(super) fn stop_height_reached(limit: Option, known_height: Option) matches!((limit, known_height), (Some(limit), Some(height)) if height >= limit) } +/// The app hash the block commits: from consensus version 2 the commit's, the state after the +/// block's random draws, which the header's (the state before them) is not; before, the +/// header's. fn extract_expected_app_hash(request: &RequestFinalizeBlock) -> Option> { request - .block + .commit .as_ref() - .and_then(|block| block.header.as_ref()) - .map(|header| header.app_hash.clone()) + .map(|commit| commit.app_hash.clone()) + .filter(|app_hash| !app_hash.is_empty()) + .or_else(|| { + request + .block + .as_ref() + .and_then(|block| block.header.as_ref()) + .map(|header| header.app_hash.clone()) + }) } fn format_height_round(height: Option, round: Option) -> String { @@ -555,7 +587,7 @@ fn format_height_round(height: Option, round: Option) -> String { mod tests { use super::*; use tenderdash_abci::proto::abci::{ - RequestExtendVote, RequestFinalizeBlock, RequestInfo, RequestInitChain, + CommitInfo, RequestExtendVote, RequestFinalizeBlock, RequestInfo, RequestInitChain, RequestPrepareProposal, RequestProcessProposal, RequestVerifyVoteExtension, }; use tenderdash_abci::proto::types::{Block, Header}; @@ -636,6 +668,33 @@ mod tests { assert_eq!(hash, Some(vec![1, 2, 3, 4])); } + /// From consensus version 2 the block commits the state after its random draws, which the + /// commit carries and the header does not. + #[test] + fn should_expect_the_commit_app_hash_from_consensus_version_2() { + let request = RequestFinalizeBlock { + commit: Some(CommitInfo { + app_hash: vec![5, 6, 7, 8], + ..Default::default() + }), + block: Some(Block { + header: Some(Header { + app_hash: vec![1, 2, 3, 4], + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }; + assert_eq!(extract_expected_app_hash(&request), Some(vec![5, 6, 7, 8])); + + let request = RequestFinalizeBlock { + commit: Some(CommitInfo::default()), + ..request + }; + assert_eq!(extract_expected_app_hash(&request), Some(vec![1, 2, 3, 4])); + } + #[test] fn extract_expected_app_hash_no_block() { let request = RequestFinalizeBlock { diff --git a/packages/rs-drive-abci/src/test/helpers/fast_forward_to_block.rs b/packages/rs-drive-abci/src/test/helpers/fast_forward_to_block.rs index e991b965860..e7d8eb288b5 100644 --- a/packages/rs-drive-abci/src/test/helpers/fast_forward_to_block.rs +++ b/packages/rs-drive-abci/src/test/helpers/fast_forward_to_block.rs @@ -109,6 +109,8 @@ pub(crate) fn process_epoch_change( block_address_balance_changes: Default::default(), block_platform_state: platform_state, proposer_results: None, + random_requests: Default::default(), + applied_randomness: None, } .into(); diff --git a/packages/rs-drive-abci/tests/strategy_tests/test_cases/credit_pool_backfill_tests.rs b/packages/rs-drive-abci/tests/strategy_tests/test_cases/credit_pool_backfill_tests.rs index 320f1be86fc..ad7fa1b06b1 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/test_cases/credit_pool_backfill_tests.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/test_cases/credit_pool_backfill_tests.rs @@ -104,6 +104,9 @@ mod tests { quorum_hash: proposal.quorum_hash.clone(), block_signature: vec![0u8; 96], threshold_vote_extensions: vec![], + // No random beacon: nothing is drawn, and the block commits its header's state + beacon: vec![], + app_hash: vec![], }), misbehavior: vec![], hash: proposal.hash.clone(), diff --git a/packages/rs-drive-abci/tests/strategy_tests/test_cases/mod.rs b/packages/rs-drive-abci/tests/strategy_tests/test_cases/mod.rs index 4190abd35c4..b5320e86928 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/test_cases/mod.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/test_cases/mod.rs @@ -10,6 +10,7 @@ mod identity_and_document_tests; mod identity_create_proposal_tests; mod identity_transfer_tests; mod process_proposal_collision_tests; +mod random_beacon_tests; mod required_since_update_tests; // TODO: re-enable once OperationType shielded variants are implemented // mod shielded_tests; diff --git a/packages/rs-drive-abci/tests/strategy_tests/test_cases/random_beacon_tests.rs b/packages/rs-drive-abci/tests/strategy_tests/test_cases/random_beacon_tests.rs new file mode 100644 index 00000000000..be15a7ed1f7 --- /dev/null +++ b/packages/rs-drive-abci/tests/strategy_tests/test_cases/random_beacon_tests.rs @@ -0,0 +1,213 @@ +//! Same-block randomness across many blocks (protocol version 15): documents of a type with a +//! `sys.random.uniform` property are created block after block through the mimic, which hands +//! each block a stand-in random beacon through `ApplyBlockRandomness` and signs the commit +//! over the state after the draws. Every transition's proof is verified against that state, the +//! drawn values lie in their range, and the draws replaced the placeholder. + +#[cfg(test)] +mod tests { + use crate::execution::run_chain_for_strategy; + use crate::strategy::NetworkStrategy; + use dash_platform_macros::stack_size; + use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::data_contract::document_type::random_document::{ + DocumentFieldFillSize, DocumentFieldFillType, + }; + use dpp::data_contract::DataContractFactory; + use dpp::document::DocumentV0Getters; + use dpp::identifier::Identifier; + use dpp::platform_value::platform_value; + use dpp::version::PlatformVersion; + use drive::drive::document::query::QueryDocumentsOutcomeV0Methods; + use drive::query::DriveDocumentQuery; + use drive_abci::config::{ + ChainLockConfig, ExecutionConfig, InstantLockConfig, PlatformConfig, PlatformTestConfig, + ValidatorSetConfig, + }; + use drive_abci::test::helpers::setup::TestPlatformBuilder; + use strategy_tests::frequency::Frequency; + use strategy_tests::operations::{DocumentAction, DocumentOp, Operation, OperationType}; + use strategy_tests::{IdentityInsertInfo, StartAddresses, StartIdentities, Strategy}; + + #[stack_size(4 * 1024 * 1024)] + #[test] + async fn should_draw_every_random_property_from_its_block_beacon() { + let platform_version = PlatformVersion::latest(); + + // A `card` whose `rarity` is drawn from 1 to 100 and indexed. The strategy's random + // documents leave it out, as a client does: the platform refuses a sent value. + let created_contract = DataContractFactory::new(platform_version.protocol_version) + .expect("expected a contract factory") + .create_with_value_config( + Identifier::default(), + 1, + platform_value!({ + "card": { + "type": "object", + "properties": { + "name": { "type": "string", "maxLength": 32, "position": 0 }, + "rarity": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "generatedFrom": { "function": "sys.random.uniform" }, + "position": 1 + } + }, + "indices": [{ "name": "byRarity", "properties": [{ "rarity": "asc" }] }], + "required": ["name", "rarity"], + "additionalProperties": false + } + }), + None, + None, + ) + .expect("expected the card contract"); + + let contract = created_contract.data_contract(); + + let document_op = DocumentOp { + contract: contract.clone(), + action: DocumentAction::DocumentActionInsertRandom( + DocumentFieldFillType::FillIfNotRequired, + DocumentFieldFillSize::AnyDocumentFillSize, + ), + document_type: contract + .document_type_for_name("card") + .expect("expected the card document type") + .to_owned_document_type(), + }; + + let strategy = NetworkStrategy { + strategy: Strategy { + start_contracts: vec![(created_contract, None)], + operations: vec![Operation { + op_type: OperationType::Document(document_op), + frequency: Frequency { + times_per_block_range: 1..3, + chance_per_block: None, + }, + }], + start_identities: StartIdentities::default(), + start_addresses: StartAddresses::default(), + identity_inserts: IdentityInsertInfo { + frequency: Frequency { + times_per_block_range: 1..2, + chance_per_block: None, + }, + ..Default::default() + }, + identity_contract_nonce_gaps: None, + signer: None, + }, + total_hpmns: 100, + extra_normal_mns: 0, + validator_quorum_count: 24, + chain_lock_quorum_count: 24, + upgrading_info: None, + proposer_strategy: Default::default(), + rotate_quorums: false, + failure_testing: None, + query_testing: None, + // Each transition's proof is verified against the state the commit signs, after + // the block's draws + verify_state_transition_results: true, + ..Default::default() + }; + let config = PlatformConfig { + validator_set: ValidatorSetConfig::default_100_67(), + chain_lock: ChainLockConfig::default_100_67(), + instant_lock: InstantLockConfig::default_100_67(), + execution: ExecutionConfig { + verify_sum_trees: true, + ..Default::default() + }, + block_spacing_ms: 3000, + testing_configs: PlatformTestConfig::default_minimal_verifications(), + ..Default::default() + }; + let mut platform = TestPlatformBuilder::new() + .with_config(config.clone()) + .with_latest_protocol_version() + .build_with_mock_rpc(); + + let outcome = run_chain_for_strategy( + &mut platform, + 15, + strategy, + config, + 15, + &mut None, + &mut None, + ) + .await; + + for results in outcome.state_transition_results_per_block.values() { + for (state_transition, result) in results { + assert_eq!( + result.code, 0, + "state transition got code {} : {:?}", + result.code, state_transition + ); + } + } + + // The runner derives the contract id at deployment, so read it back from the outcome's + // strategy + let contract_id = outcome + .strategy + .strategy + .start_contracts + .first() + .expect("expected the start contract") + .0 + .data_contract() + .id(); + let fetched_contract = outcome + .abci_app + .platform + .drive + .fetch_contract(contract_id.to_buffer(), None, None, None, platform_version) + .unwrap() + .expect("expected to fetch the contract") + .expect("expected the contract to exist"); + + let query = DriveDocumentQuery::from_sql_expr( + "select * from card", + &fetched_contract.contract, + None, + platform_version, + ) + .expect("expected a card query"); + let cards = outcome + .abci_app + .platform + .drive + .query_documents(query, None, false, None, None) + .expect("expected to query the cards") + .documents() + .to_vec(); + assert!( + cards.len() >= 5, + "cards are created over many blocks, got {}", + cards.len() + ); + + let rarities: Vec = cards + .iter() + .map(|card| { + card.get("rarity") + .and_then(|rarity| rarity.to_integer::().ok()) + .expect("expected every card to hold a rarity") + }) + .collect(); + assert!( + rarities.iter().all(|rarity| (1..=100).contains(rarity)), + "every drawn rarity lies in its range: {rarities:?}" + ); + assert!( + rarities.iter().any(|rarity| *rarity != 1), + "the draws replaced the placeholder: {rarities:?}" + ); + } +} diff --git a/packages/rs-drive-abci/tests/strategy_tests/test_cases/vote_extension_round_tests.rs b/packages/rs-drive-abci/tests/strategy_tests/test_cases/vote_extension_round_tests.rs index a36e22923c2..cca84f70a3d 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/test_cases/vote_extension_round_tests.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/test_cases/vote_extension_round_tests.rs @@ -201,6 +201,9 @@ mod tests { quorum_hash: proposal.quorum_hash.clone(), block_signature: vec![0u8; 96], threshold_vote_extensions: vec![], + // No random beacon: nothing is drawn, and the block commits its header's state + beacon: vec![], + app_hash: vec![], }), misbehavior: vec![], hash: proposal.hash.clone(), 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 6fdb9ebb1af..7805bea53bf 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 @@ -2,7 +2,8 @@ use dpp::address_funds::PlatformAddress; use dpp::block::block_info::BlockInfo; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::config::v0::DataContractConfigGettersV0; -use dpp::data_contract::document_type::accessors::DocumentTypeV0Getters; +use dpp::data_contract::document_type::accessors::{DocumentTypeV0Getters, DocumentTypeV2Getters}; +use dpp::data_contract::document_type::{DocumentTypeRef, SystemFunction}; use dpp::document::{Document, DocumentV0Getters, DocumentV0Setters}; use dpp::fee::Credits; use dpp::identity::accessors::IdentityGettersV0; @@ -598,6 +599,11 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( expected_document .set_contract_version(document.contract_version()); } + adopt_drawn_random_properties( + document_type, + &mut expected_document, + &document, + ); assert_eq!(document, expected_document); } else { //there is the possibility that the state transition was not executed because it already existed, @@ -1396,3 +1402,35 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( true } + +/// A `sys.random` property is drawn from the block's random beacon once the block's transitions +/// ran (protocol version 15), so the create action this harness rebuilds holds the placeholder, +/// the range's minimum, where the proved document holds the drawn value. Checks that the drawn +/// value lies in the property's range and takes it into `expected`. +fn adopt_drawn_random_properties( + document_type: DocumentTypeRef, + expected: &mut Document, + proved: &Document, +) { + for (path, generated_from) in document_type.generated_from_fields() { + let (SystemFunction::Random(_), Some(range)) = + (generated_from.function, generated_from.range) + else { + continue; + }; + let drawn = proved + .get(path) + .cloned() + .unwrap_or_else(|| panic!("the proved document holds the drawn {path}")); + let value = drawn + .to_integer::() + .unwrap_or_else(|_| panic!("the drawn {path} is an integer")); + assert!( + (range.minimum()..=range.maximum()).contains(&value), + "the drawn {path} {value} lies in {}..={}", + range.minimum(), + range.maximum() + ); + expected.set(path, drawn); + } +} diff --git a/packages/rs-drive-proof-verifier/Cargo.toml b/packages/rs-drive-proof-verifier/Cargo.toml index 44382ce7fd6..dbd2b584eb8 100644 --- a/packages/rs-drive-proof-verifier/Cargo.toml +++ b/packages/rs-drive-proof-verifier/Cargo.toml @@ -34,7 +34,7 @@ dash-context-provider = { path = "../rs-context-provider", features = [ bincode = { workspace = true, features = ["serde"] } platform-serialization-derive = { path = "../rs-platform-serialization-derive", optional = true } platform-serialization = { path = "../rs-platform-serialization" } -tenderdash-abci = { git = "https://github.com/dashpay/rs-tenderdash-abci", tag = "v1.8.1", features = [ +tenderdash-abci = { git = "https://github.com/dashpay/rs-tenderdash-abci", rev = "e80c8a7361eda12918d938acd4ca077478419902", features = [ "crypto", ], default-features = false } tracing = { version = "0.1.41" } diff --git a/packages/rs-drive/src/drive/document/draw_random_document_properties/v0/mod.rs b/packages/rs-drive/src/drive/document/draw_random_document_properties/v0/mod.rs index 9de486069fc..bac8050caac 100644 --- a/packages/rs-drive/src/drive/document/draw_random_document_properties/v0/mod.rs +++ b/packages/rs-drive/src/drive/document/draw_random_document_properties/v0/mod.rs @@ -335,19 +335,6 @@ mod tests { const OWNER: [u8; 32] = [42; 32]; const BEACON: [u8; 96] = [7; 96]; - /// The latest platform version with `sys.random.uniform` admitted, which no protocol - /// version does until block execution draws the values. - fn drawing_platform_version() -> PlatformVersion { - let mut platform_version = PlatformVersion::latest().clone(); - platform_version - .dpp - .contract_versions - .document_type_versions - .schema - .random_generated_from = Some(0); - platform_version - } - /// A contract with an immutable `card` type, its `rarity` drawn from 1 to 100 and indexed, /// and a `plain` type drawing nothing, registered at `platform_version`. fn contract(platform_version: &PlatformVersion) -> DataContract { @@ -495,9 +482,9 @@ mod tests { #[test] fn should_draw_into_an_immutable_document_and_move_its_index_entry() { - let platform_version = drawing_platform_version(); - let (drive, contract) = setup(&platform_version); - let id = insert_card(&drive, &contract, 1, &platform_version); + let platform_version = PlatformVersion::latest(); + let (drive, contract) = setup(platform_version); + let id = insert_card(&drive, &contract, 1, platform_version); let request = request(contract.id(), "card", id); let expected = draw_v0(&BEACON, &request, "rarity", rarity_range()).expect("draws"); assert!((1..=100).contains(&expected)); @@ -508,12 +495,12 @@ mod tests { &[request], &BlockInfo::default(), None, - &platform_version, + platform_version, ) .expect("the draw runs"); assert_eq!(drawn, 1); - let found = cards_with_rarity(&drive, &contract, expected, &platform_version); + let found = cards_with_rarity(&drive, &contract, expected, platform_version); assert_eq!(found.len(), 1); assert_eq!(found[0].id(), id); assert_eq!( @@ -523,7 +510,7 @@ mod tests { Some(expected) ); if expected != 1 { - assert!(cards_with_rarity(&drive, &contract, 1, &platform_version).is_empty()); + assert!(cards_with_rarity(&drive, &contract, 1, platform_version).is_empty()); } } @@ -565,8 +552,8 @@ mod tests { #[test] fn should_skip_a_deleted_document_and_a_type_drawing_nothing() { - let platform_version = drawing_platform_version(); - let (drive, contract) = setup(&platform_version); + let platform_version = PlatformVersion::latest(); + let (drive, contract) = setup(platform_version); let drawn = drive .draw_random_document_properties( &BEACON, @@ -576,7 +563,7 @@ mod tests { ], &BlockInfo::default(), None, - &platform_version, + platform_version, ) .expect("the draw runs"); assert_eq!(drawn, 0); @@ -584,7 +571,7 @@ mod tests { #[test] fn should_not_draw_before_protocol_version_15() { - let (drive, _contract) = setup(&drawing_platform_version()); + let (drive, _contract) = setup(PlatformVersion::latest()); let result = drive.draw_random_document_properties( &BEACON, &[], @@ -600,8 +587,8 @@ mod tests { #[test] fn should_prepay_the_entries_of_the_indexes_holding_a_random_property_only() { - let platform_version = drawing_platform_version(); - let (drive, contract) = setup(&platform_version); + let platform_version = PlatformVersion::latest(); + let (drive, contract) = setup(platform_version); let prepay = |document: &Document, document_type_name: &str| { drive .prepay_random_document_index_entries( @@ -613,7 +600,7 @@ mod tests { .expect("the type"), &BlockInfo::default(), None, - &platform_version, + platform_version, ) .expect("the prepayment is priced") }; diff --git a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_create_transition_action/v0/transformer.rs b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_create_transition_action/v0/transformer.rs index 0240b499a81..bc30aef001e 100644 --- a/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_create_transition_action/v0/transformer.rs +++ b/packages/rs-drive/src/state_transition_action/batch/batched_transition/document_transition/document_create_transition_action/v0/transformer.rs @@ -271,19 +271,6 @@ mod tests { const OWNER: [u8; 32] = [42; 32]; const CONTRACT_ID: [u8; 32] = [9; 32]; - /// The latest platform version with `sys.random.uniform` admitted, which no protocol - /// version does until block execution draws the values. - fn drawing_platform_version() -> PlatformVersion { - let mut platform_version = PlatformVersion::latest().clone(); - platform_version - .dpp - .contract_versions - .document_type_versions - .schema - .random_generated_from = Some(0); - platform_version - } - /// A card whose `rarity` is drawn from 1 to 100, indexed by it when `indexed`. fn card_schema(indexed: bool) -> Value { let mut schema = platform_value!({ @@ -423,9 +410,9 @@ mod tests { mut data: BTreeMap, ) -> FeeResult { data.insert("name".to_string(), Value::Text("Ace".to_string())); - let platform_version = drawing_platform_version(); - let drive = setup_drive_with_initial_state_structure(Some(&platform_version)); - let contract = contract(&platform_version); + let platform_version = PlatformVersion::latest(); + let drive = setup_drive_with_initial_state_structure(Some(platform_version)); + let contract = contract(platform_version); let transition = DocumentCreateTransitionV0 { base: DocumentBaseTransition::V0(DocumentBaseTransitionV0 { id: Identifier::from([1; 32]), @@ -446,7 +433,7 @@ mod tests { &BlockInfo::default(), 0, |_| Ok(contract.clone()), - &platform_version, + platform_version, ) .expect("the action builds"); assert!( 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 8ad1605ec8e..9cac19f41f7 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 @@ -212,10 +212,11 @@ pub struct DocumentTypeSchemaVersions { pub apply_generated_from: OptionalFeatureVersion, /// Admits the `generatedFrom` function `sys.random.uniform` in `apply_generated_from` 1: /// an integer property the platform draws from the block's random beacon once the block's - /// transitions are accepted. `None` refuses the function at registration, which every - /// table does for now, protocol version 15's included: block execution does not draw the - /// values yet (`draw_random_document_properties` waits for the beacon Tenderdash - /// delivers), so a document would keep its placeholder for good. 0 parses it. + /// transitions are accepted (`draw_random_document_properties`, called by + /// `apply_random_beacon` with the beacon Tenderdash delivers from consensus version 2 on). + /// `None` refuses the function at registration, which every table before protocol version + /// 15's does: a block of theirs receives no beacon, so a document would keep its + /// placeholder for good. 0 parses it (protocol version 15). pub random_generated_from: OptionalFeatureVersion, /// `DocumentType::expand_property_type_shorthands`: rewrites the property /// type shorthands of a document type schema and of the contract's 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 e2b60e41711..e1d59e1e463 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 @@ -22,9 +22,10 @@ use crate::version::dpp_versions::dpp_contract_versions::{ /// private document type, which only the native schema check of registration admits; /// - `apply_generated_from`, `Some(1)` rather than `Some(0)`: a `generatedFrom` property may /// also be generated with `sys.tokens.decimals`, the decimals of the token an identifier -/// property names, on an integer property. Generation 1 parses `sys.random.uniform` too, but -/// `random_generated_from` stays `None`, as in v6, so registration refuses it until block -/// execution draws the values; +/// property names, on an integer property. Generation 1 parses `sys.random.uniform` too; +/// - `random_generated_from`, `Some(0)` rather than `None`: registration admits +/// `sys.random.uniform`, whose values block execution draws from the random beacon Tenderdash +/// delivers from consensus version 2 on (v15 notes 18 and 19); /// - `contract_storage_serialization_version`, 2 rather than 1: Drive stores a contract in /// format version 2, its document types compiled, while state transitions and clients keep /// format version 1. @@ -49,8 +50,9 @@ pub const CONTRACT_VERSIONS_V7: DPPContractVersions = DPPContractVersions { // changed: the `private` keyword declares a private document type parse_private_document: Some(0), apply_generated_from: Some(1), // changed: admits sys.tokens.decimals - // unchanged: sys.random.uniform stays refused until block execution draws the values - random_generated_from: None, + // changed: sys.random.uniform is admitted, block execution drawing its values from + // each block's random beacon + random_generated_from: Some(0), ..CONTRACT_VERSIONS_V6.document_type_versions.schema }, ..CONTRACT_VERSIONS_V6.document_type_versions 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 5d8814783d9..d5793b7f631 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 @@ -3,6 +3,7 @@ use versioned_feature_core::{FeatureVersionBounds, OptionalFeatureVersion}; pub mod v1; pub mod v2; pub mod v3; +pub mod v4; #[derive(Clone, Debug, Default)] pub struct DPPStateTransitionSerializationVersions { @@ -57,10 +58,11 @@ pub struct DPPStateTransitionSerializationVersions { /// A token transfer or a token mint moving a random amount: format version 1 of either, /// which carries `random_minimum_amount`. `StateTransition::active_version_range` makes a /// batch carrying one active from the first protocol version whose table has this `Some`. - /// `None` refuses the batch at decode, which every table does for now, protocol version - /// 15's included (it shares `STATE_TRANSITION_SERIALIZATION_VERSIONS_V3` with 14): block - /// execution does not settle the amounts yet (`settle_random_token_amounts` waits for the - /// beacon Tenderdash delivers), so a reservation would never be paid out. 0 decodes it. + /// `None` refuses the batch at decode, which every table before protocol version 15's + /// does: no binary of theirs decodes the format, and a block of theirs receives no random + /// beacon, so a reservation would never be paid out. 0 decodes it (protocol version 15, + /// `STATE_TRANSITION_SERIALIZATION_VERSIONS_V4`), where block execution settles the + /// amounts from the beacon Tenderdash delivers (`settle_random_token_amounts`). pub random_token_amount: OptionalFeatureVersion, } 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 880a9eff169..15ff0b52d0b 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 @@ -223,7 +223,7 @@ pub const STATE_TRANSITION_SERIALIZATION_VERSIONS_V3: DPPStateTransitionSerializ max_version: 0, default_current_version: 0, }, - // Protocol version 15 shares this table: random token amounts stay refused until block - // execution settles them + // No binary of protocol version 14 decodes a random token amount; protocol version 15 + // admits it in V4 random_token_amount: None, }; diff --git a/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v4.rs b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v4.rs new file mode 100644 index 00000000000..e20632e930a --- /dev/null +++ b/packages/rs-platform-version/src/version/dpp_versions/dpp_state_transition_serialization_versions/v4.rs @@ -0,0 +1,14 @@ +//! V4 (PV15): identical to V3 except `random_token_amount` is `Some(0)`: a batch carrying a +//! version 1 token transfer or mint, which can move a random amount, is active, as block +//! execution settles the amounts from the random beacon Tenderdash delivers from consensus +//! version 2 on (`settle_random_token_amounts`). + +use crate::version::dpp_versions::dpp_state_transition_serialization_versions::v3::STATE_TRANSITION_SERIALIZATION_VERSIONS_V3; +use crate::version::dpp_versions::dpp_state_transition_serialization_versions::DPPStateTransitionSerializationVersions; + +pub const STATE_TRANSITION_SERIALIZATION_VERSIONS_V4: DPPStateTransitionSerializationVersions = + DPPStateTransitionSerializationVersions { + // changed: version 1 token transfers and mints, which carry a random amount, decode + random_token_amount: Some(0), + ..STATE_TRANSITION_SERIALIZATION_VERSIONS_V3 + }; 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 8e3ad58748c..9e5410a734a 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 @@ -166,6 +166,12 @@ pub struct DriveAbciBlockEndMethodVersions { /// 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, + /// Fills a block's `sys.random` document properties and settles its random token amounts + /// from the block's random beacon, which Tenderdash hands over in `ApplyBlockRandomness` + /// once the block's prevotes combine into it. `None` before protocol version 15, whose + /// Tenderdash consensus version 2 is the first to send a beacon; a block then collects no + /// random requests and applies none. + pub apply_random_beacon: 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 af852d395eb..b74e299bd9c 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 @@ -137,6 +137,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V1: DriveAbciMethodVersions = DriveAbciMeth prune_shielded_pool_anchors: None, record_token_shielded_pool_anchors: None, expire_documents: None, + apply_random_beacon: 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 733e2490659..63207edb033 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 @@ -164,6 +164,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V10: DriveAbciMethodVersions = DriveAbciMet prune_shielded_pool_anchors: Some(0), record_token_shielded_pool_anchors: Some(0), expire_documents: Some(0), // new in v14: document ttl cleanup + apply_random_beacon: 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 index 4d5bb4133d1..3a89eadfedc 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,7 +1,8 @@ use super::v10::DRIVE_ABCI_METHOD_VERSIONS_V10; use super::{ - DkgMiningSchedule, DriveAbciCoreChainLockMethodVersionsAndConstants, - DriveAbciEngineMethodVersions, DriveAbciMethodVersions, DriveAbciVotingMethodVersions, + DkgMiningSchedule, DriveAbciBlockEndMethodVersions, + DriveAbciCoreChainLockMethodVersionsAndConstants, DriveAbciEngineMethodVersions, + DriveAbciMethodVersions, DriveAbciVotingMethodVersions, }; /// Dash Core's DKG schedule (`llmq/params.h`) of the non-rotating quorum types, the ones chain @@ -103,12 +104,21 @@ const DKG_MINING_SCHEDULES_V1: &[DkgMiningSchedule] = &[ /// `submitchainlock`, which skips that check for a height Core already has a chain lock at or /// above. `verify_chain_lock_locally` 1 leaves to Core chain locks that 0 rejected, so it is /// only safe together with this check. -/// - `engine.consensus_params_update` 2 installs the full evidence policy when it changes. +/// - `engine.consensus_params_update` 2 installs the full evidence policy when it changes, and +/// gives the protocol version's own Tenderdash consensus version in an emergency update. +/// +/// `block_end.apply_random_beacon` is `Some(0)`: a block collects the documents it creates and +/// its random token transfers and mints, and `ApplyBlockRandomness` (or `FinalizeBlock`, with +/// the commit's beacon) draws their values from the block's random beacon. pub const DRIVE_ABCI_METHOD_VERSIONS_V11: DriveAbciMethodVersions = DriveAbciMethodVersions { engine: DriveAbciEngineMethodVersions { consensus_params_update: 2, ..DRIVE_ABCI_METHOD_VERSIONS_V10.engine }, + block_end: DriveAbciBlockEndMethodVersions { + apply_random_beacon: Some(0), + ..DRIVE_ABCI_METHOD_VERSIONS_V10.block_end + }, core_chain_lock: DriveAbciCoreChainLockMethodVersionsAndConstants { verify_chain_lock_locally: 1, verify_chain_lock_through_core: 1, 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 02192421217..01e68a51c1f 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 @@ -138,6 +138,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V2: DriveAbciMethodVersions = DriveAbciMeth prune_shielded_pool_anchors: None, record_token_shielded_pool_anchors: None, expire_documents: None, + apply_random_beacon: 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 562ef11f09f..a8ab847dbd0 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 @@ -137,6 +137,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V3: DriveAbciMethodVersions = DriveAbciMeth prune_shielded_pool_anchors: None, record_token_shielded_pool_anchors: None, expire_documents: None, + apply_random_beacon: 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 a80bf22c093..3461e57bf2e 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 @@ -137,6 +137,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V4: DriveAbciMethodVersions = DriveAbciMeth prune_shielded_pool_anchors: None, record_token_shielded_pool_anchors: None, expire_documents: None, + apply_random_beacon: 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 df85fd7f139..60968c412de 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 @@ -141,6 +141,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V5: DriveAbciMethodVersions = DriveAbciMeth prune_shielded_pool_anchors: None, record_token_shielded_pool_anchors: None, expire_documents: None, + apply_random_beacon: 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 4be3341af89..4310eacd50e 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 @@ -139,6 +139,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V6: DriveAbciMethodVersions = DriveAbciMeth prune_shielded_pool_anchors: None, record_token_shielded_pool_anchors: None, expire_documents: None, + apply_random_beacon: 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 78ce2f621e0..cbe4e52b8b7 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 @@ -148,6 +148,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V7: DriveAbciMethodVersions = DriveAbciMeth prune_shielded_pool_anchors: None, record_token_shielded_pool_anchors: None, expire_documents: None, + apply_random_beacon: 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 f176a345790..87375249411 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 @@ -148,6 +148,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V8: DriveAbciMethodVersions = DriveAbciMeth prune_shielded_pool_anchors: Some(0), record_token_shielded_pool_anchors: None, expire_documents: None, + apply_random_beacon: 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 7a7b56b48bc..58e34096440 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 @@ -166,6 +166,7 @@ pub const DRIVE_ABCI_METHOD_VERSIONS_V9: DriveAbciMethodVersions = DriveAbciMeth prune_shielded_pool_anchors: Some(0), record_token_shielded_pool_anchors: None, expire_documents: None, + apply_random_beacon: None, }, platform_state_storage: DriveAbciPlatformStateStorageMethodVersions { fetch_platform_state: 0, 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 8c59c8e6036..f8d6173717e 100644 --- a/packages/rs-platform-version/src/version/mocks/v3_test.rs +++ b/packages/rs-platform-version/src/version/mocks/v3_test.rs @@ -172,6 +172,7 @@ pub const TEST_PLATFORM_V3: PlatformVersion = PlatformVersion { prune_shielded_pool_anchors: None, record_token_shielded_pool_anchors: None, expire_documents: None, + apply_random_beacon: None, }, platform_state_storage: DriveAbciPlatformStateStorageMethodVersions { fetch_platform_state: 0, diff --git a/packages/rs-platform-version/src/version/v15.rs b/packages/rs-platform-version/src/version/v15.rs index e891c3b362b..5180d5bea23 100644 --- a/packages/rs-platform-version/src/version/v15.rs +++ b/packages/rs-platform-version/src/version/v15.rs @@ -8,7 +8,7 @@ use crate::version::dpp_versions::dpp_identity_versions::v1::IDENTITY_VERSIONS_V 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::v2::STATE_TRANSITION_METHOD_VERSIONS_V2; -use crate::version::dpp_versions::dpp_state_transition_serialization_versions::v3::STATE_TRANSITION_SERIALIZATION_VERSIONS_V3; +use crate::version::dpp_versions::dpp_state_transition_serialization_versions::v4::STATE_TRANSITION_SERIALIZATION_VERSIONS_V4; use crate::version::dpp_versions::dpp_state_transition_versions::v4::STATE_TRANSITION_VERSIONS_V4; 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; @@ -393,7 +393,7 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// `creationRestrictionMode` 2 refuses a create, and the data trigger `bindings` 3 /// (`DRIVE_ABCI_VALIDATION_VERSIONS_V11`) refuse a replace or a delete. /// -/// 18. **Properties drawn at random, inactive**: `apply_generated_from` 1 parses the +/// 18. **Properties drawn at random**: `apply_generated_from` 1 parses the /// `generatedFrom` function `sys.random.uniform`, which takes no `params` (they may be left /// out, or be an empty list), on an integer property of at most 64 bits declaring both a /// `minimum` and a `maximum` inside its type and no `enum`, `const`, `exclusiveMinimum`, @@ -411,19 +411,46 @@ pub const PROTOCOL_VERSION_15: ProtocolVersion = 15; /// which the draw writes again unbilled (`prepay_random_document_index_entries` 0). The /// compiled form of a contract (format version 2, note 8) stores the range with the /// declaration, and reading one back refuses a range upside down or of more than 2^64 -/// values. The function stays inactive: `CONTRACT_VERSIONS_V7`'s `random_generated_from` -/// is `None`, as in every earlier table, so registration refuses it with -/// `InvalidContractStructure` until block execution draws the values from the beacon -/// Tenderdash delivers, which a later change wires and activates. The call sites this +/// values. The function is active: `CONTRACT_VERSIONS_V7`'s `random_generated_from` is +/// `Some(0)` (`None` in every earlier table, where registration refuses it with +/// `InvalidContractStructure`), since block execution draws the values from the beacon +/// Tenderdash delivers (note 19). The call sites this /// extended in place (the document create and replace transformers, the parser's read -/// checks, the reference validation's referenced side) are inert at every version, where -/// no type declares such a function. +/// checks, the reference validation's referenced side) are inert at every earlier version, +/// where no type declares such a function. +/// 19. **Same-block randomness**: `tenderdash_consensus_version` is 2, which consensus +/// parameter update 2 sends Tenderdash on the block that switches to this version, so +/// Tenderdash recovers a random beacon from each block's prevotes from the next height on, +/// hands it to Drive in `ApplyBlockRandomness` before the precommit (or before +/// `FinalizeBlock` with the commit's beacon), and has the precommits sign the app hash Drive +/// returns. `DRIVE_ABCI_METHOD_VERSIONS_V11` sets `block_end.apply_random_beacon` to +/// `Some(0)`: a block collects the documents it creates and its random token transfers and +/// mints, and the beacon fills their `sys.random` properties +/// (`draw_random_document_properties`) and settles their amounts +/// (`settle_random_token_amounts`). Every earlier table keeps it `None` and consensus +/// version 1, where nothing is collected and Tenderdash sends no beacon. Version 1 token +/// transfers and mints, which carry a random amount, are active from this version: +/// `STATE_TRANSITION_SERIALIZATION_VERSIONS_V4` differs from V3 only in +/// `random_token_amount` being `Some(0)`. +/// +/// The block that switches to this version still runs under consensus version 1, as +/// Tenderdash applies the consensus parameters a block returns from the next height on, so +/// it receives no beacon and nothing would draw what it asks for. The batch transformer +/// refuses, unpaid, what would ask that block for a random value: a random token transfer +/// or mint, and a create of a document whose type declares a `sys.random` property +/// (`RandomBeaconUnavailableError`, 10604). Both are accepted from the next block on. +/// `run_block_proposal` records the protocol version a block switches from on the block's +/// platform state (`switched_from_protocol_version`, never saved, and cleared when the +/// block is committed), whose `tenderdash_consensus_platform_version` the gate reads. +/// From consensus version 2 on, `FinalizeBlock` also refuses a commit whose app hash is +/// not the state after the block's draws, or whose block id carries a state id other than +/// the hash of the header's state id with that app hash. pub const PLATFORM_V15: PlatformVersion = PlatformVersion { protocol_version: PROTOCOL_VERSION_15, - drive: DRIVE_VERSION_V10, // changed: apply_contract_with_serialization 1 writes nothing for a contract whose format version 1 bytes match the stored one's (note 8); vote method versions v4: register_identity_vote 1 registers a yes/no vote, remove_all_votes_given_by_identities 1 also removes the yes/no votes of a masternode that left the list, every yes_no method and remove_vote_poll_end_date_query_operations Some(0) make yes/no polls active, add_vote_poll_end_date_query_operations 1 adds only the entry when the batch already queues the end date's tree with other storage flags, and verify methods v4 make the yes/no proof verifiers active (note 10) and verify_private_document_entries Some(0) a page of a private type's store (note 13); state transition method versions v5: document_create_transition 1 deletes a consumed document from what its lookup read (note 11); document method versions v5: insert_empty_private_document_store_operation Some(0) creates a private document type's store, and fetch_private_document_entries and prove_private_document_entries Some(0) read and prove a page of it (note 13), and draw_random_document_properties Some(0) draws the sys.random properties of a block's documents and prices what their create prepays, which nothing calls while the function is inactive (note 18); apply_drive_operations 2 resolves a batch's estimate, remove_reference_for_index_level_for_contract_operations 2, stateless_delete_of_non_tree_for_costs 1 and for_total_system_credits_update 1 describe their layers as they are (note 14); create_initial_state_structure 5 adds the token marketplace escrow trees; calculate_total_tokens_balance 2 counts escrowed tokens; the escrow primitives; the identity balance, token balance, prefunded specialized balance and contract fee pot estimations (for_balances 1, for_token_balances 1, estimated_cost_for_prefunded_specialized_balance_update 2, add_estimation_costs_for_contract_fee_pot_update 1) keep a deeper layer description (note 17) + drive: DRIVE_VERSION_V10, // changed: apply_contract_with_serialization 1 writes nothing for a contract whose format version 1 bytes match the stored one's (note 8); vote method versions v4: register_identity_vote 1 registers a yes/no vote, remove_all_votes_given_by_identities 1 also removes the yes/no votes of a masternode that left the list, every yes_no method and remove_vote_poll_end_date_query_operations Some(0) make yes/no polls active, add_vote_poll_end_date_query_operations 1 adds only the entry when the batch already queues the end date's tree with other storage flags, and verify methods v4 make the yes/no proof verifiers active (note 10) and verify_private_document_entries Some(0) a page of a private type's store (note 13); state transition method versions v5: document_create_transition 1 deletes a consumed document from what its lookup read (note 11); document method versions v5: insert_empty_private_document_store_operation Some(0) creates a private document type's store, and fetch_private_document_entries and prove_private_document_entries Some(0) read and prove a page of it (note 13), and draw_random_document_properties Some(0) draws the sys.random properties of a block's documents and prices what their create prepays (notes 18 and 19); apply_drive_operations 2 resolves a batch's estimate, remove_reference_for_index_level_for_contract_operations 2, stateless_delete_of_non_tree_for_costs 1 and for_total_system_credits_update 1 describe their layers as they are (note 14); create_initial_state_structure 5 adds the token marketplace escrow trees; calculate_total_tokens_balance 2 counts escrowed tokens; the escrow primitives; the identity balance, token balance, prefunded specialized balance and contract fee pot estimations (for_balances 1, for_token_balances 1, estimated_cost_for_prefunded_specialized_balance_update 2, add_estimation_costs_for_contract_fee_pot_update 1) keep a deeper layer description (note 17) drive_abci: DriveAbciVersion { structs: DRIVE_ABCI_STRUCTURE_VERSIONS_V2, - methods: DRIVE_ABCI_METHOD_VERSIONS_V11, // changed: the yes/no poll's tally, record, clean-up and finish hook are Some(0) (note 10); verify_chain_lock_locally 1 rejects a chain lock locally only when Platform holds the quorums active at its signing height, and verify_chain_lock_through_core 1 has Core's verifychainlock check the others before submitchainlock; consensus_params_update 2 installs the full evidence policy (note 16) + methods: DRIVE_ABCI_METHOD_VERSIONS_V11, // changed: the yes/no poll's tally, record, clean-up and finish hook are Some(0) (note 10); verify_chain_lock_locally 1 rejects a chain lock locally only when Platform holds the quorums active at its signing height, and verify_chain_lock_through_core 1 has Core's verifychainlock check the others before submitchainlock; consensus_params_update 2 installs the full evidence policy (note 16); block_end.apply_random_beacon Some(0) draws a block's random values from its beacon (note 19) validation_and_processing: DRIVE_ABCI_VALIDATION_VERSIONS_V11, // changed: identity update advanced_structure 1 refuses unpaid an identity update whose added key's proof of possession does not verify (note 2); create_domain_data_trigger 2 (note 3); contract update transform_into_action 1 holds an update to the compiled size cap in check_tx (note 8); batch advanced_structure 2 refuses a token config update on a read-only contract (note 9); masternode vote transform_into_action 1 looks up a masternode's previous answer on a yes/no poll (note 10); document_reference_validation 1 keeps what a consumed commitment's lookup read (note 11); document create structure_validation 2 holds a decision proposal to its fund, rule and end date (note 12); data trigger bindings 3 refuse a replace or a delete of a token market order (note 17) withdrawal_constants: DRIVE_ABCI_WITHDRAWAL_CONSTANTS_V3, query: DRIVE_ABCI_QUERY_VERSIONS_V2, @@ -432,11 +459,11 @@ pub const PLATFORM_V15: PlatformVersion = PlatformVersion { dpp: DPPVersion { costs: DPP_COSTS_VERSIONS_V1, validation: DPP_VALIDATION_VERSIONS_V5, - state_transition_serialization_versions: STATE_TRANSITION_SERIALIZATION_VERSIONS_V3, + state_transition_serialization_versions: STATE_TRANSITION_SERIALIZATION_VERSIONS_V4, // changed: random_token_amount 0 activates version 1 token transfers and mints, which carry a random amount (note 19) state_transition_conversion_versions: STATE_TRANSITION_CONVERSION_VERSIONS_V2, state_transition_method_versions: STATE_TRANSITION_METHOD_VERSIONS_V2, state_transitions: STATE_TRANSITION_VERSIONS_V4, - contract_versions: CONTRACT_VERSIONS_V7, // changed: parse_property_constraints 1 lets countOf and sumOf match byte array keys (note 4); validate_document 2 checks documents natively (note 6); check_document_type_schema 1 checks schemas natively at registration (note 7); contract_storage_serialization_version 2 stores contracts compiled (note 8); parse_private_document 0 parses private document types (note 13); apply_generated_from 1 admits sys.tokens.decimals (note 15) and parses sys.random.uniform, which random_generated_from None still refuses (note 18) + contract_versions: CONTRACT_VERSIONS_V7, // changed: parse_property_constraints 1 lets countOf and sumOf match byte array keys (note 4); validate_document 2 checks documents natively (note 6); check_document_type_schema 1 checks schemas natively at registration (note 7); contract_storage_serialization_version 2 stores contracts compiled (note 8); parse_private_document 0 parses private document types (note 13); apply_generated_from 1 admits sys.tokens.decimals (note 15) and parses sys.random.uniform, which random_generated_from 0 admits (note 18) document_versions: DOCUMENT_VERSIONS_V4, identity_versions: IDENTITY_VERSIONS_V1, voting_versions: VOTING_VERSION_V3, // changed: the yes/no poll's stored info, id, parameter rules and decision rules are active (note 10), and the decision proposal's poll (note 12) @@ -449,7 +476,7 @@ pub const PLATFORM_V15: PlatformVersion = PlatformVersion { fee_version: FEE_VERSION4, // changed: decision proposals prefund their poll with 0.5 Dash (note 12) system_limits: SYSTEM_LIMITS_V5, // changed: max_compiled_contract_size caps a contract stored compiled at 1 MiB (note 8); max_private_document_entry_size caps a private type's entries at 4096 bytes, and the private document store limits size its store (note 13) consensus: ConsensusVersions { - tenderdash_consensus_version: 1, + tenderdash_consensus_version: 2, // changed: Tenderdash recovers each block's random beacon and calls ApplyBlockRandomness (note 19) evidence: Some(ConsensusEvidenceParams { max_bytes: 0, max_age_num_blocks: 100_000, diff --git a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs index f260c65f182..c3bc4d401a3 100644 --- a/packages/wasm-dpp/src/errors/consensus/consensus_error.rs +++ b/packages/wasm-dpp/src/errors/consensus/consensus_error.rs @@ -180,7 +180,7 @@ use dpp::consensus::state::shielded::insufficient_shielded_fee_error::Insufficie 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::basic::state_transition::{StateTransitionNotActiveError, TransitionOverMaxInputsError, TransitionOverMaxOutputsError, InputWitnessCountMismatchError, TransitionNoInputsError, TransitionNoOutputsError, FeeStrategyEmptyError, FeeStrategyDuplicateError, FeeStrategyIndexOutOfBoundsError, FeeStrategyTooManyStepsError, InputBelowMinimumError, OutputBelowMinimumError, InputOutputBalanceMismatchError, OutputsNotGreaterThanInputsError, WithdrawalBalanceMismatchError, InsufficientFundingAmountError, InputsNotLessThanOutputsError, OutputAddressAlsoInputError, InvalidRemainderOutputCountError, WithdrawalBelowMinAmountError, ShieldedNoActionsError, ShieldedTooManyActionsError, ShieldedEmptyProofError, ShieldedZeroAnchorError, ShieldedInvalidValueBalanceError, ShieldedEncryptedNoteSizeMismatchError, ShieldedImplicitFeeCapExceededError, ShieldedInvalidDenominationError}; +use dpp::consensus::basic::state_transition::{RandomBeaconUnavailableError, StateTransitionNotActiveError, TransitionOverMaxInputsError, TransitionOverMaxOutputsError, InputWitnessCountMismatchError, TransitionNoInputsError, TransitionNoOutputsError, FeeStrategyEmptyError, FeeStrategyDuplicateError, FeeStrategyIndexOutOfBoundsError, FeeStrategyTooManyStepsError, InputBelowMinimumError, OutputBelowMinimumError, InputOutputBalanceMismatchError, OutputsNotGreaterThanInputsError, WithdrawalBalanceMismatchError, InsufficientFundingAmountError, InputsNotLessThanOutputsError, OutputAddressAlsoInputError, InvalidRemainderOutputCountError, WithdrawalBelowMinAmountError, ShieldedNoActionsError, ShieldedTooManyActionsError, ShieldedEmptyProofError, ShieldedZeroAnchorError, ShieldedInvalidValueBalanceError, ShieldedEncryptedNoteSizeMismatchError, ShieldedImplicitFeeCapExceededError, ShieldedInvalidDenominationError}; use dpp::consensus::state::document::referenced_contract_requirement_not_met_error::ReferencedContractRequirementNotMetError; use dpp::consensus::state::document::referenced_identity_key_requirement_not_met_error::ReferencedIdentityKeyRequirementNotMetError; use dpp::consensus::state::document::referenced_document_lookup_invalid_error::ReferencedDocumentLookupInvalidError; @@ -1454,6 +1454,9 @@ fn from_basic_error(basic_error: &BasicError) -> JsValue { BasicError::RandomTokenMintNotAllowedError(e) => { generic_consensus_error!(RandomTokenMintNotAllowedError, e).into() } + BasicError::RandomBeaconUnavailableError(e) => { + generic_consensus_error!(RandomBeaconUnavailableError, e).into() + } } } From 5d0f0ad718810206f872dd883a1092e7c6624304 Mon Sep 17 00:00:00 2001 From: QuantumExplorer Date: Wed, 4 Nov 2026 14:22:22 +0700 Subject: [PATCH 2/3] test(drive-abci): settle random token amounts through the block lifecycle - Replay stands in for Tenderdash, so it sends Info with this build's ABCI version: a log recorded under ABCI 1.4 replays again. - The ApplyBlockRandomness draw test pins the drawn rarity and checks the root moved, unconditionally. The retry test also settles a random transfer, whose removed reservation would fail a second settlement. - The finalize-only fallback test settles a random transfer from the commit's beacon: the recipient holds the drawn amount, once. - New strategy test: random transfers and mints over many blocks through prepare, process, ApplyBlockRandomness, commit and finalize, with proofs of the settled history and balances, supply and escrow checked against the drawn amounts. The strategy gains random_token_minimum_amount, and its proof check accepts a settled amount within the transition's range. Co-Authored-By: Claude Opus 5.5 --- .../abci/handler/apply_block_randomness.rs | 158 +++++++++- .../src/abci/handler/finalize_block.rs | 51 +++- packages/rs-drive-abci/src/replay/runner.rs | 50 ++- .../tests/strategy_tests/strategy.rs | 38 ++- .../test_cases/random_beacon_tests.rs | 288 +++++++++++++++++- .../verify_state_transitions.rs | 42 ++- 6 files changed, 585 insertions(+), 42 deletions(-) diff --git a/packages/rs-drive-abci/src/abci/handler/apply_block_randomness.rs b/packages/rs-drive-abci/src/abci/handler/apply_block_randomness.rs index e558db47963..a2d24f70609 100644 --- a/packages/rs-drive-abci/src/abci/handler/apply_block_randomness.rs +++ b/packages/rs-drive-abci/src/abci/handler/apply_block_randomness.rs @@ -203,6 +203,7 @@ pub(super) mod tests { use crate::platform_types::withdrawal::unsigned_withdrawal_txs::v0::UnsignedWithdrawalTxs; use crate::rpc::core::MockCoreRPCLike; use crate::test::helpers::setup::{TempPlatform, TestPlatformBuilder}; + use dpp::balances::credits::TokenAmount; use dpp::block::block_info::BlockInfo; use dpp::data_contract::accessors::v0::DataContractV0Getters; use dpp::data_contract::DataContractFactory; @@ -212,17 +213,28 @@ pub(super) mod tests { use dpp::version::PlatformVersion; use drive::drive::document::draw_random_document_properties::RandomDocumentRequest; use drive::drive::document::query::QueryDocumentsOutcomeV0Methods; + use drive::drive::tokens::settle_random_token_amounts::{ + RandomTokenRequest, RandomTokenRequestKind, + }; use drive::query::DriveDocumentQuery; + use drive::util::batch::drive_op_batch::TokenOperationType; + use drive::util::batch::DriveOperation; use drive::util::object_size_info::DocumentInfo::DocumentRefInfo; use drive::util::object_size_info::{DocumentAndContractInfo, OwnedDocumentInfo}; use drive::util::storage_flags::StorageFlags; use std::borrow::Cow; use std::collections::BTreeMap; - const OWNER: [u8; 32] = [42; 32]; + pub(in crate::abci::handler) const OWNER: [u8; 32] = [42; 32]; pub(in crate::abci::handler) const BEACON: [u8; 96] = [7; 96]; const BLOCK_HASH: [u8; 32] = [0xAA; 32]; const HEIGHT: u64 = 10; + /// The rarity the card draws from [`BEACON`], pinned so a skipped draw, which leaves the + /// placeholder 1, fails + const DRAWN_RARITY: i128 = 26; + const TOKEN: [u8; 32] = [5; 32]; + pub(in crate::abci::handler) const RECIPIENT: [u8; 32] = [8; 32]; + pub(in crate::abci::handler) const TOKEN_SUPPLY: TokenAmount = 10_000; /// A contract with an immutable `card` type, its `rarity` drawn from 1 to 100 and indexed. pub(in crate::abci::handler) fn card_contract() -> DataContract { @@ -440,6 +452,124 @@ pub(super) mod tests { ) } + /// Reserves, in `transaction`, a random transfer of 100 to 1,000 of `OWNER`'s tokens to + /// [`RECIPIENT`], as the execution of a random transfer does, and returns the request the + /// block records for it. + pub(in crate::abci::handler) fn reserve_random_transfer_in( + platform: &TempPlatform, + transaction: &Transaction, + ) -> RandomTokenRequest { + let platform_version = PlatformVersion::latest(); + let request = RandomTokenRequest { + token_id: Identifier::from(TOKEN), + owner_id: Identifier::from(OWNER), + identity_contract_nonce: 1, + minimum: 100, + maximum: 1_000, + kind: RandomTokenRequestKind::Transfer { + sender_id: Identifier::from(OWNER), + recipient_id: Identifier::from(RECIPIENT), + }, + }; + platform + .drive + .create_token_trees( + Identifier::from([4; 32]), + 0, + TOKEN, + false, + false, + &BlockInfo::default(), + true, + Some(transaction), + platform_version, + ) + .expect("expected to create the token trees"); + platform + .drive + .token_mint( + TOKEN, + OWNER, + TOKEN_SUPPLY, + false, + false, + &BlockInfo::default(), + true, + Some(transaction), + platform_version, + ) + .expect("expected to mint the supply"); + platform + .drive + .apply_drive_operations( + vec![DriveOperation::TokenOperation( + TokenOperationType::TokenReserveRandomTransfer { + token_id: request.token_id, + sender_id: request.owner_id, + recipient_id: Identifier::from(RECIPIENT), + identity_contract_nonce: request.identity_contract_nonce, + maximum: request.maximum, + }, + )], + true, + &BlockInfo::default(), + Some(transaction), + platform_version, + None, + ) + .expect("expected the reservation to apply"); + request + } + + /// [`reserve_random_transfer_in`] the block's transaction, recording the request in the + /// block's context. + fn reserve_random_transfer( + platform: &TempPlatform, + app: &FullAbciApplication, + ) -> RandomTokenRequest { + let request = { + let transaction_guard = app.transaction.read().unwrap(); + reserve_random_transfer_in( + platform, + transaction_guard.as_ref().expect("expected a transaction"), + ) + }; + let mut context_guard = app.block_execution_context.write().unwrap(); + let Some(BlockExecutionContext::V0(context)) = context_guard.as_mut() else { + panic!("expected the block's context"); + }; + context.random_requests.tokens.push(request.clone()); + request + } + + /// The token balance of `identity_id` and what the escrow holds under `request`'s + /// reservation, in the block's transaction. + fn token_state_in_block( + platform: &TempPlatform, + app: &FullAbciApplication, + identity_id: [u8; 32], + request: &RandomTokenRequest, + ) -> (Option, Option) { + let platform_version = PlatformVersion::latest(); + let transaction_guard = app.transaction.read().unwrap(); + let transaction = transaction_guard.as_ref().expect("expected a transaction"); + let balance = platform + .drive + .fetch_identity_token_balance(TOKEN, identity_id, Some(transaction), platform_version) + .expect("expected to fetch a token balance"); + let reserved = platform + .drive + .fetch_token_order_escrow( + TOKEN, + request.reservation_id(), + Some(transaction), + &mut vec![], + platform_version, + ) + .expect("expected to fetch the reservation"); + (balance, reserved) + } + /// A platform holding the card contract in committed state. fn setup() -> (TempPlatform, DataContract) { let platform = TestPlatformBuilder::new() @@ -494,11 +624,8 @@ pub(super) mod tests { let root_after = transaction_root(&app); assert_eq!(response.app_hash, root_after.to_vec()); - let drawn = rarity_in_block(&platform, &app, &contract); - assert!((1..=100).contains(&drawn)); - if drawn != 1 { - assert_ne!(root_before, root_after, "the drawn value moved the root"); - } + assert_eq!(rarity_in_block(&platform, &app, &contract), DRAWN_RARITY); + assert_ne!(root_before, root_after, "the drawn value moved the root"); assert_eq!( app.block_execution_context .read() @@ -519,11 +646,26 @@ pub(super) mod tests { let latest = PlatformVersion::latest().protocol_version; let (platform, contract) = setup(); let app = start_block(&platform, &contract, latest); + // A settlement removes its reservation, and a second one of the same request is + // refused, so a settlement run twice would fail the second answer + let transfer = reserve_random_transfer(&platform, &app); + assert_eq!( + token_state_in_block(&platform, &app, RECIPIENT, &transfer), + (Some(0), Some(transfer.maximum)) + ); let first = apply_block_randomness::<_, MockCoreRPCLike>(&app, request(0, BLOCK_HASH, &BEACON)) .expect("expected the randomness to apply"); let drawn = rarity_in_block(&platform, &app, &contract); + let (paid, reserved) = token_state_in_block(&platform, &app, RECIPIENT, &transfer); + let paid = paid.expect("expected the recipient to hold the drawn amount"); + assert!((transfer.minimum..=transfer.maximum).contains(&paid)); + assert_eq!(reserved, None, "the settlement removed the reservation"); + assert_eq!( + token_state_in_block(&platform, &app, OWNER, &transfer).0, + Some(TOKEN_SUPPLY - paid) + ); let second = apply_block_randomness::<_, MockCoreRPCLike>(&app, request(0, BLOCK_HASH, &BEACON)) @@ -531,6 +673,10 @@ pub(super) mod tests { assert_eq!(first, second); assert_eq!(second.app_hash, transaction_root(&app).to_vec()); assert_eq!(rarity_in_block(&platform, &app, &contract), drawn); + assert_eq!( + token_state_in_block(&platform, &app, RECIPIENT, &transfer), + (Some(paid), None) + ); // A round's prevotes combine into one beacon: another one for the same round is refused let result = diff --git a/packages/rs-drive-abci/src/abci/handler/finalize_block.rs b/packages/rs-drive-abci/src/abci/handler/finalize_block.rs index f6275f91af0..d21f756d614 100644 --- a/packages/rs-drive-abci/src/abci/handler/finalize_block.rs +++ b/packages/rs-drive-abci/src/abci/handler/finalize_block.rs @@ -375,7 +375,8 @@ mod tests { use super::*; use crate::abci::app::{FullAbciApplication, TransactionalApplication}; use crate::abci::handler::apply_block_randomness::tests::{ - apply_card_contract, insert_placeholder_card, stored_rarity, BEACON, + apply_card_contract, insert_placeholder_card, reserve_random_transfer_in, stored_rarity, + BEACON, OWNER, RECIPIENT, TOKEN_SUPPLY, }; use crate::config::{CheckpointStep, PlatformConfig}; use crate::execution::types::block_execution_context::v0::BlockExecutionContextV0; @@ -392,6 +393,7 @@ mod tests { use dpp::block::block_info::BlockInfo; use dpp::prelude::DataContract; use dpp::version::PlatformVersion; + use drive::drive::tokens::settle_random_token_amounts::RandomTokenRequest; use drive::grovedb::Transaction; use std::sync::{Arc, Mutex, RwLock}; use tenderdash_abci::proto::abci::CommitInfo; @@ -1314,6 +1316,8 @@ mod tests { root_before: [u8; 32], /// The root the block's draws give drawn_root: [u8; 32], + /// The random token transfer the block reserved and the commit's beacon settles + transfer: RandomTokenRequest, /// What `finalize_block` returned result: Result, } @@ -1346,7 +1350,8 @@ mod tests { .expect("expected to hash the state id") } - /// Finalizes a block that created one card holding the placeholder rarity, with a commit + /// Finalizes a block that created one card holding the placeholder rarity and reserved one + /// random token transfer, with a commit /// carrying the random beacon and the app hash `commit_app_hash` makes of the root the /// draws give, on a node that did not apply the beacon. The block id carries the state id /// of the app hash `block_id_app_hash` makes of that root, and the zero state id for `None`. @@ -1370,7 +1375,7 @@ mod tests { .unwrap() .expect("expected a root hash"); - let (drawn_root, result) = { + let (drawn_root, transfer, result) = { let app = FullAbciApplication::new(&platform.platform); app.start_transaction(); let platform_state = (**platform.state.load()).clone(); @@ -1378,12 +1383,14 @@ mod tests { let block_time_ms = 1_700_000_000_000u64; let mut context = block_execution_context(platform_state, 1, block_time_ms, None); - let drawn_root = { + let (drawn_root, transfer) = { let transaction_guard = app.transaction.read().unwrap(); let transaction = transaction_guard.as_ref().expect("expected a transaction"); let card = insert_placeholder_card(&platform, &contract, transaction); + let transfer = reserve_random_transfer_in(&platform, transaction); let BlockExecutionContext::V0(context_v0) = &mut context; context_v0.random_requests.documents.push(card); + context_v0.random_requests.tokens.push(transfer.clone()); // The root the commit signs, from draws this test rolls back, so the block // reaches `finalize_block` holding the placeholder as on a node that did not vote @@ -1399,7 +1406,7 @@ mod tests { transaction .rollback_to_savepoint() .expect("expected to roll the draws back"); - drawn_root + (drawn_root, transfer) }; app.block_execution_context @@ -1427,6 +1434,7 @@ mod tests { ( drawn_root, + transfer, finalize_block::<_, MockCoreRPCLike>(&app, request), ) }; @@ -1436,6 +1444,7 @@ mod tests { contract, root_before, drawn_root, + transfer, result, } } @@ -1443,10 +1452,12 @@ mod tests { #[test] fn should_apply_the_commit_beacon_this_node_did_not_apply() { let grove_version = &PlatformVersion::latest().drive.grove_version; + let platform_version = PlatformVersion::latest(); let FinalizedWithTheCommitBeacon { platform, contract, drawn_root, + transfer, result, .. } = finalize_with_the_commit_beacon(|drawn_root| drawn_root.to_vec(), Some); @@ -1467,6 +1478,36 @@ mod tests { let transaction = platform.drive.grove.start_transaction(); assert!((1..=100).contains(&stored_rarity(&platform, &contract, &transaction))); + // The commit's beacon settled the transfer once: the reservation is gone, the recipient + // holds the drawn amount and the sender the rest of the supply + let balance = |identity_id: [u8; 32]| { + platform + .drive + .fetch_identity_token_balance( + transfer.token_id.to_buffer(), + identity_id, + None, + platform_version, + ) + .expect("expected to fetch a token balance") + }; + let paid = balance(RECIPIENT).expect("expected the recipient to hold the drawn amount"); + assert!((transfer.minimum..=transfer.maximum).contains(&paid)); + assert_eq!(balance(OWNER), Some(TOKEN_SUPPLY - paid)); + assert_eq!( + platform + .drive + .fetch_token_order_escrow( + transfer.token_id.to_buffer(), + transfer.reservation_id(), + None, + &mut vec![], + platform_version, + ) + .expect("expected to fetch the reservation"), + None + ); + // The beacon alone is applied too let FinalizedWithTheCommitBeacon { platform, diff --git a/packages/rs-drive-abci/src/replay/runner.rs b/packages/rs-drive-abci/src/replay/runner.rs index cdb7d6f3a8a..a5359c3d0a9 100644 --- a/packages/rs-drive-abci/src/replay/runner.rs +++ b/packages/rs-drive-abci/src/replay/runner.rs @@ -154,8 +154,13 @@ where .as_ref() .map(|info| info.basic_info().height); } - LoadedRequest::Info(request) => { + LoadedRequest::Info(mut request) => { tracing::debug!("executing info from {}", origin); + // The recorded ABCI version is that of the Tenderdash that wrote the log. The replay + // stands in for Tenderdash here, so it speaks this build's version, and a log + // recorded under an older one still replays; `info` still checks the state it + // reports + request.abci_version = tenderdash_abci::proto::ABCI_VERSION.to_string(); let response = app .info(request) .map_err(|err| logged_error(format!("info failed for {}: {:?}", origin, err)))?; @@ -990,4 +995,47 @@ mod tests { let rate = reporter.rate_per_minute(Instant::now(), Duration::from_secs(60)); assert!(rate.is_none()); } + + // ---- execute_request ---- + + #[test] + fn should_replay_an_info_request_recorded_under_an_older_abci_version() { + use crate::abci::app::FullAbciApplication; + use crate::replay::log_ingest::LogRequestStream; + use crate::test::helpers::setup::TestPlatformBuilder; + use std::io::Write as _; + + let dir = tempfile::tempdir().expect("expected a temporary directory"); + let log_path = dir.path().join("abci.log"); + { + let mut file = std::fs::File::create(&log_path).expect("expected to create the log"); + writeln!( + file, + r#"{{"timestamp":"2026-01-01T00:00:00Z","fields":{{"message":"received ABCI request","request":"Request {{ value: Some(Info(RequestInfo {{ version: \"1.8.0\", block_version: 14, p2p_version: 10, abci_version: \"1.4.0\" }})) }}"}},"span":{{"endpoint":"info"}}}}"# + ) + .expect("expected to write the log"); + } + let mut stream = LogRequestStream::open(&log_path).expect("expected to open the log"); + let item = stream + .next_item() + .expect("expected to read the log") + .expect("expected the info request"); + let LoadedRequest::Info(recorded) = &item.request else { + panic!("expected an info request, got {}", item.describe()); + }; + assert_eq!(recorded.abci_version, "1.4.0"); + assert!( + !tenderdash_abci::check_version(&recorded.abci_version), + "expected this build to refuse an ABCI 1.4 handshake from a live Tenderdash" + ); + + let platform = TestPlatformBuilder::new() + .with_latest_protocol_version() + .build_with_mock_rpc(); + let app = FullAbciApplication::new(&platform.platform); + assert_eq!( + execute_request(&app, item, None).expect("expected the recorded info to replay"), + None + ); + } } diff --git a/packages/rs-drive-abci/tests/strategy_tests/strategy.rs b/packages/rs-drive-abci/tests/strategy_tests/strategy.rs index 5c930ba3ac0..41985601416 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/strategy.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/strategy.rs @@ -363,6 +363,10 @@ pub struct NetworkStrategy { /// are reproducible; tests exercising behaviour that keys on how old a /// block is relative to the wall clock (checkpoints) set it near now. pub start_time_ms: u64, + /// The least every token transfer and mint of the strategy moves when its amount is random + /// (protocol version 15): set, they move an amount drawn from this to the operation's + /// amount from the block's random beacon; `None` keeps their fixed amounts. + pub random_token_minimum_amount: Option, } impl Default for NetworkStrategy { @@ -388,6 +392,7 @@ impl Default for NetworkStrategy { sign_chain_locks: false, sign_instant_locks: false, start_time_ms: GENESIS_TIME_MS, + random_token_minimum_amount: None, } } } @@ -1836,20 +1841,23 @@ impl NetworkStrategy { .entry((operation_owner_id, contract.id())) .or_default(); *identity_contract_nonce += 1; - let token_mint_transition: TokenMintTransition = TokenMintTransitionV0 { - base: TokenBaseTransitionV0 { - identity_contract_nonce: *identity_contract_nonce, - token_contract_position: *token_pos, - data_contract_id: contract.id(), - token_id: *token_id, - using_group_info: None, + let mut token_mint_transition: TokenMintTransition = + TokenMintTransitionV0 { + base: TokenBaseTransitionV0 { + identity_contract_nonce: *identity_contract_nonce, + token_contract_position: *token_pos, + data_contract_id: contract.id(), + token_id: *token_id, + using_group_info: None, + } + .into(), + issued_to_identity_id: Some(*recipient), + amount: *amount, + public_note: note.clone(), } - .into(), - issued_to_identity_id: Some(*recipient), - amount: *amount, - public_note: note.clone(), - } - .into(); + .into(); + token_mint_transition + .set_random_minimum_amount(self.random_token_minimum_amount); let batch_transition: BatchTransition = BatchTransitionV1 { owner_id: identity.id, @@ -1919,7 +1927,7 @@ impl NetworkStrategy { .entry((operation_owner_id, contract.id())) .or_default(); *identity_contract_nonce += 1; - let token_transfer_transition: TokenTransferTransition = + let mut token_transfer_transition: TokenTransferTransition = TokenTransferTransitionV0 { base: TokenBaseTransitionV0 { identity_contract_nonce: *identity_contract_nonce, @@ -1936,6 +1944,8 @@ impl NetworkStrategy { private_encrypted_note: private_encrypted_note.clone(), } .into(); + token_transfer_transition + .set_random_minimum_amount(self.random_token_minimum_amount); let batch_transition: BatchTransition = BatchTransitionV1 { owner_id: identity.id, diff --git a/packages/rs-drive-abci/tests/strategy_tests/test_cases/random_beacon_tests.rs b/packages/rs-drive-abci/tests/strategy_tests/test_cases/random_beacon_tests.rs index be15a7ed1f7..add554243ab 100644 --- a/packages/rs-drive-abci/tests/strategy_tests/test_cases/random_beacon_tests.rs +++ b/packages/rs-drive-abci/tests/strategy_tests/test_cases/random_beacon_tests.rs @@ -1,32 +1,52 @@ //! Same-block randomness across many blocks (protocol version 15): documents of a type with a -//! `sys.random.uniform` property are created block after block through the mimic, which hands -//! each block a stand-in random beacon through `ApplyBlockRandomness` and signs the commit -//! over the state after the draws. Every transition's proof is verified against that state, the -//! drawn values lie in their range, and the draws replaced the placeholder. +//! `sys.random.uniform` property, and random token transfers and mints, are sent block after +//! block through the mimic, which hands each block a stand-in random beacon through +//! `ApplyBlockRandomness` and signs the commit over the state after the draws. Every +//! transition's proof is verified against that state, the drawn values lie in their range, and +//! the draws replaced the placeholder. #[cfg(test)] mod tests { use crate::execution::run_chain_for_strategy; use crate::strategy::NetworkStrategy; use dash_platform_macros::stack_size; - use dpp::data_contract::accessors::v0::DataContractV0Getters; + use dpp::dash_to_duffs; + use dpp::data_contract::accessors::v0::{DataContractV0Getters, DataContractV0Setters}; + 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::TokenDistributionRulesV0Setters; + use dpp::data_contract::associated_token::token_keeps_history_rules::accessors::v0::TokenKeepsHistoryRulesV0Setters; use dpp::data_contract::document_type::random_document::{ DocumentFieldFillSize, DocumentFieldFillType, }; - use dpp::data_contract::DataContractFactory; - use dpp::document::DocumentV0Getters; + use dpp::data_contract::{DataContract, DataContractFactory}; + use dpp::document::{Document, DocumentV0Getters}; use dpp::identifier::Identifier; + use dpp::identity::accessors::IdentityGettersV0; + use dpp::identity::Identity; use dpp::platform_value::platform_value; + use dpp::state_transition::StateTransition; + use dpp::system_data_contracts::{load_system_data_contract, SystemDataContract}; + use dpp::tests::json_document::json_document_to_created_contract; + use dpp::tokens::token_event::TokenEvent; use dpp::version::PlatformVersion; use drive::drive::document::query::QueryDocumentsOutcomeV0Methods; + use drive::drive::tokens::settle_random_token_amounts::random_token_reservation_id; + use drive::drive::Drive; use drive::query::DriveDocumentQuery; use drive_abci::config::{ ChainLockConfig, ExecutionConfig, InstantLockConfig, PlatformConfig, PlatformTestConfig, ValidatorSetConfig, }; use drive_abci::test::helpers::setup::TestPlatformBuilder; + use rand::rngs::StdRng; + use rand::SeedableRng; + use simple_signer::signer::SimpleSigner; use strategy_tests::frequency::Frequency; - use strategy_tests::operations::{DocumentAction, DocumentOp, Operation, OperationType}; + use strategy_tests::operations::{ + DocumentAction, DocumentOp, Operation, OperationType, TokenOp, + }; + use strategy_tests::transitions::create_state_transitions_for_identities; use strategy_tests::{IdentityInsertInfo, StartAddresses, StartIdentities, Strategy}; #[stack_size(4 * 1024 * 1024)] @@ -210,4 +230,256 @@ mod tests { "the draws replaced the placeholder: {rarities:?}" ); } + + /// The amounts the token history documents of `document_type_name` (`transfer` or `mint`) + /// hold. + fn history_amounts(drive: &Drive, document_type_name: &str) -> Vec { + let platform_version = PlatformVersion::latest(); + let token_history = + load_system_data_contract(SystemDataContract::TokenHistory, platform_version) + .expect("expected the token history contract"); + let query = DriveDocumentQuery::from_sql_expr( + &format!("select * from {document_type_name}"), + &token_history, + None, + platform_version, + ) + .expect("expected a history query"); + drive + .query_documents(query, None, false, None, None) + .expect("expected to query the history") + .documents() + .iter() + .map(|document: &Document| { + document + .get("amount") + .and_then(|amount| amount.to_integer::().ok()) + .expect("expected every history document to hold an amount") + }) + .collect() + } + + #[stack_size(4 * 1024 * 1024)] + #[test] + async fn should_settle_every_random_token_amount_from_its_block_beacon() { + const BASE_SUPPLY: u64 = 100_000; + const MINIMUM: u64 = 100; + const MAXIMUM: u64 = 1_000; + let platform_version = PlatformVersion::latest(); + + // A token keeping its transfer and minting history, whose owner mints to a chosen + // recipient + let mut created_contract = json_document_to_created_contract( + "tests/supporting_files/contract/basic-token/basic-token.json", + 1, + true, + platform_version, + ) + .expect("expected the token contract"); + + let mut rng = StdRng::seed_from_u64(567); + let mut signer = SimpleSigner::default(); + let (mut owner, owner_keys) = Identity::random_identity_with_main_keys_with_private_key::< + Vec<_>, + >(3, &mut rng, platform_version) + .expect("expected an identity"); + let (mut player, player_keys) = + Identity::random_identity_with_main_keys_with_private_key::>( + 3, + &mut rng, + platform_version, + ) + .expect("expected an identity"); + signer.add_identity_public_keys(owner_keys); + signer.add_identity_public_keys(player_keys); + let start_identities: Vec<(Identity, Option)> = + create_state_transitions_for_identities( + vec![&mut owner, &mut player], + &(dash_to_duffs!(1)..=dash_to_duffs!(1)), + &signer, + &mut rng, + platform_version, + ) + .await + .into_iter() + .map(|(identity, transition)| (identity, Some(transition))) + .collect(); + + let contract = created_contract.data_contract_mut(); + let token_configuration = contract + .token_configuration_mut(0) + .expect("expected the token configuration"); + token_configuration + .distribution_rules_mut() + .set_minting_allow_choosing_destination(true); + token_configuration + .keeps_history_mut() + .set_keeps_transfer_history(true); + token_configuration + .keeps_history_mut() + .set_keeps_minting_history(true); + contract.set_owner_id(owner.id()); + contract.set_id(DataContract::generate_data_contract_id_v0(owner.id(), 1)); + let token_id = contract.token_id(0).expect("expected the token id"); + + let token_operation = |action: TokenEvent| Operation { + op_type: OperationType::Token(TokenOp { + contract: contract.clone(), + token_id, + token_pos: 0, + use_identity_with_id: Some(owner.id()), + action, + }), + frequency: Frequency { + times_per_block_range: 1..2, + chance_per_block: None, + }, + }; + let operations = vec![ + token_operation(TokenEvent::Transfer(player.id(), None, None, None, MAXIMUM)), + token_operation(TokenEvent::Mint(MAXIMUM, player.id(), None)), + ]; + + let strategy = NetworkStrategy { + strategy: Strategy { + start_contracts: vec![(created_contract, None)], + operations, + start_identities: StartIdentities { + hard_coded: start_identities, + ..Default::default() + }, + start_addresses: StartAddresses::default(), + identity_inserts: IdentityInsertInfo::default(), + identity_contract_nonce_gaps: None, + signer: Some(signer), + }, + total_hpmns: 100, + extra_normal_mns: 0, + validator_quorum_count: 24, + chain_lock_quorum_count: 24, + upgrading_info: None, + proposer_strategy: Default::default(), + rotate_quorums: false, + failure_testing: None, + query_testing: None, + // Each transition's proof is verified against the state the commit signs, after + // the block's settlements + verify_state_transition_results: true, + // Every transfer and mint moves an amount drawn from the minimum to its amount + random_token_minimum_amount: Some(MINIMUM), + ..Default::default() + }; + let config = PlatformConfig { + validator_set: ValidatorSetConfig::default_100_67(), + chain_lock: ChainLockConfig::default_100_67(), + instant_lock: InstantLockConfig::default_100_67(), + execution: ExecutionConfig { + verify_sum_trees: true, + ..Default::default() + }, + block_spacing_ms: 3000, + testing_configs: PlatformTestConfig::default_minimal_verifications(), + ..Default::default() + }; + let mut platform = TestPlatformBuilder::new() + .with_config(config.clone()) + .with_latest_protocol_version() + .build_with_mock_rpc(); + + let outcome = run_chain_for_strategy( + &mut platform, + 12, + strategy, + config, + 15, + &mut None, + &mut None, + ) + .await; + + let mut sent = 0u64; + for results in outcome.state_transition_results_per_block.values() { + for (state_transition, result) in results { + assert_eq!( + result.code, 0, + "state transition got code {} : {:?}", + result.code, state_transition + ); + if matches!(state_transition, StateTransition::Batch(_)) { + sent += 1; + } + } + } + + let drive = &outcome.abci_app.platform.drive; + let transferred = history_amounts(drive, "transfer"); + let minted = history_amounts(drive, "mint"); + assert!( + transferred.len() >= 5 && minted.len() >= 5, + "random transfers and mints are settled over many blocks: {transferred:?}, {minted:?}" + ); + assert_eq!( + (transferred.len() + minted.len()) as u64, + sent, + "every random transfer and mint wrote its history" + ); + let drawn: Vec = transferred.iter().chain(&minted).copied().collect(); + assert!( + drawn + .iter() + .all(|amount| (MINIMUM..=MAXIMUM).contains(amount)), + "every drawn amount lies in its range: {drawn:?}" + ); + assert!( + drawn.iter().any(|amount| *amount != MINIMUM), + "the settlements replaced the placeholder: {drawn:?}" + ); + + // Each settlement paid out once, the drawn amount: the balances and the supply are + // those of plain transfers and mints of the amounts the history holds + let transferred_total: u64 = transferred.iter().sum(); + let minted_total: u64 = minted.iter().sum(); + let balance = |identity: &Identity| { + drive + .fetch_identity_token_balance( + token_id.to_buffer(), + identity.id().to_buffer(), + None, + platform_version, + ) + .expect("expected to fetch a token balance") + }; + assert_eq!(balance(&owner), Some(BASE_SUPPLY - transferred_total)); + assert_eq!(balance(&player), Some(transferred_total + minted_total)); + assert_eq!( + drive + .fetch_token_total_supply(token_id.to_buffer(), None, platform_version) + .expect("expected to fetch the total supply"), + Some(BASE_SUPPLY + minted_total) + ); + + // No reservation is left in the escrow, and the token totals balance + for nonce in 1..=sent { + assert_eq!( + drive + .fetch_token_order_escrow( + token_id.to_buffer(), + random_token_reservation_id(&token_id, &owner.id(), nonce), + None, + &mut vec![], + platform_version, + ) + .expect("expected to fetch a reservation"), + None, + "the reservation of nonce {nonce} was settled" + ); + } + let total = drive + .calculate_total_tokens_balance(None, platform_version) + .expect("expected to sum the token balances"); + assert!( + total.ok().expect("expected the sums to compare"), + "tokens are not balanced: {total}" + ); + } } 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 7805bea53bf..89ab6cd2c9e 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 @@ -776,7 +776,7 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( "we expect a token history document" ); - let expected_document = token_transition_action + let mut expected_document = token_transition_action .build_historical_document( token_id, batch_transition.owner_id(), @@ -790,21 +790,47 @@ pub(crate) fn verify_state_transitions_were_or_were_not_executed( platform_version, ) .expect("expected to build historical document"); + let history_document_type = token_transition_action + .historical_document_type(&token_history) + .expect("expected document type"); + let serialized_document = serialized_document.unwrap(); + + // A random transfer or mint (protocol version 15) writes its + // history holding the range's minimum, and the block's + // settlement rewrites the amount to the one drawn, which the + // committed state proves: it must lie in the range + if let Some((minimum, maximum)) = + token_transition_action.random_amount_range() + { + let proved_document = Document::from_bytes( + &serialized_document, + history_document_type, + platform_version, + ) + .expect("expected the history document to decode"); + let amount = proved_document + .get("amount") + .cloned() + .expect("expected the history to hold an amount"); + let drawn = amount + .to_integer::() + .expect("expected an integer amount"); + assert!( + (minimum..=maximum).contains(&drawn), + "the settled amount {drawn} lies in {minimum}..={maximum}" + ); + expected_document.set("amount", amount); + } let serialized_expected_document = expected_document .serialize( - token_transition_action - .historical_document_type(&token_history) - .expect("expected document type"), + history_document_type, &token_history, platform_version, ) .expect("expected to serialize"); - assert_eq!( - serialized_document.unwrap(), - serialized_expected_document - ); + assert_eq!(serialized_document, serialized_expected_document); } else { todo!(); } From dc261cfcc26428828c3cbdf0a6d45a2ca66af4c2 Mon Sep 17 00:00:00 2001 From: QuantumExplorer Date: Thu, 5 Nov 2026 00:52:23 +0700 Subject: [PATCH 3/3] docs(changelog): list the Rust API changes of same-block randomness Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96b2c2ae621..60d845d4cc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,22 @@ a required `random_minimum_amount` method, and `TokenTransitionV0Methods` a required `random_amount_range` method. An implementation outside these crates returns `None` for a fixed amount. +- **platform-version, dpp, drive-abci (Rust API):** same-block randomness through + `ApplyBlockRandomness` (protocol version 15) changes these items for Rust code whichever + protocol version it runs: + - `DriveAbciBlockEndMethodVersions` gains a public `apply_random_beacon` field. A struct + literal sets it to `None` in a table that does not draw from the beacon. + - `BasicError` gains `RandomBeaconUnavailableError` (code 10604). An exhaustive match needs + the new arm. + - drive-abci's `PlatformState` gains a public `switched_from_protocol_version` field, + `BlockExecutionContextV0` public `random_requests` and `applied_randomness` fields, and + `CleanedCommitInfo` public `beacon` and `app_hash` fields. A struct literal sets them to + `None`, and `random_requests` to `BlockRandomRequests::default()`. + - `PlatformStateV0Methods` gains the required methods `switched_from_protocol_version` and + `set_switched_from_protocol_version`, `BlockExecutionContextV0Getters` the required + methods `random_requests` and `applied_randomness`, and `BlockExecutionContextV0Setters` + the required method `set_applied_randomness`. An implementation outside drive-abci + returns `None`, an empty `BlockRandomRequests`, or ignores the value set. - **drive (Rust API):** Drive's cost estimators write their layer descriptions into `drive::util::estimated_layers::EstimatedLayers` instead of `HashMap`, in `DriveLowLevelOperationConverter`