Friday Night Challenge: Worst case DUST

Only for the cool kids.

The challenge: What’s the worst case DUST needed to cover a relay-valid output (i.e. ignoring things that only miners can do)?

The motivation for the question is if you have a contract that needs to enforce output amounts, what is a very safe DUST that will cover 99.9% of cases.

The estimate to beat here (from discussion on BCHN slack which will eventually sink under the threshold of slack free tier).

Libauth provides a readable basis for the underlying rules here.

2 Likes

Didn’t we figure it out already? 1332.

I’ll add the explanation here for future reference.

Formula is implemented here, and can be rearranged to: (output_size_in_bytes)*3 + 444.

So the question is: what’s the biggest output acceptable by network relay rules?

Recall the token output format:

Field Length Format Description
value 8 bytes unsigned integer(LE) The number of satoshis to be transferred.
token prefix and locking script length variable variable length integer The combined size of full token prefix and the locking script in bytes.
PREFIX_TOKEN 1 byte constant Magic byte defined at codepoint 0xef (239) and indicates the presence of a token prefix.
token category ID 32 bytes bytes After the PREFIX_TOKEN byte, a 32-byte “token category ID” is required, encoded in OP_HASH256 byte order.
token bitfield 1 byte bitfield A bitfield encoding two 4-bit fields is required.
[NFT commitment length] variable variable length integer The size of the NFT commitment in bytes. Present only if token bitfield bit 0x40 is set.
[NFT commitment] variable bytes The contents of the NFT commitment. Present only if token bitfield bit 0x40 is set.
[FT amount] variable variable length integer An amount of fungible tokens, present only if token bitfield bit 0x10 is set.
locking script variable bytes(BE) The contents of the locking script.

From this, we can work out the biggest output:

  • satoshi amount is always 8 bytes
  • The token prefix and locking script length can be 1 or 3 bytes, depending on combined length of token payload and locking script.
  • The biggest token payload is easy to work out, it is 84 bytes (fungible token amount >= 4294967296 so it requires 9-byte encoding, and NFT commitment of 40 bytes).
  • The biggest locking script require us to recall standard locking script patterns (see below) and it will be the M of 3 “bare” multisig (P2MS), giving us 201 bytes.

Total will then be 201+84+3+8 == 296 bytes, and plugging it into dust limit formula we get (296*3)+444 == 1332 bytes.

For reference, transaction with such output mined on testnet4: [TEST4] - Transaction 5325145f86ff4544bfedd628f6a7dee5877e86825da3a7bbca145d56f9eebb15

What are the possible locking bytecode sizes?

Only the 6 locking bytecode patterns are acceptable:

  1. P2PK: OP_DATA_X {pubkey} OP_CHECKSIG. The pubkey can be 33 or 65 bytes, depending on whether compressed or uncompressed public key is used, so these will have locking bytecode of 35 or 67 bytes (2 + {33|65}).
  2. P2PKH: OP_DUP OP_HASH160 OP_DATA_20 {pubkeyHash} OP_EQUALVERIFY OP_CHECKSIG. The pubkeyHash is constant 20, so the whole locking bytecode will be constant 25.
  3. P2SH20: OP_HASH160 OP_DATA_20 {redeemScriptHash20} OP_EQUAL. The redeem script hash is constant 20, so the whole locking bytecode will be constant 23.
  4. P2SH32: OP_HASH256 OP_DATA_32 {redeemScriptHash32} OP_EQUAL. The redeem script hash is constant 32, so the whole locking bytecode will be constant 35. (note that docs haven’t yet been updated with this one, see the P2SH32 CHIP for details).
  5. P2MS: OP_M {OP_DATA_X {pubkey_n}}{x1-3} OP_N OP_CHECKMULTISIG. Here the key can be 33 or 65 bytes, so the total size can vary between (3 + 1*34) and (3 + 3*66), i.e. between 37 and 201 bytes.
  6. OP_RETURN {data pushes} with total size limited to 223.

OP_RETURN is the biggest but it doesn’t have a dust limit because it’s provably unspendable so never becomes part of UTXO set. The next biggest is P2MS of 201, then P2PK of 67, then P2SH32 of 35, then P2PKH of 25, then P2SH20 of 23.

We can easily calculate biggest dust limits for each type:

  • P2PK: 67+84+1+8 == 160 bytes, dust limit 924 (672 if no tokens)
  • P2PK compressed: 35+84+1+8 == 128 bytes, dust limit 828 (576 if no tokens)
  • P2PKH: 25+84+1+8 == 118 bytes, dust limit 798 (546 if no tokens)
  • P2SH20: 23+84+1+8 == 116 bytes, dust limit 792 (540 if no tokens)
  • P2SH32: 35+84+1+8 == 128 bytes, dust limit 828 (576 if no tokens)
  • P2MS: 201+84+3+8 == 296 bytes, dust limit 1332 (1080 if no tokens)
  • P2MS compressed: 105+84+1+8 == 198 bytes, dust limit 1038 (786 if no tokens)
9 Likes

I think you did. And also thank you for explaining it here in great detail!!

