Repository navigation
v11.2.0
Release Notes - Counterparty Core v11.2.0 (2026-07-08)
Counterparty Core v11.2.0 focuses on smaller databases, faster API paths, clearer API validation and a set of gated protocol fixes. The upgrade path is the same as usual; the main operational difference is a one-time Ledger DB compaction on first start, so operators should schedule the restart for a window where a longer startup is acceptable.
Upgrading
Upgrade Instructions:
To upgrade, download the latest version of counterparty-core and restart counterparty-server.
With Docker Compose:
cd counterparty-core
git pull
docker compose stop counterparty-core
docker compose --profile mainnet up -dor use ctrl-c to interrupt the server:
cd counterparty-core
git pull
cd counterparty-rs
pip install -e .
cd ../counterparty-core
pip install -e .
counterparty-server startThe Ledger DB is automatically compacted on first start of v11.2.0 (migration 0010.compact_hash_storage). No manual reparse is required. On large databases this can take a while and temporarily needs extra free disk space while SQLite rewrites and vacuums the DB; keeping a normal pre-upgrade backup or snapshot is recommended.
ChangeLog
API Breaking Changes
- Unknown query parameters are now rejected with a
400error instead of being silently ignored; each route only accepts its own documented parameters. - Stricter parameter validation: booleans, enums, dust sizes, fees,
confirmation_target(1–1008),utxo_value,more_outputs/inputs_setvalues, duplicate or out-of-range UTXOs, dispense quantity and memo types are now validated. Previously-tolerated invalid values now return a clean400instead of being coerced or causing a500. compose/dispenseresponse change: in the returnedparams,sourceis renamed toaddressanddestinationtodispenser, andquantity_normalizedis added.- The non-functional
sortparameter was removed from the multi-address balances route (/v2/addresses/<addresses>/balances).
Protocol Changes
The activation block heights are 961100 (mainnet), 5064400 (testnet3), 147200 (testnet4) and 316200 (signet) — targeting roughly 2026-08-03; the one exception is fairmint_pool, which is also active on signet from genesis (signet_block_index: 0) for testing:
sweep_skip_zero_balances: sweeps no longer include zero-quantity balances, the anti-spam fee is computed only over the balances/ownerships selected by the sweepflags, and an empty sweep is no longer charged the legacy flat fee.issuance_callable_lock_fix: removes the obsolete "cannot change callability / advance call date / reduce call price" reissuance restrictions (theissuance_callability_parameters_removalguard is preserved).multisig_utxo_addresses: bare multisig (P2MS) UTXOs now resolve to an address instead of failing with "vout does not have an address".fix_fairminter_commission_minimum: rejects aminted_asset_commissionbetween 0 and 0.0000001 (which would round to zero).- Add fairmint pool seeding behind a new
fairmint_poolgate:compose_fairminteraccepts two new optional fields —pool_quantity(tokens to reserve for the AMM pool) andlp_asset(numeric asset to use as LP token, auto-generated if omitted) — which, when the fairminter closes at soft cap, automatically creates a constant-product AMM pool seeded withpool_quantityof the minted asset and the corresponding XCP proceeds;hard_capmust equalexisting_supply + premint_quantity + pool_quantity + soft_cap(all mintable supply is reserved for buyers or the pool),burn_paymentis disallowed, and the issuer must hold sufficient XCP to cover the pool-deposit gas fee at compose time; pool creation is deferred to theafter_blockhook so it is gated on the same block as fairminter close; the LP token is earmarked against the active fairminter to prevent griefing. fix_pool_deposit_product_overflow: removes a redundantquantity_a * quantity_b > MAX_INToverflow check inpooldeposit.validate()that rejected otherwise-valid large AMM deposits — it capped each side of a balanced divisible pair atfloor(sqrt(MAX_INT))raw units (~30.37 divisible units) even when both quantities, the minted LP amount, balances, and stored reserves were each individually valid (#3324).fix_pool_best_price_routing: fixes AMM best-price routing under integer pool math.compute_pool_input_for_target_price()solved a continuous-price quadratic while execution floors the pool output to an integer (compute_pool_output), so a pool fill that was actually cheaper than the next book order could floor to zero output and send the taker to the worse book order (and misroute in the other direction); routing is now consistent with the integer execution (#3325).enforce_utxo_vout_max: atx_hash:voutUTXO reference whosevoutexceeds0xFFFFFFFF(4294967295) is no longer recognized as a valid UTXO format. UTXO-format detection routes a transaction's attach/detach and debit/credit classification, so this is a consensus change; before activation the legacy behavior is kept (any non-negative integer vout that round-trips throughstr(int(...))is accepted).
Performance
- Compact hash storage in the Ledger DB: transaction hashes, block hashes and other fixed-size hex strings are now stored as raw 32-byte BLOBs instead of 64-char hex strings, cutting hash-column storage roughly in half. A new migration (
0010.compact_hash_storage) rewrites all affected tables and runsVACUUMafterwards; a newhashcodecmodule handles encoding/decoding transparently so the rest of the codebase is unaffected. The migration runs automatically during upgrade and may need additional temporary disk space while it completes. - Composite match-ID normalization (folded into
0010.compact_hash_storage): the 129-chartx0hash_tx1hashTEXT match id is replaced by an integer(tx0_index, tx1_index)foreign-key pair onorder_matches,bet_matches,rps_matches, the three*_match_expirationstables,bet_match_resolutions,btcpaysandrpsresolves. The textualid/*_match_idis reconstructed on read at the API and consensus boundaries, so the public API surface and the consensus hashes are unchanged. - Asset normalization (folded into
0010.compact_hash_storage): theassetstable gains a compact, sequentially-assignedasset_index INTEGER PRIMARY KEYsurrogate, and every asset-name TEXT column (asset,give_asset,get_asset,forward_asset,backward_asset,dividend_asset,asset_parent,asset_a,asset_bonbalances,credits,debits,sends,orders,order_matches,issuances,dividends,dispensers,dispenses,dispenser_refills,fairminters,fairmints,destructions,pools,pool_deposits,pool_withdrawals,pool_matches) is replaced by the compact integerasset_indexforeign key — 1–3 bytes per reference instead of the full asset name. The protocolassets.asset_idandlp_assetcolumns stay TEXT. The asset name is transparently restored on read (therowtracerdecodesasset_index→ name) and re-encoded on write, so consensus hashes and the public API (which still returns/filters by asset name) are unchanged. The State DB keeps asset names (the consolidation decodes index → name while the Ledger DB is attached). Note: invalid records referencing a never-registered asset storeNULL. - Address normalization (folded into
0010.compact_hash_storage): a newaddress_list(address_id INTEGER PRIMARY KEY, address TEXT UNIQUE)table assigns every distinct address a compact id, and every address TEXT column (source,destination,address,source_address,destination_address,issuer,feed_address,tx0_address,tx1_address,winner,oracle_address,origin,last_status_tx_source,utxo_addressacross the ledger tables) is replaced by the compact integeraddress_idforeign key. The address string is transparently restored on read (therowtracerdecodesaddress_id→ address) and re-encoded on write, so consensus hashes and the public API (which still returns/filters by address) are unchanged. The State DB keeps addresses as strings (the consolidation decodes id → address while the Ledger DB is attached). The existingaddressesoptions-history table is preserved; itsaddresscolumn is FK'd ontoaddress_list.mempool.addresses(a comma-separated list) andmempool_transactionsare left as TEXT. - UTXO column compaction (folded into
0010.compact_hash_storage): theutxo TEXTcolumn (tx_hash:vout, ~67 chars) onbalances,creditsanddebitsis replaced by a compact(utxo_tx_hash BLOB(32), utxo_vout INTEGER)pair (~36 bytes). Thetx_hashis stored as a raw BLOB rather than atx_indexforeign key because an attached UTXO may reference any valid bitcoin transaction (not only Counterparty transactions). Theutxostring is transparently reconstructed on read by therowtracer, so consensus hashes and the public API are unchanged.
Bugfixes
-
Close fairminter when hard cap is hit after the soft-cap deadline has passed
-
Close non-pool fairminter when hard cap is hit before the soft-cap deadline (was leaving the fairminter open after escrow distribution)
-
Fix stale-row lookup in
get_fairminters_by_soft_cap_deadline(was returning superseded rows) -
Fix
connection_countleak inAPSWConnectionPoolcausingMAINPROCESS_POOLto exhaust over time (per-request threads in APIv1 left cached connections counted forever) -
Fix
NotSupportedTransactionsCacheunbounded growth: the in-memory cache of not-supported transactions grew without limit (reaching hundreds of MB) and was rewritten to disk and reloaded across restarts; it is now size-bounded and no longer persisted on every update (#3397) -
Don't charge an oracle fee when closing a dispenser
-
Preserve subasset longname case in balance lookups and sort balances by
assetusing the subasset longname -
Resolve subasset longnames when composing fairmints
-
Fix MPMA sends: correct per-asset balance accumulation, byte-accurate text-memo length encoding, require a destination per asset, and reject duplicate asset/destination pairs
-
Fix holder/supply consolidation that skipped deduplication (
idused instead of the record key) -
Default a missing bet
target_valueto zero and clarify BTC dividend "below dust" errors -
Consensus safety — halt instead of silently dropping a confirmed transaction when VIN/prevout resolution fails: a transient Bitcoin RPC failure while fetching a prevout now surfaces as
BitcoindRPCError(retried, and ultimately halting catch-up) instead of being swallowed into a dropped transaction, which could permanently fork the ledger (observed at block 510556); the batch RPC path now also retries502/504and matches responses byid -
Consensus safety — don't write a non-deterministic
utxo_addresson a transient RPC failure:safe_get_utxo_addressreturns"unknown"only for the deterministic address-less case (a resolvable output with no decodable address, which is canonical consensus history), while a transient RPC fetch failure now raisesBitcoindRPCErrorand propagates. Previously a transient failure was indistinguishable from a genuinely address-less output and could poisonutxo_address, forking consensus (observed on signet at block 267825) -
Consensus safety — gate the
get_vin_info_legacy(RPC-fallback)is_segwitcomputation onfix_is_segwit: the fallback path derivedis_segwitfrom the prevout's own script ungated, applying the block-902000fix_is_segwitbehavior early and mis-encoding a bech32 source as base58 on that path (fork at block 832867); it now follows the parent-transaction-segwit rule gated at the same height as the main path -
Stop reporting shutdown interrupts as errors: when the server is stopping,
RawMempoolParser.stop()callsdb.interrupt()on the connection shared with the blockchain watcher, aborting any in-flight parse asParseTransactionError("interrupted"); the watcher now treats this as expected teardown noise (logged at debug) instead of logging an error, paging Sentry, and forcing a "fail loud" restart -
Expose a unique
credit_index/debit_indexfield oncredits/debitsrows. Identical rows can be written within a single transaction (e.g. an MPMA send or a dividend crediting the same address+asset more than once), making them byte-identical and indistinguishable to API consumers; the new field carries the row's stable unique id so they can be told apart (#3320) -
Expose
lp_assetfield incompose_fairminterendpoint; extendfairminterstable withpool_quantityandlp_assetcolumns -
New routes:
GET /v2/addresses/<address>(and/options),GET /v2/addresses/<address>/dispensers/sourceand/dispensers/origin,GET /v2/bitcoin/transactions/<tx_hash>/info, andPOST /v2/compose/detach(detach assets from several UTXOs at once) -
sortparameter support added to the sends, issuances, dispenses, broadcasts and dividends routes, with sortable fields documented per route -
Compose responses now include
xcp_fee(dividend, sweep, pooldeposit, poolwithdraw, attach); order and order-match results includemarket_price_normalized -
New compose aliases
message_only(forreturn_only_data) andcustom_inputs(deprecated, forinputs_set); segwit outputs now use a dedicatedsegwit_dust_size -
Enum parameters accept comma-separated values, and dispenser
statusaccepts numeric values -
/v2/transactions/inforecovers source/destination/amount for BTC-only transactions even without prevouts -
Add
Cache-Controlheaders and a--disable-api-cacheflag, add API error context to Sentry reports, format IPv6 Gunicorn bind addresses, and report invalid transaction status and empty sweeps clearly -
Add
--api-cache-size N(configAPI_CACHE_SIZE, default1000— unchanged behavior) to bound the API response cache (BLOCK_CACHE) by entry count instead of the old hardcoded 1000-entry cap; lowering it caps the API process's resident memory (trading a small tail-latency increase for a bounded working set),0effectively disables caching (#3443) -
Documentation: API response codes, Counterparty transaction data format, the
messagestable, and sortable route fields
Codebase
- Fix intermittent BIP143 signature mismatches in regtest by broadcasting tx1 before signing tx2, so the wallet sees tx1's UTXOs in its mempool view
- Add a
bootstrap-oncecatch-up mode (downloads the bootstrap only when no database exists) - Re-enable the profiler on Python 3.12, skip the count query for single-row selects, and normalize null transaction
btc_amountto0 - Serve the
result_countof/v2/eventsand/v2/events/<event>from the pre-aggregatedevents_counttable instead of a fullmessagestable scan (turning a ~222ms count on every cache-miss request into a sub-millisecond aggregate); the public query parameters are unchanged - Cache the enriched, ready-to-serve API response in
BLOCK_CACHEinstead of the rawQueryResult, so a cache hit now skips the per-row verbose enrichment (inject_details) and its DB lookups that previously re-ran on every request — collapsing the repeated-request cost that dominated the heavy-endpoint latency tail (production p95/p99). API output and status codes are byte-identical (enrichment still runs once per cache miss, outside the function-calltry, so error semantics are unchanged); cache entries are now larger (enriched payloads), so--api-cache-size/API_CACHE_SIZEshould be sized accordingly (#3444) - Add
--api-cache-max-rows N(configAPI_CACHE_MAX_ROWS, default50000;0disables the row bound) to bound the API response cache (BLOCK_CACHE) by total rows in addition to the entry count (--api-cache-size). An entry-count cap alone forces a memory↔hit-rate tradeoff (a small cap lowers the hit rate, roughly doubling p95/p99 on real traffic); since most cached responses are tiny, a row budget caps memory (driven by the few large result sets) while letting thousands of small entries stay cached — bounded memory without sacrificing hit rate. Row sizing is an O(1) proxy (result-list length, not byte sizing, to keep the cache-miss path cheap) and the entry-count cap remains a backstop; the running total is updated lock-free across the API worker threads, so the budget is approximate but self-healing (over-counts evict slightly early, under-counts are bounded by the entry cap). The worst single cached entry isAPI_LIMIT_ROWSrows. (#3445) - Migrate the API documentation from Apiary (being retired by Oracle) to OpenAPI 3.1 rendered with Scalar on GitHub Pages:
genapidoc.pynow emitsopenapi.jsonfrom the same regtest-driven workflow, and contract testing moves fromdreddtoschemathesis(#3401) - Add a
--memory-profile-tracemallocflag and trackbackend.bitcoindcaches in the memory profiler (#3397) - Update Python dependencies: Flask 3.0.0→3.1.3, pytest 7.4.4→9.0.3, requests 2.32.4→2.33.0, Werkzeug 3.1.4→3.1.6, itsdangerous 2.1.2→2.2.0
- Update Rust dependencies: openssl 0.10.79→0.10.81, openssl-sys 0.9.115→0.9.117, pyo3 0.24.2→0.25.1 (migrate
IntoPy/into_pyto theIntoPyObjectAPI, since the old trait was removed in pyo3 0.25)
Credits
- Ouziel Slama
- Dan Anderson
- Adam Krellenstein
- caydyan