Skip to content

feat: attest an index's rate of change at a point in time - #1415

Merged
MicBun merged 1 commit into
mainfrom
feat/index-change-attestation
Aug 24, 2026
Merged

feat: attest an index's rate of change at a point in time#1415
MicBun merged 1 commit into
mainfrom
feat/index-change-attestation

Conversation

@MicBun

@MicBun MicBun commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Streams that publish an index level rather than a rate cannot back an inflation-rate market. BLS CPI publishes ~335 and the Truflation PCE Index ~155, so the value actions compare a level against a rate and the outcome means nothing. get_index_change computes the rate but returns a series, and multi-row actions are blocked from attestation.

This adds index_change_in_range, action id 12: a single-row binary action that computes the percentage change over an interval and returns one boolean.

After this PR the action can be attested. Settling a market on it needs IsBinaryAction to accept id 12, which is the next PR.

The action

index_change_in_range(data_provider, stream_id, timestamp, base_time,
                      time_interval, min_change, max_change, frozen_at)

timestamp stays at argument 2 and frozen_at stays last, matching every binary action in migration 040, so the SDK and indexer decoders need a name added rather than a rewrite.

Nullable bounds cover every outcome of a bucketed market with one action id:

min max outcome
NULL 1.335 Below 1.335%
1.335 1.605 1.335% - 1.605%
2.246 NULL Above 2.246%
0 NULL did the rate rise at all?

That last row is the market the goal issue describes, so one action covers both it and the strike buckets. Three separate actions would cost three ids and three bodies in each of six hand-maintained registries and buy nothing, since the bounds already carry the below/between/above distinction.

Bounds are half-open, [min, max), so the five buckets of a market tile the number line exactly once. The 040 family is inclusive on both ends of value_in_range, which lets a value landing exactly on an interior boundary satisfy two adjacent buckets. testIndexChangeInRangeBucketsTileOnce asserts the new behaviour.

It reads through get_index, not primitive_events

The number a market settles on has to be the number the rest of the product displays, which is what get_index_change returns for the same base_time. For a primitive stream the base value cancels out of the ratio and the choice makes no difference. For a composed stream it does: get_index_composed weights indexed children, so the ratio of composed raw records is not the ratio of the composed index. testIndexChangeInRangeMatchesIndexChangeComposed covers that case with unequal child weights.

Staleness

The current anchor keeps the one-day rule the 040 actions use. It is a freshness check: it refuses to settle today's market on last week's value.

The prior anchor scales with the interval instead. Staleness means nothing for a historical lookup, so its only job is to refuse when the stream has a hole where the comparison point belongs. A year-over-year market accepts a prior print up to a year old, a month-over-month market accepts a month, and a stream too young to have a comparison point is refused rather than settled on the wrong number.

Both checks compare event_time explicitly rather than narrowing the range, because get_record unions an LOCF anchor at or before from. A read of [T - W, T] returns a value even when the window itself is empty, and that value can be arbitrarily old, so an empty result cannot be used to mean "nothing fresh here".

Two things outside the action

call_dispatch was discarding every action error. It called the engine and threw away the call result, and an ERROR() raised inside a dispatched action arrives there rather than in err. So request_attestation charged its 40 TRUF, ran an action that failed, and stored an attestation over an empty row set instead of refusing. This affected every attestation action, not the new one: "no data found", "wallet not allowed to read", a stale value.

It is fixed here because the new action's refusals depend on it. A refused attestation now fails the request and the settlement scheduler retries, which is what the surrounding code already assumed.

Two test groups were passing vacuously, which the fix surfaced.

TestRequestAttestationFees listed its setup as a FunctionTest of its own. Every function test in a suite runs against a fresh container, so the stream it created was gone before any test ran. All five tests dispatched get_last_record against a stream that did not exist and passed anyway. The fee assertions still held, since the fee is charged before dispatch, so the suite was measuring fees correctly and attesting nothing. Setup now runs inside each test.

Test 6 of TestAttestationDateRangeValidation requested a price_above_threshold attestation at timestamp 1000000 with the block clock at 0, so validate_not_before_timestamp refused it every time while the test asserted it should pass. The request helper now takes a block timestamp.

Verification