However, the discussion that led to the answer was surprisingly fresh. I thought it would be an old road, traveled many times and well documented. This is an opportunity for someone to snipe the answer with something surprising.

… your post here probably belongs in the BCH specification as an exploration of the boundaries.

6 Likes

Note that the worst case dust calculations are about to change with the May 15th network upgrade, which will allow nft commitments up to 128 bytes (up from 40bytes)

so this part of the calculation changes:

4 Likes

I updated the calculations from @bitcoincashautist above with the help of GPT 5.2

summary of worst case dust (with maximum length tokens), after May 15th network upgrade:

  • P2SH32: 1092 sats (up from 828)
  • P2PKH: 1062 sats (up from 798)
  • P2SH20: 1056 sats (up from 792)

So 1,000 sats which was used as an always safe dust amount by cashtokens wallet would now be insufficient when handling NFTs with maximum size commitments.

Taking into account only dus amounts for end-user wallets using P2PKH and rounding up for a new standard dust amount we would use 1,100 sats (rounded up from the 1062 sats above).

I wonder if this means we think it would be best for wallets to increase the default cashtokens dust amount? we could differentiate either between FTs and NFTs or wallets could try to be clever and calculate the required dust on the fly based on the lengths?

The worst case calculation for the new P2S output type is the same as for P2MS.
The updated worst-case dust limit is for P2MS and P2S outputs at 1596 satoshis


Worst Case Dust (Updated)

  • The biggest token payload is now 172 bytes:

    • PREFIX_TOKEN = 1
    • token category ID = 32
    • token bitfield = 1 (both NFT + FT present)
    • NFT commitment length = 1 (since 128 < 253, it fits in a 1-byte varint)
    • NFT commitment = 128
    • FT amount = 9 (worst-case varint, i.e. amount ≥ 2³² = 4,294,967,296)
    • Total: 1 + 32 + 1 + 1 + 128 + 9 = 172

Important nuance introduced by the larger commitment: P2MS compressed now crosses the 253-byte threshold, so its “token prefix and locking script length” varint becomes 3 bytes (it used to be 1 byte under the 40-byte commitment assumption).

So the biggest output becomes:

  • Combined token prefix + locking script size = 172 + 201 = 373, so the length varint is 3 bytes (because 373 ≥ 253).
  • Total output size = 201 + 172 + 3 + 8 = 384 bytes.
  • Plugging into the dust formula: (384 * 3) + 444 = 1596.

So the updated worst-case dust limit is 1596 satoshis (for the max-size standard, token-bearing output).


We can easily calculate biggest dust limits for each type (updated)

Using max token payload 172 bytes:

  • P2PK (67 bytes): 67 + 172 + 1 + 8 = 248 bytes → dust 1188 (672 if no tokens)
  • P2PK compressed (35 bytes): 35 + 172 + 1 + 8 = 216 bytes → dust 1092 (576 if no tokens)
  • P2PKH (25 bytes): 25 + 172 + 1 + 8 = 206 bytes → dust 1062 (546 if no tokens)
  • P2SH20 (23 bytes): 23 + 172 + 1 + 8 = 204 bytes → dust 1056 (540 if no tokens)
  • P2SH32 (35 bytes): 35 + 172 + 1 + 8 = 216 bytes → dust 1092 (576 if no tokens)
  • P2MS (≤201 bytes): 201 + 172 + 3 + 8 = 384 bytes → dust 1596 (1074 if no tokens)
  • P2MS compressed (≤105 bytes): 105 + 172 + 3 + 8 = 288 bytes → dust 1308 (786 if no tokens)
  • P2S (≤201 bytes) : 201 + 172 + 3 + 8 = 384 bytes → dust 1596 ( 1074 if no tokens)
1 Like

awesome update, thank you Mathieu.

I’ll take the opportunity for a reminder to all wallet devs that after they create a tx they will send it to someplace and that someplace will give you a “rejected” message should the transaction fail. For instance due to dust limits. It is essential that wallets wait for that message on the open connection because otherwise they’ll never learn that the transaction is not accepted by that peer.

As an aside, there is no “approved” message. We’ve had some people suggest such a feedback on the p2p network and also via fulcrum, some people think it may be a nice optional addition for wallets to use.

Either way, a reject message is useful to realize at least one utxo has insufficient funds and won’t ever get confirmed.

1 Like

All libauth based wallets should be using getDustThreshold imo.

https://libauth.org/functions/getDustThreshold.html

2 Likes

That works when you have control of the transaction. It doesn’t work when your contract is constructing outputs itself.

Contract is not doing anything by itself. Contract may be leaving out a “dust allowance” from contract’s own funds, or allowing the possibility of spender providing a funding input. Either way, the software that constructs the satisfying transaction is in control of the final amount, and it can call on getDustThreshold.

Exception are tight contracts that lock both the TX inputs and and the fee amount and enforce == on “dust” outputs.

Typically, contracts that require the TX to have this or that output don’t give freedom to NFT commitment, so I doubt any contract would get (temporarily) broken.

It’s good to expose this edge case, and it would be good if CHIP had mentioned it.

Or Explorers to that regard*. Its not only used by wallets.