Skip to content

fix(dpp)!: version-gate distribution function floating-point evaluation - #3462

Open
PastaPastaPasta wants to merge 2 commits into
v4.2-devfrom
fix/version-gate-float-reward-evaluation
Open

fix(dpp)!: version-gate distribution function floating-point evaluation#3462
PastaPastaPasta wants to merge 2 commits into
v4.2-devfrom
fix/version-gate-float-reward-evaluation

Conversation

@PastaPastaPasta

@PastaPastaPasta PastaPastaPasta commented Apr 8, 2026

Copy link
Copy Markdown
Member

Issue being fixed or feature implemented

DistributionFunction::evaluate() uses f64 transcendental functions (powf/exp/ln) to compute consensus-critical token rewards. The std implementations are platform-dependent (varying across CPU architectures and libm versions), risking consensus divergence between nodes computing different integer rewards for the same claim.

Concrete proof: 125^(1/3) yields 4 on some platforms (std powf) vs 5 on others. The deterministic libm path always returns 5.

What was done?

Gate transcendental float operations behind distribution_function_evaluate_version in DPPTokenVersions:

  • Version 0 (all current protocol versions through v12): preserves original std behavior (.powf(), .exp(), .ln())
  • Version 1+: uses deterministic libm functions (pow, exp, log)

Changes:

  • Add distribution_function_evaluate_version field to DPPTokenVersions
  • Create TOKEN_VERSIONS_V3 with deterministic evaluation enabled (for use in a future protocol version)
  • Add libm = "0.2" dependency to rs-dpp
  • Thread platform_version through evaluate()evaluate_interval()rewards_in_interval() call chain
  • Version-gate 4 transcendental call sites: Polynomial (pow), Exponential (exp), Logarithmic (log), InvertedLogarithmic (log)
  • 5 unaffected variants use only integer math: FixedAmount, Random, StepDecreasingAmount, Stepwise, Linear

Note: TOKEN_VERSIONS_V3 is created but not yet assigned to a platform version. A future PR creating PLATFORM_V13 should reference it to activate deterministic evaluation on the network.

How Has This Been Tested?

  • All 50 evaluate() unit tests pass (47 existing + 3 new determinism tests)
  • All 82 validation.rs tests pass
  • All 72 evaluate_interval tests pass (with token-reward-explanations feature)
  • Determinism regression tests for all 4 affected variants using distribution_function_evaluate_version = 1
  • cargo check -p dpp -p drive compiles clean

Breaking Changes

Function signature changes (compile-time only, no behavioral change for existing protocol versions):

  • DistributionFunction::evaluate() now requires platform_version: &PlatformVersion
  • DistributionFunction::evaluate_interval() now requires platform_version: &PlatformVersion
  • RewardDistributionType::rewards_in_interval() now requires platform_version: &PlatformVersion

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed

Summary by CodeRabbit

  • New Features

    • Added version control for token distribution evaluation methods, enabling consistent calculation results across different platform configurations.
  • Improvements

    • Updated token perpetual distribution calculations to support versioned mathematical evaluation methods based on platform version.
    • Token reward interval calculations and validations now properly propagate platform version information for deterministic results.

DistributionFunction::evaluate() uses f64 transcendental functions (pow/exp/log) to compute consensus-critical token rewards. The std implementations are platform-dependent, risking consensus divergence between nodes with different architectures or libm versions.

Gate these operations behind distribution_function_evaluate_version in DPPTokenVersions. Version 0 preserves the original std behavior (.powf/.exp/.ln) for existing protocol versions. Version 1+ uses deterministic libm functions for cross-platform consistency.

Changes: - Add distribution_function_evaluate_version field to DPPTokenVersions - Create TOKEN_VERSIONS_V3 with deterministic evaluation enabled - Add libm 0.2 dependency to rs-dpp - Thread platform_version through evaluate() -> evaluate_interval() -> rewards_in_interval() call chain - Version-gate 4 transcendental call sites: Polynomial (pow), Exponential (exp), Logarithmic (log), InvertedLogarithmic (log) - Add determinism regression tests for all 4 affected variants
@coderabbitai

coderabbitai Bot commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR enables platform-version-controlled selection of math implementations for token distribution evaluation. It adds libm 0.2 as a dependency, extends the platform version schema with a distribution_function_evaluate_version field, and modifies the evaluation call chain to route between standard f64 transcendental operations and deterministic libm equivalents based on protocol version.

Changes

Token Distribution Math Versioning

Layer / File(s) Summary
Platform version infrastructure for distribution evaluation
packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/mod.rs, packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v1.rs, packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v2.rs, packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v3.rs
DPPTokenVersions struct gains distribution_function_evaluate_version field; TOKEN_VERSIONS_V1 and V2 set it to 0 (std math); new TOKEN_VERSIONS_V3 sets it to 1 (libm math).
Core DistributionFunction::evaluate with versioned math
packages/rs-dpp/Cargo.toml, packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate.rs
Add libm 0.2 dependency and update evaluate signature to accept platform_version. Route Polynomial/Exponential/Logarithmic/InvertedLogarithmic variants through version-driven switch between powf/exp/ln (version 0) and libm::pow/exp/log (version 1), returning UnknownVersionMismatch for unsupported versions.
Thread platform_version through interval evaluation
packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate_interval.rs
Update evaluate_interval and evaluate_interval_with_explanation to accept and forward platform_version to self.evaluate calls.
Update reward distribution methods for platform_version
packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/evaluate_interval.rs
Extend rewards_in_interval and rewards_in_interval_with_explanation to accept platform_version and forward to underlying evaluate_interval calls.
Update distribution validation to use platform_version
packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/validation.rs
Pass platform_version into internal evaluate calls for all distribution variants (Linear, Polynomial, Exponential, Logarithmic, InvertedLogarithmic) in DistributionFunction::validate.
Integrate platform_version into token claim reward calculation
packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs
Pass platform_version into rewards_in_interval calls for ContractOwner, Identity, and EvonodesByParticipation distribution recipients.
Test updates for versioned evaluation
packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate.rs, packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate_interval.rs, packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs
Update all test invocations to pass PlatformVersion::latest(); add deterministic behavior tests for libm path by forcing distribution_function_evaluate_version to 1.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Suggested labels

