STAS 3.0 v0.1 §8.1 / §9.5 atomic-swap and merge "piece array" trailing parameters for STAS 3.0 unlocking scripts.
Background
For atomic-swap (txType = 1) and merge transactions (txType = 2..7),
the STAS unlocking script appends a trailing block whose layout depends
on txType:
# txType = 1 (atomic swap)
counterparty_locking_script ← full locking script of the OTHER party's STAS UTXO
piece_count ← 1-byte unsigned integer
piece_array ← pieces, each LENGTH-PREFIXED (1-byte u8 length, then bytes)
# txType = 2..7 (merge)
piece_count ← 1-byte unsigned integer; value MUST equal txType
piece_array ← pieces, each LENGTH-PREFIXED (1-byte u8 length, then bytes)What "pieces" are (spec §9.5)
The spec §9.5 wording — "the reverse-ordered array of pieces is delimited by space (' ') character" — is misleading: the engine ASM is the source of truth. The engine ASM repeats this atom to consume the piece array:
OP_1 OP_SPLIT OP_IFDUP OP_IF OP_SWAP OP_SPLIT OP_ENDIFwhich reads each piece as length-prefixed — 1 byte off as the piece
length, then that many bytes off as the piece body. A 0x20-separator
encoding desynchronises the loop and leaves residual bytes that the
engine eventually mis-uses as a script-number, producing an OP_VERIFY
or InvalidStackOperation failure. Both Rust and Elixir SDKs originally
implemented the encoder as space-delimited based on the spec wording;
this module now matches the engine.
Concretely, given the preceding transaction (the tx that produced the swap input UTXO):
- For each named asset output (
asset_output_indices), locate the locking script. Within that script, identify the "asset script" — i.e. the bytes from the engine prefix (0x6D 0x82 0x73 0x63) all the way to the end of the script. Everything BEFORE that prefix belongs to the two var fields (ownerpush +var2push) of the STAS frame and is NOT excised. - The remaining tx bytes (the parts that AREN'T the excised regions) are the pieces. They are split into contiguous slices: one before the first excised region, one between each pair of adjacent excised regions, and one after the last.
- Reverse the piece order.
- Concatenate the reversed pieces, each prefixed by a 1-byte length.
Each piece MUST be at most 127 bytes. The 1-byte length prefix is read
by OP_1 OP_SPLIT as a signed Bitcoin script-num: 0x80 (128) and above
are treated as negative, causing OP_SPLIT to fail. Pieces exceeding
127 bytes cause the encoder to return {:error, :invalid_piece}.
This module exposes:
encode_atomic_swap_pieces/3— build the trailing block for txType=1encode_merge_pieces/3— build the trailing block for txType=2..7parse/2— decode a previously-encoded trailing block
Strict boundaries are enforced: at least one asset output must be named,
the merge piece count must be in 2..7, the inner array piece count
must equal the leading length byte, and pushes after the leading
counterparty-script length use Bitcoin pushdata framing (so reading
back is unambiguous).
Summary
Functions
Build the trailing-parameters block for a txType = 1 atomic-swap
unlocking script.
Build the trailing-parameters block for a merge unlocking script
(txType = 2..7).
Parse a previously-encoded trailing parameter block.
Types
Functions
@spec encode_atomic_swap_pieces(binary(), binary(), [non_neg_integer()]) :: {:ok, binary()} | {:error, term()}
Build the trailing-parameters block for a txType = 1 atomic-swap
unlocking script.
Returns the raw byte sequence:
counterparty_script_push ‖ piece_count_byte ‖ piece_arraywhere piece_array is the reverse-ordered, length-prefixed result of
excising every named asset script from preceding_tx.
asset_output_indices MUST list at least one valid output index in
preceding_tx.
@spec encode_merge_pieces(2..7, binary(), [non_neg_integer()]) :: {:ok, binary()} | {:error, term()}
Build the trailing-parameters block for a merge unlocking script
(txType = 2..7).
Returns:
piece_count_byte ‖ piece_arrayPer spec §8.1, piece_count MUST equal the merge txType (2..7) and
MUST equal the resulting number of pieces in the array. With K
excised asset-script regions in preceding_tx, the resulting array
has K + 1 pieces (the slice before the first excision, the slices
between adjacent excisions, and the slice after the last). So
length(asset_output_indices) MUST equal piece_count - 1.
Examples
- txType=2 with one asset excision → 2 pieces
- txType=3 with two asset excisions → 3 pieces
- txType=7 with six asset excisions → 7 pieces
@spec parse(binary(), 1..7) :: {:ok, parsed_swap() | parsed_merge()} | {:error, term()}
Parse a previously-encoded trailing parameter block.
tx_type selects the layout:
1— atomic swap: leading pushdata-framed counterparty script,then 1-byte count, then length-prefixed piece array. Returns `{:ok, %{counterparty_script: _, piece_count: _, pieces: _}}`.2..7— merge: 1-byte count (must equaltx_type), thenlength-prefixed piece array. Returns `{:ok, %{piece_count: _, pieces: _}}`.
On malformed input — bad framing, count mismatch with array length,
unsupported tx_type — returns {:error, reason}.