aboutsummaryrefslogtreecommitdiff
path: root/test/functional/interface_usdt_coinselection.py
blob: f684848aedfcd98bfe058c273e0f88886e3b9aad (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
#!/usr/bin/env python3
# Copyright (c) 2022 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.

"""  Tests the coin_selection:* tracepoint API interface.
     See https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#context-coin_selection
"""

# Test will be skipped if we don't have bcc installed
try:
    from bcc import BPF, USDT # type: ignore[import]
except ImportError:
    pass
from test_framework.test_framework import BitcoinTestFramework
from test_framework.util import (
    assert_equal,
    assert_greater_than,
    assert_raises_rpc_error,
)

coinselection_tracepoints_program = """
#include <uapi/linux/ptrace.h>

#define WALLET_NAME_LENGTH 16
#define ALGO_NAME_LENGTH 16

struct event_data
{
    u8 type;
    char wallet_name[WALLET_NAME_LENGTH];

    // selected coins event
    char algo[ALGO_NAME_LENGTH];
    s64 target;
    s64 waste;
    s64 selected_value;

    // create tx event
    bool success;
    s64 fee;
    s32 change_pos;

    // aps create tx event
    bool use_aps;
};

BPF_QUEUE(coin_selection_events, struct event_data, 1024);

int trace_selected_coins(struct pt_regs *ctx) {
    struct event_data data;
    __builtin_memset(&data, 0, sizeof(data));
    data.type = 1;
    bpf_usdt_readarg_p(1, ctx, &data.wallet_name, WALLET_NAME_LENGTH);
    bpf_usdt_readarg_p(2, ctx, &data.algo, ALGO_NAME_LENGTH);
    bpf_usdt_readarg(3, ctx, &data.target);
    bpf_usdt_readarg(4, ctx, &data.waste);
    bpf_usdt_readarg(5, ctx, &data.selected_value);
    coin_selection_events.push(&data, 0);
    return 0;
}

int trace_normal_create_tx(struct pt_regs *ctx) {
    struct event_data data;
    __builtin_memset(&data, 0, sizeof(data));
    data.type = 2;
    bpf_usdt_readarg_p(1, ctx, &data.wallet_name, WALLET_NAME_LENGTH);
    bpf_usdt_readarg(2, ctx, &data.success);
    bpf_usdt_readarg(3, ctx, &data.fee);
    bpf_usdt_readarg(4, ctx, &data.change_pos);
    coin_selection_events.push(&data, 0);
    return 0;
}

int trace_attempt_aps(struct pt_regs *ctx) {
    struct event_data data;
    __builtin_memset(&data, 0, sizeof(data));
    data.type = 3;
    bpf_usdt_readarg_p(1, ctx, &data.wallet_name, WALLET_NAME_LENGTH);
    coin_selection_events.push(&data, 0);
    return 0;
}

int trace_aps_create_tx(struct pt_regs *ctx) {
    struct event_data data;
    __builtin_memset(&data, 0, sizeof(data));
    data.type = 4;
    bpf_usdt_readarg_p(1, ctx, &data.wallet_name, WALLET_NAME_LENGTH);
    bpf_usdt_readarg(2, ctx, &data.use_aps);
    bpf_usdt_readarg(3, ctx, &data.success);
    bpf_usdt_readarg(4, ctx, &data.fee);
    bpf_usdt_readarg(5, ctx, &data.change_pos);
    coin_selection_events.push(&data, 0);
    return 0;
}
"""


class CoinSelectionTracepointTest(BitcoinTestFramework):
    def add_options(self, parser):
        self.add_wallet_options(parser)

    def set_test_params(self):
        self.num_nodes = 1
        self.setup_clean_chain = True

    def skip_test_if_missing_module(self):
        self.skip_if_platform_not_linux()
        self.skip_if_no_bitcoind_tracepoints()
        self.skip_if_no_python_bcc()
        self.skip_if_no_bpf_permissions()
        self.skip_if_no_wallet()

    def get_tracepoints(self, expected_types):
        events = []
        try:
            for i in range(0, len(expected_types) + 1):
                event = self.bpf["coin_selection_events"].pop()
                assert_equal(event.wallet_name.decode(), self.default_wallet_name)
                assert_equal(event.type, expected_types[i])
                events.append(event)
            else:
                # If the loop exits successfully instead of throwing a KeyError, then we have had
                # more events than expected. There should be no more than len(expected_types) events.
                assert False
        except KeyError:
            assert_equal(len(events), len(expected_types))
            return events


    def determine_selection_from_usdt(self, events):
        success = None
        use_aps = None
        algo = None
        waste = None
        change_pos = None

        is_aps = False
        sc_events = []
        for event in events:
            if event.type == 1:
                if not is_aps:
                    algo = event.algo.decode()
                    waste = event.waste
                sc_events.append(event)
            elif event.type == 2:
                success = event.success
                if not is_aps:
                    change_pos = event.change_pos
            elif event.type == 3:
                is_aps = True
            elif event.type == 4:
                assert is_aps
                if event.use_aps:
                    use_aps = True
                    assert_equal(len(sc_events), 2)
                    algo = sc_events[1].algo.decode()
                    waste = sc_events[1].waste
                    change_pos = event.change_pos
        return success, use_aps, algo, waste, change_pos

    def run_test(self):
        self.log.info("hook into the coin_selection tracepoints")
        ctx = USDT(pid=self.nodes[0].process.pid)
        ctx.enable_probe(probe="coin_selection:selected_coins", fn_name="trace_selected_coins")
        ctx.enable_probe(probe="coin_selection:normal_create_tx_internal", fn_name="trace_normal_create_tx")
        ctx.enable_probe(probe="coin_selection:attempting_aps_create_tx", fn_name="trace_attempt_aps")
        ctx.enable_probe(probe="coin_selection:aps_create_tx_internal", fn_name="trace_aps_create_tx")
        self.bpf = BPF(text=coinselection_tracepoints_program, usdt_contexts=[ctx], debug=0, cflags=["-Wno-error=implicit-function-declaration"])

        self.log.info("Prepare wallets")
        self.generate(self.nodes[0], 101)
        wallet = self.nodes[0].get_wallet_rpc(self.default_wallet_name)

        self.log.info("Sending a transaction should result in all tracepoints")
        # We should have 5 tracepoints in the order:
        # 1. selected_coins (type 1)
        # 2. normal_create_tx_internal (type 2)
        # 3. attempting_aps_create_tx (type 3)
        # 4. selected_coins (type 1)
        # 5. aps_create_tx_internal (type 4)
        wallet.sendtoaddress(wallet.getnewaddress(), 10)
        events = self.get_tracepoints([1, 2, 3, 1, 4])
        success, use_aps, _algo, _waste, change_pos = self.determine_selection_from_usdt(events)
        assert_equal(success, True)
        assert_greater_than(change_pos, -1)

        self.log.info("Failing to fund results in 1 tracepoint")
        # We should have 1 tracepoints in the order
        # 1. normal_create_tx_internal (type 2)
        assert_raises_rpc_error(-6, "Insufficient funds", wallet.sendtoaddress, wallet.getnewaddress(), 102 * 50)
        events = self.get_tracepoints([2])
        success, use_aps, _algo, _waste, change_pos = self.determine_selection_from_usdt(events)
        assert_equal(success, False)

        self.log.info("Explicitly enabling APS results in 2 tracepoints")
        # We should have 2 tracepoints in the order
        # 1. selected_coins (type 1)
        # 2. normal_create_tx_internal (type 2)
        wallet.setwalletflag("avoid_reuse")
        wallet.sendtoaddress(address=wallet.getnewaddress(), amount=10, avoid_reuse=True)
        events = self.get_tracepoints([1, 2])
        success, use_aps, _algo, _waste, change_pos = self.determine_selection_from_usdt(events)
        assert_equal(success, True)
        assert_equal(use_aps, None)

        self.log.info("Change position is -1 if no change is created with APS when APS was initially not used")
        # We should have 2 tracepoints in the order:
        # 1. selected_coins (type 1)
        # 2. normal_create_tx_internal (type 2)
        # 3. attempting_aps_create_tx (type 3)
        # 4. selected_coins (type 1)
        # 5. aps_create_tx_internal (type 4)
        wallet.sendtoaddress(address=wallet.getnewaddress(), amount=wallet.getbalance(), subtractfeefromamount=True, avoid_reuse=False)
        events = self.get_tracepoints([1, 2, 3, 1, 4])
        success, use_aps, _algo, _waste, change_pos = self.determine_selection_from_usdt(events)
        assert_equal(success, True)
        assert_equal(change_pos, -1)

        self.log.info("Change position is -1 if no change is created normally and APS is not used")
        # We should have 2 tracepoints in the order:
        # 1. selected_coins (type 1)
        # 2. normal_create_tx_internal (type 2)
        wallet.sendtoaddress(address=wallet.getnewaddress(), amount=wallet.getbalance(), subtractfeefromamount=True)
        events = self.get_tracepoints([1, 2])
        success, use_aps, _algo, _waste, change_pos = self.determine_selection_from_usdt(events)
        assert_equal(success, True)
        assert_equal(change_pos, -1)

        self.bpf.cleanup()


if __name__ == '__main__':
    CoinSelectionTracepointTest(__file__).main()