Trustless, time-phased issuance & distribution for Counterparty assets using a Taproot vault with a NUMS internal key, fully auditable and broadcastable by anyone. Includes a sweep-fallback path with a published, deterministic policy.
This toolchain builds and operates an XCP-69 vault:
-
Vault type: Taproot P2TR output with no keypath spend (NUMS internal key), script-path only via CLTV leaves.
-
Phases:
- T0: Fairminter create
- T1: Order ladder
- T2a: Fees & destroys
- T2b: Dividends (per-unit rungs)
-
Publication model: You publish a complete bundle (manifest) with all parent transactions and pre-signed CPFP children. Anyone can verify and broadcast.
-
Fallback: If dividend is griefed (e.g., Sybil dust), you execute a Counterparty
sweepto your platform address and do a manual distribution from there under a published rule (see “Fallback policy” below).
- No keypath spend: The internal key is a standard BIP-341 NUMS point
H, tweaked by a randomryou disclose (K_int = H + r·G), so nobody has a private key forH. All spends must go through the tapscripts you published. - Tapscript leaves (CLTV): Four leaves (T0, T1, T2a, T2b) each enforce height gates. The witness for every parent is
[schnorr_sig, leaf_script, control_block]. - Auditable control: Anyone can recompute the Taproot commit
(K_int, merkle_root) → Qand matchQto the vault address.
No. Every parent spend here consumes a single vault UTXO controlled by Q. Outsiders can’t produce alternative inputs that satisfy the spend. They can rebroadcast the exact transactions you already signed (see “Broadcast & fee bumping”).
Counterparty charges an issuer fee per recipient for dividends. An attacker could scatter dust balances across many new addresses to inflate that fee and sabotage the dividend.
Defense used here:
- You pre-publish all dividend rungs and a fallback sweep plan.
- At T2b you evaluate a deterministic rule. If griefed, you broadcast a Counterparty
sweepof balances to your platform address and manually distribute XCP from there.
Fallback policy (published and binding):
- Eligibility threshold: Holders must have more than 1,000 whole units of the token (strict
> 1000) to receive the fallback distribution. - Rationale: This caps “dust recipient” explosion while preserving fairness for real holders.
- Transparency: You publish the block-height snapshot, the filtered recipient list, and the distribution transactions.
We eliminate selective reveal by publishing everything (PSBTs, fully signed parents, and CPFP children) up-front. The bundle is hashed and (optionally) pinned on-chain.
- Anyone can broadcast any of the published parent transactions at/after their CLTV height.
- Anyone can fee-bump by rebroadcasting the pre-signed CPFP children you published.
- Only you (holder of the anchor key) can author new CPFP transactions beyond the published ones. (Design choice: we publish ready-to-use children, but keep the anchor key controlled to avoid abuse.)
-
Rust toolchain (edition 2021)
-
Build:
cargo build --release
-
Binaries:
xcp69_setup(main CLI)verify_bundle(bundle auditor)
-
Setup — grind NUMS vanity, build tapscripts & unsigned parent PSBTs, and create a funding PSBT template:
xcp69_setup setup \ --network mainnet \ --cp-base https://api.counterparty.io:4000/v2 \ --asset YOURTOKEN \ --mend <block> \ --gap 20 \ --expiration 8064 \ --platform-xcp-dest <XCP_ADDR> \ --mode suffix --pattern <bech32-suffix> --threads <N> \ --funding-utxos "txid:vout:value_sat,..." \ --funding-change <FEE_RETURN_ADDR> \ --funding-type p2wpkh \ --export-secret p_script.hex \ --out bundle.json
-
Fund — sign and broadcast the funding PSBT from your fee wallet (external wallet). Wait for confirmation.
-
Prepare funding (optional helper) — discover fee wallet UTXOs and build a funding PSBT automatically:
xcp69_setup prepare-funding \ --bundle bundle.json \ --fee-key <WIF_or_hex> \ --rpc-url http://user:pass@127.0.0.1:8332 \ --network mainnet \ --out bundle_with_funding.json
-
Patch/Finalize — patch the real funding txid and produce fully signed parents + CPFP children, plus a commitment hash:
xcp69_setup finalize-parents \ --bundle bundle_with_funding.json \ --funding-txid <FUNDING_TXID> \ --p-script-file p_script.hex \ --out finalized_bundle.json
-
Publish — share
finalized_bundle.json(the manifest). Optionally publishsha256(finalized_bundle.json)via OP_RETURN or a Git tag and sign with PGP. -
Verify (community / you):
cargo run --bin verify_bundle -- --bundle finalized_bundle.json
-
Operate — at each phase height, anyone may broadcast the relevant parent + (if needed) rebroadcast the matching CPFP child to bump fees.
-
Dividend vs. Sweep decision at T2b — follow the published rule:
- If holder set looks clean: broadcast pre-published dividend rungs (largest→smallest within budget).
- If griefed or fee-infeasible: broadcast the published Counterparty
sweep(see below) and perform manual distribution using the eligibility rule> 1000units.
Fields include:
-
Vault & tapscript
- Network, vault address,
nums_proof(H,r,K_int,Q) tapscript_pubkey(P_scriptxonly),merkle_root- Phase heights
t0, t1, t2a, t2b
- Network, vault address,
-
Funding
funding_psbt_b64, number/value of intended vault UTXOs
-
Entries (per parent)
{name, phase, lock_height, purpose, opret_hex, psbt_b64, utxo, value_sat}
-
Finalized artifacts
parent_txs_hex[](all signed parents)cpfp_txs_hex[](all signed children)commitment_hash(SHA256 of the ordered parents)
-
Sweep fallback
- The composed
sweepdata (see below) and the published policy text you include with the release.
- The composed
The verify_bundle tool checks:
- CLTV heights and scripts match the phase
- Control blocks commit to
(K_int, merkle_root) → Q - Schnorr sigs verify against
P_script - OP_RETURN bytes equal
opret_hex commitment_hashmatches the includedparent_txs_hex[]
When dividend griefing (or fee infeasibility) is detected by the published rule, execute a balance sweep of the vault’s address to your platform address.
Compose (documented by CP API):
-
Endpoint:
GET /v2/addresses/{address}/compose/sweep -
Required params:
destination=<YOUR_PLATFORM_P2TR_or_P2WSH_or_P2WPKH>flags=<FLAG_BALANCES or FLAG_BALANCES|FLAG_OWNERSHIP>(For balance-only:flags=1)memo=<hex>(you can use a short tag, e.g.,DEADBEAF)
-
Suggested params:
return_only_data=truesegwit=trueverbose=true
You include the returned data blob (hex) as opret_hex for a single T2b parent that performs the sweep via the vault UTXO, analogous to other parents. (In this design we replace multi-rung XCP “sweeps” with one Counterparty Sweep instruction.)
Note: the codebase includes a
compose_sweephelper (mirrors othercp_compose_*helpers) and swaps the prior “post-dividend sweep rungs” with a single sweep entry.
When the sweep path is triggered, you will manually distribute XCP from the platform address to eligible holders only.
Policy:
-
Eligibility: token balance strictly greater than 1,000 whole units (
balance > 1000 * 10^8atomic). -
Snapshot height:
t2b - 1(published). -
Exclusions: known Sybil/dust patterns may be excluded if they evade the threshold.
-
Transparency: publish
- the snapshot (address, balance),
- the filtered list (≥ 1,000 units),
- the distribution amounts & transaction IDs.
Why > 1000? It removes cost-effective dust attacks while keeping real holders in scope.
If you prefer, you can commit to a hard cap on recipients (e.g., ≤ 25k) or a quality metric (median balance floor) — but the explicit
> 1000rule is simple, verifiable, and already sufficient to blunt dust Sybils.
-
Broadcast: Any observer can push any included parent tx hex once CLTV is met.
-
Fee bumping: Any observer can push the matching pre-signed CPFP child you published to raise effective fee.
- Only the anchor key can author new CPFPs, but this is not required for liveness if you publish viable children.
Creates tapscript tree, grinds a vanity Taproot vault using NUMS, composes all Counterparty messages, builds unsigned PSBTs, and emits bundle.json.
Key options:
--network mainnet|testnet|signet--asset YOURTOKEN--mend <block>(end of minting window)--gap 20(phase spacing)--expiration 8064(order expiration blocks)--platform-xcp-dest <ADDR>- Vanity grind:
--mode prefix|suffix --pattern <bech32> - Funding:
--funding-utxos "txid:vout:value,..." --funding-change <ADDR> --funding-type p2wpkh --export-secret p_script.hex--out bundle.json
Optionally finds fee wallet UTXOs and builds a funding PSBT automatically.
Injects the real funding txid, recomputes every parent sighash with the proper prevout, signs with P_script, builds CPFP children, and writes finalized_bundle.json with commitment_hash.
Audits a bundle: addresses, Taproot commitments, CLTV leaves, OP_RETURN payloads, Schnorr signatures, and the commitment hash.
- Anchors: Keep
anchor_satsgenerous (we default higher than bare minimum) so the pre-signed CPFP child has meaningful bump headroom at a wide fee range. - Per-unit dividend rungs: The tool includes a 2^n “qpu” ladder capped at 0.0001 XCP per unit. You broadcast largest→smallest until budget is consumed only when the clean dividend path is chosen.
- Publication: Always publish the full bundle + signature before phase heights. That’s your anti-“selective reveal” guarantee.
Q: What exactly proves “no keypath spend”?
A: You publish H (the BIP-341 NUMS point), your random r, and show K_int = H + r·G. Because H has no known discrete log and you only disclose r (not r + x_H), no key exists for K_int + tweak·G (the output key Q) other than the tapscripts you published.
Q: Why not let anyone author new CPFPs? A: That invites anchor theft & grief. We publish ready-to-use children so anyone can bump fees without granting new signing power.
Q: How do I prove I followed the fallback policy?
A: Publish the block-indexed snapshot (hash the CSV), the filtered list (balance > 1000 * 10^8), and the outgoing payments from the platform address. Third parties can reproduce the filter from chain data.
- No warranty. Test on signet/testnet first.
- Review the bundle with
verify_bundleand independent tooling. - Counterparty server semantics (fees, flags) may evolve; pin the API version you use in production documentation.