Tuliproot: Implementation Guide
Michael Tidwell
Working implementation design | October 23, 2026
Scope and status
This guide describes how to turn Tuliproot into software: a local wallet, a deterministic verifier, an optional seeding coordinator, an optional MuSig2 coordinator, and a separate web-of-trust indexer. It expands the technical specification draft. It does not introduce an implemented wire protocol or claim that the constructions have passed transaction testing.
Agreed rules below reflect the protocol design. Proposed implementation choices describe a way to build it. Unresolved requirements identify decisions that must be settled before independent implementations can interoperate. Names such as Encode, record fields, command names, and API routes are explanatory; they are not assigned bytes or existing commands.
The first milestone should be a solo identity on Bitcoin regtest, with an independent verifier able to replay its complete history. Batching comes afterward. This isolates state and authorization errors before introducing coordinator sessions or pairing cryptography.
1. Software boundaries
| Component | Responsibility | Material it must not require |
|---|---|---|
| Local wallet | Generate keys, construct policies, retain scripts and receipts, review and sign records and transactions. | A coordinator's private key or database. |
| Core verifier | Decode records, authenticate evidence, enforce reservations and action order, replay reorganizations. | Identity secrets or guardian secrets. |
| Chain adapter | Supply validated blocks, full transactions, witnesses, and spent-output context. | Trust in a coordinator's claimed confirmation. |
| Seeding coordinator | Validate signed records, construct the KZG batch, anchor its declaration, deliver receipts. | Participant identity or recovery secrets. |
| Public-registration coordinator | Freeze a manifest, run MuSig2 sessions, publish the authenticated records. | Control over individual TULIP spending policies. |
| WoT indexer | Apply accepted outgoing vouches and follow identity changes. | A recipient's consent to receive a vouch. |
Keep canonical decoding, hashing, descriptor derivation, and transition validation in one reusable core library. The wallet and server should call that library. An independent verifier should also be possible, so published vectors must describe inputs and expected outputs without depending on the coordinator's database.
A useful initial source layout is:
core/ canonical codec, hashes, descriptors, event validation
chain/ Bitcoin Core adapter, event extraction, block replay
wallet/ policy construction, backups, signing, transaction building
seed/ batch sessions, KZG adapter, receipt production
public/ manifests, MuSig2 sessions, publication
wot/ grouped updates and graph materialization
vectors/ byte fixtures, accepted histories, rejection cases
regtest/ reproducible transaction and reorganization scenarios
These are proposed module boundaries, not files already implemented in this repository.
2. Canonical records and transport
Every signed or hashed structure needs exactly one byte representation. JSON can be a debugging or HTTP representation, but its ordinary text serialization must not silently become the signed message.
The wire specification must choose field order, integer endianness, length encoding, optional-field discriminators, versioning, domain tags, and maximum sizes. It must reject duplicate fields, trailing data, invalid points, out-of-range integers, and alternative encodings of the same value. The decoder should produce a typed object only after those checks.
Suggested conceptual records are:
GenesisDescriptor {
version, chain_id, identity_nonce,
initial_internal_key, initial_tree_root
}
SeedEnrollment {
descriptor, coordinator_key, batch_id, challenge,
enrollment_nonce, delegation_scope, validity_conditions,
participant_signature
}
SeedDeclaration {
version, chain_id, batch_id, coordinator_key,
scheme_id, setup_id, domain_id, domain_size, record_count,
commitment, declaration_authentication
}
PublicEntry {
descriptor, provenance_mode,
prior_evidence_if_present, validity_conditions
}
Action {
version, chain_id, identity_id, epoch, predecessor,
action_type, complete_action_payload
}
The identity ID is derived from the genesis descriptor under the technical draft's domain-separated construction. A successor changes the current key and tree root while retaining that ID. Existing illustrative Rooted/... domain tags must not be silently renamed by one implementation.
Transport and availability are distinct from signatures. A digest can authenticate bytes only when the verifier has those bytes. Before accepting an onchain event, the carrier profile must define how the complete manifest, optional prior evidence, and action payload are recovered and authenticated. A coordinator URL alone is insufficient for a history intended to outlive that coordinator.
Unresolved: the carrier encoding, witness framing, fragmentation rules, within-transaction event positions, and whole-batch versus per-entry rejection policy. A prototype must declare its chosen experimental profile and must not present it as canonical v1.
3. Constructing a TULIP locally
The wallet generates an everyday identity key and separate authority for the spending paths selected by its owner. Rotation and recovery should use different keys. Neither should require the everyday key merely because it is the internal key.
Build a finite precommitted rotation chain from its end backward. Construct the last policy first; use its known successor information while constructing its predecessor. Never make a script depend on a final identity ID whose derivation already depends on that script's tree root. The identity nonce, chain, and epoch can provide construction context without that circular dependency.
Taproot address derivation, leaf hashing, branch hashing, x-only point handling, tweak validity, and control-block parity must follow BIP 341. Use a reviewed library and compare its results against independent vectors. Store the exact scripts and tree structure, not merely the resulting address.
A wallet backup needs:
- The genesis descriptor and stable identity ID.
- Each current and planned policy descriptor, leaf script, and control path.
- Rotation preimages and independent recovery material.
- Accepted transition locators and the current epoch.
- Signed enrollment records and receipts when seeding was used.
- The chosen experimental profile version and cryptographic parameter identifiers.
Public descriptors and secret recovery material need separate export handling. A public receipt must never contain spending secrets. Shamir shares of one key are backups of that secret; they are not a distributed signing protocol.
An illustrative recovery path
A separately held recovery key can authorize a transaction that spends a current TULIP output and commits the recovery record in an output. The wallet signs only after reviewing the complete transaction and successor descriptor. The verifier checks both Bitcoin validity and the recognized recovery template's relationship to that record.
For the initial prototype, use a signature mode that covers the action-bearing outputs and reject signature modes the selected template does not support. Do not assume that every Bitcoin-valid signature authenticates every output. Tapscript execution and its signature rules are defined by BIP 342; the protocol still needs a precise template for recognizing a recovery action.
4. Two-preimage rotation
The agreed rotation rule is a valid spend through the current TULIP's rotation path, with two required preimages and the proper ROTATE protocol header. One preimage reveals the next internal key. There is no universal requirement for an additional detached identity signature.
An implementation must verify the following relationship, rather than merely searching the witness for a recognizable word:
accepted current policy
-> spent output and verified control path
-> recognized rotation leaf
-> correct ROTATE header and both committed preimages
-> permitted successor key and resolved successor policy
-> next epoch under the same identity ID
A proposed leaf can use separate hash commitments to check two stack values. The exact hash operation, stack order, size checks, header commitment, and any additional signatures or timelocks belong in the template. A header dropped from the stack without being constrained cannot establish what the path authorizes.
Unresolved: the second preimage's contents and security role, the complete successor-tree resolution rule, and the exact ROTATE framing. They must be specified together. A key preimage alone does not identify the next tree root. In particular, a prototype must not assume without agreement that the second preimage is that root or that it is a secret authorization token.
This affects the claimed security after internal-key compromise. If both preimages and every other required value are public, and the leaf requires no independent secret or signature, anyone able to fund and spend that leaf may satisfy it. That can be an intentional permissionless transition to a precommitted successor, but it must not be described as exclusive owner authorization. The selected TULIP decides the additional conditions.
5. Solo public registration
Solo operation is the baseline, not an emergency dependency on a coordinator.
- Construct and locally verify the genesis descriptor.
- Select FRESH or prior-seed provenance. If prior evidence is selected, assemble it before signing the first public entry.
- Query the local verifier for prior accepted use of the initial key. This is a preflight check, not a reservation.
- Encode the complete entry and its chain, version, purpose, and session context.
- Sign the designated public-registration message using the initial identity key.
- Construct the carrier transaction. Fund a TULIP output immediately only if wanted.
- Verify the transaction and record locally, broadcast, and wait for the selected confirmation policy.
- Export the accepted descriptor, evidence, and anchor locator.
The first valid confirmed public enrollment wins. An earlier private seed receipt does not reserve the key. A preflight query cannot prevent a competing valid enrollment from confirming first.
Funding is separate from registration. Any later payment to the accepted TULIP address can provide an output for an action. Ordinary withdrawals, including withdrawal of every output, leave identity state unchanged.
Lost-key enrollment remains unresolved: a privately seeded user who loses the initial signing key needs a defined profile joining the earlier signed enrollment to committed action authority. This guide does not invent that authorization. A solo prototype can first support registration while the initial key is available and independent recovery after public registration.
6. Seeding coordinator on BLS12-381
The seeding stage uses KZG polynomial commitments over BLS12-381. It is often described in discussion as the BLS route, but it does not require participants to create BLS signing keys. Participants sign enrollment consent with their secp256k1 identity keys.
Session lifecycle
OPEN -> FROZEN -> COMMITTED -> ANCHORED -> RECEIPTS_DELIVERED
\-> ABORTED
While OPEN, the coordinator supplies its public identity, batch identifier, challenge, supported profile, and limits. Participants submit signed enrollments. The server validates canonical encoding, participant signatures, descriptor consistency, duplicate entries, validity conditions, and authorization scope before admission.
At FROZEN, fix the record order and actual record count. No record can be inserted, removed, or changed without creating a new commitment and associated declaration. Persist the batch inputs before publishing its anchor so receipts can be regenerated after a server restart.
Commitment construction
y_i = HashToField(selected_profile, canonical_signed_enrollment_i)
x_i = DomainPoint(domain_id, index_i)
f(x_i) = y_i
C = Commit(pinned_setup, f)
proof_i = Open(pinned_setup, f, x_i)
The cryptographic profile must pin the setup artifact and digest, supported domain, point ordering, padding, hash-to-field method, and point-validation rules. The declaration binds those choices, batch context, count, and commitment. Enrollment signatures alone do not bind a coordinator's later claim about which commitment was anchored.
Use an existing cryptographic implementation behind a narrow adapter. For example, c-kzg-4844 implements Ethereum-oriented KZG APIs; adopting it would require checking that its domain, field representation, setup size, and APIs match Tuliproot's chosen profile. Its availability does not establish compatibility with the larger domain discussed in the technical draft. Do not substitute an arbitrary-point proof for a fully specified indexed-membership scheme.
Receipt delivery and independent verification
A receipt contains the signed enrollment, authenticated declaration, index, opening, and confirmed anchor reference. The client verifies its own receipt before considering onboarding complete:
verify participant signature and descriptor
verify declaration authentication and chain anchor
require 0 <= index < record_count <= domain_size
derive the specified domain point and field value
verify the KZG opening under the pinned setup
persist receipt and policy backups independently
A commitment cannot restore records the user failed to retain. Receipt delivery should be retryable and authenticated; batch-level publication should expose no individual private enrollment unless the chosen profile requires it. Coordinator logs must not become an accidental public directory of private identities.
7. MuSig2 public-registration coordinator
The coordinator accepts either fresh entries or entries carrying complete prior-seed evidence. Both follow the same public registration rules. This is the join between the optional private seeding stage and public blooming.
Freeze the manifest before signing
Validate entries first, then freeze a manifest containing the chain, version, coordinator key, ordered entries, purpose, session identifier, and validity conditions. For immediate funding, include the agreed output commitment and verify the exact matching outputs. Each participant receives the complete manifest and recomputes the message locally.
The signer list follows the technical draft: distinct participant keys and the coordinator key in a prescribed order. Identity x-only keys are represented as their even-y compressed points for the signing profile. Duplicate signer keys are rejected by this proposed profile.
Signing lifecycle
VALIDATING -> MANIFEST_FROZEN -> NONCES_COLLECTED
-> PARTIAL_SIGNATURES_COLLECTED -> FINALIZED -> PUBLISHED
Use BIP 327 directly for aggregation, nonce handling, session processing, partial signatures, and final verification. MuSig2 requires all listed signers; it is not k-of-n threshold signing. The wallet must never reuse a secret nonce, including after an abort or crash. A changed manifest starts a new session with new nonces.
The coordinator checks partial signatures before finalization. Every participant verifies the final signature against the locally derived aggregate key and frozen message. The aggregate signature authorizes this registration manifest; it does not grant the coordinator future spending authority over the individual TULIPs.
Persist session identifiers, the frozen manifest hash, public nonces, public progress, and an abort reason. Keep secret nonces in a signing subsystem with a one-use lifecycle. Do not restore a consumed nonce from a database backup. On uncertain consumption after a crash, discard the nonce and restart the session.
If a participant disappears, abort or create a new batch without them and restart signing. Removing them from an already signed manifest is invalid. Participants retain enough enrollment material to register independently through a supported solo profile.
Suggested service interface
POST /public/sessions propose an entry
GET /public/sessions/{id}/manifest review frozen bytes and signer list
POST /public/sessions/{id}/nonces submit session-bound public nonces
POST /public/sessions/{id}/partials submit session-bound partial signature
GET /public/sessions/{id}/result obtain final manifest and anchor
These routes are an implementation sketch. Each submission needs authentication, size limits, idempotency rules, explicit session binding, and a defined error response. HTTP success is not proof of registration; confirmed validation is.
8. Deterministic verifier and storage
A practical local indexer can use a transactional database with tables for identities, accepted keys, authenticated evidence, accepted events, block metadata, and undo records. A key reservation must be unique across the entire accepted chain history, including retired and revoked keys.
IdentityState: ID, genesis, provenance, public_anchor,
epoch, current_key, current_root, predecessor, status
AcceptedKey: public_key -> ID, first_accepted_event
Event: block_hash, height, tx_position, action_position,
decoded_record, accepted_or_rejected, reason
Undo: previous values and newly created rows for one block
For each connected block, extract candidates using a bounded canonical parser. Process transactions in block order and candidates in the wire profile's defined action order. Validate before mutating state. A malformed early candidate cannot suppress a valid later one simply by naming the same identity.
apply_action(candidate, chain_context):
state = lookup(candidate.identity_id)
require state exists and state.status == LIVE
require candidate.epoch == state.epoch
require candidate.predecessor == state.predecessor
require candidate.chain == active_chain
verify current-policy output, script path and action payload
if candidate is ROTATE or RECOVER:
verify complete successor descriptor and target constraints
require successor key is not in AcceptedKey
verify successor acceptance if required by this policy
atomically reserve successor and advance state by one epoch
else if candidate is REVOKE:
atomically mark state REVOKED
else:
dispatch only recognized, authenticated nonterminal actions
Registration needs its own path: validate the full authorization and provenance, check ID and key uniqueness, then create epoch zero and its initial reservation atomically. For a batch, the failure policy must be fixed before applying entries; it remains an open wire-profile decision.
Recognizing an action is more than confirming that a spend is valid. The verifier must bind the spent output to the accepted (P, M), recognize the authorized leaf and witness structure, and validate every required successor or WoT field. A key-path withdrawal has no identity effect.
9. Reorganizations, discovery, and lookup
Use the block hash, not height alone, as the chain cursor. Commit a block's state updates and undo data together. On disconnection, reverse its accepted events, restore old identity state, remove reservations created only by the disconnected history, and rewind dependent WoT updates. Then replay the replacement branch.
The indexer should distinguish confirmed state from speculative mempool observations. A mempool event can be displayed as pending but must not reserve a key or revoke a confirmed identity in the authoritative store.
A lookup response should include the stable ID, current key and root, epoch, live/revoked status, accepted event locator, tip hash and height, and confirmation depth. Authentication services also need a fresh challenge signed by the currently accepted key. Historical enrollment consent is not a login signature.
A chain adapter using a validating Bitcoin node is the simplest initial trust model. A light client needs an explicit proof strategy for witnesses and history completeness. A transaction inclusion proof alone cannot establish that no later recovery or revocation occurred. This guide does not claim a complete light-client protocol.
10. Constructing and racing an action transaction
The wallet selects an output under the current policy, resolves its script and control block, fills the complete action payload, and asks the relevant independent authority to authorize the spend. Before broadcast, it checks the current epoch again and simulates the same validation path used by the indexer.
For recovery, the payload carries a fresh successor public key and complete policy context. Never place the private key in OP_RETURN. For rotation, the payload and witness must resolve the precommitted successor according to the finalized template. For revocation, the verifier records a terminal state without a successor.
The experimental fee-funded defense uses a zero-fee version-3 parent creating a small current-TULIP output, followed by an action child whose fee is supplied by a separate protected input. The initial package has only the parent and child. An optional P2A output belongs in the parent and is consumed by that same child; it does not create a third unconfirmed transaction.
If a competing key-path spend confirms instead of the action child, the child's protected fee input remains unspent, assuming it was not otherwise exposed. The owner loses the small contested output. An attacker pays a fee only when its conflicting transaction confirms; merely replacing an unconfirmed bid does not burn the full bid repeatedly. A successful recovery pays its own actual transaction fee.
Before presenting this as a demonstrated defense, reproduce the package and replacement cases against a pinned Bitcoin Core version and configuration. Record raw transactions, output values, virtual sizes, package acceptance results, conflicts, confirmations, and both parties' net balances. Consult the technical draft's fee-funded recovery section for the proposed accounting and limits.
11. Web-of-trust indexing
Keep WoT as a separate consumer of authenticated Tuliproot history. Its graph is directed: the source identity makes a statement about a target key. The recipient does not sign or approve that statement.
Store the source ID, target key or resolved target identity, weight, and originating event position. Weight 8 is the default; weight 0 removes a connection; positive weights through 16 express stronger vouches. Their application meaning is left to graph consumers.
Grouped changes share one weight across a group of keys. A proposed two-byte header needs enough information to distinguish operations and count the following keys. Its actual bit layout, count bounds, default-list discriminator, and replace-set semantics remain unresolved. Do not silently assign one implementation's encoding as v1.
Retain the originally named target key as well as its resolution. Follow accepted rotations and recoveries for registered targets. Mark a live connection dead on target revocation. A private or unregistered target remains a key reference until the chosen mapping rules resolve it; do not infer an unpublished seed record from a bare key.
Replacement sets should be applied atomically once their ordering and scope are defined. Undo WoT changes with the same block history that authorized them. Malformed graph data must not alter core identity state.
12. Validation plan and implementation milestones
| Milestone | Required demonstration |
|---|---|
| Canonical experimental profile | Byte-for-byte fixtures, round trips, rejection of ambiguous encodings, fixed version and domain tags. |
| Solo registration | One confirmed identity, deferred funding, duplicate and wrong-chain rejection. |
| Independent policy actions | Rotation, recovery without the old key, terminal revocation, ordinary withdrawal with no state change. |
| Ordered replay | Conflicting actions in both block orders; explicit within-transaction order; stale output rejection; no key reuse. |
| Reorganization | Disconnect recovery or revocation, restore state and reservations, replay a competing branch. |
| Seed coordinator | Independently checked receipts, wrong-index and wrong-setup rejection, coordinator shutdown followed by supported solo publication. |
| MuSig2 coordinator | Full manifest review, valid aggregate consent, abort/retry with fresh nonces, signer-list and funding substitution rejection. |
| WoT layer | Default and explicit weights, zero removal, grouped updates, successor following, revocation and undo. |
| Fee-funded recovery | Real package relay and replacement results with measured victim/attacker accounting. |
Include negative cases that target the design rather than just checking successful code paths: an invalid candidate preceding a valid one, a seed proof for another descriptor, a changed manifest after nonce collection, a dropped header, an unsigned substituted successor, and a successor key accepted earlier by another identity.
No coordinator is required for the solo milestone. No BLS secret is required for participant enrollment. No production deployment should depend on an unspecified second preimage, carrier format, or lost-key public-enrollment rule. Resolving those details and publishing independent vectors turns this implementation design into an interoperable protocol specification.
Primary references
- Technical specification draft: the project's detailed design and open decisions.
- BIP 340: participant Schnorr signatures.
- BIP 341: Taproot construction and spending rules.
- BIP 342: tapscript execution.
- BIP 327: MuSig2.
- RFC 9380: hash-to-field methods relevant to the pending scalar-field profile.
- c-kzg-4844: an implementation to assess for compatible KZG operations, not a selected Tuliproot dependency.