Skip to content

Instantly share code, notes, and snippets.

@LarryRuane
Created April 16, 2020 17:35
Show Gist options
  • Save LarryRuane/b70a6d691e855cfa8d8f34499db58c67 to your computer and use it in GitHub Desktop.
Save LarryRuane/b70a6d691e855cfa8d8f34499db58c67 to your computer and use it in GitHub Desktop.
reorg testing using darksidewalletd

Reorg testing

Currently, in this branch, the lightwalletd knows a pre-determined range of 101 blocks, 663150 - 663250. These are real mainnet blocks that contain several transactions that include shielded payments to or from the "developer wallet".

From now on, we'll just abbreviate these heights by dropping the 663 prefix.

Initial state

When lightwalltd starts up, it pretends that the latest and only block it knows about is 150, which it also advertises as the Sapling activation height. The wallets initially can fetch only this one block (using either GetBlock() or GetBlockRange()); attempts to fetch at heights beyond than this return a no-such-block error, simulating that this is the blockchain tip, no more blocks have been mined.

SetState

While in "darkside" mode, the lightwalletd accepts a SetState() gRPC that takes two arguments:

  • LatestHeight
  • ReorgHeight

Wallet test code (probably not within the wallet executable itself, but in the form of a script that can run a program like grpcurl) calls this gRPC.

The LatestHeight argument advances the simulated tip height; the call

SetState(LatestHeight=200, ReorgHeight=0)

makes blocks 150 through 200 available to GetBlock() and GetBlockRange(). If ReorgHeight is zero, then all we're doing is advancing the tip. The maximum LatestHeight argument is 250 since that's the highest height that the darkwalletd knows about. (The range can easily be changed, but requires a commit in the lightwalletd GitHub repository.)

If ReorgHeight is non-zero, then we're simulating a reorg. The block at the given height is replaced by a new version, but its parent (at ReorgHeight-1) is unchanged. Also, there are new versions of all blocks out to LatestHeight. And if LatestHeight is later increased, those blocks will be different from before. In other words, it acts exactly like a real reorg.

It's important to know that when doing a reorg, the LatestBlock value can be either less than, greater than, or the same as the previous LatestBlock. For example, we can do the following sequence:

  • SetState(LatestHeight=200, ReorgHeight=0)
  • SetState(LatestHeight=180, ReorgHeight=200)

This means we first extend the chain out to height 200, then reorg back to (and including) 180, then immediately back out to 200, so we end up at the same tip height.

Or, we can do:

  • SetState(LatestHeight=200, ReorgHeight=0)
  • SetState(LatestHeight=180, ReorgHeight=160)

This time, we first extend to 200, then reorg back to 180, then extend out only to 160 (so the chain got shorter, perhaps not as likely in real life but it definitely can happen). Or, the reorged chain can be longer:

  • SetState(LatestHeight=200, ReorgHeight=0)
  • SetState(LatestHeight=180, ReorgHeight=210)

It's not possible to "back up" the chain without doing a reorg because that really doesn't make sense (this doesn't work):

  • SetState(LatestHeight=200, ReorgHeight=0)
  • SetState(LatestHeight=180, ReorgHeight=0)

Another restriction is that ReorgHeight (if nonzero) must be less than or equal to the current tip height, and greater or equal to the new LatestHeight.

When darkwalletd receives unexpected arguments like this happens, it crashes rather than return an error, because this indicates a bug in the test and we want to make sure it's not papered over. The debug.log file should make it clear exactly what caused the crash.

A test scenario

Here's a possible test sequence using SetState(). There are almost limiteless ways to use SetState(); here are some ideas to give you a flavor.

The wallet-side test code could have a static, manually-crafted table that records the expected state (for example, balance) at each height. The test would advance the height one block at a time (no reorg) and verify that the wallet state is as expected:

assert(state() == expectedstate[150])
SetState(LatestHeight=151, ReorgHeight=0)
assert(state() == expectedstate[151])
SetState(LatestHeight=152, ReorgHeight=0)
assert(state() == expectedstate[152])
...
SetState(LatestHeight=250, ReorgHeight=0)
assert(state() == expectedstate[250])

Then reorg (replace) only the latest block and make sure the state doesn't change:

SetState(LatestHeight=250, ReorgHeight=250)
assert(state() == expectedstate[250])

(From now on, to reduce clutter in this document, I'll stop showing the asserts, but it's implied that after each SetState(), we assert that the current state is equal to the expected state at that LatestHeight.

Now maybe reorg such that the chain backs up by one block:

SetState(LatestHeight=249, ReorgHeight=249)
SetState(LatestHeight=248, ReorgHeight=248)
...
SetState(LatestHeight=150, ReorgHeight=150)

Let's do longer reorgs, such as a series of 10-block reorgs:

SetState(LatestHeight=160, ReorgHeight=0)
SetState(LatestHeight=170, ReorgHeight=150)
SetState(LatestHeight=180, ReorgHeight=170)
SetState(LatestHeight=190, ReorgHeight=180)
...

Reset back to 150, then maybe we do a 50-block reorgs:

SetState(LatestHeight=150, ReorgHeight=150)
SetState(LatestHeight=200, ReorgHeight=0)
SetState(LatestHeight=250, ReorgHeight=150)
...

Fuzz testing

Here's another idea, we could fuzz-test this by calling SetState() with random arguments, and assert that the state is as expected after each call. That would test all kinds of scenarios that we can't even think of. If the test runs a few thousand iterations of that, it's hard to imagine there could be any reorg bugs.

Transaction migration

What I've described so far doesn't have reorg move transactions to different block heights. The transactions initially in block height N will always be in block height N (never N-1 or N+1). I would think this is okay; it's hard to imagine a bug that would allow the testing just described to pass, but if a transaction moves to a different block height following a reorg, the bug triggers. The wallets should be processing (adding) transactions as they appear in a block and undoing (removing) the effects of transactions when a block gets reorged away, and they should not behave differently when a transaction that previously was in block N is now in block N-1 or N+1, or even know that's happened.

It wouldn't be hard for darkwalletd to move transactions around (to different block heights) randomly as part of a reorg. But the problem I see is that the wallet test code won't know which state to expect at each height. Instead of moving transactions randomly, we could add an argument to SetState that tells darkwalletd which heights to move which transactions to. But that seems pretty complicated for limited or no benefit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment