feat: attest an index's rate of change at a point in time - #1415
Conversation
Time Submission Status
Submit or update total time with: Add time on top of previous submission with: See available commands to help comply with our Guidelines. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughAdds attestation action 12, ChangesIndex Change Attestation
Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: ⚪ Minimal · up to 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
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
@holdex pr submit-time 5h |
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_changecomputes 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
IsBinaryActionto 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)timestampstays at argument 2 andfrozen_atstays 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:
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 ofvalue_in_range, which lets a value landing exactly on an interior boundary satisfy two adjacent buckets.testIndexChangeInRangeBucketsTileOnceasserts 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_changereturns for the samebase_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_composedweights indexed children, so the ratio of composed raw records is not the ratio of the composed index.testIndexChangeInRangeMatchesIndexChangeComposedcovers 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_timeexplicitly rather than narrowing the range, becauseget_recordunions an LOCF anchor at or beforefrom. 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_dispatchwas discarding every action error. It called the engine and threw away the call result, and anERROR()raised inside a dispatched action arrives there rather than inerr. Sorequest_attestationcharged 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.
TestRequestAttestationFeeslisted its setup as aFunctionTestof 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 dispatchedget_last_recordagainst 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
TestAttestationDateRangeValidationrequested aprice_above_thresholdattestation at timestamp 1000000 with the block clock at 0, sovalidate_not_before_timestamprefused 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 thecall_dispatchchange.kwil-cli utils parseon the migration.The agreement with
get_index_changeis 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
Bug Fixes
Tests