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
147 changes: 147 additions & 0 deletions extensions/tn_utils/precompiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/binary"
"fmt"
"math"
"math/big"

"github.com/trufnetwork/kwil-db/common"
"github.com/trufnetwork/kwil-db/core/types"
Expand All @@ -27,6 +28,7 @@ func buildPrecompile() precompiles.Precompile {
encodeUintMethod("encode_uint64", 64),
canonicalToDataPointsABIMethod(),
forceLastArgFalseMethod(),
parseAttestationBooleanMethod(),
},
}
}
Expand Down Expand Up @@ -426,3 +428,148 @@ func forceLastArgFalseHandler(ctx *common.EngineContext, app *common.App, inputs

return resultFn([]any{modifiedArgsBytes})
}

// parseAttestationBooleanMethod extracts a boolean result from an attestation's
// result_canonical field. This is used for prediction market settlement where
// attestations return boolean outcomes (YES=true, NO=false).
func parseAttestationBooleanMethod() precompiles.Method {
return precompiles.Method{
Name: "parse_attestation_boolean",
AccessModifiers: []precompiles.Modifier{precompiles.VIEW, precompiles.PUBLIC},
Parameters: []precompiles.PrecompileValue{
precompiles.NewPrecompileValue("result_canonical", types.ByteaType, false),
},
Returns: &precompiles.MethodReturn{
IsTable: false,
Fields: []precompiles.PrecompileValue{
precompiles.NewPrecompileValue("outcome", types.BoolType, false),
},
},
Handler: parseAttestationBooleanHandler,
}
}