codex, aardvark

Suggested reviewers

  • QuantumExplorer
  • thepastaclaw

Poem

🐰 Math goes versioned, libm shines bright,
Deterministic paths for token flight,
Platform versions guide the way,
Transcendental functions, precise and astray,
From v0 to v1, precision takes flight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: version-gating distribution function floating-point evaluation for deterministic consensus-critical token reward computation.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/version-gate-float-reward-evaluation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added this to the v3.1.0 milestone Apr 8, 2026
@QuantumExplorer QuantumExplorer modified the milestones: v4.0.0, v4.1.0 Jun 1, 2026
@QuantumExplorer QuantumExplorer self-assigned this Jun 1, 2026
@QuantumExplorer

Copy link
Copy Markdown
Member

Will review for 4.1

@QuantumExplorer
QuantumExplorer marked this pull request as ready for review June 1, 2026 15:23
@QuantumExplorer
QuantumExplorer self-requested a review as a code owner June 1, 2026 15:23
@thepastaclaw

thepastaclaw commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

✅ Final review complete — no blockers (commit 2e06247)

@codecov

codecov Bot commented Jun 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.46862% with 18 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (v4.2-dev@11d80f5). Learn more about missing BASE report.

Files with missing lines Patch % Lines
...ual_distribution/distribution_function/evaluate.rs 91.22% 15 Missing ⚠️
...ibution/distribution_function/evaluate_interval.rs 94.91% 3 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             v4.2-dev    #3462   +/-   ##
===========================================
  Coverage            ?   87.12%           
===========================================
  Files               ?     2625           
  Lines               ?   321377           
  Branches            ?        0           
===========================================
  Hits                ?   279987           
  Misses              ?    41390           
  Partials            ?        0           
Components Coverage Δ
dpp 87.73% <0.00%> (?)
drive 86.05% <0.00%> (?)
drive-abci 89.28% <0.00%> (?)
sdk ∅ <0.00%> (?)
dapi-client ∅ <0.00%> (?)
platform-version ∅ <0.00%> (?)
platform-value 92.17% <0.00%> (?)
platform-wallet ∅ <0.00%> (?)
drive-proof-verifier 47.85% <0.00%> (?)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

PR correctly version-gates four f64 transcendental call sites in DistributionFunction::evaluate behind a new distribution_function_evaluate_version field, with the new libm path activated only by TOKEN_VERSIONS_V3 (which is intentionally not yet wired to any PlatformVersion). The change is consensus-safe (v0 path preserved bit-for-bit) and threaded cleanly through evaluate_interval/rewards_in_interval. Main feedback is that the new version dispatch uses a wildcard fallback instead of the codebase's explicit-arms + UnknownVersionMismatch convention; plus minor test-comment and orphan-constant nits.

