Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions extensions/tn_utils/precompiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -747,8 +747,8 @@ const MaxAttestationDateRangeSeconds int64 = 90 * 24 * 60 * 60 // 7,776,000 seco

// IndexChangeInRangeActionID is the attestation action id of index_change_in_range, registered by
// migration 055. Untyped so it can serve both the int64 comparison in the validation handler and the
// uint16 entry in getActionIDNumber, which have to agree with each other and with the
// attestation_actions row.
// uint16 entries in getActionIDNumber and IsBinaryAction, which have to agree with each other and
// with the attestation_actions row.
const IndexChangeInRangeActionID = 12

// validateAttestationDateRangeMethod validates attestation action eligibility:
Expand Down Expand Up @@ -983,7 +983,7 @@ func parseAttestationBooleanMethod() precompiles.Method {
// parseAttestationBooleanHandler parses result_canonical to extract a boolean outcome.
//
// This handler supports both:
// 1. Binary action results (action_id 6-9): Direct boolean encoded as abi.encode(bool)
// 1. Binary action results (action_id 6-9 and 12): Direct boolean encoded as abi.encode(bool)
// 2. Numeric results (action_id 1-5): Interpreted as value > 0 = TRUE, value == 0 = FALSE
//
// The result_canonical format is:
Expand Down Expand Up @@ -1076,7 +1076,7 @@ func parseAttestationBooleanHandler(ctx *common.EngineContext, app *common.App,

resultPayload := resultCanonical[offset : offset+int(resultLength)]

// Check if this is a binary action (action_id 6-9)
// Check if this is a binary action (action_id 6-9 and 12)
// Binary actions return abi.encode(bool) directly
if IsBinaryAction(actionID) {
return parseBinaryActionResult(resultPayload, resultFn)
Expand Down Expand Up @@ -1248,8 +1248,13 @@ func getActionIDNumber(actionName string) (uint16, error) {

// IsBinaryAction returns true if the action ID corresponds to a binary action
// that returns TABLE(result BOOLEAN) instead of TABLE(event_time INT8, value NUMERIC)
//
// Membership is explicit rather than a range. Ids 10 and 11 (get_high_value, get_low_value) sit
// between the 040 family and index_change_in_range and are numeric, so widening the bound to reach
// 12 would also admit them to settlement under the "value > 0 = YES" rule, where a high or low
// price resolves YES for essentially any stream.
func IsBinaryAction(actionID uint16) bool {
return actionID >= 6 && actionID <= 9
return (actionID >= 6 && actionID <= 9) || actionID == IndexChangeInRangeActionID
}

// unpackQueryComponents extracts (dataProvider, streamID, actionID, args) from ABI-encoded bytes.
Expand Down
46 changes: 46 additions & 0 deletions extensions/tn_utils/precompiles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,49 @@ func computeExpectedAttestationHash(t *testing.T, dataProvider, streamID, action
hash := sha256.Sum256(buffer.Bytes())
return hash[:]
}

// TestIsBinaryAction pins which action ids settle as a boolean rather than as a number.
//
// The set is deliberately not contiguous. Ids 10 and 11 (get_high_value, get_low_value) were
// registered for attestation by migration 045 and were never wired for settlement, and they are
// numeric: settlement would read them under the "value > 0 = YES" rule, so a market on either would
// resolve YES for essentially any price stream. Reaching id 12 by widening the upper bound would
// admit them silently, which is why the assertions below cover the gap and not just the members.
func TestIsBinaryAction(t *testing.T) {
binary := map[uint16]string{
6: "price_above_threshold",
7: "price_below_threshold",
8: "value_in_range",
9: "value_equals",
12: "index_change_in_range",
}
for actionID, name := range binary {
require.True(t, IsBinaryAction(actionID), "%s (id %d) should settle as a boolean", name, actionID)
}

numeric := map[uint16]string{
1: "get_record",
2: "get_index",
3: "get_change_over_time",
4: "get_last_record",
5: "get_first_record",
10: "get_high_value",
11: "get_low_value",
}
for actionID, name := range numeric {
require.False(t, IsBinaryAction(actionID), "%s (id %d) should not settle as a boolean", name, actionID)
}

require.False(t, IsBinaryAction(0), "id 0 is not an action")
require.False(t, IsBinaryAction(13), "id 13 is unregistered")
}

// TestIndexChangeInRangeActionIDAgreesWithRegistry keeps the constant, the name map, and the
// settlement predicate from drifting apart. Nothing else checks that they agree, and a mismatch
// would let markets be created against an id that can never be settled.
func TestIndexChangeInRangeActionIDAgreesWithRegistry(t *testing.T) {
id, err := getActionIDNumber("index_change_in_range")
require.NoError(t, err, "index_change_in_range should be a known action")
require.Equal(t, uint16(IndexChangeInRangeActionID), id, "name map must agree with the constant")
require.True(t, IsBinaryAction(id), "index_change_in_range must settle as a boolean")
}
Loading
Loading