// 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)
//
// The result_canonical format is:
// - version (uint8, 1 byte)
// - algo (uint8, 1 byte)
// - height (uint64, 8 bytes)
// - length_prefix(data_provider) (4 bytes length + N bytes data)
// - length_prefix(stream) (4 bytes length + N bytes data)
// - action_id (uint16, 2 bytes)
// - 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.
func parseAttestationBooleanHandler(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error {
resultCanonical, err := toByteSliceAllowNil(inputs[0])
if err != nil {
return fmt.Errorf("result_canonical must be bytea: %w", err)
}
if resultCanonical == nil || len(resultCanonical) == 0 {
return fmt.Errorf("result_canonical cannot be empty")
}

// Parse the canonical format
offset := 0

// Skip version (1 byte)
if offset+1 > len(resultCanonical) {
return fmt.Errorf("invalid result_canonical: too short for version")
}
offset += 1

// Skip algo (1 byte)
if offset+1 > len(resultCanonical) {
return fmt.Errorf("invalid result_canonical: too short for algo")
}
offset += 1

// Skip height (8 bytes, big-endian uint64)
if offset+8 > len(resultCanonical) {
return fmt.Errorf("invalid result_canonical: too short for height")
}
offset += 8

// Skip length_prefix(data_provider)
if offset+4 > len(resultCanonical) {
return fmt.Errorf("invalid result_canonical: too short for data_provider length")
}
dpLength := binary.BigEndian.Uint32(resultCanonical[offset : offset+4])
offset += 4 + int(dpLength)
if offset > len(resultCanonical) {
return fmt.Errorf("invalid result_canonical: data_provider data extends beyond buffer")
}

// Skip length_prefix(stream)
if offset+4 > len(resultCanonical) {
return fmt.Errorf("invalid result_canonical: too short for stream length")
}
streamLength := binary.BigEndian.Uint32(resultCanonical[offset : offset+4])
offset += 4 + int(streamLength)
if offset > len(resultCanonical) {
return fmt.Errorf("invalid result_canonical: stream data extends beyond buffer")
}

// Skip action_id (2 bytes, big-endian uint16)
if offset+2 > len(resultCanonical) {
return fmt.Errorf("invalid result_canonical: too short for action_id")
}
offset += 2

// Skip length_prefix(args)
if offset+4 > len(resultCanonical) {
return fmt.Errorf("invalid result_canonical: too short for args length")
}
argsLength := binary.BigEndian.Uint32(resultCanonical[offset : offset+4])
offset += 4 + int(argsLength)
if offset > len(resultCanonical) {
return fmt.Errorf("invalid result_canonical: args data extends beyond buffer")
}

// Read length_prefix(result_payload)
if offset+4 > len(resultCanonical) {
return fmt.Errorf("invalid result_canonical: too short for result_payload length")
}
resultLength := binary.BigEndian.Uint32(resultCanonical[offset : offset+4])
offset += 4
if offset+int(resultLength) > len(resultCanonical) {
return fmt.Errorf("invalid result_canonical: result_payload data extends beyond buffer")
}

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
decoded, err := dataPointsABIArgs.Unpack(resultPayload)
if err != nil {
return fmt.Errorf("failed to decode ABI result payload: %w", err)
}

if len(decoded) != 2 {
return fmt.Errorf("expected 2 arrays (timestamps, values), got %d", len(decoded))
}

// Extract values array (second element)
values, ok := decoded[1].([]*big.Int)
if !ok {
return fmt.Errorf("values must be []*big.Int, got %T", decoded[1])
}

if len(values) == 0 {
return fmt.Errorf("result payload contains no values")
}

// Use the latest value for settlement (last element)
// Prediction market pattern: value > 0 = YES (TRUE), value == 0 = NO (FALSE)
latestValue := values[len(values)-1]
outcome := latestValue.Sign() > 0

return resultFn([]any{outcome})
}
142 changes: 142 additions & 0 deletions internal/migrations/032-order-book-actions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2128,3 +2128,145 @@ CREATE OR REPLACE ACTION change_ask(
-- - Shares adjusted (pulled from or returned to holdings)
-- - FIFO priority maintained
};

-- =============================================================================
-- settle_market: Settle a prediction market using attestation results
-- =============================================================================
/**
* Settles a prediction market by retrieving the signed attestation and marking
* the winning outcome. This is a permissionless action - anyone can settle a
* market once the settle_time has been reached and the attestation is available.
*
* Settlement Process:
* 1. Validate market exists, not already settled, and settle_time reached
* 2. Query attestation by market hash (market.hash = attestation.attestation_hash)
* 3. Verify attestation has been signed
* 4. Parse result_canonical to extract boolean outcome (TRUE = YES wins, FALSE = NO wins)
* 5. Mark market as settled with winning_outcome and settled_at timestamp
*
* After settlement:
* - All trading is permanently blocked (buy, sell, split, cancel, change orders)
* - Users must call claim_payout() to redeem winning shares (Issue 8)
* - Market state is frozen and cannot be changed
*
* Parameters:
* - $query_id: Market ID from ob_queries.id
*
* Returns: Nothing (void action)
*
* Errors:
* - If market doesn't exist
* - If market is already settled
* - If settle_time has not been reached
* - If attestation doesn't exist for market hash
* - If attestation is not yet signed
* - If result parsing fails
*
* Examples:
* settle_market(1) -- Settle market ID 1 using its attestation
*
* Note: This action is part of Issue 7 (Manual Settlement). Automatic settlement
* via extension (Issue 7B) will call this action on a schedule.
*/
CREATE OR REPLACE ACTION settle_market(
$query_id INT
) PUBLIC {
-- ==========================================================================
-- SECTION 1: VALIDATE MARKET AND TIMING
-- ==========================================================================

-- Validate query_id
if $query_id IS NULL OR $query_id < 1 {
ERROR('Invalid query_id');
}

-- Get market details
$market_hash BYTEA;
$settle_time INT8;
$settled BOOL;
$market_found BOOL := false;

for $row in SELECT hash, settle_time, settled
FROM ob_queries
WHERE id = $query_id {
$market_hash := $row.hash;
$settle_time := $row.settle_time;
$settled := $row.settled;
$market_found := true;
}

if NOT $market_found {
ERROR('Market does not exist (query_id: ' || $query_id::TEXT || ')');
}

-- Check if already settled
if $settled {
ERROR('Market has already been settled');
}

-- Check if settle_time has been reached
-- Note: @block_timestamp is unix epoch seconds of current block
if @block_timestamp < $settle_time {
ERROR('Settlement time not yet reached. settle_time: ' || $settle_time::TEXT ||
', current time: ' || @block_timestamp::TEXT);
}

-- ==========================================================================
-- SECTION 2: RETRIEVE ATTESTATION
-- ==========================================================================

-- Query attestation by hash
-- Note: market.hash should match attestation.attestation_hash
-- This links the market to the cryptographic attestation of the query result
$result_canonical BYTEA;
$signature BYTEA;
$attestation_found BOOL := false;

for $row in SELECT result_canonical, signature
FROM attestations
WHERE attestation_hash = $market_hash
ORDER BY signed_height DESC
LIMIT 1 {
$result_canonical := $row.result_canonical;
$signature := $row.signature;
$attestation_found := true;
}

if NOT $attestation_found {
ERROR('Attestation not found for market hash. Market cannot be settled without attestation.');
}

-- Verify attestation has been signed
if $signature IS NULL {
ERROR('Attestation not yet signed by validator. Please wait for signing to complete.');
}

-- ==========================================================================
-- SECTION 3: PARSE ATTESTATION RESULT
-- ==========================================================================

-- Parse result_canonical to extract boolean outcome
-- Uses tn_utils.parse_attestation_boolean() precompile
-- Returns: TRUE (YES wins) or FALSE (NO wins)
$winning_outcome BOOL := tn_utils.parse_attestation_boolean($result_canonical);

if $winning_outcome IS NULL {
ERROR('Failed to parse attestation result (result is NULL)');
}

-- ==========================================================================
-- SECTION 4: MARK MARKET AS SETTLED
-- ==========================================================================

-- Update market with settlement information
UPDATE ob_queries
SET settled = true,
winning_outcome = $winning_outcome,
settled_at = @block_timestamp
WHERE id = $query_id;

-- Success: Market settled
-- - All trading is now blocked
-- - winning_outcome is recorded (TRUE = YES wins, FALSE = NO wins)
-- - Users can now call claim_payout() to redeem winning shares (Issue 8)
};
Loading
Loading