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.
test/htlc_test.go— Non-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.go— Non-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.
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"
}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.
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"
}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"
}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"
}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.
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 vinis unique across the packet (an entry per vin, never two)- No trailing bytes after the last entry
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.
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 |
- Go 1.26+
- Docker and Docker Compose
- Buf CLI (for protocol buffer generation)
- Nigiri (for integration testing)
# Generate protocol buffer stubs
make proto
# Build the application
make build# Run with development configuration
make run# Run unit tests
make test
# Run docker regtest environment
nigiri start
make docker-run
# Run integration tests
make integrationtestThe following opcodes are supported by the Arkade script engine. They extend Bitcoin Script with additional introspection, data manipulation, and cryptographic operations.
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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 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. |
| 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. |
| 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. |
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. |
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).
| 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. |
| Word | Opcode | Hex | Input | Output | Description |
|---|---|---|---|---|---|
| OP_INSPECTASSETGROUPMETADATAHASH | 233 | 0xe9 | k | hash32 | Returns the immutable metadata Merkle root (set at genesis). |
| 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
| 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. |
| 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. |