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
78 changes: 76 additions & 2 deletions extensions/tn_utils/datapoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import (
"math/big"

gethAbi "github.com/ethereum/go-ethereum/accounts/abi"
"github.com/trufnetwork/kwil-db/common"
"github.com/trufnetwork/kwil-db/core/types"
)

const dataPointTargetScale uint16 = 18

var dataPointsABIArgs gethAbi.Arguments
var (
dataPointsABIArgs gethAbi.Arguments
booleanABIArgs gethAbi.Arguments
)

func init() {
uint256Slice, err := gethAbi.NewType("uint256[]", "", nil)
Expand All @@ -25,16 +29,86 @@ func init() {
{Type: uint256Slice},
{Type: int256Slice},
}

// Boolean ABI type for binary action results
boolType, err := gethAbi.NewType("bool", "", nil)
if err != nil {
panic(fmt.Sprintf("tn_utils: failed to initialise bool ABI type: %v", err))
}
booleanABIArgs = gethAbi.Arguments{
{Type: boolType},
}
}

// EncodeDataPointsABI converts the canonical result serialization produced by
// call_dispatch into abi.encode(uint256[] timestamps, int256[] values).
// call_dispatch into ABI-encoded format.
//
// For numeric data actions (get_record, get_index, etc.):
// - Returns abi.encode(uint256[] timestamps, int256[] values)
//
// For binary actions (price_above_threshold, value_in_range, etc.):
// - Returns abi.encode(bool result)
//
// Binary results are auto-detected: single row with single boolean column.
func EncodeDataPointsABI(canonical []byte) ([]byte, error) {
rows, err := DecodeQueryResultCanonical(canonical)
if err != nil {
return nil, err
}

// Check if this is a binary action result (single row, single boolean column)
if isBooleanResult(rows) {
return encodeBooleanResult(rows[0])
}

// Standard numeric datapoints encoding
return encodeNumericDataPoints(rows)
}

// isBooleanResult checks if the result is from a binary action
// Binary actions return exactly 1 row with 1 boolean column
func isBooleanResult(rows []*common.Row) bool {
if len(rows) != 1 {
return false
}
if len(rows[0].Values) != 1 {
return false
}

// Check if the value is a boolean
switch rows[0].Values[0].(type) {
case bool, *bool:
return true
default:
return false
}
}

// encodeBooleanResult encodes a single boolean result as abi.encode(bool)
func encodeBooleanResult(row *common.Row) ([]byte, error) {
var result bool

switch v := row.Values[0].(type) {
case bool:
result = v
case *bool:
if v == nil {
return nil, fmt.Errorf("boolean result is NULL")
}
result = *v
default:
return nil, fmt.Errorf("expected boolean result, got %T", row.Values[0])
}

packed, err := booleanABIArgs.Pack(result)
if err != nil {
return nil, fmt.Errorf("abi encode boolean: %w", err)
}
return packed, nil
}

