1
+ //
2
+ // CTV Congestion Control
3
+ //
4
+ // See https://utxos.org/uses/scaling/ for an overview. From the link:
5
+ //
6
+ // When there is a high demand for blockspace it becomes very expensive to make transactions.
7
+ // By using OP_CTV, a large volume payment processor may aggregate all their payments
8
+ // into a single O(1) transaction for purposes of confirmation. Then, some time later, the
9
+ // payments can be expanded out of that UTXO when the demand for blockspace is decreased.
10
+
11
+
12
+ //
13
+ // Tree Setup & Funding
14
+ //
15
+
16
+ // Split up the flat list of $payments into a CTV transaction tree
17
+ fn ctvPayTree($payments, $tx_fee) {
18
+ if len($payments) == 1 {
19
+ // A final leaf node. Represents a payment output to a recipient's scriptPubKey
20
+ [ "script": $payments.0.0, "amount": $payments.0.1 ]
21
+ } else {
22
+ // A branch node. Represents a CTV output that expands into a transaction with 2 outputs,
23
+ // where each output can either be another CTV expansion branch or a final leaf payment.
24
+ $left_size = len($payments) / 2;
25
+ $right_size = len($payments) - $left_size;
26
+ $left = ctvPayTree(slice($payments, 0, $left_size), $tx_fee);
27
+ $right = ctvPayTree(slice($payments, $left_size, $right_size), $tx_fee);
28
+ // Uses a radix of 2 to keep the example simpler, 4-5 for would be more optimal.
29
+
30
+ // ctv::hash() adds a default tx input (spending 000000...:0) when there are no inputs. The prevout txid:vout
31
+ // doesn't affect the CTV hash, and will get updated later once the outpoint funding the tree root is known.
32
+ $tx = tx [
33
+ "outputs": [
34
+ $left->script: $left->amount,
35
+ $right->script: $right->amount,
36
+ ]
37
+ ];
38
+
39
+ [ "script": `ctv::hash($tx) OP_CTV`,
40
+ "amount": $left->amount + $right->amount + $tx_fee, // fee added
41
+ "tx": $tx, "left": $left, "right": $right ]
42
+ }
43
+ }
44
+
45
+ // Construct the tree
46
+ $tree = ctvPayTree([
47
+ tb1qrs85zz939tsf3nd3wsmjk3ye68zyfk4lxlkhmn: 0.1 BTC,
48
+ tb1qv6rm00j52pvjqwv5dd2un9d6vfnqdp0m8fx8vx: 0.2 BTC,
49
+ 2MuSM1zTT3AUawArKp1MAc9KjpwTEsLZA75: 0.3 BTC,
50
+ tb1qqlzzgn3x5xnuhmz7e7xen8l8sll9h9lnf43ekl: 0.4 BTC,
51
+ 2Mxo1dGnHBPZjR9Gone5fyxnMSn7qJcaizt: 0.5 BTC,
52
+ ], 200 sat);
53
+
54
+ // The total funding amount needed, including transaction fees for the entire tree
55
+ $total_amount = $tree->amount;
56
+
57
+ // $tree->script can be paid to directly as a bare scriptPubKey, however wrapping in P2WSH makes
58
+ // it easier to fund it from a wallet. The rest of the tree expansion will use bare scriptPubKeys.
59
+ $tree_address = address(wsh($tree->script));
60
+
61
+ // The outpoint funding the tree's address/scriptPubKey, used as the initial input for expanding the tree.
62
+ // Could specify just the txid:vout if bare scriptPubKey was used. For P2WSH, the witnessScript needs to be provided too.
63
+ // $init_input = 0808943750af3eeac3cc0d5e74823a3f3bec154af98579fd1c1344b40181d00f:0;
64
+ $init_input = [ "prevout": 0808943750af3eeac3cc0d5e74823a3f3bec154af98579fd1c1344b40181d00f:0, "witness": [ $tree->script ] ];
65
+
66
+
67
+ //
68
+ // Tree Expansion
69
+ //
70
+
71
+ // With the $init_input funding outpoint known, we can finalize the tree with the actual outpoints descending from $init_input.
72
+ // Returned as a flat list of nodes, each with the `path` leading to it, its final `outpoint` and the updated transaction.
73
+ fn finalizeNodes($node, $outpoint, $path=[]) {
74
+ $node = $node + [ "outpoint": $outpoint, "path": $path ];
75
+ if $node->tx? {
76
+ // Update the transaction input to spend the $outpoint, keeping other transactions fields as they were
77
+ $tx = tx [ "input": $outpoint, "outputs": $node->tx->outputs, "version": $node->tx->version, "locktime": $node->tx->locktime ];
78
+ $node = update($node, [ "tx": $tx ]);
79
+ $left_flat = finalizeNodes($node->left, txid($tx):0, $path+[0]);
80
+ $right_flat = finalizeNodes($node->right, txid($tx):1, $path+[1]);
81
+ [ remove($node, [ "left", "right" ]) ] + $left_flat + $right_flat
82
+ } else {
83
+ [ $node ]
84
+ }
85
+ }
86
+
87
+ // Finalize the $tree to descent from the $init_input
88
+ $tree_nodes = finalizeNodes($tree, $init_input);
89
+
90
+ // The full set of transactions needed to fully expand the tree
91
+ $tree_txs_hex = filterMap($tree_nodes, |$n| if $n->tx? then hex($n->tx) else filterMap::skip);
92
+
93
+ //
94
+ // Recipient's PoV
95
+ //
96
+
97
+ // Get the chain of transactions leading up to the $destination:$amount payment
98
+ fn findPayRoute($tree_nodes, [$destination, $amount]) {
99
+ // Find the node paying $amount to the $destination
100
+ $pay_node = find($tree_nodes, |$n| $n->script == $destination && $n->amount == $amount);
101
+ $pay_node != null || throw("Payment not found");
102
+
103
+ // Find all nodes leading up to the payment, excluding the payment node itself
104
+ $route = filter($tree_nodes, |$n| startsWith($pay_node->path, $n->path) && $n != $pay_node);
105
+
106
+ // Returns a tuple with the expansion transactions and the final outpoint funding $destination:$amount
107
+ [ map($route, |$n| $n->tx), $pay_node->outpoint ]
108
+ }
109
+
110
+ // The subset of transactions a recipient cares about to get their coins at $final_outpoint.
111
+ // This can also serve as a proof of payment inclusion.
112
+ [$pay_route_txs, $final_outpoint] = findPayRoute($tree_nodes, tb1qqlzzgn3x5xnuhmz7e7xen8l8sll9h9lnf43ekl:0.4 BTC);
113
+
114
+ // If $init_input was updated to an outpoint funding $tree_address with $tree->amount (1.500008 BTC),
115
+ // it should be possible to broadcast these transactions in order:
116
+ $pay_route_hex = map($pay_route_txs, hex);
117
+
118
+ // The last transaction in the chain includes the recipient's $final_outpoint
119
+ assert::eq(txid(last($pay_route_txs)), $final_outpoint.0);
120
+ assert::eq(last($pay_route_txs)->outputs.($final_outpoint.1)->script_pubkey, tb1qqlzzgn3x5xnuhmz7e7xen8l8sll9h9lnf43ekl->script_pubkey);
121
+ assert::eq(last($pay_route_txs)->outputs.($final_outpoint.1)->amount, 0.4 BTC);
122
+
123
+ env::pretty()
0 commit comments