Expanding our bitcoin-spv libraries

Recently, Summa received a grant from the Nervos Foundation to extend our work on bitcoin-spv to support Nervos CKB scripts. In doing so, we aim to enable cross-chain communication like swaps, relays, and bridges between Bitcoin and CKB. These bridges will allow Nervos to reach broader markets, expand its feature-sets, and access new liquidity in cross-chain marketplaces.

Testing in CKB

As part of our existing work, we’ve written in-depth test suites for code in a half-dozen languages. Working with CKB, however, presented a few unique challenges. CKB Scripts are written in C, Ruby, or JS and compiled to RISC-V binaries for deployment. Because we expect our work to be used as a library by other projects, we opted to use C. For our testing suite, we wanted to test the library’s stateless code (unit tests), as well as within a CKB Script (integration tests).

Unit tests with libcheck

For bitcoin-spv, we chose to write table tests because they directly map inputs to outputs — making them ideal for testing pure functions. Unlike typical golang table tests, we chose to use JSON as a portable data format. JSON allows us to use the same tables across multiple implementations in different languages. From our work on bitcoin-spv in other languages, we’ve compiled an extensive set of table tests. They check that all versions of the library have the same behavior and that the behavior is correct.

Since we already have a good set of test tables, the next step is getting access to them. For parsing the JSON test vector file, we chose JSMN. Most of our testing code is a minimal JSON parser using JSMN to get info from the test vectors file. You can check that out here. The parser is not recommended for production use but is sufficient for testing.

Now, we need a solid framework for running our tests. We’re looking for a C library that can automate test running, and report line and branch test coverage. We aim to get 100% test coverage in bitcoin-spv at all times. For the C library, we opted to use libcheck, although there are many equally good alternatives. Just like mocha, jest, pytest, and other testing frameworks, Check allows for coverage and a number of other useful features. First, we set up the case and runner. We also give it access to the test tables by calling our parser. An “unchecked” fixture means that the code is run once per case, rather than once per test.

From there, we lean on Check’s unit test tooling. Check’s START_TEST and END_TEST macros wrap a test function, which you then register as part of the test case. Once the tests are added to the case, we can instruct the runner to run them. To run our table tests, we wrote a couple of quick supplemental macros. The first macro loads a test table and starts a loop, and the second macro ends it.

Now writing our unit tests is easy. Each test starts with the check macro and our loop macro. Then the rest of our test function is inside the test loop. Our test function parses the input and output of the test vector, then calls the function being tested, and finally asserts that the actual output matches the expected output. After cleanup, the loop repeats until it runs out of tests in the table. We close out our function by ending our two macros. Doing so allows us to write tests as if a single input/output pair were being tested, and the macros handle running that code on all vectors.

That covers the simple library testing. In Part 2, we’ll cover testing your scripts by simulating Nervos transactions.

Summa provides cross-chain architecture and interoperability as a service solutions. We work actively with Ethereum (EIP-152), Keep, The Electric Coin Company (ZIP-221), and the Zcash Foundation. Our partners include Cross-Chain Group, Interchain Foundation, Nervos, NEAR Protocol, Agoric, and Celo.

Stay up to date with Summa on Twitter