🟡 1 suggestion(s) | 💬 2 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate.rs`:
- [SUGGESTION] packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate.rs:228-235: Wildcard `_ =>` dispatch diverges from the repo's fail-closed version pattern
  All four new version gates (lines 228-235, 339-346, 421-428, 566-573) match `0 => std` and `_ => libm`, so any future `distribution_function_evaluate_version` value (2, 3, …) silently executes the v1 libm semantics instead of failing closed. Elsewhere in `rs-dpp` (e.g. `document/extended_document/v0/serialize.rs:130-133`, `document/serialization_traits/cbor_conversion/mod.rs:40-43`, `document/v0/serialize.rs:1457`) the codebase consistently enumerates known versions and ends with `version => Err(ProtocolError::UnknownVersionMismatch { method: ..., known_versions, received: version })`. Since this is consensus-critical reward math, a future protocol version bump that intends a third algorithm (e.g. fixed-point) but forgets to update this site would silently pay rewards under v1 semantics on this code path rather than producing a clean version-mismatch error. Tightening to explicit `1 => libm` + `version => Err(UnknownVersionMismatch …)` makes any future version change a typechecked code edit. Apply to all four call sites in this file.

Comment on lines +228 to +235
let diff_exp = match platform_version
.dpp
.token_versions
.distribution_function_evaluate_version
{
0 => (diff as f64).powf(exponent),
_ => pow(diff as f64, exponent),
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Wildcard _ => dispatch diverges from the repo's fail-closed version pattern

All four new version gates (lines 228-235, 339-346, 421-428, 566-573) match 0 => std and _ => libm, so any future distribution_function_evaluate_version value (2, 3, …) silently executes the v1 libm semantics instead of failing closed. Elsewhere in rs-dpp (e.g. document/extended_document/v0/serialize.rs:130-133, document/serialization_traits/cbor_conversion/mod.rs:40-43, document/v0/serialize.rs:1457) the codebase consistently enumerates known versions and ends with version => Err(ProtocolError::UnknownVersionMismatch { method: ..., known_versions, received: version }). Since this is consensus-critical reward math, a future protocol version bump that intends a third algorithm (e.g. fixed-point) but forgets to update this site would silently pay rewards under v1 semantics on this code path rather than producing a clean version-mismatch error. Tightening to explicit 1 => libm + version => Err(UnknownVersionMismatch …) makes any future version change a typechecked code edit. Apply to all four call sites in this file.

source: ['claude', 'codex']

Comment on lines +1 to +10
use crate::version::dpp_versions::dpp_token_versions::DPPTokenVersions;

pub const TOKEN_VERSIONS_V3: DPPTokenVersions = DPPTokenVersions {
identity_token_info_default_structure_version: 0,
identity_token_status_default_structure_version: 0,
token_contract_info_default_structure_version: 0,
token_config_update_action_id_version: 1,
token_set_price_action_id_version: 1,
distribution_function_evaluate_version: 1,
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: TOKEN_VERSIONS_V3 has no PlatformVersion consumer

TOKEN_VERSIONS_V3 is the only constant that sets distribution_function_evaluate_version = 1, but no PlatformVersion::PLATFORM_V* references it (V1–V12 still bind TOKEN_VERSIONS_V1 or _V2). The PR description states activation is deferred to a follow-up PLATFORM_V13 PR, which is fine, but the protective effect is entirely deferred. Recommend ensuring a tracking issue exists for the activation PR (so the orphaned constant isn't forgotten) and considering a brief module-level doc comment in v3.rs pointing readers to the follow-up so future maintainers don't think it's wired up.

source: ['claude', 'codex']

Comment on lines +1604 to +1606
fn test_inverted_logarithmic_deterministic_libm_path() {
// f(x) = 10 * ln(100 / (1 * x)) / 1 + 5
// At x=1 (with o=1, so arg = 100/1 = 100): ln(100) ≈ 4.605, * 10 = 46.05 + 5 = 51

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: Comment in inverted-log determinism test says x=1 but the call passes x=0

The comment reads At x=1 (with o=1, so arg = 100/1 = 100) but the test invokes distribution.evaluate(0, 0, &deterministic_version) (x=0). The numeric expectation 51 still holds because with start_moment=Some(0) and o=1, diff = 0 - 0 + 1 = 1 and argument = n/(m*diff) = 100/(1*1) = 100. Either update the comment to At x=0 or pass x=1 and recompute expected (which would give the same value here since start_moment=Some(0) is fixed and only diff depends on x). The assertion is correct; only the comment is misleading.

Suggested change
fn test_inverted_logarithmic_deterministic_libm_path() {
// f(x) = 10 * ln(100 / (1 * x)) / 1 + 5
// At x=1 (with o=1, so arg = 100/1 = 100): ln(100) ≈ 4.605, * 10 = 46.05 + 5 = 51
// f(x) = 10 * ln(100 / (1 * (x - s + o))) / 1 + 5
// At x=0 (with s=0, o=1, so arg = 100/(1*1) = 100): ln(100) ≈ 4.605, * 10 = 46.05 + 5 = 51

source: ['claude']

Replace wildcard libm dispatch with explicit version arms (0/1) ending in UnknownVersionMismatch across all four evaluate.rs call sites. Fix a misleading determinism-test comment and document that TOKEN_VERSIONS_V3 has no PlatformVersion consumer yet.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate.rs (1)

670-678: ⚡ Quick win

Pin the baseline assertions to an explicit evaluation version.

Most of this suite uses PlatformVersion::latest(), so once a future PR points latest() at TOKEN_VERSIONS_V3, these tests will silently stop exercising v0 semantics and some expectations may flip without any change in this file. A small helper that clones latest() and forces distribution_function_evaluate_version = 0 would keep the legacy-path coverage stable, while the version-1 determinism cases stay explicit.

♻️ Example helper
 mod tests {
     use super::*;
     use platform_version::version::PlatformVersion;
     use std::collections::BTreeMap;
+
+    fn legacy_distribution_math_version() -> PlatformVersion {
+        let mut version = PlatformVersion::latest().clone();
+        version
+            .dpp
+            .token_versions
+            .distribution_function_evaluate_version = 0;
+        version
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate.rs`
around lines 670 - 678, The tests call DistributionFunction::evaluate(...) with
PlatformVersion::latest(), which will change behavior when latest() advances;
update the tests (e.g., test_fixed_amount and other evaluate tests) to pin the
legacy evaluation path by cloning PlatformVersion::latest() into a mutable
variable and setting distribution_function_evaluate_version = 0 before passing
it to distribution.evaluate so the assertions remain stable; locate uses of
PlatformVersion::latest() in the evaluate tests and replace them with the
cloned-and-modified PlatformVersion instance referenced when calling
DistributionFunction::evaluate.
packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate_interval.rs (1)

1903-1920: ⚡ Quick win

Pin these explanation tests to a fixed math version too.

These assertions currently inherit whatever PlatformVersion::latest() means at the time the test runs. When latest() eventually switches to the deterministic token version, this suite will stop validating the legacy interval totals by default. Mirroring the explicit v0/v1 fixtures here would keep the call-chain coverage stable across future platform bumps.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate_interval.rs`
around lines 1903 - 1920, The test uses PlatformVersion::latest() which makes it
float as the platform evolves; change the call in
test_fixed_amount_explanation_first_claim (and sibling explanation tests) to use
an explicit legacy math platform version instead of latest() so the assertions
remain fixed — replace PlatformVersion::latest() with a pinned PlatformVersion
representing the legacy math (e.g., the v0/v1 fixture you use elsewhere such as
PlatformVersion::v0() or PlatformVersion::new(0), depending on your API) when
calling DistributionFunction::evaluate_interval_with_explanation to ensure
deterministic behavior.
packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/validation.rs (1)

1825-1825: ⚡ Quick win

Pin the platform version in this exact-value test.

evaluate() is version-gated now, so PlatformVersion::latest() will make this assertion drift when a later protocol version flips distribution_function_evaluate_version. Prefer a fixed platform version here, or explicitly override just the distribution-function evaluation version used by the test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/validation.rs`
at line 1825, The test calls dist.evaluate(0, 4, PlatformVersion::latest()),
which will drift as distribution_function_evaluate_version changes; replace
PlatformVersion::latest() with a pinned PlatformVersion instance (or construct a
PlatformVersion and explicitly set distribution_function_evaluate_version to the
expected version) so that evaluate() is invoked with a fixed protocol version;
update the call site (the evaluate invocation) to pass that pinned/overridden
PlatformVersion instead of PlatformVersion::latest().
packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs (1)

2296-2335: ⚡ Quick win

Avoid PlatformVersion::latest() in exact numeric regression checks.

These assertions now depend on version-gated math behavior, so they will become brittle as soon as latest() starts consuming the deterministic evaluator. Pin the platform version used by the test, or construct a test-only version with the intended distribution_function_evaluate_version, so the expected constants stay stable.

