BSV.Tokens.Script.Stas3Pieces (bsv_sdk v1.5.0)

Copy Markdown View Source

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_ENDIF

which 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):

  1. 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 (owner push + var2 push) of the STAS frame and is NOT excised.
  2. 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.
  3. Reverse the piece order.
  4. Concatenate the reversed pieces, each prefixed by a 1-byte length.

Each piece MUST fit in a u8 (≤ 255 bytes); larger pieces cause the encoder to return {:error, :invalid_piece}.

This module exposes:

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

Types

Result of parse/2 for a merge trailing block.

Result of parse/2 for a swap trailing block.

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

parsed_merge()

@type parsed_merge() :: %{piece_count: 2..7, pieces: [binary()]}

Result of parse/2 for a merge trailing block.

parsed_swap()

@type parsed_swap() :: %{
  counterparty_script: binary(),
  piece_count: non_neg_integer(),
  pieces: [binary()]
}

Result of parse/2 for a swap trailing block.

Functions

encode_atomic_swap_pieces(counterparty_locking_script, preceding_tx, asset_output_indices)

@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_array

where 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.

encode_merge_pieces(piece_count, preceding_tx, asset_output_indices)

@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_array

Per 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

parse(bin, tx_type)

@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 equal tx_type), then

          length-prefixed piece array. Returns `{:ok, %{piece_count:
          _, pieces: _}}`.

On malformed input — bad framing, count mismatch with array length, unsupported tx_type — returns {:error, reason}.