recovery_test.go raw

   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