Data encoding
Overview
- Assumptions: All protocol changes are enabled. Prefix is
CNTRPRTY. - Coverage: OP_RETURN and Taproot encoding only.
- Message envelope:
[message_type_id] || payload- With short IDs enabled: 1 byte if 0 < ID < 256; otherwise 4 bytes big‑endian.
OP_RETURN encoding (legacy and non‑segwit data)
- Where: a single
OP_RETURN <PUSHDATA>output. - ARC4 key: the first input txid (vin[0].txid) as standard hex decoded to bytes.
- Encoding (compose):
ARC4(key, (PREFIX || message)) - Decoding:
- If
opreturn_push_bytesis exactlyCNTRPRTY(literal, not encrypted): this is a Taproot commit marker; actual data is in the reveal transaction witness (see below). - Else compute
plain = ARC4(key, opreturn_push_bytes). - If
plainstarts withPREFIX (CNTRPRTY), thenmessage = plain[8:].
- If
Taproot witness encoding (commit + reveal)
-
Two‑tx flow:
- Commit tx: sends to a P2TR address whose script tree includes the envelope script (no OP_RETURN here).
- Reveal tx: spends the commit UTXO; includes an
OP_RETURNoutput with literalCNTRPRTYand carries the envelope script in the witness.
-
Two envelope styles are supported in the witness script:
- Ordinals “xcp” envelope (preferred when a contract carries content)
- Script form:
OP_FALSE OP_IF "ord" 0x07 "xcp" 0x01 <mime_type> 0x05 <CBOR metadata chunks...> (OP_0|OP_FALSE|empty) <content chunks...> OP_ENDIF <xonly_pubkey> OP_CHECKSIG
- Extraction:
- Concatenate all CBOR metadata chunks; decode to a CBOR array.
- The first element is
message_type_id(uint). Remove it. Appendmime_type(text) and optionalcontent(raw bytes) to the array. - Re‑encode the modified array as CBOR and prefix with one byte
message_type_id→ finalmessagebytes.
- When used: the composer emits this style only if
inscription=trueand the message type is one of issuance (standard/subasset, including LR variants), broadcast or fairminter, and the provided content is non‑empty; otherwise it falls back to the generic envelope.
- Script form:
- Generic inscription envelope
- Script form:
OP_FALSE OP_IF <data chunks...> OP_ENDIF <xonly_pubkey> OP_CHECKSIG - Extraction: concatenate all pushed chunks between
OP_IFandOP_ENDIF→ finalmessagebytes.
- Script form:
- Ordinals “xcp” envelope (preferred when a contract carries content)
Message IDs and payload formats (taproot_support enabled)
All payloads below are CBOR arrays unless noted.
-
Enhanced send (
ID = 2)[asset_id:uint64, quantity:int, short_address_bytes:21, memo:bytes]
-
Sweep (
ID = 4)[short_address_bytes:21, flags:uint8, memo:bytes]
-
Issuance (standard) (
IDs = 20, 22accepted)[asset_id:uint64, quantity:int, divisible:bool, lock:bool, reset:bool, mime_type:text, description:bytes|null]
-
Issuance (subasset) (
IDs = 21, 23accepted)[asset_id:uint64, quantity:int, divisible:int(0|1), lock:int(0|1), reset:int(0|1), compacted_subasset_length:int, compacted_subasset_longname:bytes, mime_type:text, description:bytes|null]
-
Broadcast (
ID = 30)[timestamp:int, value:float, fee_fraction_int:uint32, mime_type:text, text:bytes]
-
Fairminter (v2) (
ID = 90)[asset_id:uint64, asset_parent_id:uint64(0 if none), price:int, quantity_by_price:int, max_mint_per_tx:int, max_mint_per_address:int, hard_cap:int, premint_quantity:int, start_block:int, end_block:int, soft_cap:int, soft_cap_deadline_block:int, minted_asset_commission_int:int(1e8), burn_payment:bool, lock_description:bool, lock_quantity:bool, divisible:bool, mime_type:text, description:bytes]
-
Fairmint (v2) (
ID = 91)[asset_id:uint64, quantity:int]
-
Attach (
ID = 101) — not CBOR- Payload is UTF‑8 string:
"asset|quantity|destination_vout"(destination_vout may be empty).
- Payload is UTF‑8 string:
-
Detach (bulk from UTXO) (
ID = 102) — not CBOR- Payload is either a UTF‑8 destination address or the single byte
0x30(string "0") meaning no explicit destination (credit back to the UTXO’s address per asset balance).
- Payload is either a UTF‑8 destination address or the single byte
-
Order (
ID = 10) — legacy struct- Binary struct
>QQQQHQ=[give_id:uint64, give_quantity:int64, get_id:uint64, get_quantity:int64, expiration:uint16, fee_required:int64]
- Binary struct
-
BTC Pay (
ID = 11) — legacy struct- Binary struct
>32s32s=[tx0_hash:32 bytes, tx1_hash:32 bytes](order_match_id is derived from these)
- Binary struct
-
Dispenser (
ID = 12) — legacy struct + optional packed addresses- Binary struct
>QQQQB=[asset_id:uint64, give_quantity:int64, escrow_quantity:int64, satoshirate:int64, status:uint8] - Optionally followed by
action_address(21‑byte packed address) and optionallyoracle_address(21‑byte packed address) depending on status and protocol flags.
- Binary struct
-
Dispense (
ID = 13) — minimal- Payload bytes:
0x00(a single zero byte), BTC amount is carried in the Bitcoin output; matching against dispenser state determines asset quantity dispensed.
- Payload bytes:
-
Dividend (
ID = 50) — legacy struct- If
new_dividend_formatactive:>QQQ=[quantity_per_unit:int64, asset_id:uint64, dividend_asset_id:uint64] - Else:
>QQ=[quantity_per_unit:int64, asset_id:uint64]anddividend_asset = XCP.
- If
-
Cancel (
ID = 70) — legacy struct>32s=[offer_hash_bytes:32]where offer can be an order or a bet.
-
Destroy (
ID = 110) — legacy struct with trailing tag>QQ=[asset_id:uint64, quantity:int64]followed by an optional tag (bytes, up to 34, truncated).
-
MPMA send (
ID = 3) — custom binary bitstream (not CBOR)- Purpose: batch multiple sends across one or more assets with compact addressing.
- Top‑level layout:
[LUT] || [BITSTREAM] - LUT (address lookup table):
num_addresses:uint16 (big‑endian)thennum_addresses × short_address(21 bytes each), where short_address isaddress.pack_legacy(addr).nbits = ceil(log2(num_addresses))(ifnum_addresses == 1, thennbits = 0).
- BITSTREAM:
global_memo_present:1- If 1:
global_memo_is_hex:1,global_memo_len:6,global_memo:len bytes(UTF‑8 if not hex; raw bytes if hex).
- If 1:
- Zero or more send‑groups, each prefixed by a
1bit; terminated by a single0bit; then zero padding to the next byte boundary. - Send‑group payload:
asset_id:uint64 (big‑endian)recipients_minus_one:nbits(omitted ifnbits==0, implying exactly 1 recipient)- For each recipient (count =
recipients_minus_one + 1, or 1 whennbits==0):lut_index:nbits(omitted ifnbits==0, implied 0)quantity:uint64 (big‑endian)memo_present:1; if 1 thenmemo_is_hex:1,memo_len:6,memo:memo_len bytes(UTF‑8 if not hex; raw bytes if hex)
- Semantics:
- Destinations are referenced by LUT indices; the LUT lists unique destinations sorted lexicographically.
- Assets are grouped by
asset_id; groups are typically emitted in lexicographic order of asset names at compose time (order is not required for decoding). - A global memo (if present) is applied to recipients that do not carry a per‑recipient memo.
- Only legacy short‑encodable destinations are supported (no Taproot/P2TR).
- Decoding:
_decode_mpma_send_decodeimplements the above and yields{asset_name: [(addr, quantity[, memo_bytes])...]}.
Notes:
- Where a field is
bytesand represents human text, the composer uses the declaredmime_typeto pack/unpack. Your parser can treat it as raw bytes and, if desired, decode usingmime_type. short_address_bytesis a fixed 21‑byte packed address format used by Counterparty (pack/unpack is outside this spec).
Parsing algorithm (TypeScript outline)
function parseCounterparty(tx: BitcoinTx): ParsedMessage | null {
// 1) Try OP_RETURN
const opret = findSingleOpReturn(tx);
if (opret) {
if (bytesEq(opret.pushdata, ascii("CNTRPRTY"))) return { kind: "taproot_commit" };
const key = hexToBytes(tx.vin[0].txid); // first input txid
const plain = rc4(key, opret.pushdata);
if (startsWith(plain, ascii("CNTRPRTY"))) {
const message = plain.slice(8);
return decodeMessage(message);
}
}
// 2) Try Taproot witness revelation
const w = getFirstWitnessWithScript(tx);
if (!w) return null;
const script = hexToBytes(w.scriptHex);
const message = extractFromEnvelope(script); // ord/xcp or generic
return decodeMessage(message);
}
function decodeMessage(message: Uint8Array): ParsedMessage {
const { id, rest } = readMessageTypeId(message); // 1 byte unless 0 → 4 bytes
switch (id) {
case 2: return decodeEnhancedSend(rest);
case 4: return decodeSweep(rest);
case 20:
case 22: return decodeIssuance(rest);
case 21:
case 23: return decodeIssuanceSubasset(rest);
case 30: return decodeBroadcast(rest);
case 90: return decodeFairminter(rest);
case 91: return decodeFairmint(rest);
case 101: return decodeAttachPipe(rest);
default: return { id, raw: rest };
}
}
Segwit examples (schema‑level)
- Commit transaction output
- P2TR output to
taproot_address([[envelope_script]])(no OP_RETURN)
- P2TR output to
- Reveal transaction output[0]
OP_RETURN 0x08 "434e545250525459"// literalCNTRPRTY
- Reveal transaction witness (generic envelope)
- Witness stack:
[<schnorr_sig>, <script>, <control_block>] <script>:OP_FALSE OP_IF <( type_id || cbor_payload ) chunked ≤520B> OP_ENDIF <xonly_pubkey> OP_CHECKSIG
- Witness stack:
- Reveal transaction witness (ord/xcp envelope)
<script>:OP_FALSE OP_IF "ord" 0x07 "xcp" 0x01 <mime> 0x05 <cbor_meta_chunks> OP_0 <content_chunks> OP_ENDIF <xonly_pubkey> OP_CHECKSIG