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
16 changes: 7 additions & 9 deletions deployments/infra/go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
module github.com/trufnetwork/node/infra

go 1.24.1

toolchain go1.24.4
go 1.25.3

require (
github.com/BurntSushi/toml v1.5.0
Expand All @@ -15,7 +13,7 @@ require (
github.com/aws/jsii-runtime-go v1.110.0
github.com/caarlos0/env/v11 v11.3.1
github.com/sebdah/goldie/v2 v2.5.5
github.com/stretchr/testify v1.10.0
github.com/stretchr/testify v1.11.1
github.com/trufnetwork/node v1.2.0
go.uber.org/zap v1.27.0
gopkg.in/yaml.v3 v3.0.1
Expand All @@ -42,7 +40,7 @@ require (
github.com/spf13/cast v1.7.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sync v0.18.0 // indirect
)

require (
Expand All @@ -53,9 +51,9 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/yuin/goldmark v1.4.13 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/crypto v0.44.0 // indirect
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/tools v0.31.0 // indirect
golang.org/x/mod v0.29.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/tools v0.38.0 // indirect
)
6 changes: 6 additions & 0 deletions deployments/infra/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
Expand All @@ -90,28 +91,33 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4=
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
2 changes: 1 addition & 1 deletion extensions/tn_lp_rewards/tn_lp_rewards.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const (

// Default configuration
DefaultSamplingIntervalBlocks = 10
DefaultMaxMarketsPerRun = 50
DefaultMaxMarketsPerRun = 1000
)

// Extension holds the singleton state for LP rewards sampling
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ require (
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.11.1
github.com/testcontainers/testcontainers-go v0.37.0
github.com/trufnetwork/kwil-db v0.10.3-0.20260216231327-01b863886682
github.com/trufnetwork/kwil-db/core v0.4.3-0.20260216231327-01b863886682
github.com/trufnetwork/kwil-db v0.10.3-0.20260303100144-0119418a1a7c
github.com/trufnetwork/kwil-db/core v0.4.3-0.20260303100144-0119418a1a7c
github.com/trufnetwork/sdk-go v0.6.4-0.20260224122406-a741343e2f37
go.uber.org/zap v1.27.0
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1242,8 +1242,12 @@ github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPD
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
github.com/trufnetwork/kwil-db v0.10.3-0.20260216231327-01b863886682 h1:Gqee9/lNZMohOQEq8McpjLXMhpD60CqBxVlZPYdFdL4=
github.com/trufnetwork/kwil-db v0.10.3-0.20260216231327-01b863886682/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo=
github.com/trufnetwork/kwil-db v0.10.3-0.20260303100144-0119418a1a7c h1:lvyTdrm1gzLqCmS+sqsg2JZWnoWo0ORKKBL8Z91C/JU=
github.com/trufnetwork/kwil-db v0.10.3-0.20260303100144-0119418a1a7c/go.mod h1:LiBAC48uZl2B0IiLtD2hpOce7RNfpuDdghVAOc3u1Qo=
github.com/trufnetwork/kwil-db/core v0.4.3-0.20260216231327-01b863886682 h1:iaxXr8D3dU79MBhmS/uCuBhnlc+gbLvCvV6GtAz3ukw=
github.com/trufnetwork/kwil-db/core v0.4.3-0.20260216231327-01b863886682/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ=
github.com/trufnetwork/kwil-db/core v0.4.3-0.20260303100144-0119418a1a7c h1:O5pyUJqZNNIi/l1vXc9fxycdEU9OxF5z8UQprTn4zZE=
github.com/trufnetwork/kwil-db/core v0.4.3-0.20260303100144-0119418a1a7c/go.mod h1:HnOsh9+BN13LJCjiH0+XKaJzyjWKf+H9AofFFp90KwQ=
github.com/trufnetwork/openzeppelin-merkle-tree-go v0.0.2 h1:DCq8MzbWH0wZmICNmMVsSzUHUPl+2vqRhluEABjxl88=
github.com/trufnetwork/openzeppelin-merkle-tree-go v0.0.2/go.mod h1:Y0MJpPp9QXU5vC6Gpoilql2NkgmGNcbHm9HYC2v2N8s=
github.com/trufnetwork/sdk-go v0.6.4-0.20260224122406-a741343e2f37 h1:VD/GWxLTshaXpLukEc1SXbG7QA9HrFzF8JvxJAJ/x7Q=
Expand Down
234 changes: 104 additions & 130 deletions internal/migrations/033-order-book-settlement.sql
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,6 @@ CREATE OR REPLACE ACTION distribute_fees(
$query_id INT,
$total_fees NUMERIC(78, 0)
) PRIVATE {
-- Early return if no fees to distribute
if $total_fees = '0'::NUMERIC(78, 0) {
RETURN;
}

-- Get market's bridge for unlock operations
$bridge TEXT;
for $row in SELECT bridge FROM ob_queries WHERE id = $query_id {
Expand All @@ -141,144 +136,124 @@ CREATE OR REPLACE ACTION distribute_fees(
$block_count := $row.cnt;
}

-- Edge case: No samples recorded → fees remain in vault (safe accumulation)
if $block_count = 0 {
RETURN;
-- Step 2: Generate distribution ID and create summary record ALWAYS
-- This provides visibility into settled markets even if no rewards were distributed
-- or if zero fees were collected.
$distribution_id INT;
for $row in SELECT COALESCE(MAX(id), 0) + 1 as next_id FROM ob_fee_distributions {
$distribution_id := $row.next_id;
}

-- Step 2-4: Calculate rewards with zero-loss distribution
-- Improved algorithm: Calculate total percentage first, then distribute
-- Remainder (dust) is given to the first participant to ensure all fees are distributed
-- Default values for summary
$actual_fees_distributed NUMERIC(78, 0) := '0'::NUMERIC(78, 0);
$lp_count INT := 0;

-- If we have samples AND fees to distribute, calculate rewards
$wallet_addresses TEXT[];
$amounts NUMERIC(78, 0)[];

for $result in
WITH participant_totals AS (
-- Sum each participant's reward percentages across all sampled blocks
-- Cast to INT to truncate decimal (64.00 → 64)
SELECT
r.participant_id,
p.wallet_address,
SUM(r.reward_percent)::INT as total_percent_int
FROM ob_rewards r
JOIN ob_participants p ON r.participant_id = p.id
WHERE r.query_id = $query_id
GROUP BY r.participant_id, p.wallet_address
),
calculated_rewards AS (
-- Calculate base reward using integer division: (total_fees * total_percent_int) / (100 * block_count)
-- This truncates fractional rewards, creating "dust" that will be distributed to first LP
SELECT
participant_id,
wallet_address,
(($total_fees * total_percent_int::NUMERIC(78, 0)) / (100::NUMERIC(78, 0) * $block_count::NUMERIC(78, 0)))::NUMERIC(78, 0) as base_reward
FROM participant_totals
),
total_check AS (
-- Calculate total distributed to find dust (remainder from integer division)
SELECT COALESCE(SUM(base_reward)::NUMERIC(78, 0), '0'::NUMERIC(78, 0)) as total_distributed
FROM calculated_rewards
),
with_remainder AS (
-- Distribute remainder to first participant (lowest participant_id)
-- This ensures zero fee loss - all settlement fees go to LPs as intended
SELECT
participant_id,
wallet_address,
base_reward + CASE
WHEN participant_id = (SELECT MIN(participant_id) FROM calculated_rewards)
THEN $total_fees - (SELECT total_distributed FROM total_check)
ELSE '0'::NUMERIC(78, 0)
END as final_reward
FROM calculated_rewards
),
aggregated AS (
-- Aggregate into arrays for batch processing (same pattern as process_settlement)
SELECT
ARRAY_AGG('0x' || encode(wallet_address, 'hex') ORDER BY participant_id) as wallets,
ARRAY_AGG(final_reward ORDER BY participant_id) as amounts
FROM with_remainder
)
SELECT wallets, amounts FROM aggregated
{
$wallet_addresses := $result.wallets;
$amounts := $result.amounts;
}
if $block_count > 0 AND $total_fees > '0'::NUMERIC(78, 0) {
-- Step 3: Calculate rewards with zero-loss distribution
-- We calculate all distribution arrays in a SINGLE query using simplified logic
-- to avoid performance bottlenecks in the Kwil engine.

-- Get the first participant ID to handle the remainder (dust)
$min_participant_id INT;
for $row in SELECT MIN(participant_id) as mid FROM ob_rewards WHERE query_id = $query_id {
$min_participant_id := $row.mid;
}

-- Step 5: Batch unlock to all LPs (single call, no loops)
if $wallet_addresses IS NOT NULL AND COALESCE(array_length($wallet_addresses), 0) > 0 {
ob_batch_unlock_collateral($bridge, $wallet_addresses, $amounts);
}
-- Calculate total distributed to find the remainder
$total_distributed_base NUMERIC(78, 0) := '0'::NUMERIC(78, 0);
for $row in
SELECT SUM((($total_fees::NUMERIC(78, 20) * total_percent_numeric) / (100::NUMERIC(78, 20) * $block_count::NUMERIC(78, 20)))::NUMERIC(78, 0))::NUMERIC(78, 0) as total
FROM (
SELECT SUM(reward_percent)::NUMERIC(78, 20) as total_percent_numeric
FROM ob_rewards
WHERE query_id = $query_id
GROUP BY participant_id
) AS pt
{
$total_distributed_base := $row.total;
}

-- Step 5.5: CREATE AUDIT RECORDS
-- Insert distribution summary and per-LP details BEFORE deleting ob_rewards.
-- This ensures full traceability for compliance and user verification.
$remainder NUMERIC(78, 0) := $total_fees - $total_distributed_base;

-- Only create audit if distribution actually occurred
if $wallet_addresses IS NOT NULL AND COALESCE(array_length($wallet_addresses), 0) > 0 {
-- Generate distribution ID (MAX+1 pattern, safe in Kwil sequential execution)
$distribution_id INT;
for $row in SELECT COALESCE(MAX(id), 0) + 1 as next_id FROM ob_fee_distributions {
$distribution_id := $row.next_id;
-- Aggregate into arrays for batch processing
for $result in
WITH participant_totals AS (
SELECT
r.participant_id,
p.wallet_address,
SUM(r.reward_percent)::NUMERIC(78, 20) as total_percent_numeric
FROM ob_rewards r
JOIN ob_participants p ON r.participant_id = p.id
WHERE r.query_id = $query_id
GROUP BY r.participant_id, p.wallet_address
),
calculated_rewards AS (
SELECT
participant_id,
wallet_address,
(($total_fees::NUMERIC(78, 20) * total_percent_numeric) / (100::NUMERIC(78, 20) * $block_count::NUMERIC(78, 20)))::NUMERIC(78, 0) +
(CASE WHEN participant_id = $min_participant_id THEN $remainder ELSE '0'::NUMERIC(78, 0) END) as final_reward
FROM participant_totals
),
aggregated AS (
SELECT
ARRAY_AGG('0x' || encode(wallet_address, 'hex') ORDER BY participant_id) as wallets,
ARRAY_AGG(final_reward ORDER BY participant_id) as amounts
FROM calculated_rewards
WHERE final_reward > '0'::NUMERIC(78, 0)
)
SELECT wallets, amounts FROM aggregated
{
$wallet_addresses := $result.wallets;
$amounts := $result.amounts;
}

-- Insert distribution summary
INSERT INTO ob_fee_distributions (
id,
query_id,
total_fees_distributed,
total_lp_count,
block_count,
distributed_at
) VALUES (
$distribution_id,
$query_id,
$total_fees,
COALESCE(array_length($wallet_addresses), 0),
$block_count,
@block_timestamp
);

-- Insert per-LP details
-- Match the distributed amounts (from arrays) with participant data from ob_rewards
-- This creates audit records showing exactly who got what
$idx INT := 1;
for $w_row in SELECT wallet FROM UNNEST($wallet_addresses) AS w(wallet) {
$wallet_hex TEXT := $w_row.wallet;

-- Get corresponding amount (arrays are same length, ordered by participant_id)
$reward_amount NUMERIC(78, 0);
for $a_row in
SELECT amount
FROM UNNEST($amounts) AS a(amount)
LIMIT 1 OFFSET ($idx - 1)
{
$reward_amount := $a_row.amount;
}
if $wallet_addresses IS NOT NULL AND COALESCE(array_length($wallet_addresses), 0) > 0 {
$lp_count := array_length($wallet_addresses);
$actual_fees_distributed := $total_fees;

-- Step 4: Batch unlock to all qualifying LPs
ob_batch_unlock_collateral($bridge, $wallet_addresses, $amounts);
}
}

-- Step 5: Insert distribution summary
INSERT INTO ob_fee_distributions (
id,
query_id,
total_fees_distributed,
total_lp_count,
block_count,
distributed_at
) VALUES (
$distribution_id,
$query_id,
$actual_fees_distributed,
$lp_count,
$block_count,
@block_timestamp
);

-- Step 6: Insert per-LP details (only if LPs exist)
if $lp_count > 0 {
for $payout in SELECT wallet, amount FROM UNNEST($wallet_addresses, $amounts) AS p(wallet, amount) {
$wallet_hex TEXT := $payout.wallet;
$reward_amount NUMERIC(78, 0) := $payout.amount;

-- Get participant info by matching wallet address
$pid INT;
$wallet_bytes BYTEA;
$total_reward_pct NUMERIC(10, 2);

for $p_data in
SELECT
p.id,
p.wallet_address
FROM ob_participants p
WHERE '0x' || encode(p.wallet_address, 'hex') = $wallet_hex
{
for $p_data in SELECT id, wallet_address FROM ob_participants WHERE '0x' || encode(wallet_address, 'hex') = $wallet_hex {
$pid := $p_data.id;
$wallet_bytes := $p_data.wallet_address;

-- Calculate total_reward_percent: average percentage across all sampled blocks
-- Sum of percentages / block_count = normalized to 0-100 range

$total_reward_pct := 0::NUMERIC(10,2);
for $pct_row in
SELECT SUM(reward_percent::NUMERIC(10,2))::NUMERIC(10,2) as sum_pct
FROM ob_rewards
WHERE query_id = $query_id AND participant_id = $pid
{
for $pct_row in SELECT SUM(reward_percent::NUMERIC(10,2))::NUMERIC(10,2) as sum_pct FROM ob_rewards WHERE query_id = $query_id AND participant_id = $pid {
if $pct_row.sum_pct IS NOT NULL {
$total_reward_pct := $pct_row.sum_pct / $block_count::NUMERIC(10,2);
}
Expand All @@ -298,14 +273,13 @@ CREATE OR REPLACE ACTION distribute_fees(
$reward_amount,
$total_reward_pct
);

$idx := $idx + 1;
}
}

-- Step 6: Cleanup - delete processed rewards to save storage
-- NOW SAFE: Audit records created above preserve distribution history
DELETE FROM ob_rewards WHERE query_id = $query_id;
-- Step 7: Cleanup
if $lp_count > 0 {
DELETE FROM ob_rewards WHERE query_id = $query_id;
}
};

-- Process settlement: Pay winners (minus 2% fee), refund open buys, distribute LP rewards
Expand Down
Loading