Also applies to: 2413-2416

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs`
around lines 2296 - 2335, The test uses PlatformVersion::latest() when calling
InvertedLogarithmic::evaluate (and in other nearby assertions), which makes
numeric expectations brittle; change those calls to a pinned PlatformVersion
that encodes the deterministic distribution evaluation you expect (or build a
test-only PlatformVersion with the intended
distribution_function_evaluate_version) instead of PlatformVersion::latest();
update all evaluate invocations in this test (and the similar calls at the other
assertions) to pass that pinned/versioned PlatformVersion so the
InvertedLogarithmic::evaluate results remain stable for the asserted constants.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate_interval.rs`:
- Around line 1903-1920: The test uses PlatformVersion::latest() which makes it
float as the platform evolves; change the call in
test_fixed_amount_explanation_first_claim (and sibling explanation tests) to use
an explicit legacy math platform version instead of latest() so the assertions
remain fixed — replace PlatformVersion::latest() with a pinned PlatformVersion
representing the legacy math (e.g., the v0/v1 fixture you use elsewhere such as
PlatformVersion::v0() or PlatformVersion::new(0), depending on your API) when
calling DistributionFunction::evaluate_interval_with_explanation to ensure
deterministic behavior.

In
`@packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate.rs`:
- Around line 670-678: The tests call DistributionFunction::evaluate(...) with
PlatformVersion::latest(), which will change behavior when latest() advances;
update the tests (e.g., test_fixed_amount and other evaluate tests) to pin the
legacy evaluation path by cloning PlatformVersion::latest() into a mutable
variable and setting distribution_function_evaluate_version = 0 before passing
it to distribution.evaluate so the assertions remain stable; locate uses of
PlatformVersion::latest() in the evaluate tests and replace them with the
cloned-and-modified PlatformVersion instance referenced when calling
DistributionFunction::evaluate.

In
`@packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/validation.rs`:
- Line 1825: The test calls dist.evaluate(0, 4, PlatformVersion::latest()),
which will drift as distribution_function_evaluate_version changes; replace
PlatformVersion::latest() with a pinned PlatformVersion instance (or construct a
PlatformVersion and explicitly set distribution_function_evaluate_version to the
expected version) so that evaluate() is invoked with a fixed protocol version;
update the call site (the evaluate invocation) to pass that pinned/overridden
PlatformVersion instead of PlatformVersion::latest().

In
`@packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs`:
- Around line 2296-2335: The test uses PlatformVersion::latest() when calling
InvertedLogarithmic::evaluate (and in other nearby assertions), which makes
numeric expectations brittle; change those calls to a pinned PlatformVersion
that encodes the deterministic distribution evaluation you expect (or build a
test-only PlatformVersion with the intended
distribution_function_evaluate_version) instead of PlatformVersion::latest();
update all evaluate invocations in this test (and the similar calls at the other
assertions) to pass that pinned/versioned PlatformVersion so the
InvertedLogarithmic::evaluate results remain stable for the asserted constants.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b32316de-a0c3-4b69-b6a4-4cbc192dcab7

📥 Commits

Reviewing files that changed from the base of the PR and between 11d80f5 and 2e06247.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • packages/rs-dpp/Cargo.toml
  • packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate.rs
  • packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate_interval.rs
  • packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/validation.rs
  • packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/reward_distribution_type/evaluate_interval.rs
  • packages/rs-drive-abci/src/execution/validation/state_transition/state_transitions/batch/tests/token/distribution/perpetual/block_based.rs
  • packages/rs-drive/src/state_transition_action/batch/batched_transition/token_transition/token_claim_transition_action/v0/transformer.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/mod.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v1.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v2.rs
  • packages/rs-platform-version/src/version/dpp_versions/dpp_token_versions/v3.rs

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Latest commit 2e06247 resolves all three prior findings: the four version-dispatch sites in evaluate.rs now fail closed with ProtocolError::UnknownVersionMismatch (explicit 0/1 arms), v3.rs has a doc comment documenting the deferred PLATFORM_V13 activation, and the inverted-log test comment matches the actual call. One in-scope suggestion remains: the new error path has no regression test. The TOKEN_VERSIONS_V3-unwired nitpicks are intentionally staged per the PR description and not actionable here.

