Skip to content

ArkLabsHQ/introspector

Repository files navigation

Introspector

test quality Trivy Security Scan

Introspector is a signing service for the Arkade protocol, executing Arkade Script.

This is achieved by signing any Ark transaction (offchain or intent proof) expecting the signature of a tweaked public key. The tweaked key is introspector_key + hash(arkade_script), where the script hash is a tagged hash ("ArkScriptHash"). The Arkade script is revealed via an Introspector Packet committed inside an ARK extension OP_RETURN output. An ARK extension is a TLV stream prefixed with magic bytes ARK (0x41524b); the Introspector Packet is one of its packet types (0x01), containing per-input entries with the script bytecode and optional witness arguments.

ArkadeScript examples

  • test/htlc_test.goNon-interactive HTLC. A 2-of-2 (arkd + introspector-tweaked) VTXO with a claim path gated by HASH160(preimage) and a refund path gated by absolute timelock. Neither the receiver nor the sender ever signs — an arkade covenant enforcing destination + amount replaces both their signatures.
  • test/delegate_test.goNon-interactive delegate. A 2-of-2 (arkd + introspector-tweaked) VTXO refreshed through batch settlement by any solver, with a CSV exit leaf reserved for the user. The arkade covenant is a self-send (preserves the input's scriptPubKey + value on output 0) gated to intent-proof transactions (OP_INSPECTVERSION == 2) so it cannot be drained via off-chain self-send loops.

API

GetInfo

Returns service metadata including the signer's public key. The public key should be tweaked with the Arkade script hash before being used in a VTXO tapscript.

Endpoint: GET /v1/info

Response:

{
  "version": "0.0.1",
  "signer_pubkey": "compressed_public_key"
}

SubmitTx

Validates and signs the Ark transaction inputs owned by this introspector, and signs their matching checkpoint transactions. Arkade scripts are executed only on the Ark transaction, not on checkpoints.

If this introspector is the last required non-arkd signer for all owned inputs matched by the introspector packet, each checkpoint PSBT must already include any other required non-arkd signatures; otherwise the request fails. In that case, the introspector submits the signed transaction set to arkd, merges arkd's checkpoint signatures, finalizes the transaction, and returns the finalized Ark PSBT plus updated checkpoint PSBTs. Otherwise it returns only this introspector's added signatures without calling arkd.

Endpoint: POST /v1/tx

Request:

{
  "ark_tx": "base64_encoded_psbt",
  "checkpoint_txs": ["base64_encoded_checkpoint_psbt1", "..."]
}

Response:

{
  "signed_ark_tx": "base64_encoded_signed_psbt",
  "signed_checkpoint_txs": ["base64_encoded_signed_checkpoint_psbt1", "..."]
}

signed_ark_tx may be either partially signed or finalized, depending on whether this introspector is the last required non-arkd signer for all owned inputs matched by the introspector packet.

SubmitIntent

Signs an intent proof after validating the register message and executing Arkade scripts on the proof transaction. Must be called before intent registration.

Endpoint: POST /v1/intent

Request:

{
  "intent": {
    "proof": "base64_encoded_psbt",
    "message": "base64_encoded_register_message"
  }
}

Response:

{
  "signed_proof": "base64_encoded_signed_psbt"
}

SubmitFinalization

Conditionally signs forfeit and/or boarding inputs during batch finalization. Only signs if the signer's signature is found in the intent proof. The connector tree is used to verify the forfeits are part of a real batch session.

Endpoint: POST /v1/finalization

Request:

{
  "signed_intent": {
    "proof": "base64_encoded_signed_psbt",
    "message": "base64_encoded_register_message"
  },
  "forfeits": ["base64_encoded_forfeit_psbt1", "..."],
  "connector_tree": [
    {
      "txid": "transaction_id",
      "tx": "base64_encoded_transaction",
      "children": {
        "0": "child_txid_1",
        "1": "child_txid_2"
      }
    }
  ],
  "commitment_tx": "base64_encoded_psbt"
}

Response:

{
  "signed_forfeits": ["base64_encoded_signed_forfeit_psbt1", "..."],
  "signed_commitment_tx": "base64_encoded_signed_psbt"
}

SubmitOnchainTx

Validates and signs the inputs of a plain Bitcoin transaction whose tapscripts contain the introspector's tweaked key (e.g. a VTXO unrolled onchain). Each input may carry an optional PrevoutTxField PSBT unknown field (key "prevouttx") holding the raw previous transaction, required only by arkade opcodes that introspect it.

Inputs whose tapscript closure also contains the arkd signer pubkey are rejected — those must go through SubmitTx so checkpoint and forfeit checks are enforced.

Endpoint: POST /v1/onchain-tx

Request:

{
  "tx": "base64_encoded_psbt"
}

Response:

{
  "signed_tx": "base64_encoded_signed_psbt"
}

Introspector Packet

The Introspector Packet is the data structure that reveals which inputs of a transaction must be checked by the introspector, the Arkade script bytecode to execute for each, and any witness arguments the script consumes. It lives inside an ARK extension — an OP_RETURN output whose payload starts with the magic prefix ARK (0x41 0x52 0x4b) followed by a sequence of (type, length, value) packets. The introspector packet has type byte 0x01 and shares the envelope with other ARK packets (e.g. the asset packet, type 0x00); a single OP_RETURN can carry both, and helpers like addIntrospectorPacket merge the introspector packet into an existing extension when one is already present.

The packet content (the value side of the outer TLV) has the following layout — varint denotes a Bitcoin-style compact size integer:

Field Type Notes
entry_count varint Number of entries. Must be >= 1 and <= 1000.
entry[0..entry_count] per-entry block (below) Repeated entry_count times.

Each entry block is:

Field Type Notes
vin u16 LE Input index this entry applies to. Must be unique across the packet.
script_len varint Length of script in bytes. Must be >= 1 and <= 10_000.
script bytes Arkade Script bytecode.
witness_len varint Length of the encoded witness blob in bytes. Must be <= 1_000_000.
witness bytes Witness blob (see below). May be empty (witness_len = 0).

The witness blob is encoded with psbt.WriteTxWitness / txutils.ReadTxWitness, not raw Bitcoin wire-format witness. Concretely, the blob is varint(num_items) followed by varint(item_len) + item_bytes for each stack item.

The serialized packet is the value of an outer TLV record (0x01, varint(content_len), content) written into the ARK extension, which itself is wrapped in an OP_RETURN output. The encoder bypasses txscript.ScriptBuilder's 520-byte data push cap so the OP_RETURN can hold the full extension regardless of size.

Validation rules

Validate() enforces, and any non-Go decoder must enforce:

  • 1 <= entry_count <= 1000
  • For every entry: 1 <= len(script) <= 10_000, len(witness_blob) <= 1_000_000
  • vin is unique across the packet (an entry per vin, never two)
  • No trailing bytes after the last entry

Consensus relevance

The Arkade opcodes OP_INSPECTPACKET (0xf4) and OP_INSPECTINPUTPACKET (0xf5) read the raw packet bytes for a given type from the current transaction or a previous Ark transaction's extension. Any Arkade script that uses these opcodes is sensitive to the exact serialized form of the packet — i.e. the wire format above is part of the consensus surface for those scripts, and changes to it must be treated as a protocol change.

Configuration

The service can be configured using environment variables:

Variable Description Default
INTROSPECTOR_SECRET_KEY Private key for signing (hex encoded) Required
INTROSPECTOR_DATADIR Data directory path OS-specific app data dir
INTROSPECTOR_PORT Server port (gRPC + HTTP REST gateway) 7073
INTROSPECTOR_NO_TLS Disable TLS encryption false
INTROSPECTOR_TLS_EXTRA_IPS Additional IPs for TLS cert []
INTROSPECTOR_TLS_EXTRA_DOMAINS Additional domains for TLS cert []
INTROSPECTOR_LOG_LEVEL Log level (0-6) 4 (Debug)
INTROSPECTOR_ARKD_URL URL of the arkd instance used for attempted finalization in SubmitTx Required

Development

Prerequisites

  • Go 1.26+
  • Docker and Docker Compose
  • Buf CLI (for protocol buffer generation)
  • Nigiri (for integration testing)

Building

# Generate protocol buffer stubs
make proto

# Build the application
make build

Running

# Run with development configuration
make run

Testing

# Run unit tests
make test

# Run docker regtest environment
nigiri start
make docker-run

# Run integration tests
make integrationtest

Supported Opcodes

The following opcodes are supported by the Arkade script engine. They extend Bitcoin Script with additional introspection, data manipulation, and cryptographic operations.

Transaction Introspection (Inputs)

Word Opcode Hex Input Output Description
OP_INSPECTINPUTOUTPOINT 199 0xc7 index txid index Pushes the transaction ID (32 bytes) and output index (scriptNum) of the input at the given index onto the stack.
OP_INSPECTINPUTARKADESCRIPTHASH 200 0xc8 index script_hash Pushes the 32-byte Arkade script hash (tagged_hash("ArkScriptHash", script)) of the IntrospectorEntry for the input at the given index. This is the same hash used as the tweak scalar in ComputeArkadeScriptPublicKey. Fails if no entry exists.
OP_INSPECTINPUTVALUE 201 0xc9 index value Pushes the satoshi value of the previous output spent by the input at the given index, as a minimally-encoded BigNum.
OP_INSPECTINPUTSCRIPTPUBKEY 202 0xca index program version For witness programs: pushes the witness program (2-40 bytes) and segwit version (scriptNum). For non-native segwit: pushes SHA256 hash of scriptPubKey and -1.
OP_INSPECTINPUTSEQUENCE 203 0xcb index sequence Pushes the sequence number (4 bytes, little-endian) of the input at the given index.
OP_PUSHCURRENTINPUTINDEX 205 0xcd Nothing index Pushes the current input index (scriptNum) being evaluated onto the stack.
OP_INSPECTINPUTARKADEWITNESSHASH 206 0xce index witness_hash Pushes the 32-byte Arkade witness hash (tagged_hash("ArkWitnessHash", witness)) of the IntrospectorEntry for the input at the given index. Pushes 32 zero bytes if witness is empty. Fails if no entry exists.

Transaction Introspection (Outputs)

Word Opcode Hex Input Output Description
OP_INSPECTOUTPUTVALUE 207 0xcf index value Pushes the satoshi value of the output at the given index, as a minimally-encoded BigNum.
OP_INSPECTOUTPUTSCRIPTPUBKEY 209 0xd1 index program version For witness programs: pushes the witness program (2-40 bytes) and segwit version (scriptNum). For non-native segwit: pushes SHA256 hash of scriptPubKey and -1.

Transaction Introspection (Transaction)

Word Opcode Hex Input Output Description
OP_INSPECTVERSION 210 0xd2 Nothing version Pushes the transaction version (4 bytes, little-endian) onto the stack.
OP_INSPECTLOCKTIME 211 0xd3 Nothing locktime Pushes the transaction locktime (4 bytes, little-endian) onto the stack.
OP_INSPECTNUMINPUTS 212 0xd4 Nothing numInputs Pushes the number of inputs in the transaction (scriptNum) onto the stack.
OP_INSPECTNUMOUTPUTS 213 0xd5 Nothing numOutputs Pushes the number of outputs in the transaction (scriptNum) onto the stack.
OP_TXWEIGHT 214 0xd6 Nothing weight Pushes the transaction weight (4 bytes, little-endian) onto the stack. Weight is calculated as SerializeSizeStripped() * 4.
OP_TXID 243 0xf3 Nothing txid Pushes the current transaction hash (32 bytes) onto the stack.

Packet Introspection

Word Opcode Hex Input Output Description
OP_INSPECTPACKET 244 0xf4 packet_type content 1 (or <empty> 0) Looks up the packet with the given type in the current transaction's extension. On hit: pushes the raw packet content and 1. Not found: pushes an empty byte array and 0.
OP_INSPECTINPUTPACKET 245 0xf5 packet_type input_index content 1 (or <empty> 0) Looks up the packet with the given type in the ark extension of the previous ark transaction spent by the input at input_index. On hit: pushes the raw packet content and 1. Not found: pushes an empty byte array and 0. Fails on negative / out-of-range input_index.

Data Manipulation

Word Opcode Hex Input Output Description
OP_CAT 126 0x7e x1 x2 x1|x2 Concatenates two byte arrays.
OP_SUBSTR 127 0x7f x n size x[n:n+size] Returns a substring of byte array x starting at position n with length size.
OP_LEFT 128 0x80 x n x[:n] Returns the first n bytes of byte array x.
OP_RIGHT 129 0x81 x n x[len(x)-n:] Returns the last n bytes of byte array x.

Bitwise Logic

Word Opcode Hex Input Output Description
OP_INVERT 131 0x83 x ~x Flips all bits in the input (bitwise NOT).
OP_AND 132 0x84 x1 x2 x1&x2 Boolean AND between each bit in the inputs. Operands must be the same length.
OP_OR 133 0x85 x1 x2 x1|x2 Boolean OR between each bit in the inputs. Operands must be the same length.
OP_XOR 134 0x86 x1 x2 x1^x2 Boolean exclusive OR between each bit in the inputs. Operands must be the same length.

Arithmetic

Arithmetic operands and results use the VM's minimally encoded BigNum format and can be up to the maximum script element size. OP_NUM2BIN and OP_BIN2NUM bridge between BigNum values and fixed-width byte strings.

Word Opcode Hex Input Output Description
OP_2MUL 141 0x8d x x*2 Multiplies the input by 2.
OP_2DIV 142 0x8e x x/2 Divides the input by 2.
OP_MUL 149 0x95 a b a*b Multiplies two numbers.
OP_DIV 150 0x96 a b a/b Divides a by b. Fails if b is zero.
OP_MOD 151 0x97 a b a%b Returns the remainder after dividing a by b. Fails if b is zero.
OP_LSHIFT 152 0x98 x n x<<n Logical left shift by n bits. Sign data is discarded.
OP_RSHIFT 153 0x99 x n x>>n Logical right shift by n bits. Sign data is discarded.
OP_NUM2BIN 215 0xd7 num size bytes Pads a BigNum to exactly size bytes. Fails if the number does not fit or size is negative or greater than the maximum script element size.
OP_BIN2NUM 216 0xd8 bytes num Normalizes a byte string into a minimally encoded BigNum.

Cryptography

Word Opcode Hex Input Output Description
OP_CHECKSIGFROMSTACK 204 0xcc sig pubkey message True/false Verifies a Schnorr signature. Pops signature (64 bytes), public key (32 bytes), and message from the stack. Returns 1 if valid, 0 otherwise. If signature is empty, pushes empty vector.
OP_MERKLEBRANCHVERIFY 179 0xb3 leaf_tag branch_tag proof leaf_data computed_root Computes a Merkle root using BIP-341 tagged hashes. If leaf_tag is empty, leaf_data (32 bytes) is used as a raw hash; otherwise computes tagged_hash(leaf_tag, leaf_data). Walks the proof path with lexicographic sibling ordering. Pushes the 32-byte computed root. Use with OP_EQUALVERIFY to verify against an expected root.

Elliptic Curve Operations

Word Opcode Hex Input Output Description
OP_ECMULSCALARVERIFY 227 0xe3 k P Q Nothing/fail Verifies that Q = k*P where k is a 32-byte scalar, P is a compressed public key, and Q is a compressed public key. Fails if verification fails.
OP_TWEAKVERIFY 228 0xe4 P k Q Nothing/fail Verifies that Q = P + k*G where P is a 32-byte X-only internal key, k is a 32-byte big-endian scalar, Q is a 33-byte compressed point, and G is the generator point. Fails if verification fails.

SHA256 Streaming Operations

These opcodes allow incremental SHA256 hashing by maintaining hash state on the stack.

Word Opcode Hex Input Output Description
OP_SHA256INITIALIZE 196 0xc4 data state Initializes a SHA256 context with the given data and pushes the hash state onto the stack.
OP_SHA256UPDATE 197 0xc5 data state newState Updates a SHA256 context by adding data to the stream being hashed. Pushes the updated state.
OP_SHA256FINALIZE 198 0xc6 data state hash Finalizes a SHA256 hash by adding data and completing padding. Pushes the final 32-byte hash value.

Asset Introspection Opcodes

These opcodes provide access to the Arkade Asset V1 packet embedded in the transaction. Asset IDs are represented as two stack items: (txid32, gidx_u16).

Packet & Groups

Word Opcode Hex Input Output Description
OP_INSPECTNUMASSETGROUPS 229 0xe5 Nothing K Returns the number of asset groups in the packet.
OP_INSPECTASSETGROUPASSETID 230 0xe6 k txid32 gidx_u16 Returns the Asset ID of group k. Fresh groups use this transaction's ID.
OP_INSPECTASSETGROUPCTRL 231 0xe7 k -1 or txid32 gidx_u16 Returns the control Asset ID if present, else -1.
OP_FINDASSETGROUPBYASSETID 232 0xe8 txid32 gidx_u16 -1 or k Finds group index by Asset ID, or -1 if absent.

Metadata

Word Opcode Hex Input Output Description
OP_INSPECTASSETGROUPMETADATAHASH 233 0xe9 k hash32 Returns the immutable metadata Merkle root (set at genesis).

Per-Group I/O

Word Opcode Hex Input Output Description
OP_INSPECTASSETGROUPNUM 234 0xea k source_u8 count_u16 or in_u16 out_u16 Returns count of inputs/outputs. source: 0=inputs, 1=outputs, 2=both.
OP_INSPECTASSETGROUP 235 0xeb k j source_u8 type_u8 [data...] amount Returns j-th input/output of group k. source: 0=input, 1=output. Amounts are pushed as BigNums.
OP_INSPECTASSETGROUPSUM 236 0xec k source_u8 sum or in_sum out_sum Returns sum of amounts with overflow safety. source: 0=inputs, 1=outputs, 2=both. Amounts are pushed as BigNums.

OP_INSPECTASSETGROUP return values by type:

  • LOCAL input (0x01): type_u8 input_index_u32 amount
  • INTENT input (0x02): type_u8 txid_32 output_index_u32 amount
  • LOCAL output (0x01): type_u8 output_index_u32 amount

Cross-Output (Multi-Asset per UTXO)

Word Opcode Hex Input Output Description
OP_INSPECTOUTASSETCOUNT 237 0xed o n Returns number of asset entries assigned to output o.
OP_INSPECTOUTASSETAT 238 0xee o t txid32 gidx_u16 amount Returns t-th asset at output o. Amount is pushed as a BigNum.
OP_INSPECTOUTASSETLOOKUP 239 0xef o txid32 gidx_u16 amount or -1 Returns amount of asset at output o, or -1 if not found. Amount is pushed as a BigNum.

Cross-Input (Packet-Declared)

Word Opcode Hex Input Output Description
OP_INSPECTINASSETCOUNT 240 0xf0 i n Returns number of assets declared for input i.
OP_INSPECTINASSETAT 241 0xf1 i t txid32 gidx_u16 amount Returns t-th asset declared for input i. Amount is pushed as a BigNum.
OP_INSPECTINASSETLOOKUP 242 0xf2 i txid32 gidx_u16 amount or -1 Returns declared amount for asset at input i, or -1 if not found. Amount is pushed as a BigNum.

About

Introspection is all you need

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages