How to Reverse Engineer Counterparty TX’s

For my pet-project Electrum Counterparty I need to replicate the various op_return messages used by Counterparty.

Beware, this blog post is very technical – not very interesting for most people – but I share my insights to other developers who may be interested.

A good starting point is the counterpartylib python code. Each transaction type (also called message) has its own .py script. The overall structure is added to a variable called data. The necessary details are not that obvious though, and I find it difficult to dive deeper into the code.

For Enhanced Send I could find a full documentation. For other message types I did not.

My project uses the Freeport Wallet’s JavaScript libraries. Beyond sends, I think it supports broadcast, DEX order and asset issuance. You can test the wallet with your browser’s debugger to get to the inner workings of Counterparty transactions.

All right, that’s all fine. Now to the fun part. Let’s do some reverse engineering!

Find a transaction of the type you’re interested in on xchain.

Click on the transaction hash. This takes you to the Blockstream Explorer. Two pieces of info are relevant; the UTXO and the OP_RETURN.

The UTXO is used to encode the Counterparty message. This differs from Omni (Mastercoin). I believe the Counterparty devs did this to allow chaining of unspent transactions. (?)

Anyway, the first step to reverse engineer the transaction is to run the function xcp_rc4() from Freeport’s library.

let decoded = xcp_rc4("2220245a421b452392c83531e30cd4a0536afa71a96ff185d6885f178e5c29c5", "0466488eb3ed3de8c72601bcdd1bbbfb6cd1d37ad047796f9a2111b19938d7c6092853268239331ae77772df8c80");

The returned value is 434e54525052545902000000000002926e00000000000000280539f20c2e30805f2947caf5fb0e8a5217c02d9229.
This is the Counterparty message data!

For the record, xcp_rc4() is a two-way function. Simply run it with the decoded value to get back the encoded value.

let encoded = xcp_rc4("2220245a421b452392c83531e30cd4a0536afa71a96ff185d6885f178e5c29c5", "434e54525052545902000000000002926e00000000000000280539f20c2e30805f2947caf5fb0e8a5217c02d9229");
//returns 0466488eb3ed3de8c72601bcdd1bbbfb6cd1d37ad047796f9a2111b19938d7c6092853268239331ae77772df8c80
        

The Counterparty message begins with 434e545250525459. In fact, this is hex for CNTRPRTY and all Counterparty transactions have this prefix.

The following two characters 02 is the message ID for enhanced send.

Next comes the 16 characters 000000000002926e, which translated to decimal is 168558. Since you are reverse engineering a send of JPJA tokens, you should already have looked up JPJA’s asset info. Now you can confirm that JPJA’s ID is indeed 168558. Actually, there’s also a two-function between ID and name (for a regular asset) which you can find in the Freeport library.

The following 16 characters 0000000000000028 specify the quantity. Forty tokens were sent and 28 is the hex representation of this number.

Note all those leading zeros both for asset ID and quantity. This is typical since a fixed amount of bytes is used. 16 hex characters represent 8 bytes.

Everything that comes after this is the recipient address 0539f20c2e30805f2947caf5fb0e8a5217c02d9229. It naturally looks random because addresses are randomly generated in the first place. An address is always represented with 42 characters (21 bytes) by the way.

The address you see here is actually the public key halfway through the steps toward a real address. Enter 0539f20c2e30805f2947caf5fb0e8a5217c02d9229 at step 4 and you will see that the resulting address matches the destination address on xchain.

A memo was not included in this send. Any data following these 16+2+16+16+42 hex characters is assumed to be memo.

Javascript Decoder

I wrote a script which extracts Counterparty data directly from a Bitcoin transaction. It uses Blockcypher’s api. Try it out.

You can view the source code on Github. The code is pretty straight forward. Read from top to bottom like a cooking recipe. No external libraries were needed. Only a few helper functions were used, primarily to convert between bin, hex and ascii, as well as for rc4 encoding.

Most message types and encodings are supported, but there are a few still missing.

Below are some example transactions explained. Perhaps easier to grasp if you’re not used to reading code.

Dispenser

Here’s a dispenser with 26 XCP at a satoshi rate (mainchainrate) of 0.00029000 BTC per one XCP.

The op_return message is
434e545250525459|0c|0000000000000001|0000000005f5e100|000000009af8da00|0000000000007148|00

