-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexamples_test.go
More file actions
343 lines (314 loc) · 12 KB
/
Copy pathexamples_test.go
File metadata and controls
343 lines (314 loc) · 12 KB
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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
package regtest
import (
"context"
"errors"
"path/filepath"
"testing"
"time"
)
// TestMineUntilActive_Testdummy is the streamlined version of
// TestExampleActivateTestdummy: same harness, same testdummy deployment,
// but the mining loop is replaced with a single MineUntilActive call.
// Pins that the helper composes correctly with VBParams + WarpContext +
// DeploymentStatusContext.
func TestMineUntilActive_Testdummy(t *testing.T) {
rt, err := New(testdummyConfig(t, 19900))
if err != nil {
t.Fatalf("New: %v", err)
}
if err := rt.Start(); err != nil {
t.Fatalf("Start: %v", err)
}
defer rt.Stop()
// Inquisition exposes a testdummy entry but it isn't BIP9-overridable via
// -vbparams the way Core's is, so MineUntilActive can't drive it to
// Active. PR2's SupportsBIP will replace this Variant check with a
// BIP-aware skip.
if v, _ := rt.Variant(); v == VariantInquisition {
t.Skip("testdummy not driveable via -vbparams on Inquisition")
}
if err := rt.EnsureWallet("miner"); err != nil {
t.Fatalf("EnsureWallet: %v", err)
}
miner, err := rt.GenerateBech32("miner")
if err != nil {
t.Fatalf("GenerateBech32: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
mined, err := rt.MineUntilActiveContext(ctx, "testdummy", miner, 2000)
if err != nil {
t.Fatalf("MineUntilActive: %v", err)
}
t.Logf("testdummy activated after %d blocks", mined)
status, err := rt.DeploymentStatus("testdummy")
if err != nil {
t.Fatalf("DeploymentStatus: %v", err)
}
if status != SoftForkActive {
t.Errorf("post-MineUntilActive status = %v, want SoftForkActive", status)
}
}
// TestMineUntilActive_UnknownDeployment pins the early-exit contract:
// MineUntilActive returns ErrUnknownDeployment without mining a single
// block when the deployment name isn't known to bitcoind. This is the
// signal a soft-fork-specific test should use to t.Skip cleanly when
// run against mainline Core.
func TestMineUntilActive_UnknownDeployment(t *testing.T) {
rt, err := New(nil)
if err != nil {
t.Fatalf("New: %v", err)
}
if err := rt.Start(); err != nil {
t.Fatalf("Start: %v", err)
}
defer rt.Stop()
if err := rt.EnsureWallet("miner"); err != nil {
t.Fatalf("EnsureWallet: %v", err)
}
miner, err := rt.GenerateBech32("miner")
if err != nil {
t.Fatalf("GenerateBech32: %v", err)
}
startHeight, err := rt.GetBlockCount()
if err != nil {
t.Fatalf("GetBlockCount: %v", err)
}
mined, err := rt.MineUntilActive("does-not-exist", miner, 1000)
if err == nil {
t.Fatal("expected ErrUnknownDeployment, got nil")
}
if !errors.Is(err, ErrUnknownDeployment) {
t.Errorf("expected errors.Is(err, ErrUnknownDeployment), got %v", err)
}
if mined != 0 {
t.Errorf("expected 0 blocks mined for unknown deployment, got %d", mined)
}
endHeight, err := rt.GetBlockCount()
if err != nil {
t.Fatalf("GetBlockCount: %v", err)
}
if endHeight != startHeight {
t.Errorf("unknown-deployment path mined blocks: %d -> %d", startHeight, endHeight)
}
}
// TestExampleActivateTestdummy is a narrated end-to-end example of
// activating Bitcoin Core's "testdummy" BIP9 soft-fork deployment from
// scratch. testdummy is the deployment Core ships specifically for testing
// the activation machinery — it requires no new consensus code, just
// exercises the version-bit signaling state machine end-to-end.
//
// Once this test passes, the same template (with a different deployment
// name and a patched bitcoind binary in $PATH) is the recipe for testing
// real future soft-forks: SIGHASH_ANYPREVOUT (eltoo, see #25), CTV
// (LNHANCE), CSFS, and so on. See the Phase 5 roadmap (#83) for the full
// picture.
//
// The BIP9 state machine has four interesting transitions:
//
// DEFINED → STARTED: once the median-time-past >= start_time, at the
// next retarget boundary (regtest period is 144
// blocks).
// STARTED → LOCKED_IN: if at least 108 of the previous 144 blocks
// signaled the deployment's bit, at the next
// retarget boundary.
// LOCKED_IN → ACTIVE: automatically at the next retarget boundary
// after lock-in.
// any → FAILED: if the timeout passes without lock-in.
//
// On regtest with our VBParams (start_time=0, timeout=far-future) and
// Bitcoin Core's default block-version policy that signals for all
// currently-STARTED deployments, activation completes in ~3 retarget
// windows (~432 blocks).
func TestExampleActivateTestdummy(t *testing.T) {
// 1) Start a node with testdummy configured for fast activation and
// -acceptnonstdtxn=1. testdummyConfig produces:
//
// VBParams: [{testdummy, start=0, timeout=9999999999, min=0}]
// AcceptNonstdTxn: true
//
// See the testdummyConfig helper in regtest_test.go.
rt, err := New(testdummyConfig(t, 19800))
if err != nil {
t.Fatalf("New: %v", err)
}
if err := rt.Start(); err != nil {
t.Fatalf("Start: %v", err)
}
defer rt.Stop()
// Inquisition exposes a testdummy entry but it isn't BIP9-overridable via
// -vbparams the way Core's is. PR2's SupportsBIP will replace this
// Variant check with a BIP-aware skip.
if v, _ := rt.Variant(); v == VariantInquisition {
t.Skip("testdummy not driveable via -vbparams on Inquisition")
}
// 2) Set up a wallet so coinbase rewards have somewhere to land while
// we mine activation blocks.
if err := rt.EnsureWallet("miner"); err != nil {
t.Fatalf("EnsureWallet: %v", err)
}
miner, err := rt.GenerateBech32("miner")
if err != nil {
t.Fatalf("GenerateBech32: %v", err)
}
// 3) Initial status check. On a fresh regtest node testdummy is
// typically reported as DEFINED until the first retarget boundary,
// or STARTED if Core has already evaluated the start time. Either
// is acceptable as a starting point — Active would mean the
// deployment was somehow already activated, which would defeat the
// purpose of this test.
initial, err := rt.DeploymentStatus("testdummy")
if err != nil {
t.Fatalf("DeploymentStatus initial: %v", err)
}
t.Logf("initial status: %s", initial)
if initial == SoftForkActive {
t.Fatalf("testdummy is unexpectedly active on a fresh node (status=%s)", initial)
}
// 4) Mine in 144-block (one retarget window) chunks, observing the
// state machine progress after each. GenerateToAddress (under
// Warp) signals for all currently-STARTED BIP9 deployments by
// default on regtest, so every block counts toward the threshold.
// Pre-allocate `observed` so unparam doesn't trip on the append.
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
const window = 144
const maxWindows = 10
observed := []SoftForkStatus{initial}
for w := 1; w <= maxWindows; w++ {
if err := rt.WarpContext(ctx, window, miner); err != nil {
t.Fatalf("Warp window %d: %v", w, err)
}
status, err := rt.DeploymentStatusContext(ctx, "testdummy")
if err != nil {
t.Fatalf("DeploymentStatus window %d: %v", w, err)
}
t.Logf("window %d (block %d): %s", w, w*window, status)
if observed[len(observed)-1] != status {
observed = append(observed, status)
}
if status == SoftForkActive {
break
}
if status == SoftForkFailed {
t.Fatalf("testdummy reached SoftForkFailed unexpectedly")
}
}
// 5) Final assertion. If we hit maxWindows without activating, the
// bitcoind binary either doesn't signal by default or doesn't
// know testdummy.
final, err := rt.DeploymentStatus("testdummy")
if err != nil {
t.Fatalf("DeploymentStatus final: %v", err)
}
if final != SoftForkActive {
t.Fatalf("testdummy did not reach SoftForkActive within %d windows (final=%s, observed=%v)",
maxWindows, final, observed)
}
t.Logf("testdummy activated successfully — observed transitions: %v", observed)
}
// TestExampleTimeoutWithoutLockin is the worked example for the BIP9 failure
// path: a deployment whose timeout passes before the signaling threshold is
// met transitions to SoftForkFailed. The Phase 6 time API (WarpTime + sticky
// mocktime) makes this observable in seconds rather than waiting through
// real-world time.
//
// On Bitcoin Core's BIP9 state machine, the transitions are:
//
// at boundary 144: DEFINED → STARTED (MTP >= start_time)
// at boundary 288: STARTED → FAILED (MTP >= timeout, no lock-in)
//
// So we need to mine to height 288 with MTP past the configured timeout.
// This test sets a far-future timeout, uses WarpTime to push MTP past it,
// then mines through the second retarget boundary.
//
// Inquisition replaces BIP9 with its "heretical" state machine which doesn't
// honor -vbparams overrides for testdummy, so the test skips there. PR2's
// SupportsBIP could substitute once we add a BIP for testdummy, but the
// Variant check is sufficient and self-documenting.
func TestExampleTimeoutWithoutLockin(t *testing.T) {
const timeout = 4_000_000_000 // year ~2096; under the uint32 block-timestamp cap
cfg := &Config{
Host: "127.0.0.1:19851",
User: "user",
Pass: "pass",
DataDir: filepath.Join(t.TempDir(), "regtest"),
// -blockversion=0x20000000 (536870912) is the BIP9 baseline with no
// deployment bits set. Without it, regtest's default block-version
// policy signals for every STARTED deployment, so testdummy would
// LOCKED_IN before the timeout check (Bitcoin Core's BIP9 evaluates
// signaling threshold before the timeout). Suppressing signaling is
// the only way to demonstrate the FAILED transition.
ExtraArgs: []string{"-blockversion=536870912"},
VBParams: []VBParam{{
Deployment: "testdummy",
StartTime: 0,
Timeout: timeout,
MinActivationHeight: 0,
}},
AcceptNonstdTxn: true,
}
rt, err := New(cfg)
if err != nil {
t.Fatalf("New: %v", err)
}
if err := rt.Start(); err != nil {
t.Fatalf("Start: %v", err)
}
defer rt.Stop()
if v, _ := rt.Variant(); v == VariantInquisition {
t.Skip("testdummy not driveable via -vbparams on Inquisition (heretical state machine)")
}
if err := rt.EnsureWallet("miner"); err != nil {
t.Fatalf("EnsureWallet: %v", err)
}
miner, err := rt.GenerateBech32("miner")
if err != nil {
t.Fatalf("GenerateBech32: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
// Pre-fill 11 blocks so MTP has a populated window before we warp.
if err := rt.WarpContext(ctx, 11, miner); err != nil {
t.Fatalf("Warp pre-fill: %v", err)
}
initial, err := rt.DeploymentStatusContext(ctx, "testdummy")
if err != nil {
t.Fatalf("DeploymentStatus initial: %v", err)
}
t.Logf("initial status: %s", initial)
if initial == SoftForkFailed {
t.Fatalf("testdummy started in SoftForkFailed; expected DEFINED/STARTED on a pre-warp chain")
}
// Compute a duration that pushes MTP comfortably past timeout regardless
// of system time. WarpTime sets mocktime sticky, so subsequent Warp /
// MineToHeight calls also stamp blocks at the warped time — keeping the
// MTP window populated past timeout for the BIP9 evaluation at 288.
preInfo, err := rt.GetBlockChainInfoContext(ctx)
if err != nil {
t.Fatalf("GetBlockChainInfo: %v", err)
}
need := time.Duration(timeout-preInfo.MedianTime+86400) * time.Second // +1 day cushion
newMTP, err := rt.WarpTimeContext(ctx, need, miner)
if err != nil {
t.Fatalf("WarpTime: %v", err)
}
t.Logf("warped MTP from %d to %d (timeout=%d)", preInfo.MedianTime, newMTP, timeout)
if newMTP < timeout {
t.Fatalf("WarpTime produced MTP=%d, still below timeout=%d", newMTP, timeout)
}
// Mine to the second retarget boundary so BIP9 evaluates STARTED → FAILED.
// Mocktime is still set from WarpTime, so all new blocks stamp at the
// warped time — MTP at 288 stays past timeout.
if err := rt.MineToHeightContext(ctx, 288, miner); err != nil {
t.Fatalf("MineToHeight 288: %v", err)
}
final, err := rt.DeploymentStatusContext(ctx, "testdummy")
if err != nil {
t.Fatalf("DeploymentStatus final: %v", err)
}
t.Logf("final status at height 288: %s", final)
if final != SoftForkFailed {
t.Fatalf("testdummy final status = %s, want SoftForkFailed", final)
}
}