Skip to main content

Install the SDK

If you haven’t already, install the SDK and set up your environment
The Dryja-Poon construction enables bidirectional payment channels with revocation-based state updates.
PathConditionWhen to use
Funding (collaborative)Alice + Bob + server signaturesOpen channel instantly
Funding (unilateral)Alice + Bob signatures (after exit delay)Fallback if server unresponsive
To-local (revocation)Counterparty revocation signaturePunish old state broadcast
To-local (normal)Self signature (after CSV delay)Claim own balance
To-remoteCounterparty signatureImmediate claim

Build the Tapscript

import { Script } from '@scure/btc-signer';
import { hex } from '@scure/base';
import {
  RestArkProvider,
  SingleKey,
  VtxoScript,
  networks
} from '@arkade-os/sdk';

// Setup
const arkProvider = new RestArkProvider('https://mutinynet.arkade.sh');
const info = await arkProvider.getInfo();
const serverPubkey = hex.decode(info.signerPubkey).slice(1); // x-only
const exitDelay = BigInt(info.exitDelay);

// Channel participants
const alice = SingleKey.fromRandomBytes();
const bob = SingleKey.fromRandomBytes();
const alicePubkey = await alice.xOnlyPublicKey();
const bobPubkey = await bob.xOnlyPublicKey();

// Standard Lightning CSV delay for to_local
const toLocalDelay = 144n; // ~1 day in blocks

Funding output

2-of-2 multisig between Alice and Bob, with Ark timeout path:
Collaborative: <alicePubkey> OP_CHECKSIGVERIFY <bobPubkey> OP_CHECKSIGVERIFY <serverPubkey> OP_CHECKSIG
Unilateral:    <exitDelay> OP_CSV OP_DROP <alicePubkey> OP_CHECKSIGVERIFY <bobPubkey> OP_CHECKSIG
// Collaborative: aliceSig + bobSig + serverSig
const fundingCollaborative = Script.encode([
  alicePubkey,
  'CHECKSIGVERIFY',
  bobPubkey,
  'CHECKSIGVERIFY',
  serverPubkey,
  'CHECKSIG'
]);

// Unilateral: after exitDelay, aliceSig + bobSig
const fundingUnilateral = Script.encode([
  exitDelay,
  'CHECKSEQUENCEVERIFY',
  'DROP',
  alicePubkey,
  'CHECKSIGVERIFY',
  bobPubkey,
  'CHECKSIG'
]);

const fundingScript = new VtxoScript([fundingCollaborative, fundingUnilateral]);
const fundingAddress = fundingScript.address(networks.mutinynet.hrp, serverPubkey).encode();

console.log('Funding address:', fundingAddress);

To-local output (commitment transaction)

Funds belonging to the broadcaster, with revocation for punishment:
Revocation: <revocationPubkey> OP_CHECKSIG
Normal:     <toLocalDelay> OP_CSV OP_DROP <localPubkey> OP_CHECKSIG
// Revocation keys are derived per commitment (simplified here)
function buildToLocalScript(
  localPubkey: Uint8Array,
  revocationPubkey: Uint8Array,
  delay: bigint
) {
  // Revocation path: counterparty can punish old state
  const revocationPath = Script.encode([
    revocationPubkey,
    'CHECKSIG'
  ]);

  // Normal path: broadcaster claims after delay
  const normalPath = Script.encode([
    delay,
    'CHECKSEQUENCEVERIFY',
    'DROP',
    localPubkey,
    'CHECKSIG'
  ]);

  // Ark unilateral exit variant
  const unilateralPath = Script.encode([
    exitDelay,
    'CHECKSEQUENCEVERIFY',
    'DROP',
    delay,
    'CHECKSEQUENCEVERIFY',
    'DROP',
    localPubkey,
    'CHECKSIG'
  ]);

  return new VtxoScript([revocationPath, normalPath, unilateralPath]);
}

// Alice's to_local in her commitment
const aliceRevocationPubkey = /* derived from Bob's revocation basepoint */;
const aliceToLocal = buildToLocalScript(alicePubkey, aliceRevocationPubkey, toLocalDelay);

To-remote output (commitment transaction)

Funds belonging to the counterparty, immediately spendable:
Normal:     <remotePubkey> OP_CHECKSIG
Unilateral: <exitDelay> OP_CSV OP_DROP <remotePubkey> OP_CHECKSIG
function buildToRemoteScript(remotePubkey: Uint8Array) {
  // Normal: counterparty claims immediately
  const normalPath = Script.encode([
    remotePubkey,
    'CHECKSIG'
  ]);

  // Ark unilateral exit
  const unilateralPath = Script.encode([
    exitDelay,
    'CHECKSEQUENCEVERIFY',
    'DROP',
    remotePubkey,
    'CHECKSIG'
  ]);

  return new VtxoScript([normalPath, unilateralPath]);
}

// Bob's to_remote in Alice's commitment
const bobToRemote = buildToRemoteScript(bobPubkey);

The dual-path pattern

Every Lightning script gets two Taproot leaves:
Taproot tree:
├── <standard lightning script>
└── <exitDelay> OP_CSV OP_DROP <standard lightning script>
The first leaf is vanilla Lightning. The second adds a CSV delay matching the Batch expiry for Ark unilateral exit. The server’s key never appears in commitment or HTLC scripts.

Script breakdown

OpcodeEffect
CHECKSIGVerify signature, return result
CHECKSIGVERIFYVerify signature, continue if valid
CHECKSEQUENCEVERIFYEnforce relative timelock (CSV)
DROPRemove top stack element

Next steps