These 42 bytes translate to
434e545250525459 = CNTRPRTY (Prefix)
0c = 13 (ID for dispenser)
0000000000000001 = 1 (Asset ID for XCP)
0000000005f5e100 = 100,000,000 (Give quantity in satoshis = 1 XCP)
000000009af8da00 = 2,600,000,000 (Escrow quantity in sats = 26 XCP)
0000000000007148 = 29,000 (Mainchainrate in sats = 0.00029000 BTC)
00 = 0 (Status Open)

Dispenser (Separate address)

Address 1K2e.. set up a dispenser for address bc1qg8..

The op_return message is
434e545250525459|0c|00000000000174a4|00000002540be400|000009184e72a000|0000000000002710|01|8041d9f6b0f6b5760ea607f094463cc1055c3ae21c

These 63 bytes translate to
434e545250525459 = CNTRPRTY (Prefix)
0c = 13 (ID for dispenser)
00000000000174a4 = 95,396 (Asset ID for FLDC)
00000002540be400 = 10,000,000,000 (Give quantity in satoshis = 100 FLDC)
000009184e72a000 = 10,000,000,000,000 (Escrow quantity in sats = 100,000 FLDC)
0000000000002710 = 10,000 (Mainchainrate in sats = 0.00010000 BTC)
01 = 1 (Status Open Empty Address)
8041d9f6b0f6b5760ea607f094463cc1055c3ae21c => bc1qg8vldv8kk4mqafs87z2yv0xpq4wr4csucr3cj7 (Dispenser address)

The difference from a regular dispenser is the last byte (1 instead of 0) followed by the new address. Here it’s a bech32 address encoded in 21 bytes. This is no different from a regular address. The JS function for bech32 I’ve written for enhanced send shall work here too.

Close Dispenser

In this transaction I closed an OLGA dispenser.

The op_return message is
434e545250525459|0c|000000000003ded8|000000000000000000000000000000000000000000000000|0a

As expected, the message begins with the CNTRPRTY prefix and ID 13 for dispenser. I confirmed that the next 8 bytes (16 hex chars) are the ID for OLGA. The following 24 bytes are empty (48 zeros) and finally the status (hex:0a = decimal:10 for Status Closed).

Transfer of Issuance Right

Here the issuance right (sometimes called ownership) to PEPE is transferred from 1AUd.. to 1J5Q..

The op_return message is
434e545250525459|00000014|00000000000411f2|0000000000000000|01|00|00000000|00000000|1550455045202a2a2a20464f522053414c45202a2a2a

This translates to
434e545250525459 = CNTRPRTY (Prefix)
00000014 = 20 (ID for issuance)
00000000000411f2 = 266738 (Asset ID for PEPE)
0000000000000000 = 0 (Quantity)
01 = 1 (Divisibility, 1 = divisible)
00 = 0 (Callable, 0 = false)
00000000 = 0 (Call date)
00000000 = 0 (Call price)
1550455045202a2a2a20464f522053414c45202a2a2a = PEPE *** FOR SALE *** (Description)

This transaction also included a 0.00005430 BTC (5430 sat) transfer to 1J5Q.. which is what caused the transfer of issuance right. At today’s bitcoin price this is more than three dollars’ worth. I’m pretty sure there’s no rule as to how much BTC to send but as of 2021 most nodes require a 546 sat minimum.

Also worth noting is the ordering of transaction outputs. First is the recipient, then op_return and last comes the change.

I suspect Counterparty requires the first two to be in this order, and everything after is ignored. E.g a change address is not strictly needed, or you can split the change over several addresses (but please first test if this is true).

I am not sure if you can also increase the token supply (assuming unlocked) and change the description in the same issuance. Most likely you can but it needs to be tested.

The other parameters, which are unchangeable, must match the asset’s properties or else the entire transaction fails. Okay, I haven’t tested this either but I’m confident this is how it works.

Callability is deprecated so the relevant parameters shall be set to zero.

Lock Issuance

With this transaction I locked the supply of CHURCHHILL.

The op_return message is
434e545250525459|14|00000b5b4a037cd1|0000000000000000|00|00|00000000|00000000|044c4f434b

