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 in a transaction OP_RETURN output. The packet is a TLV (Type-Length-Value) stream with magic bytes ARK (0x41524b), containing per-input entries with the script bytecode and optional witness arguments.
This example builds a VTXO that can only be spent if two specific outputs are created with exact amounts. The introspector enforces these conditions via an Arkade script. See the full test in test/pay_2_out_test.go.
The script uses introspection opcodes to verify the transaction outputs match the expected addresses and amounts:
arkadeScript, _ := txscript.NewScriptBuilder().
// output 0 must pay to alice
AddInt64(0).AddOp(arkade.OP_INSPECTOUTPUTSCRIPTPUBKEY).
AddOp(arkade.OP_1).AddOp(arkade.OP_EQUALVERIFY). // segwit v1
AddData(alicePkScript[2:]).AddOp(arkade.OP_EQUALVERIFY). // witness program
// output 0 must have exact amount
AddInt64(0).AddOp(arkade.OP_INSPECTOUTPUTVALUE).
AddData(uint64LE(aliceAmount)).AddOp(arkade.OP_EQUALVERIFY).
// output 1 must pay to bob
AddInt64(1).AddOp(arkade.OP_INSPECTOUTPUTSCRIPTPUBKEY).
AddOp(arkade.OP_1).AddOp(arkade.OP_EQUALVERIFY).
AddData(bobPkScript[2:]).AddOp(arkade.OP_EQUALVERIFY).
// output 1 must have exact amount
AddInt64(1).AddOp(arkade.OP_INSPECTOUTPUTVALUE).
AddData(uint64LE(bobAmount)).AddOp(arkade.OP_EQUAL).
Script()The VTXO uses a MultisigClosure with three keys: the ark server, the user and the introspector's tweaked key.
scriptHash := arkade.ArkadeScriptHash(arkadeScript)
tweakedKey := arkade.ComputeArkadeScriptPublicKey(introspectorPubKey, scriptHash)
vtxoScript := script.TapscriptsVtxoScript{
Closures: []script.Closure{
&script.MultisigClosure{
PubKeys: []*btcec.PublicKey{aliceKey, tweakedKey, serverKey},
},
},
}
vtxoTapKey, _, _ := vtxoScript.TapTree()Build the offchain transaction with outputs matching the script, then commit the Arkade script in an OP_RETURN Introspector Packet:
tx, checkpoints, _ := offchain.BuildTxs(
[]offchain.VtxoInput{vtxoInput},
[]*wire.TxOut{
{Value: aliceAmount, PkScript: alicePkScript},
{Value: bobAmount, PkScript: bobPkScript},
},
checkpointScript,
)
// build the introspector packet with script for input 0
packet := &arkade.IntrospectorPacket{
Entries: []arkade.IntrospectorEntry{
{Vin: 0, Script: arkadeScript},
},
}
opReturnScript, _ := arkade.BuildOpReturnScript(nil, packet)
tx.UnsignedTx.AddTxOut(&wire.TxOut{Value: 0, PkScript: opReturnScript})
tx.Outputs = append(tx.Outputs, psbt.POutput{})The introspector decodes the tapscript, verifies it is a MultisigClosure containing the expected tweaked key, executes the Arkade script against the Ark transaction, and signs both the matching Ark input and checkpoint if it passes. If it is the last required non-arkd signer for all owned inputs matched by the introspector packet, it then submits the transaction set to arkd, merges arkd's checkpoint signatures, and finalizes the transaction:
signedTx, signedCheckpoints, _ := introspectorClient.SubmitTx(ctx, encodedTx, encodedCheckpoints)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 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 |
gRPC server port | 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.25+
- 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 value (8 bytes, little-endian) of the previous output spent by the input at the given index. |
| 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 value (8 bytes, little-endian) of the output at the given index. |
| 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. |
| 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. |
| 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. |
These opcodes perform 64-bit arithmetic with overflow checking. All operands must be 8-byte little-endian values.
| Word | Opcode | Hex | Input | Output | Description |
|---|---|---|---|---|---|
| OP_ADD64 | 215 | 0xd7 | a b | sum 1 (or a b 0) | Adds two 64-bit signed integers. On success: pushes sum and 1. On overflow: restores operands and pushes 0. |
| OP_SUB64 | 216 | 0xd8 | a b | diff 1 (or a b 0) | Subtracts b from a (64-bit signed). On success: pushes difference and 1. On overflow: restores operands and pushes 0. |
| OP_MUL64 | 217 | 0xd9 | a b | product 1 (or a b 0) | Multiplies two 64-bit signed integers. On success: pushes product and 1. On overflow: restores operands and pushes 0. |
| OP_DIV64 | 218 | 0xda | a b | remainder quotient 1 (or a b 0) | Divides a by b (64-bit signed). On success: pushes remainder, quotient, and 1. On division by zero or overflow: restores operands and pushes 0. |
| OP_NEG64 | 219 | 0xdb | x | -x 1 (or x 0) | Negates a 64-bit signed integer. On success: pushes result and 1. On overflow: restores operand and pushes 0. |
| OP_LESSTHAN64 | 220 | 0xdc | a b | 1/0 | Returns 1 if a < b (64-bit signed), 0 otherwise. |
| OP_LESSTHANOREQUAL64 | 221 | 0xdd | a b | 1/0 | Returns 1 if a ≤ b (64-bit signed), 0 otherwise. |
| OP_GREATERTHAN64 | 222 | 0xde | a b | 1/0 | Returns 1 if a > b (64-bit signed), 0 otherwise. |
| OP_GREATERTHANOREQUAL64 | 223 | 0xdf | a b | 1/0 | Returns 1 if a ≥ b (64-bit signed), 0 otherwise. |
| Word | Opcode | Hex | Input | Output | Description |
|---|---|---|---|---|---|
| OP_SCRIPTNUMTOLE64 | 224 | 0xe0 | scriptNum | le64 | Converts a scriptNum to an 8-byte little-endian value. |
| OP_LE64TOSCRIPTNUM | 225 | 0xe1 | le64 | scriptNum | Converts an 8-byte little-endian value to a scriptNum. |
| OP_LE32TOLE64 | 226 | 0xe2 | le32 | le64 | Converts a 4-byte little-endian value to an 8-byte little-endian value (sign-extended). |
| 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_u64 | Returns j-th input/output of group k. source: 0=input, 1=output. |
| OP_INSPECTASSETGROUPSUM | 236 | 0xec | k source_u8 | sum_u64 or in_u64 out_u64 | Returns sum of amounts with overflow safety. source: 0=inputs, 1=outputs, 2=both. |
OP_INSPECTASSETGROUP return values by type:
- LOCAL input (0x01):
type_u8 input_index_u32 amount_u64 - INTENT input (0x02):
type_u8 txid_32 output_index_u32 amount_u64 - LOCAL output (0x01):
type_u8 output_index_u32 amount_u64
| 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_u64 | Returns t-th asset at output o. |
| OP_INSPECTOUTASSETLOOKUP | 239 | 0xef | o txid32 gidx_u16 | amount_u64 or -1 | Returns amount of asset at output o, or -1 if not found. |
| 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_u64 | Returns t-th asset declared for input i. |
| OP_INSPECTINASSETLOOKUP | 242 | 0xf2 | i txid32 gidx_u16 | amount_u64 or -1 | Returns declared amount for asset at input i, or -1 if not found. |