go test -tags kwiltest ./tests/streams/attestation/ and ./tests/streams/ -run TestIndexChangeInRange, both green, plus ./tests/streams/order_book/ for the call_dispatch change. kwil-cli utils parse on the migration.

The agreement with get_index_change is asserted by squeezing the action between two probes: [V, ∞) resolving TRUE and [V + 1ulp, ∞) resolving FALSE means the change equals V at all 18 decimal places. It runs at six points of a series with a deliberate gap, so the fallback both sides make to an earlier record is covered, and again on a composed stream.

Problem

Summary by CodeRabbit

  • New Features

    • Added an attestation action to verify whether an index change falls within a specified range.
    • Supports bounded and open-ended ranges, historical lookups, and percentage-change calculations.
    • Added validation for timing, intervals, stale data, and range boundaries.
  • Bug Fixes

    • Errors returned by dispatched actions are now surfaced correctly instead of appearing successful.
  • Tests

    • Added comprehensive coverage for valid, invalid, boundary, historical, and stale-data scenarios.

@MicBun MicBun self-assigned this Aug 24, 2026
@holdex

holdex Bot commented Aug 24, 2026

Copy link
Copy Markdown

Time Submission Status

Member # Time Running Total Status Last Update
MicBun 5h ✅ Submitted Aug 24, 2026, 5:37 PM

Submit or update total time with:

@holdex pr submit-time 2h

Add time on top of previous submission with:

@holdex pr add-time 1h30m

See available commands to help comply with our Guidelines.

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6c692e22-e11b-42b0-897e-038547f540b9

📥 Commits

Reviewing files that changed from the base of the PR and between b91481b and 1e26aae.

📒 Files selected for processing (5)
  • extensions/tn_utils/precompiles.go
  • internal/migrations/055-index-change-attestation-action.sql
  • tests/streams/attestation/attestation_date_range_test.go
  • tests/streams/attestation/request_attestation_fee_test.go
  • tests/streams/index_change_in_range_test.go

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Adds attestation action 12, index_change_in_range, with interval validation, historical indexed-value lookup, percentage-change range checks, dispatch error propagation, and integration coverage.

Changes

Index Change Attestation

Layer / File(s) Summary
Dispatch registration and validation
extensions/tn_utils/precompiles.go
Registers action 12, maps index_change_in_range for hash construction, validates positive intervals, and propagates dispatched action errors.
Indexed percentage-change action
internal/migrations/055-index-change-attestation-action.sql
Adds indexed-value lookup, input validation, freshness checks, percentage-change calculation, and half-open range evaluation.
Action behavior integration coverage
tests/streams/index_change_in_range_test.go
Tests bounded and open ranges, boundary behavior, composed streams, stale data, validation errors, timestamps, error propagation, and fixed-point precision.
Attestation test context updates
tests/streams/attestation/attestation_date_range_test.go, tests/streams/attestation/request_attestation_fee_test.go
Adds action registration tests, explicit block timestamps, and per-test environment setup for attestation fee scenarios.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to 1e26a

This PR adds a localized index-change attestation action and related test fixes; no actionable merge-blocking risk remains after normal checks and review.

Sequence Diagram(s)

sequenceDiagram
  participant AttestationRequest
  participant callDispatchHandler
  participant index_change_in_range
  participant get_indexed_value_at
  participant get_index
  AttestationRequest->>callDispatchHandler: dispatch action 12
  callDispatchHandler->>index_change_in_range: execute validated arguments
  index_change_in_range->>get_indexed_value_at: retrieve current and prior values
  get_indexed_value_at->>get_index: query indexed value at timestamp
  get_index-->>get_indexed_value_at: return indexed value
  get_indexed_value_at-->>index_change_in_range: return fresh values
  index_change_in_range-->>callDispatchHandler: return boolean or action error
  callDispatchHandler-->>AttestationRequest: return result or propagated error
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring check was indeterminate for this PR — some files could not be analyzed in time. Not blocking.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding attestation support for an index's rate of change at a specified time.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/index-change-attestation

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.

@MicBun

MicBun commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

@holdex pr submit-time 5h

@MicBun
MicBun merged commit 7bbbd21 into main Aug 24, 2026
9 checks passed
@MicBun
MicBun deleted the feat/index-change-attestation branch August 24, 2026 17:52
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.

1 participant