This translates to
434e545250525459 = CNTRPRTY (Prefix)
14 = 20 (ID for issuance)
00000b5b4a037cd1 = 12486711672017 (Asset ID for CHURCHHILL)
0000000000000000 = 0 (Quantity)
00 = 0 (Divisibility, 0 = indivisible)
00 = 0 (Callable, 0 = false)
00000000 = 0 (Call date)
00000000 = 0 (Call price)
044c4f434b = LOCK (Description)

The ID used only one byte vs 4 bytes for the previous issuance example. I believe either length is valid, so go for the more efficient one byte version.

Remember to set divisibility right, else the transaction will most likely fail.

Otherwise it’s clear that LOCK is a standard issuance transaction except the description field is set to LOCK.

The source code has a comment Allow simultaneous lock and transfer and it should be possible to transfer ownership at the same time by sending some BTC dust to the new owner.

Classic Send

The old send type is similar to issuance in that you send some BTC to the recipient.

It’s more expensive than enhanced send and therefore not really used anymore. If sending from Electrum the classic send may be the preferred choice though. The reason is that you will be able to confirm the recipient address inside Electrum. In contrast, with enhanced send the recipient is scrambled inside op_return and you need to trust an external script to encode it correctly.

This transaction is of 4.31856 FDCARD to 1HDY.. The op_return message is
434e545250525459|00000000|00000000039ff05d|0000000019bd9980

This translates to
434e545250525459 = CNTRPRTY (Prefix)
00000000 = 0 (ID for classic send)
00000000039ff05d = 60,813,405 (Asset ID for FDCARD)
0000000019bd9980 = 431,856,000 (Quantity in sat = 4.31856 FDCARD)

As expected the first transaction output is a small BTC amount to 1HDY. The second output is the op_return.

Multisig Encoding

If a Counterparty message is 81 bytes or longer, it won’t fit op_return. Multisig encoding is typically used instead. It’s much less efficient, and thankfully it’s rarely needed.

The most common uses of multisig encoding are

  • Token issuance with description >= 46 characters
  • Broadcast with text >= 55 characters

Keep in mind that a non-ASCII character counts as two or more characters in this context.

This transaction carries a broadcast with text JPJA eBook Sha256 8031025A667824A188DD5479CA5CB20C5306BE06ED01875F7BCC11ECB48251BE.

It’s encoded in one transaction which outputs two multisig scripts:

Technically 7800 satoshis are sent to each of these scripts. Each script contains three addresses as shown above in hex. Each address has the right to claim these satoshis back. The hack is that the first two addresses are ‘fake’. They contain the Counterparty message. The last (yellow) is the sender’s real address so that the bitcoin dust can be redeemed.

We need to decode the scripts separately and then join them.

From the first script take f922397f67ac489709d4bb6abedb3cd32b1148af5dede33121886abdd39081 and 600c3a22e4ba0e66661e60089e93c95290b7dc5c1c3d292e4b7a69c4e8f36d.

Notice that the first two and last two hex characters are removed.

From an api we may get the entire script (scriptpubkey) in one chunk. Extract the data like this:
data = script.substring(6, 68) + script.substring(74, 136)

Decode this in the same way as with op_return (arc4 with the UTXO) and we get:

3d|434e545250525459|1e614582b8000000000000000000000000524a504a412065426f6f6b20536861323536203830333130323541363637383234413138

3d is hex for 61 which means that all remaining 61 bytes contain Counterparty data.

434e545250525459 is the CNTRPRTY prefix

Repeat for the second script and get:

37|434e545250525459|3844443534373943413543423230433533303642453036454430313837354637424343313145434234383235314245|000000000000

37 is hex for 55 which tells Counterparty to read only the next 55 bytes and ignore the appended zeros.

The CNTRPRTY prefix is included here too.

Finally add the two strings (minus length bytes, and the second one’s prefix and appended zeros) to get the Counterparty message.

Depending on the length of the Counterparty message, almost any amount of scripts can be added to a transaction. It took 173 scripts to encode OLGA’s image.

TODO – Segwit Encoding

This is a relatively new encoding. It’s more efficient for large data.

I takes two chained transactions though, so the overhead is pretty high. I believe multisig is better for medium sized messages (<500 bytes?).

On the positive side a segwit script can be several kB and encode with a 100% marginal efficiency. I am not sure if it also can get a fee discount.

Categories: Uncategorized