Testing and localnet
This page shows how to run libspiffy’s tests, including the end-to-end tests that run against a real regtest Teranode, and how to point your own app or tests at the same local network.
libspiffy’s test suite
Section titled “libspiffy’s test suite”From a checkout of libspiffy:
dart test # everything not skipped by a tagdart test test/unit/ # unit testsdart test test/integration/ # integration testsdart test test/services/ # service testsdart test test/core_models/ # domain model testsdart_test.yaml sets a 2-minute timeout per test and concurrency: 1: suites run one at a time,
because Isar’s native core is not safe to initialize from several isolates at once.
Tags and presets
Section titled “Tags and presets”Tests that need an outside service are tagged. Two tags are skipped unless you select their preset
with -P:
| Tag | Needs | Run with |
|---|---|---|
localnet |
../localnet-teranode running (Teranode, Merkle Service, Arcade). Mines blocks on its shared chain. |
dart test -P localnet <file> |
arcade |
The BSV Association’s public testnet Arcade and WhatsOnChain testnet. | dart test -P arcade test/integration/arcade_testnet_live_test.dart |
Two more tags select without skipping: postgres (needs a running PostgreSQL; see
PostgreSQL backend) and
integration.
POSTGRES_DATABASE=libspiffy_test dart test --tags=postgres test/storage/postgres/dart test --exclude-tags=postgresThe localnet tests
Section titled “The localnet tests”These run complete LibSpiffyActorSystem nodes against a regtest Teranode: headers over the wire
protocol, broadcasts through Arcade, coins from the stack’s faucet.
dart test -P localnet test/integration/localnet_node_e2e_test.dart # header sync, restartsdart test -P localnet test/integration/localnet_payment_e2e_test.dart # invoices and paymentsdart test -P localnet test/integration/localnet_channel_e2e_test.dart # payment channelsdart test -P localnet test/integration/localnet_deferred_e2e_test.dart # deferred payments, double spendsdart test -P localnet test/integration/localnet_reorg_e2e_test.dart # chain reorganizationsdart test -P localnet test/integration/localnet_delegated_payment_e2e_test.dart # payment to an offline payeedart test -P localnet test/integration/localnet_type42_payment_e2e_test.dart # Type-42 payments- The tests expect the stack at
../localnet-teranode. SetLOCALNET_TERANODEto use another checkout. They read RPC credentials and the faucet address from its.envandminer.env. - They need Go installed: coins come from the stack’s faucet (
go run . send <address> <sats> --arcade). LOCALNET_LOG=1prints the library’s log records (INFO and above), each tagged with the node that wrote it.- They mine blocks on the shared chain, so they never assume a height.
The shared pieces are in test/integration/localnet_harness.dart. It is the best example of a
libspiffy node wired to localnet. Its LocalnetNode waits for the answer to every request it sends
with coordinator.ask(), and keeps next<T>() for events nobody requested, such as a
confirmation arriving when a block is mined:
// Sketch: how the localnet tests wait, abridged.final balance = await alice.coordinator.ask(GetBalanceQuery(walletId: 'alice'));
final confirmed = alice.next<TransactionConfirmedEvent>((e) => e.txid == txid, timeout: const Duration(minutes: 2));Its top-level answer() returns a request’s reply even when the reply reports a failure, for tests
that assert on a refusal’s fields.
libspiffy 5.0.0 has no svnode tag. Its tests went with NodeRpcDataSource, which 5.0.0 removes.
localnet-teranode
Section titled “localnet-teranode”localnet-teranode is a regtest Teranode in Docker with Arcade (an ARC-compatible API), a Merkle Service that makes the proofs, an autominer and a faucet. Its README covers installing and running it; in short:
./scripts/start.sh # first run sets everything up./scripts/faucet.sh send <address> <satoshis> --arcade # fund an address./scripts/mine.sh 1 # mine a block now./scripts/stop.sh # stop, keep the chain./scripts/reset.sh # wipe the chainThe endpoints an app needs, from the host:
| What | Where |
|---|---|
Arcade (ARC API, no /v1) |
http://127.0.0.1:23011 |
| Arcade health | http://127.0.0.1:23012/health |
| Bitcoin wire protocol (headers) | 127.0.0.1:18444 |
| DataHub | http://127.0.0.1:18090/api/v1 |
| Teranode JSON-RPC | http://127.0.0.1:19292 (basic auth from .env) |
To reach it from a phone on your LAN, set HOST_IP=0.0.0.0 in the stack’s .env, run
./scripts/start.sh again, and use your computer’s LAN address.
Point your app at localnet
Section titled “Point your app at localnet”// Sketch: a libspiffy node on localnet-teranode, as localnet_harness.dart builds one.import 'package:isar_community/isar.dart';import 'package:libspiffy/libspiffy.dart';
Future<LibSpiffyActorSystem> startLocalnetNode(String dir) async { final isar = await Isar.open(LibSpiffySchemas.allSchemas, directory: dir); final libspiffy = LibSpiffyActorSystem(); await libspiffy.initialize( isar: isar, dataDirectory: dir, secureStorage: InMemorySecureStorage(), // tests only networkType: 'regtest', enableP2P: true, peerAddresses: ['127.0.0.1:18444'], // regtest has no DNS seeds arcConfig: ArcServiceConfig(baseUrl: 'http://127.0.0.1:23011'), // no /v1 ); return libspiffy;}What matters here:
networkType: 'regtest'. Regtest has its own genesis header; addresses use testnet encoding (m…/n…).- Name the peer. Regtest has no DNS seeds. Headers come from Teranode’s wire protocol on
:18444; Arcade does not serve headers on regtest. - Keep
BSVin the user agent. Teranode bans any peer whose user agent lacksBSVorBitcoin SVfor 24 hours, and every connection from your machine shares one Docker gateway address, so one bad client locks out all of them. libspiffy’s default,/LibSpiffy-BSV:1.0/, passes. If you are banned,docker restart tnl-legacylifts it. - Give an
arcConfig. Without one, any non-mainnet network defaults to TAAL’s testnet ARC. - Fees. Arcade enforces 100 sat/kB and publishes it at
GET /policy; libspiffy pays it.
Fund a wallet with a proof
Section titled “Fund a wallet with a proof”A wallet needs a merkle proof of a payment before it counts it as confirmed. Arcade has proofs only
of transactions submitted to it, so fund through Arcade (--arcade), mine a block, then build a
BEEF from Arcade’s merklePath and import it. This is what minedBeef() and receiveMined() in
localnet_harness.dart do:
// Sketch: build a BEEF from Arcade's proof and import it into a wallet.import 'dart:convert';import 'dart:typed_data';import 'package:http/http.dart' as http;import 'package:libspiffy/coordinator.dart';import 'package:libspiffy/libspiffy.dart';
Future<List<int>> minedBeef(String txid, String rawHex) async { while (true) { final response = await http.get(Uri.parse('http://127.0.0.1:23011/tx/$txid')); if (response.statusCode == 200) { final body = jsonDecode(response.body) as Map<String, dynamic>; final path = body['merklePath'] as String?; if (body['txStatus'] == 'MINED' && path != null && path.isNotEmpty) { return BEEF.create( bumps: [BUMP.fromHex(path)], txs: [hexToBytes(rawHex)], hasMerkle: [true], bumpIndex: [0], ).serialize(); } } await Future<void>.delayed(const Duration(milliseconds: 500)); }}
Uint8List hexToBytes(String hex) => Uint8List.fromList([ for (var i = 0; i < hex.length; i += 2) int.parse(hex.substring(i, i + 2), radix: 16) ]);
Future<void> importMined(LibSpiffyActorSystem libspiffy, String walletId, String txid, String rawHex) async { final beef = await minedBeef(txid, rawHex); // Throws CoordinatorFailure when the wallet refuses the import. final imported = await libspiffy.coordinator .ask(ImportTransactionCommand(walletId: walletId, beef: beef)); print('imported ${imported.transactionId}');}The faucet prints the txid and the raw hex of the payment it sent. Before importing, wait until
your node’s header chain holds the block (libspiffy.headerChain.bestHeight), as the harness’s
headersAt() does; a proof for a block whose header the node has not seen yet waits for it.
Writing reliable localnet tests
Section titled “Writing reliable localnet tests”From localnet-teranode’s CONSUMING.md:
- Wait for a status or anything after it. A block can be mined between two polls, so a test
waiting for
SEEN_ON_NETWORKmust also acceptMINED. - Mine explicitly with
./scripts/mine.sh; do not wait for the autominer (every 10 minutes). SetMINE_INTERVAL=0in.envto turn it off. - Do not assume heights. The chain is shared and grows between runs.
- Check health first:
curl -sf http://127.0.0.1:23012/healthand./scripts/status.sh. - Run one faucet send at a time. Two concurrent sends can pick the same coin.