Skip to content

[Jet Proposal] Typed OP_RETURN data reading jets: output_op_return_get_* #337

@stringhandler

Description

@stringhandler

Summary

Add a family of jets for reading typed, raw data from pushed-data entries in OP_RETURN outputs. This enables SimplicityHL contracts to inspect oracle-posted data (e.g. prices, timestamps, signatures) that is published on-chain in OP_RETURN outputs, without requiring workarounds like encoding values as asset amounts.


AI USAGE: this issue was workshopped/rubber-ducked with Claude, hence the em-dashes.

Problem

Simplicity provides output_null_datum(u32, u32) for inspecting OP_RETURN outputs, but it has a critical limitation for arbitrary data inspection: for pushed-data entries it returns only the SHA256 hash of the pushed bytes, not the bytes themselves.

The full return type of output_null_datum is:

output_null_datum(u32, u32) -> Option<Option<Either<(u2, u256), Either<u1, u4>>>>

Where the parameters are (output_index, entry_index)entry_index selects which data entry (push item or opcode) in the OP_RETURN to inspect.

For opcode-style entries (OP_x, OP_1NEGATE, OP_RESERVED), the actual value is returned and is already usable. The gap is specifically with pushed-data entries, which are returned as:

Some(Some(Left((push_type: u2, hash: u256))))

The hash is the SHA256 of the pushed data. You can determine that data was pushed and how (immediate, PUSHDATA1/2/4), but you cannot recover the raw bytes. This makes output_null_datum unsuitable for any use case where the contract needs to read and act on the actual content of a data push.

What the docs say

From the Simplicity jet docs:

Return Some(Some(Left((x, hash)))) if the entry is pushed data. hash is the SHA256 hash of the data pushed and x indicates how the data was pushed.
Use this jet to read peg-out data from an output.

The recommended use case is peg-out data, where knowing the hash may be sufficient for verification. For oracle data that must be read and compared numerically (prices, timestamps, etc.), the hash is useless.

Motivating use case: on-chain oracles

A natural pattern for trustless data feeds on Liquid is for an oracle to post structured data in a UTXO's OP_RETURN output. For example, an oracle could push each field as a separate entry, each sized exactly to its type:

6a                           OP_RETURN
08 <8 bytes>                 entry 0: BTC price in USD (u64, stored as integer cents)
04 <4 bytes>                 entry 1: Unix timestamp (u32)
40 <64 bytes>                entry 2: Schnorr signature

A SimplicityHL contract co-spending with this oracle output currently has no way to read the price or timestamp. The only crude workaround in use today is to encode a numeric value as the amount of a synthetic asset (e.g. issue an asset with amount = BTC price). This is:

  • Semantically wrong (amounts are not data fields)
  • Limited to one unsigned integer per UTXO
  • Unable to carry timestamps, signatures, or structured data

Proposal

Add a family of jets of the form output_op_return_get_<T> that read a pushed-data entry from an OP_RETURN output and return it as type T. The pushed data must be exactly sizeof(T) bytes; any size mismatch returns Some(None).

The return type is Option<Option<T>>, mirroring the nesting of output_null_datum:

  • The outer Option indicates whether the output exists.
  • The inner Option indicates whether the entry is a valid push of the expected size.

This enforces a natural one-field-per-entry convention: the oracle pushes each value as its own entry of the exact right size, and the jet either reads it cleanly or returns Some(None).

Jet signatures

output_op_return_get_bool : (u32, u32) → Option<Option<u1>>
output_op_return_get_u8   : (u32, u32) → Option<Option<u8>>
output_op_return_get_u16  : (u32, u32) → Option<Option<u16>>
output_op_return_get_u32  : (u32, u32) → Option<Option<u32>>
output_op_return_get_u64  : (u32, u32) → Option<Option<u64>>
output_op_return_get_u256 : (u32, u32) -> Option<Option<u256>>

Arguments are (output_index, entry_index), directly mirroring the (a, b) convention of output_null_datum.

Parameters

Parameter Type Description
output_index u32 Zero-based index of the output in the current transaction.
entry_index u32 Zero-based index of the data entry within the OP_RETURN, matching the b parameter of output_null_datum.

Return value

  • Returns None if the output index is out of range.
  • Returns Some(None) if any of the following hold:
    • The output is not a null data (OP_RETURN) output.
    • The entry index is out of range (i.e., output_null_datum would return Some(None)).
    • The entry is an opcode (OP_x, OP_1NEGATE, OP_RESERVED) rather than a pushed-data entry.
    • The pushed data is not exactly sizeof(T) bytes.
  • Returns Some(Some(value)) on success.

Endianness

Values are read as big-endian, consistent with the Simplicity word type convention.

Relationship to output_null_datum

These jets are complementary to output_null_datum, not replacements. The intended usage is:

  • Use output_null_datum(output_index, entry_index) to inspect the structure of the OP_RETURN or verify a push via its hash.
  • Use output_op_return_get_<T>(output_index, entry_index) to read the raw value from a push entry of exactly the right size.

Alternatives considered

Use output_null_datum directly

As described above, output_null_datum only returns the SHA256 hash of pushed data. Hashes cannot be compared numerically or used to recover the original value. This proposal is directly motivated by that limitation.

Commit to a hash and check it

A contract could use output_null_datum to obtain the hash and verify it against an expected value passed as witness data. This works for exact equality checks but requires the spending party to supply the preimage as witness, adds witness overhead, and does not allow range comparisons (e.g. price > 90000).

Store data as asset amounts

The only current workaround for numeric oracle data. Fundamentally limited: one unsigned integer per UTXO, no structured data, misuses asset semantics.


Security considerations

  • All indices and byte_offset are bounds-checked; out-of-range accesses return None.
  • OP_RETURN outputs are provably unspendable; reading them cannot cause fund movement.
  • Contracts using these jets must not blindly trust the content of an OP_RETURN without verifying that the output belongs to the expected oracle. Transaction structure constraints (e.g. verifying which output index is used and that the transaction is signed by a trusted oracle key) are the contract designer's responsibility.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions