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.arkThis 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.arkThe 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
optionsblock for configuration - A
contractdeclaration with parameters - One or more
functiondeclarations 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