Arkade Language is a high-level contract language that compiles down to Arkade Script, an extended version of Bitcoin Script designed for the Arkade OS. Arkade Language lets developers write expressive, stateful smart contracts that compile to scripts executable by Arkade’s Virtual Machine.
Arkade Script supports advanced primitives for arithmetic, introspection, and asset flows across Virtual Transaction Outputs (VTXOs), enabling rich offchain transaction logic with unilateral onchain exit guarantees. Contracts are verified and executed inside secure Trusted Execution Environments (TEEs) and signed by the Arkade Signer, ensuring verifiable and tamper-proof execution.
This language significantly lowers the barrier for Bitcoin-native app development, allowing contracts to be written in a structured, Ivy-like syntax and compiled into Arkade-native scripts.
arkadec contract.ark
This will compile your Arkade Script contract to a JSON file that can be used with Bitcoin Taproot libraries.
The Arkade Compiler supports several command-line options:
# Output assembly instead of bytecode
arkadec --output=asm contract.ark
# Generate debug information
arkadec --debug contract.ark
# Specify output file
arkadec --output-file=contract.json contract.ark
The compiler produces a JSON file containing:
- Contract metadata (name, version, etc.)
- Constructor parameters
- Function definitions
- Generated script for each function (both cooperative and unilateral paths)
- Source map for debugging
Example output:
{
"contractName": "MyContract",
"constructorInputs": [
{ "name": "user", "type": "pubkey" },
{ "name": "server", "type": "pubkey" }
],
"functions": [
{
"name": "spend",
"functionInputs": [
{ "name": "userSig", "type": "signature" }
],
"serverVariant": true,
"require": [
{ "type": "signature" },
{ "type": "serverSignature" }
],
"asm": [
"<user>",
"<userSig>",
"OP_CHECKSIG",
"<SERVER_KEY>",
"<serverSig>",
"OP_CHECKSIG"
]
},
{
"name": "spend",
"functionInputs": [
{ "name": "userSig", "type": "signature" }
],
"serverVariant": false,
"require": [
{ "type": "signature" },
{ "type": "older", "message": "Exit timelock of 144 blocks" }
],
"asm": [
"<user>",
"<userSig>",
"OP_CHECKSIG",
"144",
"OP_CHECKLOCKTIMEVERIFY",
"OP_DROP"
]
}
],
"source": "...",
"compiler": {
"name": "arkade-script",
"version": "0.1.0"
},
"updatedAt": "2023-03-06T01:27:51.391557+00:00"
}
// Contract configuration options
options {
// Server key parameter from contract parameters
server = server;
// Exit timelock: 24 hours (144 blocks)
exit = 144;
}
contract BareVTXO(
pubkey user,
pubkey server
) {
// Single signature spend path
// This will automatically be compiled into:
// 1. Cooperative path: checkSig(user) && checkSig(server)
// 2. Exit path: checkSig(user) && after 144 blocks
function spend(signature userSig) {
require(checkSig(userSig, user));
}
}
// Contract configuration options
options {
// Server key parameter from contract parameters
server = server;
// Exit timelock: 24 hours (144 blocks)
exit = 144;
}
contract HTLC(
pubkey sender,
pubkey receiver,
bytes hash,
int refundTime,
pubkey server
) {
// Cooperative close path
function together(signature senderSig, signature receiverSig) {
require(checkMultisig([sender, receiver], [senderSig, receiverSig]));
}
// Refund path
function refund(signature senderSig) {
require(checkSig(senderSig, sender));
require(tx.time >= refundTime);
}
// Claim path
function claim(signature receiverSig, bytes preimage) {
require(checkSig(receiverSig, receiver));
require(sha256(preimage) == hash);
}
}
// Contract configuration options
options {
// Server key parameter from contract parameters
server = treasuryPk;
// Exit timelock: 24 hours (144 blocks)
exit = 144;
}
// Fuji Safe Contract
contract FujiSafe(
// The asset commitment hash (client-side validated)
bytes assetCommitmentHash,
// The amount being borrowed
int borrowAmount,
// The borrower's public key
pubkey borrowerPk,
// The treasury's public key
pubkey treasuryPk,
// The expiration timeout in blocks
int expirationTimeout,
// The price level for liquidation
int priceLevel,
// The setup timestamp
int setupTimestamp,
// The oracle's public key
pubkey oraclePk,
// The asset pair identifier
bytes assetPair
) {
// Helper function to verify Fuji token burning via Taproot output
// Takes the pubkey to use as the internal key for the P2TR output
function verifyFujiBurning(pubkey internalKey) internal {
// In Taproot, we verify the output is a P2TR that commits to our asset
// Using the provided pubkey as the internal key
bytes p2trScript = new P2TR(internalKey, assetCommitmentHash);
// Verify output 0 has the correct P2TR scriptPubKey and value
require(tx.outputs[0].scriptPubKey == p2trScript, "P2TR output mismatch");
require(tx.outputs[0].value == borrowAmount, "Value mismatch");
}
// Claim: Treasury can unlock all collateral after expiration when burning Fuji
function claim(signature treasurySig) {
// Check that expiration timeout has passed
require(tx.time >= expirationTimeout, "Expiration timeout not reached");
// Verify burning of Fuji token using treasury key
verifyFujiBurning(treasuryPk);
// Require treasury signature
require(checkSig(treasurySig, treasuryPk), "Invalid treasury signature");
}
// Liquidation: Treasury can unlock all collateral with attestation price below the liquidation target
function liquidate(int currentPrice, signature oracleSig, signature treasurySig) {
// Check price is below liquidation threshold
require(currentPrice < priceLevel, "Price not below liquidation threshold");
// Verify timestamp is after setup
require(tx.time >= setupTimestamp, "Timestamp before setup");
// Create message for oracle signature verification
bytes message = sha256(assetPair);
// Verify oracle signature on price data
require(checkSigFromStack(oracleSig, oraclePk, message), "Invalid oracle signature");
// Verify burning of Fuji token using treasury key
verifyFujiBurning(treasuryPk);
// Require treasury signature
require(checkSig(treasurySig, treasuryPk), "Invalid treasury signature");
}
// Private Redemption: Only owner can unlock all collateral with key when burning Fuji
function redeem(signature borrowerSig) {
// Verify burning of Fuji token using borrower key
verifyFujiBurning(borrowerPk);
// Require borrower signature
require(checkSig(borrowerSig, borrowerPk), "Invalid borrower signature");
}
// Treasury Renew: Treasury can unilaterally renew the expiration time
function renew(signature treasurySig) {
// For renewal, we ensure the output is another P2TR with the same key and value
// This preserves the Taproot commitment structure
// Using the new tx.input.current syntax to access the current input's properties
bytes currentScript = tx.input.current.scriptPubKey;
int currentValue = tx.input.current.value;
// Verify that output 0 has the same P2TR script as the current input
require(tx.outputs[0].scriptPubKey == currentScript, "P2TR output mismatch");
require(tx.outputs[0].value == currentValue, "Value mismatch");
// Require treasury signature
require(checkSig(treasurySig, treasuryPk), "Invalid treasury signature");
}
}
TapLang is a domain-specific language for writing Bitcoin Taproot contracts with a focus on readability and safety.
TapLang supports the following data types:
pubkey
: Bitcoin public keysignature
: Bitcoin signaturebytes
: Arbitrary byte arraybytes20
: 20-byte array (useful for hashes)bytes32
: 32-byte array (useful for hashes)int
: Integer valuebool
: Boolean valueasset
: Taproot Asset (for asset-aware contracts)
A TapLang contract consists of:
- An optional
options
block for configuration - A
contract
declaration with parameters - One or more
function
declarations that define spending paths
Example:
// Optional configuration
options {
server = treasuryPk;
exit = 144;
}
// Contract declaration with parameters
contract MyContract(
pubkey user,
pubkey server
) {
// Function declarations (spending paths)
function spend(signature userSig) {
require(checkSig(userSig, user));
}
}
The options block configures contract-wide settings:
options {
// Server key parameter from contract parameters
server = server;
// Renewal timelock: 7 days (1008 blocks)
renew = 1008;
// Exit timelock: 24 hours (144 blocks)
exit = 144;
}
Available options:
server
: Specifies which parameter contains the server public keyrenew
: Specifies the renewal timelock in blocksexit
: Specifies the exit timelock in blocks
Functions define spending paths for the contract:
function spend(signature userSig) {
require(checkSig(userSig, user));
}
Functions can be marked as internal
to indicate they are helper functions and not spending paths:
function verifyCondition() internal {
// Helper logic
}
Arkade Script supports various expressions:
// Single signature verification
require(checkSig(userSig, user));
// Multi-signature verification
require(checkMultisig([user, admin], [userSig, adminSig]));
// Signature verification from stack
require(checkSigFromStack(oracleSig, oraclePk, message));
// SHA-256 hash verification
require(sha256(preimage) == hash);
// Absolute timelock
require(tx.time >= expirationTime);
TapLang provides access to transaction data:
// Access transaction time
require(tx.time >= lockTime);
// Access outputs
require(tx.outputs[0].value == amount);
require(tx.outputs[0].scriptPubKey == script);
// Access inputs
require(tx.inputs[0].value == amount);
require(tx.inputs[0].scriptPubKey == script);
// Access the current input (new syntax)
require(tx.input.current.value == amount);
require(tx.input.current.scriptPubKey == script);
TapLang provides a special syntax for accessing the current input being spent:
// Access the current input's value
int currentValue = tx.input.current.value;
// Access the current input's scriptPubKey
bytes currentScript = tx.input.current.scriptPubKey;
// Access the current input's sequence number
int sequence = tx.input.current.sequence;
// Access the current input's outpoint
bytes outpoint = tx.input.current.outpoint;
This is more intuitive than using an index variable:
// Old approach (less intuitive)
int currentIndex = 0; // Assume the current input is at index 0
int currentValue = tx.inputs[currentIndex].value;
// New approach (more intuitive)
int currentValue = tx.input.current.value;
You can declare variables to store intermediate values:
bytes message = sha256(timestamp + currentPrice + assetPair);
bytes p2trScript = new P2TR(internalKey, assetCommitmentHash);
You can provide custom error messages for require statements:
require(tx.time >= expirationTimeout, "Expiration timeout not reached");
TapLang compiles contracts to a JSON format that can be used with Bitcoin Taproot libraries.
{
"contractName": "MyContract",
"constructorInputs": [
{ "name": "user", "type": "pubkey" },
{ "name": "server", "type": "pubkey" }
],
"functions": [
{
"name": "spend",
"functionInputs": [
{ "name": "userSig", "type": "signature" }
],
"serverVariant": true,
"require": [
{ "type": "signature" },
{ "type": "serverSignature" }
],
"asm": [
"<user>",
"<userSig>",
"OP_CHECKSIG",
"<SERVER_KEY>",
"<serverSig>",
"OP_CHECKSIG"
]
},
{
"name": "spend",
"functionInputs": [
{ "name": "userSig", "type": "signature" }
],
"serverVariant": false,
"require": [
{ "type": "signature" },
{ "type": "older", "message": "Exit timelock of 144 blocks" }
],
"asm": [
"<user>",
"<userSig>",
"OP_CHECKSIG",
"144",
"OP_CHECKLOCKTIMEVERIFY",
"OP_DROP"
]
}
],
"source": "...",
"compiler": {
"name": "taplang",
"version": "0.1.0"
},
"updatedAt": "2023-03-06T01:27:51.391557+00:00"
}
contractName
: The name of the contractconstructorInputs
: The parameters required to instantiate the contractfunctions
: The spending paths of the contract- Each function has two variants:
serverVariant: true
: Requires server signature (cooperative path)serverVariant: false
: Requires timelock (exit path)
- Each function has two variants:
require
: The requirements for each spending pathasm
: The Bitcoin Script assembly code for each spending path