🟡 1 suggestion(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate.rs`:
- [SUGGESTION] packages/rs-dpp/src/data_contract/associated_token/token_perpetual_distribution/distribution_function/evaluate.rs:228-242: No regression test for the new UnknownVersionMismatch error path
  The four new version-dispatch sites (Polynomial 228-242, Exponential 346-360, Logarithmic 435-449, InvertedLogarithmic 587-602) correctly fail closed with `ProtocolError::UnknownVersionMismatch` for any value other than 0 or 1, but the test suite only exercises the happy paths (`test_*_deterministic_libm_path` at version 1, plus the existing version-0 tests). A grep for `UnknownVersionMismatch` in this file matches only the four production sites — zero tests. Since this is consensus-critical reward math and the new behavior is specifically about rejecting unknown protocol versions, a future edit could reintroduce a wildcard arm at any of the four sites without breaking any test. Add at least one unit test that constructs a `PlatformVersion` with `distribution_function_evaluate_version = 2` (or similar) and asserts `Err(ProtocolError::UnknownVersionMismatch { .. })` for each transcendental variant, or a table-driven test across all four branches.

Comment on lines +228 to +242
let diff_exp = match platform_version
.dpp
.token_versions
.distribution_function_evaluate_version
{
0 => (diff as f64).powf(exponent),
1 => pow(diff as f64, exponent),
version => {
return Err(ProtocolError::UnknownVersionMismatch {
method: "DistributionFunction::evaluate (Polynomial)".to_string(),
known_versions: vec![0, 1],
received: version,
})
}
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: No regression test for the new UnknownVersionMismatch error path

The four new version-dispatch sites (Polynomial 228-242, Exponential 346-360, Logarithmic 435-449, InvertedLogarithmic 587-602) correctly fail closed with ProtocolError::UnknownVersionMismatch for any value other than 0 or 1, but the test suite only exercises the happy paths (test_*_deterministic_libm_path at version 1, plus the existing version-0 tests). A grep for UnknownVersionMismatch in this file matches only the four production sites — zero tests. Since this is consensus-critical reward math and the new behavior is specifically about rejecting unknown protocol versions, a future edit could reintroduce a wildcard arm at any of the four sites without breaking any test. Add at least one unit test that constructs a PlatformVersion with distribution_function_evaluate_version = 2 (or similar) and asserts Err(ProtocolError::UnknownVersionMismatch { .. }) for each transcendental variant, or a table-driven test across all four branches.

source: ['codex']

@shumkov
shumkov changed the base branch from v4.0-dev to v4.1-dev July 2, 2026 08:09
@QuantumExplorer
QuantumExplorer changed the base branch from v4.1-dev to v4.2-dev July 24, 2026 20:09
@github-actions github-actions Bot modified the milestones: v4.1.0, v4.2.0 Jul 24, 2026
while current_point <= last_step {
let base_amount =
self.evaluate(distribution_start_step.to_u64(), current_point.to_u64())?;
self.evaluate(distribution_start_step.to_u64(), current_point.to_u64(), platform_version)?;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is red before any test runs: this branch is not rustfmt-clean.

cargo fmt --check --all reports 44 unformatted hunks, all in files this PR touches (39 in distribution_function/evaluate.rs, 2 here in evaluate_interval.rs, 3 in rs-drive-abci/.../perpetual/block_based.rs). Nothing else in the workspace is dirty, so this is PR-introduced — the evaluate() signature change pushed these calls past 100 cols and cargo fmt --all was not re-run.

.github/workflows/tests-rs-workspace.yml runs cargo fmt --check --all as the Check formatting step, which is why the Rust workspace tests (macOS) job failed in 29s — the Rust test suite never executed on this PR at all. Everything else in this review was validated against a local run instead.

This line is one of the two production (non-test) offenders:

self.evaluate(distribution_start_step.to_u64(), current_point.to_u64(), platform_version)?;

Fix is just cargo fmt --all.


🤖 Posted autonomously by Claude on behalf of pasta.

"draft202012",
], optional = true }
lazy_static = { version = "1.4" }
libm = "0.2"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pin libm exactly — a caret range undermines the determinism guarantee this PR is built on.

libm = "0.2"

The entire premise of distribution_function_evaluate_version: 1 is that libm produces bit-identical results everywhere. libm is not a correctly-rounded implementation, and its 0.2.x series has shipped accuracy/algorithm changes in patch releases. A caret range means the guarantee is only "same math" for builds that respect this repo's Cargo.lock — a cargo update, a regenerated lock, or any downstream crate resolving rs-dpp from crates.io can land on a different 0.2.x.

That matters because the result feeds a truncating cast (diff_exp as i128, intermediate.floor()), so a 1-ulp difference is a whole-token difference. Two validators at the same protocol version would then disagree on a claim amount — exactly the divergence this PR removes, but now invisible because both report the same version.

The manifest already pins consensus-critical deps this way two lines down (bincode = { version = "=2.0.1", ... }). Suggest libm = "=0.2.16" (the currently locked version) plus a comment explaining why.

Concrete evidence the last bit really is in play here — on aarch64-darwin, for the exact input in your new polynomial test:

std  powf(125.0, 1.0/3.0) = 4.999999999999999  (bits 4013ffffffffffff) -> as i128 = 4
libm pow (125.0, 1.0/3.0) = 5.0                (bits 4014000000000000) -> as i128 = 5

🤖 Posted autonomously by Claude on behalf of pasta.

token_contract_info_default_structure_version: 0,
token_config_update_action_id_version: 1,
token_set_price_action_id_version: 1,
distribution_function_evaluate_version: 1,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix ships inert, and TOKEN_VERSIONS_V3 is an unreferenced pub const the compiler will never flag.

I grepped every pub const *_V<n> under packages/rs-platform-version/src: TOKEN_VERSIONS_V3 is the only version constant in the crate with no consumer. Every other *_VERSIONS_Vn is wired into some version/vN.rs or mocks/. PLATFORM_V12 (the latest) binds TOKEN_VERSIONS_V2, which carries distribution_function_evaluate_version: 0.

So after merge, every reachable PlatformVersion — including PlatformVersion::latest() — still takes the std arm. An aarch64-darwin node and an x86_64-glibc node evaluating the same Logarithmic claim can still disagree. The breaking (!) signature churn lands with zero behavior change.

The header comment documents this as intentional, so the ask is not "wire it up now" but: because it is pub in a pub mod, there is no dead-code warning and nothing anchors it. Two risks worth deciding on explicitly:

  1. If another DPPTokenVersions field is added or bumped in v1.rs/v2.rs before the follow-up lands, v3.rs gets updated from memory — and PLATFORM_V13 then activates a token version set whose other fields were frozen months earlier and never re-reviewed.
  2. No test asserts that any real PlatformVersion resolves to the libm path, so a follow-up that copy-pastes distribution_function_evaluate_version: 0 would keep the suite fully green while the fix silently never activates.

Cleanest option: drop v3.rs + pub mod v3; from this PR (it only needs the struct field and the 0 defaults), and add them in the PLATFORM_V13 PR that actually references the constant. If it stays, please add a test asserting PlatformVersion::latest().dpp.token_versions.distribution_function_evaluate_version == 1 in the activation PR.


🤖 Posted autonomously by Claude on behalf of pasta.

start_from_moment_for_distribution,
max_cycle_moment,
None,
platform_version,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Activation will retroactively re-price already-emitted cycles, making payouts depend on when an identity claims.

rewards_in_interval walks from start_from_moment_for_distribution (the last-paid moment, arbitrarily far in the past) to max_cycle_moment and calls evaluate(..., platform_version) for every step — using the version active at claim time, not the version active at each cycle's own moment. The single &PlatformVersion reaching evaluate() makes per-moment gating impossible by construction.

Scenario at the distribution_function_evaluate_version 0→1 flip: identities X and Y hold identical Polynomial { m: 1, n: 3 } perpetual distributions that accrued entirely before the fork. X claims at height N-1 and is paid on the std path; Y claims at N+1 and every one of those same pre-fork cycles is recomputed with libm. Using the measured values above, that is 5 vs 4 per cycle — over a long unclaimed interval (bounded by max_token_redemption_cycles, and the step loop by MAX_DISTRIBUTION_CYCLES_PARAM = 32_767) the gap compounds.

This is not a chain split — all nodes at a given height agree — but realized emission for a fixed block range becomes a function of claim timing, and the token's pre-fork history is rewritten for anyone who had not yet claimed. If that is the accepted tradeoff, please say so in the PR description / activation PR so it is a recorded decision rather than an emergent one.


🤖 Posted autonomously by Claude on behalf of pasta.

1 => pow(diff as f64, exponent),
version => {
return Err(ProtocolError::UnknownVersionMismatch {
method: "DistributionFunction::evaluate (Polynomial)".to_string(),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Four copies of this dispatch block mean known_versions: vec![0, 1] is hardcoded in four places — and the version check is not uniform across variants.

The same 14-line match platform_version.dpp.token_versions.distribution_function_evaluate_version { 0 => std, 1 => libm, version => Err(UnknownVersionMismatch { .. }) } appears verbatim at lines 228 (Polynomial), 346 (Exponential), 434 (Logarithmic) and 586 (InvertedLogarithmic), differing only in the two function calls and the method: string.

Two concrete consequences:

  1. Partial-edit hazard. A future distribution_function_evaluate_version: 2 touching only the polynomial path gets written at line 238; the other three sites still say vec![0, 1], so after activation every exponential/logarithmic token claim returns UnknownVersionMismatch instead of an amount — a class of tokens becomes unclaimable, and the omission is invisible in review because the four blocks look identical. The inverse miss (forgetting to add a 2 => arm to one variant) yields a silently mixed-version evaluator that still compiles.
  2. Non-uniform rejection. FixedAmount, Stepwise, Linear, StepDecreasingAmount and Random never read the field at all, and each version => arm sits after that branch's DivideByZero/Overflow guards. So an unrecognized version is a hard error for a Polynomial contract, a silent success for a FixedAmount one, and for a Polynomial with n == 0 it surfaces as DivideByZero rather than the version error. Version-incompatibility becomes contract-shape-dependent instead of a clean "this binary does not understand this protocol version".

Both dissolve with one resolution at the top of evaluate() — e.g. a small struct FloatOps { pow: fn(f64, f64) -> f64, exp: fn(f64) -> f64, log: fn(f64) -> f64 } selected by a single match with a single error site — after which the arms just call ops.pow(..). That also matches the crate's usual shape of one version match at method entry.


🤖 Posted autonomously by Claude on behalf of pasta.

max_value: *max_value,
}
.evaluate(0, start_moment)?;
.evaluate(0, start_moment, platform_version)?;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration-time validation and payout-time evaluation can straddle the activation boundary.

This call computes start_token_amount, which then drives the coherence checks a few lines below (start_token_amount == *maxInvalidTokenDistributionFunctionIncoherenceError). Pre-PR there was exactly one implementation, so those checks provably described the numbers emission would later produce. Now validation runs under the version active at registration and emission under the version active at claim.

Concretely: a contract registered under PLATFORM_V12 whose start_token_amount via powf is max_value - 1 passes the incoherence check; after activation libm::pow returns max_value, so the distribution is pinned at its cap from cycle 0 — precisely the degenerate state this validation exists to reject. It is now permanently on-chain in a state the validator would have refused, and there is no re-validation path that catches it.

The mirror case is worse: the accept/reject boundary for new contracts silently moves at the fork, with no version gate on the validation rule itself. Same at lines 447, 637, 815 and 979. Worth confirming this is understood and bounded (the window is narrow — it needs the value to land within one ulp of a clamp boundary), or gating the validation rule alongside the math.


🤖 Posted autonomously by Claude on behalf of pasta.

0 => argument.ln(),
1 => log(argument),
version => {
return Err(ProtocolError::UnknownVersionMismatch {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-existing, but directly under the new correctly-named "DistributionFunction::evaluate (Logarithmic)" string you just added: the whole Logarithmic branch reports its overflow errors as InvertedLogarithmic — at line 453 ("InvertedLogarithmic: evaluation overflow") and again at lines 473, 476 and 486. Since you are already touching these exact lines and adding an accurate method: label three lines above, worth correcting the four strings in the same pass so a logarithmic-distribution overflow does not get triaged against the wrong variant.


🤖 Posted autonomously by Claude on behalf of pasta.

while current_point <= last_step {
let base_amount =
self.evaluate(distribution_start_step.to_u64(), current_point.to_u64())?;
self.evaluate(distribution_start_step.to_u64(), current_point.to_u64(), platform_version)?;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is hard-failing on formatting, so the Rust test suite (including the four new libm tests) has never run on CI for this PR. cargo fmt --check --all reports 44 unformatted hunks, all in the 3 files this PR touched (39 in evaluate.rs, 2 production lines here at 1653/1843, 3 in block_based.rs) — introduced by appending the new platform_version/PlatformVersion::latest() arguments past 100 cols. The "Rust workspace tests" job aborts at "Check formatting" before clippy or any test. Running cargo fmt --all and pushing fixes it.


🤖 Posted autonomously by Claude on behalf of pasta.

"draft202012",
], optional = true }
lazy_static = { version = "1.4" }
libm = "0.2"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

libm = "0.2" should be pinned exactly (= "0.2.16"). The determinism guarantee of the v1 path is "every node runs the same libm code", but a caret range lets any rebuild with a refreshed lock (or a downstream consumer not using this repo's Cargo.lock) resolve a different 0.2.x, and libm has shipped accuracy changes to pow/exp/log in patch releases. A one-ulp difference flips the truncating as i128/as u64 casts and re-creates exactly the consensus divergence this PR exists to fix — now dependent on Cargo resolution instead of the OS. This manifest already pins bincode = "=2.0.1" two lines down for the same class of reason.

Notably, the boundary case the new polynomial test locks in is razor-thin: the exact value of 125^(f64(1/3)) is 4.99999999999999955…, so libm 0.2.16 returning exactly 5.0 is within half an ulp of returning 4.999999999999999 (what Apple/glibc pow return). Any future libm patch that moves pow by one ulp changes consensus results at version 1.

Suggested change
libm = "0.2"
libm = "=0.2.16"

🤖 Posted autonomously by Claude on behalf of pasta.

start_from_moment_for_distribution,
max_cycle_moment,
None,
platform_version,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Activation retroactively re-prices historical cycles. rewards_in_interval walks every cycle from the last-paid moment (bounded by max redemption cycles) and evaluates each with the platform version active at claim time, not at the cycle's own moment. At the v0→v1 flip, two identities with identical distributions and identical unclaimed pre-fork cycles get different totals depending solely on whether they claim one block before or after activation (e.g. a Polynomial{m:1,n:3} cycle paying 5 under std powf and 4 under libm, or vice versa). Realized emission for a fixed historical range stops being a function of the range.

This may be an acceptable trade-off (per-moment gating would require threading the moment→version mapping through here), but it changes already-accrued rewards and deserves an explicit decision in the PR description / activation plan rather than being implicit.


🤖 Posted autonomously by Claude on behalf of pasta.

/// NOTE: Not yet wired to any `PlatformVersion::PLATFORM_V*`. This constant sets
/// `distribution_function_evaluate_version: 1` (deterministic libm reward math), but
/// activation is deferred to a follow-up `PLATFORM_V13` PR. Until then it has no consumer.
pub const TOKEN_VERSIONS_V3: DPPTokenVersions = DPPTokenVersions {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix ships inert, and nothing will catch the follow-up not landing. TOKEN_VERSIONS_V3 has no consumer — PLATFORM_V12 (latest) uses TOKEN_VERSIONS_V2 with distribution_function_evaluate_version: 0 — so after this breaking-API change merges, every reachable code path still runs the platform-dependent std math and the cross-platform divergence in the PR title remains fully live. That is documented here as intentional, but two consequences are worth addressing now:

  1. No test asserts that any real PlatformVersion reaches version 1 (the four new tests hand-mutate a cloned version). If the PLATFORM_V13 PR copy-pastes distribution_function_evaluate_version: 0, the suite stays green and the fix silently never activates.
  2. This is the only unreferenced *_VERSIONS_Vn constant in the crate; its other five fields are frozen now and won't be re-reviewed when V13 finally references it. Consider dropping v3.rs from this PR and introducing it in the activation PR instead — this PR only needs the new field plus the 0 defaults in v1/v2.

🤖 Posted autonomously by Claude on behalf of pasta.

max_value: None,
};

// cbrt(125) is exactly 5. The std f64 powf() on some platforms rounds

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is factually inverted, and the assertion is one ulp from flipping. With m: 1, n: 3, exponent is the f64 nearest 1/3 (strictly below it), so the exact value of 125^exponent is 4.99999999999999955… — below 5. A correctly-rounded pow (Apple, glibc) returns 4.999999999999999, which truncates to 4; the libm crate returning exactly 5.0 is the implementation-specific outlier this test locks in, not the "correct" cube root the comment describes. The test is still valid as a bit-determinism regression guard (libm is the same everywhere), but the comment should say that, otherwise the natural response to a future 4 != 5 failure (e.g. a libm patch bump) will be to "fix" the assertion and silently change consensus math.

Separately: none of the four new tests asserts what v0 and v1 produce for the same fixtures (e.g. the exact-value inverted-log fixtures in block_based.rs asserting 85171/78240/16094 under v0). Parameterizing a few existing fixtures over both versions would document the magnitude of the v12→v13 reward discontinuity before the fork instead of at it.


🤖 Posted autonomously by Claude on behalf of pasta.

}

let diff_exp = (diff as f64).powf(exponent);
let diff_exp = match platform_version

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Four copies of this dispatch, and the version check is non-uniform across variants. This 15-line match … { 0 => std, 1 => libm, v => Err(UnknownVersionMismatch) } block is repeated verbatim at the Exponential, Logarithmic, and InvertedLogarithmic arms, each with its own hardcoded known_versions: vec![0, 1]. Two concrete costs:

  1. A future distribution_function_evaluate_version: 2 that changes only one operation requires editing four blocks; missing one still compiles and yields a mixed-version evaluator — either a silent consensus split or UnknownVersionMismatch on three of the four curve types while the fourth works.
  2. FixedAmount/Stepwise/Linear/StepDecreasingAmount/Random never read the version, so an unknown version is a hard error for a Polynomial contract but silent success for a FixedAmount one.

Both collapse if the version is resolved once at the top of evaluate() into three fn pointers (pow/exp/ln) — one match, one error site, uniform rejection — or via small private versioned_pow/exp/ln(pv) helpers. Sibling token methods (token_config_update, token_set_price) already follow single-point dispatch on the same dpp.token_versions struct.


🤖 Posted autonomously by Claude on behalf of pasta.

while current_point <= last_step {
let base_amount =
self.evaluate(distribution_start_step.to_u64(), current_point.to_u64())?;
self.evaluate(distribution_start_step.to_u64(), current_point.to_u64(), platform_version)?;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI is red at Check formatting, so the Rust test suite never ran on this PR. cargo fmt --check --all reports 44 unformatted hunks, all in the 3 Rust files this PR touches (39 in evaluate.rs, 2 production lines here at 1653/1843, 3 in block_based.rs) — the added platform_version / PlatformVersion::latest() arguments pushed lines past 100 cols. Because the macOS workspace job fails at the fmt step, clippy and every Rust test (including the four new libm-path tests) were skipped in CI; they currently pass only in local runs. cargo fmt --all and repush.


🤖 Posted autonomously by Claude on behalf of pasta.

"draft202012",
], optional = true }
lazy_static = { version = "1.4" }
libm = "0.2"

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

libm = "0.2" is a caret range on the crate whose bit-exact output is the entire premise of this fix. libm 0.2.x patch releases have changed pow/exp/log implementations; a regenerated lockfile (or any downstream consumer resolving a different 0.2.x) can shift results by 1 ulp, and the as i128/as u64 truncations in evaluate() turn a 1-ulp difference into a whole-token difference — the same consensus-divergence class this PR fixes, relocated from "OS libm" to "Cargo resolution". This manifest already pins bincode = "=2.0.1" two lines down for the same reason.

Suggested change
libm = "0.2"
libm = "=0.2.16"

🤖 Posted autonomously by Claude on behalf of pasta.

start_from_moment_for_distribution,
max_cycle_moment,
None,
platform_version,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Activation will retroactively re-price already-accrued cycles. rewards_in_interval walks every unclaimed cycle from the last-paid moment (bounded only by max redemption cycles) and evaluates each one with the platform version active at claim time, not at the cycle's own moment. When distribution_function_evaluate_version flips 0→1, two identities with identical pre-fork accrual get different totals depending on whether they claim before or after activation (e.g. the Polynomial{m:1,n:3} boundary case pays 5/cycle under libm where std paid 4). If that's acceptable, it's worth stating in the activation PR; if not, the version used per cycle would need to derive from the cycle's moment rather than the claim's.


🤖 Posted autonomously by Claude on behalf of pasta.

/// NOTE: Not yet wired to any `PlatformVersion::PLATFORM_V*`. This constant sets
/// `distribution_function_evaluate_version: 1` (deterministic libm reward math), but
/// activation is deferred to a follow-up `PLATFORM_V13` PR. Until then it has no consumer.
pub const TOKEN_VERSIONS_V3: DPPTokenVersions = DPPTokenVersions {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix ships inert, and nothing guards its activation. TOKEN_VERSIONS_V3 has no consumer — LATEST_PLATFORM_VERSION is PLATFORM_V12, which uses TOKEN_VERSIONS_V2 (distribution_function_evaluate_version: 0) — so every reachable PlatformVersion still takes the std powf/exp/ln path and the cross-platform divergence in the PR title remains live after merge. That's documented as intentional, but two follow-ups are unguarded: (1) no test asserts that any real platform version reaches the libm path, so a PLATFORM_V13 that copy-pastes : 0 passes green and the fix silently never activates; (2) this is the only unreferenced *_VERSIONS_Vn const in the crate, and its other five fields can drift stale before V13 lands. Consider landing this file with the activation PR instead, or adding a test pinning PLATFORM_V13.dpp.token_versions.distribution_function_evaluate_version == 1 when it exists.


🤖 Posted autonomously by Claude on behalf of pasta.

}

let diff_exp = (diff as f64).powf(exponent);
let diff_exp = match platform_version

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This 15-line version-dispatch block is copy-pasted 4× (here, :346, :435, :587), and the check is non-uniform across variants. Consequences: a future version 2 requires editing four blocks and four known_versions: vec![0, 1] literals — miss one and a single distribution shape keeps old math on the new protocol version (compiles fine, splits consensus); and FixedAmount/Stepwise/Linear/StepDecreasingAmount/Random never read the version at all, so an unknown version errors for a Polynomial claim but silently succeeds for a FixedAmount one. Resolving once at the top of evaluate() — e.g. a small FloatOps { pow, exp, ln } selected by a single match (or versioned_pow/versioned_exp/versioned_ln helpers) — gives one error site, uniform rejection, and one place to add version 2. This would also match the crate's usual pattern of dispatching once at method entry (cf. token_config_update_transition/v0_methods.rs).


🤖 Posted autonomously by Claude on behalf of pasta.

max_value: None,
};

// cbrt(125) is exactly 5. The std f64 powf() on some platforms rounds

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment has the rounding story backwards, and the assertion sits one ulp from flipping. 1.0/3.0 rounds below 1/3, so the exact value of 125^fl(1/3) is 4.99999999999999955… — a correctly-rounded pow (glibc, Apple libm) returns 4.999999999999999 and truncates to 4. It's the libm crate's pow that returns the 1-ulp-high 5.0 making this assert pass; "std powf on some platforms rounds below 5.0" describes the mathematically-correct behavior, not a platform quirk. The determinism claim (libm = same bits everywhere) still holds, but (a) if a future libm release becomes correctly rounded this test fails as a mysterious 4 != 5 and the tempting "fix" erases the only regression guard on the consensus path, and (b) no test locks the v0 vs v1 divergence on existing fixtures (e.g. the exact-value inverted-log fixtures in block_based.rs), so the magnitude of the v12→v13 reward discontinuity is undocumented until fork day. Suggest rewording the comment and adding a v0-vs-v1 comparison over the existing fixture set.


🤖 Posted autonomously by Claude on behalf of pasta.

max_value: *max_value,
}
.evaluate(0, start_moment)?;
.evaluate(0, start_moment, platform_version)?;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration-time validation and payout evaluation can disagree across the activation boundary. These coherence checks (e.g. rejecting a contract whose start_token_amount already sits at max_value) run once, at registration, with the version active then; payout runs forever with the version active at claim time. A contract validated pre-activation whose std-powf start value is one ulp below max_value can, post-activation, evaluate to exactly max_value under libm — the precise degenerate shape this check exists to reject, now permanently on-chain. Probably acceptable (the divergence window is 1 ulp), but worth a conscious decision in the activation PR rather than an accident.


🤖 Posted autonomously by Claude on behalf of pasta.

max_value: *max_value,
}
.evaluate(0, start_moment)?;
.evaluate(0, start_moment, platform_version)?;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration-time validation and payout evaluation can disagree across the activation boundary. These coherence checks (e.g. rejecting a contract whose start_token_amount already sits at max_value) now evaluate with whatever version is active at registration. A contract registered pre-activation whose v0 (powf) start value is one ulp below max_value passes validation; post-activation, v1 (pow) can put the same contract exactly at max_value from cycle 0 — the degenerate shape this check exists to reject — and it is permanently on-chain with no re-validation. Probably acceptable (any version gate on evaluation implies this), but worth stating as a known consequence, since validation runs once and evaluation runs forever.


🤖 Posted autonomously by Claude on behalf of pasta.

1 => log(argument),
version => {
return Err(ProtocolError::UnknownVersionMismatch {
method: "DistributionFunction::evaluate (Logarithmic)".to_string(),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, pre-existing but right below this correctly-labeled string: the Logarithmic arm's error messages at :454, :473, :478, :486, :492, :499 all say "InvertedLogarithmic: …" (copy-paste from the real InvertedLogarithmic arm at :587+). Since this PR is already touching this arm, worth fixing the labels so version-mismatch and overflow errors from the two branches are distinguishable in logs.


🤖 Posted autonomously by Claude on behalf of pasta.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants