1 package wallet_test
2 3 import (
4 "runtime"
5 "testing"
6 7 "github.com/p9c/p9/cmd/wallet"
8 )
9 10 // Harness holds the BranchRecoveryState being tested, the recovery window being used, provides access to the test
11 // object, and tracks the expected horizon and next unfound values.
12 type Harness struct {
13 t *testing.T
14 brs *wallet.BranchRecoveryState
15 recoveryWindow uint32
16 expHorizon uint32
17 expNextUnfound uint32
18 }
19 type (
20 // Stepper is a generic interface that performs an action or assertion against a test Harness.
21 Stepper interface {
22 // Apply performs an action or assertion against branch recovery state held by the Harness. The step index is
23 // provided so that any failures can report which Step failed.
24 Apply(step int, harness *Harness)
25 }
26 // InitialiDelta is a Step that verifies our first attempt to expand the branch recovery state's horizons tells us
27 // to derive a number of adddresses equal to the recovery window.
28 InitialDelta struct{}
29 // CheckDelta is a Step that expands the branch recovery state's horizon, and checks that the returned delta meets
30 // our expected `delta`.
31 CheckDelta struct {
32 delta uint32
33 }
34 // CheckNumInvalid is a Step that asserts that the branch recovery state reports `total` invalid children with the
35 // current horizon.
36 CheckNumInvalid struct {
37 total uint32
38 }
39 // MarkInvalid is a Step that marks the `child` as invalid in the branch recovery state.
40 MarkInvalid struct {
41 child uint32
42 }
43 // ReportFound is a Step that reports `child` as being found to the branch recovery state.
44 ReportFound struct {
45 child uint32
46 }
47 )
48 49 // Apply extends the current horizon of the branch recovery state, and checks that the returned delta is equal to the
50 // test's recovery window. If the assertions pass, the harness's expected horizon is increased by the returned delta.
51 //
52 // NOTE: This should be used before applying any CheckDelta steps.
53 func (_ InitialDelta) Apply(i int, h *Harness) {
54 curHorizon, delta := h.brs.ExtendHorizon()
55 assertHorizon(h.t, i, curHorizon, h.expHorizon)
56 assertDelta(h.t, i, delta, h.recoveryWindow)
57 h.expHorizon += delta
58 }
59 60 // Apply extends the current horizon of the branch recovery state, and checks that the returned delta is equal to the
61 // CheckDelta's child value.
62 func (d CheckDelta) Apply(i int, h *Harness) {
63 curHorizon, delta := h.brs.ExtendHorizon()
64 assertHorizon(h.t, i, curHorizon, h.expHorizon)
65 assertDelta(h.t, i, delta, d.delta)
66 h.expHorizon += delta
67 }
68 69 // Apply queries the branch recovery state for the number of invalid children that lie between the last found address
70 // and the current horizon, and compares that to the CheckNumInvalid's total.
71 func (m CheckNumInvalid) Apply(i int, h *Harness) {
72 assertNumInvalid(h.t, i, h.brs.NumInvalidInHorizon(), m.total)
73 }
74 75 // Apply marks the MarkInvalid's child index as invalid in the branch recovery state, and increments the harness's
76 // expected horizon.
77 func (m MarkInvalid) Apply(i int, h *Harness) {
78 h.brs.MarkInvalidChild(m.child)
79 h.expHorizon++
80 }
81 82 // Apply reports the ReportFound's child index as found in the branch recovery state. If the child index meets or
83 // exceeds our expected next unfound value, the expected value will be modified to be the child index + 1. Afterwards,
84 // this step asserts that the branch recovery state's next reported unfound value matches our potentially-updated value.
85 func (r ReportFound) Apply(i int, h *Harness) {
86 h.brs.ReportFound(r.child)
87 if r.child >= h.expNextUnfound {
88 h.expNextUnfound = r.child + 1
89 }
90 assertNextUnfound(h.t, i, h.brs.NextUnfound(), h.expNextUnfound)
91 }
92 93 // Compile-time checks to ensure our steps implement the Step interface.
94 var _ Stepper = InitialDelta{}
95 var _ Stepper = CheckDelta{}
96 var _ Stepper = CheckNumInvalid{}
97 var _ Stepper = MarkInvalid{}
98 var _ Stepper = ReportFound{}
99 100 // TestBranchRecoveryState walks the BranchRecoveryState through a sequence of steps, verifying that:
101 //
102 // - the horizon is properly expanded in response to found addrs
103 //
104 // - report found children below or equal to previously found causes no change
105 //
106 // - marking invalid children expands the horizon
107 func TestBranchRecoveryState(t *testing.T) {
108 const recoveryWindow = 10
109 recoverySteps := []Stepper{
110 // First, check that expanding our horizon returns exactly the recovery window (10).
111 InitialDelta{},
112 // Expected horizon: 10. Report finding the 2nd addr, this should cause our horizon to expand by 2.
113 ReportFound{1},
114 CheckDelta{2},
115 // Expected horizon: 12. Sanity check that expanding again reports zero delta, as nothing has changed.
116 CheckDelta{0},
117 // Now, report finding the 6th addr, which should expand our horizon to 16 with a detla of 4.
118 ReportFound{5},
119 CheckDelta{4},
120 // Expected horizon: 16. Sanity check that expanding again reports zero delta, as nothing has changed.
121 CheckDelta{0},
122 // Report finding child index 5 again, nothing should change.
123 ReportFound{5},
124 CheckDelta{0},
125 // Report finding a lower index that what was last found, nothing should change.
126 ReportFound{4},
127 CheckDelta{0},
128 // Moving on, report finding the 11th addr, which should extend our horizon to 21.
129 ReportFound{10},
130 CheckDelta{5},
131 // Expected horizon: 21. Before testing the lookahead expansion when encountering invalid child keys, check that
132 // we are correctly starting with no invalid keys.
133 CheckNumInvalid{0},
134 // Now that the window has been expanded, simulate deriving invalid keys in range of addrs that are being
135 // derived for the first time. The horizon will be incremented by one, as the recovery manager is expected to
136 // try and derive at least the next address.
137 MarkInvalid{17},
138 CheckNumInvalid{1},
139 CheckDelta{0},
140 // Expected horizon: 22. Chk that deriving a second invalid key shows both invalid indexes currently within
141 // the horizon.
142 MarkInvalid{18},
143 CheckNumInvalid{2},
144 CheckDelta{0},
145 // Expected horizon: 23. Lastly, report finding the addr immediately after our two invalid keys. This should
146 // return our number of invalid keys within the horizon back to 0.
147 ReportFound{19},
148 CheckNumInvalid{0},
149 // As the 20-th key was just marked found, our horizon will need to expand to 30. With the horizon at 23, the
150 // delta returned should be 7.
151 CheckDelta{7},
152 CheckDelta{0},
153 // Expected horizon: 30.
154 }
155 brs := wallet.NewBranchRecoveryState(recoveryWindow)
156 harness := &Harness{
157 t: t,
158 brs: brs,
159 recoveryWindow: recoveryWindow,
160 }
161 for i, step := range recoverySteps {
162 step.Apply(i, harness)
163 }
164 }
165 func assertHorizon(t *testing.T, i int, have, want uint32) {
166 assertHaveWant(t, i, "incorrect horizon", have, want)
167 }
168 func assertDelta(t *testing.T, i int, have, want uint32) {
169 assertHaveWant(t, i, "incorrect delta", have, want)
170 }
171 func assertNextUnfound(t *testing.T, i int, have, want uint32) {
172 assertHaveWant(t, i, "incorrect next unfound", have, want)
173 }
174 func assertNumInvalid(t *testing.T, i int, have, want uint32) {
175 assertHaveWant(t, i, "incorrect num invalid children", have, want)
176 }
177 func assertHaveWant(t *testing.T, i int, msg string, have, want uint32) {
178 _, _, line, _ := runtime.Caller(2)
179 if want != have {
180 t.Fatalf(
181 "[line: %d, step: %d] %s: got %d, want %d",
182 line, i, msg, have, want,
183 )
184 }
185 }
186