pragma silverscript ^0.1.0; // Source contract for the deployed Sompi escrow template. // The TypeScript template in src/x402/escrow-template.ts is kept // byte-identical to SilverScript compiler output for this contract. // // SompiEscrow: a one-shot Kaspa escrow scheme for x402-style HTTP payments. // // The client funds this covenant once. As it consumes API requests it signs // off-chain vouchers authorizing the server to claim a running total. Neither // side has to trust the other: // // claim — the server closes the channel with the client's latest voucher. // It may take at most the voucher amount; the remainder must // return to this same escrow script so the client can still // reclaim it. OpCheckSigFromStack proves the client authorized // exactly that amount and spend identity, so the server cannot // over-claim or replay a voucher against another UTXO. // // refund — after the timeout the client reclaims everything with its own // signature, so a vanished or unresponsive server cannot strand // the deposit. // // The voucher message binds the claim amount to the domain, network, active // serialized escrow scriptPubKey, and full outpoint of the UTXO being spent: // // sha256(domain32 || network32 || sha256(serializedInputScriptPubKey) // || outpointTxId32 || outpointIndex_le32 || amount_le64) // // This is the replay protection. A claim's change returns to escrow under a new // outpoint, so the same voucher's message no longer matches and // checkSigFromStack fails. This is the SilverScript builtin name; the compiler // lowers it to the Toccata OpCheckSigFromStack opcode. The server presents // (amount, signature); the covenant reconstructs the message from tx // introspection, hashes it explicitly, and verifies it on-chain. contract SompiEscrow(pubkey client, pubkey server, byte[32] networkHash, int timeout) { // sha256("sompi:escrow-voucher:v2") byte[32] constant VOUCHER_DOMAIN_TAG = 0x15436b1356689a0646b884da4d7599ba22dc8b49336224e48983e8e0c90e906a; // Server claims up to the voucher-authorized amount; change returns to escrow. entrypoint function claim(sig serverSig, datasig clientVoucher, byte[8] amountAuthorized) { require(checkSig(serverSig, server)); // message = domain32 + network32 + sha256(serialized inputSpk) + txid32 + vout_le4 + amount_le64. byte[] voucherMsg = byte[](VOUCHER_DOMAIN_TAG) + byte[](networkHash) + byte[](sha256(tx.inputs[this.activeInputIndex].scriptPubKey)) + byte[](tx.inputs[this.activeInputIndex].outpointTransactionHash) + byte[](byte[4](tx.inputs[this.activeInputIndex].outpointIndex)) + byte[](amountAuthorized); require(checkSigFromStack(clientVoucher, sha256(voucherMsg), client)); int authorized = int(amountAuthorized); int inputValue = tx.inputs[this.activeInputIndex].value; // Exactly two outputs: [0] server's claim, [1] change back to escrow. require(tx.outputs.length == 2); // The server takes at most what the client authorized (fee comes out of // the server's own output, never the client's change). require(tx.outputs[0].value <= authorized); // Change must return to this exact escrow script and preserve the rest. byte[] escrowScriptPubKey = tx.inputs[this.activeInputIndex].scriptPubKey; require(tx.outputs[1].scriptPubKey == escrowScriptPubKey); require(tx.outputs[1].value >= inputValue - authorized); } // Client reclaims everything after the timeout. entrypoint function refund(sig clientSig) { require(checkSig(clientSig, client)); require(tx.time >= timeout); } }