Skip to content

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.

From a checkout of libspiffy:

Terminal window
dart test # everything not skipped by a tag
dart test test/unit/ # unit tests
dart test test/integration/ # integration tests
dart test test/services/ # service tests
dart test test/core_models/ # domain model tests

dart_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.

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.

Terminal window
POSTGRES_DATABASE=libspiffy_test dart test --tags=postgres test/storage/postgres/
dart test --exclude-tags=postgres

These run complete LibSpiffyActorSystem nodes against a regtest Teranode: headers over the wire protocol, broadcasts through Arcade, coins from the stack’s faucet.

Terminal window
dart test -P localnet test/integration/localnet_node_e2e_test.dart # header sync, restarts
dart test -P localnet test/integration/localnet_payment_e2e_test.dart # invoices and payments
dart test -P localnet test/integration/localnet_channel_e2e_test.dart # payment channels
dart test -P localnet test/integration/localnet_deferred_e2e_test.dart # deferred payments, double spends
dart test -P localnet test/integration/localnet_reorg_e2e_test.dart # chain reorganizations
dart test -P localnet test/integration/localnet_delegated_payment_e2e_test.dart # payment to an offline payee
dart test -P localnet test/integration/localnet_type42_payment_e2e_test.dart # Type-42 payments
  • The tests expect the stack at ../localnet-teranode. Set LOCALNET_TERANODE to use another checkout. They read RPC credentials and the faucet address from its .env and miner.env.
  • They need Go installed: coins come from the stack’s faucet (go run . send <address> <sats> --arcade).
  • LOCALNET_LOG=1 prints 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 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:

Terminal window
./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 chain

The 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.

test/support/localnet_node.dart
// 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 BSV in the user agent. Teranode bans any peer whose user agent lacks BSV or Bitcoin SV for 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-legacy lifts 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.

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.

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_NETWORK must also accept MINED.
  • Mine explicitly with ./scripts/mine.sh; do not wait for the autominer (every 10 minutes). Set MINE_INTERVAL=0 in .env to 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/health and ./scripts/status.sh.
  • Run one faucet send at a time. Two concurrent sends can pick the same coin.