// encodeNumericDataPoints encodes numeric datapoints as abi.encode(uint256[], int256[])
func encodeNumericDataPoints(rows []*common.Row) ([]byte, error) {
timestamps := make([]*big.Int, 0, len(rows))
values := make([]*big.Int, 0, len(rows))

Expand Down
75 changes: 62 additions & 13 deletions extensions/tn_utils/precompiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,10 +455,9 @@ func parseAttestationBooleanMethod() precompiles.Method {

// parseAttestationBooleanHandler parses result_canonical to extract a boolean outcome.
//
// This handler supports both native boolean results and numeric results (standard stream format).
// For numeric results (from primitive streams):
// - value > 0 → TRUE (YES wins)
// - value == 0 → FALSE (NO wins)
// This handler supports both:
// 1. Binary action results (action_id 6-9): 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:
// - version (uint8, 1 byte)
Expand All @@ -470,7 +469,7 @@ func parseAttestationBooleanMethod() precompiles.Method {
// - length_prefix(args) (4 bytes length + N bytes data)
// - length_prefix(result_payload) (4 bytes length + N bytes data)
//
// We parse through the structure to reach result_payload, then decode the value.
// We parse through the structure to reach result_payload, then decode based on action_id.
func parseAttestationBooleanHandler(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error {
resultCanonical, err := toByteSliceAllowNil(inputs[0])
if err != nil {
Expand Down Expand Up @@ -521,10 +520,11 @@ func parseAttestationBooleanHandler(ctx *common.EngineContext, app *common.App,
return fmt.Errorf("invalid result_canonical: stream data extends beyond buffer")
}

// Skip action_id (2 bytes, big-endian uint16)
// Read action_id (2 bytes, big-endian uint16) - we need this to determine decoding format
if offset+2 > len(resultCanonical) {
return fmt.Errorf("invalid result_canonical: too short for action_id")
}
actionID := binary.BigEndian.Uint16(resultCanonical[offset : offset+2])
offset += 2

// Skip length_prefix(args)
Expand All @@ -549,8 +549,49 @@ func parseAttestationBooleanHandler(ctx *common.EngineContext, app *common.App,

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

// Decode the result payload (ABI format: abi.encode(uint256[], int256[]))
// This format is used for EVM smart contract compatibility
// Check if this is a binary action (action_id 6-9)
// Binary actions return abi.encode(bool) directly
if IsBinaryAction(actionID) {
return parseBinaryActionResult(resultPayload, resultFn)
}

// Validate action_id is in supported range before decoding
if actionID < 1 || actionID > 9 {
return fmt.Errorf("unsupported action_id %d", actionID)
}

// Numeric action (action_id 1-5) - decode as abi.encode(uint256[], int256[])
return parseNumericActionResult(resultPayload, resultFn)
}

// parseBinaryActionResult decodes abi.encode(bool) and returns the boolean directly
func parseBinaryActionResult(resultPayload []byte, resultFn func([]any) error) error {
// ABI-encoded bool is 32 bytes (padded)
if len(resultPayload) != 32 {
return fmt.Errorf("binary action result must be 32 bytes (abi-encoded bool), got %d", len(resultPayload))
}

// Decode using the boolean ABI args
decoded, err := booleanABIArgs.Unpack(resultPayload)
if err != nil {
return fmt.Errorf("failed to decode boolean ABI result: %w", err)
}

if len(decoded) != 1 {
return fmt.Errorf("expected 1 value from boolean decode, got %d", len(decoded))
}

outcome, ok := decoded[0].(bool)
if !ok {
return fmt.Errorf("decoded value is not boolean, got %T", decoded[0])
}

return resultFn([]any{outcome})
}

// parseNumericActionResult decodes abi.encode(uint256[], int256[]) and interprets as boolean
// value > 0 = TRUE (YES wins), value == 0 = FALSE (NO wins)
func parseNumericActionResult(resultPayload []byte, resultFn func([]any) error) error {
decoded, err := dataPointsABIArgs.Unpack(resultPayload)
if err != nil {
return fmt.Errorf("failed to decode ABI result payload: %w", err)
Expand Down Expand Up @@ -705,20 +746,28 @@ func computeAttestationHashHandler(ctx *common.EngineContext, app *common.App, i
// getActionIDNumber maps action name to numeric ID (must match attestation_actions table)
func getActionIDNumber(actionName string) (uint16, error) {
actionMap := map[string]uint16{
// Numeric data actions (return TABLE(event_time INT8, value NUMERIC))
"get_record": 1,
"get_index": 2,
"get_change_over_time": 3,
"get_last_record": 4,
"get_first_record": 5,
// Future binary actions will be added here:
// "price_above_threshold": 6,
// "price_below_threshold": 7,
// "value_in_range": 8,
// Binary actions (return TABLE(result BOOLEAN)) - for prediction market settlement
"price_above_threshold": 6,
"price_below_threshold": 7,
"value_in_range": 8,
"value_equals": 9,
}

id, ok := actionMap[actionName]
if !ok {
return 0, fmt.Errorf("unknown action: %s (must be one of: get_record, get_index, get_change_over_time, get_last_record, get_first_record)", actionName)
return 0, fmt.Errorf("unknown action: %s (valid actions: get_record, get_index, get_change_over_time, get_last_record, get_first_record, price_above_threshold, price_below_threshold, value_in_range, value_equals)", actionName)
}
return id, nil
}

// 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)
func IsBinaryAction(actionID uint16) bool {
return actionID >= 6 && actionID <= 9
}
Loading
Loading