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 transaction, and signs if it passes:
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"
}Signs an Ark transaction and its associated checkpoint transactions by executing Arkade scripts on the Ark transaction inputs. The scripts are executed only on the Ark transaction, not on checkpoints.
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", "..."]
}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"
}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) |
- 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_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. |