Tinylayer BIP448 client
tinylayer-client is the untrusted protocol library for Tinylayer 2. It
constructs and verifies every Bitcoin object while the Enclavia workload only
linearizes capabilities and signs 32-byte template hashes.
Public surface
The library provides:
- official-vector-pinned BIP446 TemplateHash calculation;
- funding and state Taproot trees under the BIP341 NUMS key;
- stateless initial update construction;
- lazy activation request and response verification;
- canonical signed state preparation/completion;
- complete ordered-history verification;
- latest update rebinding to funding or stale state outputs;
- version-3 P2A fee-child construction;
- delayed owner settlement construction;
- production and debug Enclavia transport.
See the protocol specification for normative scripts and transaction formats.
TemplateHash
let hash = tinylayer_client::template_hash(&transaction, 0)?;
Tinylayer supports only protocol input zero with no annex. The hash commits version, locktime, all sequences, all outputs, annex absence, and input index. It omits prevouts, amounts, and scripts as specified by BIP446.
Changing only:
transaction.input[0].previous_output = another_outpoint;
leaves hash unchanged. Any output, sequence, or locktime mutation changes it.
Funding without registration
let info = enclave.info().await?;
let keys = tinylayer_client::keys(&client_secret, info)?;
let initial = tinylayer_client::initial_state(
keys,
amount_sat,
initial_locktime,
withdrawal_pubkey,
)?;
let address = tinylayer_client::funding_address(keys, initial.template_hash);
No coin row exists yet. After the exact address is funded and confirmed, bind the placeholder prevout:
let initial = bind_initial_state(initial, funding_outpoint)?;
let metadata = metadata(funding_outpoint, amount_sat, keys, &initial)?;
The bootstrap leaf commits to the initial template hash, so state 1 is safe even if the service disappears.
Lazy activation and signing
The first sender creates:
let activation = activation_request(&metadata, capability_hash(&capability));
let status = enclave.activate(&activation).await?;
verify_activation(&metadata, &activation, &status)?;
Then prepare and durably store the exact next request before sending it:
let prepared = prepare_update(
&metadata,
&status,
&history,
&client_secret,
capability,
receiver_capability_hash,
next_lock_time,
receiver_withdrawal_pubkey,
)?;
let response = enclave.sign(prepared.request()).await?;
let state = complete_update(&metadata, prepared, &response)?;
The completed state holds independent client and Enclavia BIP340 signatures over the same TemplateHash.
Rebinding
Spend an unspent funding output with the latest signed state:
let transaction = update_for_source(&metadata, latest, UpdateSource::Funding)?;
Publish bootstrap state 1 without signatures:
let transaction = update_for_source(
&metadata,
&history[0],
UpdateSource::FundingBootstrap,
)?;
Replace a confirmed stale state:
let transaction = update_for_source(
&metadata,
latest,
UpdateSource::State {
state: stale_descriptor,
transaction: &observed_stale_transaction,
},
)?;
The function verifies the observed transaction’s template, reconstructs its Taproot tree, changes the latest update’s prevout, and attaches the old update leaf/control block. It does not create a new signature.
Fee package
Updates preserve the full coin amount and include a zero-value P2A output at index 1. A confirmed wallet fee UTXO funds the child:
let child = build_update_fee_child(
&update,
fee_outpoint,
&fee_output,
&fee_secret,
fee_rate_sat_vb,
)?;
Submit parent and child through Mutinynet package relay.
Settlement
After the update reaches the 12-block CSV age:
let settlement = build_settlement(
&metadata,
latest_state,
&confirmed_update,
&withdrawal_secret,
destination_script,
fee_rate_sat_vb,
)?;
The owner signs the settlement leaf locally. Enclavia is not contacted.
Remote Enclavia transport
Production:
let enclave = RemoteEnclave::connect(url, pinned_pcrs).await?;
Debug-attested test server:
let enclave = RemoteEnclave::connect_debug(url, pcrs).await?;
The SDK reconnects and re-attests but never silently resends an application request. Retrying exact activation/signing bytes is the caller’s responsibility.
Verification
cargo test --locked -p tinylayer-client --lib
cargo test --locked -p tinylayer-client --test bip448
cargo test --locked -p tinylayer-client --test remote
The tests cover the official BIP446 vector, committed-field mutation, activation, sibling rejection, three states, stale-state rebinding, P2A child, settlement, and attested remote calls.