- Introduction
- Compiling Contracts
- Language Basics
- Functions
- Operators
- Control Flow
- Working with Data
- Type Casting
- Built-in Functions
- Transaction Introspection
- Covenants
- Advanced Features
- Complete Examples
SilverScript is a CashScript-inspired smart contract language that compiles to Kaspa script. It enables you to write Kaspa smart contracts with a high-level, Solidity-like syntax. SilverScript contracts can enforce complex spending conditions, create covenants, and enable advanced cryptocurrency applications on the Kaspa network.
The silverc command-line tool compiles .sil source files into JSON artifacts containing the compiled bytecode and ABI.
Basic Usage:
silverc contract.silThis reads contract.sil and outputs contract.json by default.
Specify Output File:
silverc contract.sil -o output.jsonWith Constructor Arguments:
If your contract has constructor parameters, you can provide their values via a JSON file:
silverc contract.sil --constructor-args args.jsonThe args.json file should contain an array of constructor argument expressions. For example:
[
{"kind": "bytes", "data": [1, 2, 3, 4]},
{"kind": "int", "data": 12345}
]The compiled JSON output includes:
contract_name: The name of the contractscript: The compiled bytecode (as an array of bytes)ast: The abstract syntax tree of the parsed contractabi: An array of entrypoint functions with their parameter types
You can also compile contracts programmatically using the SilverScript Rust library:
use silverscript_lang::compiler::{compile_contract, CompileOptions};
use silverscript_lang::ast::Expr;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let source = r#"
pragma silverscript ^0.1.0;
contract MyContract(int x) {
entrypoint function spend(int y) {
require(y > x);
}
}
"#;
// Constructor arguments (x = 100)
let constructor_args = vec![Expr::Int(100)];
// Compile with default options
let options = CompileOptions::default();
let compiled = compile_contract(source, &constructor_args, options)?;
println!("Contract name: {}", compiled.contract_name);
println!("Script length: {} bytes", compiled.script.len());
println!("ABI: {:?}", compiled.abi);
Ok(())
}Building Signature Scripts Programmatically:
After compiling a contract, you can build signature scripts (unlocking scripts) for its entrypoint functions:
use silverscript_lang::ast::Expr;
let source = r#"
pragma silverscript ^0.1.0;
contract TransferWithTimeout(pubkey sender, pubkey recipient, int timeout) {
entrypoint function transfer(sig recipientSig) {
require(checkSig(recipientSig, recipient));
}
entrypoint function reclaim(sig senderSig) {
require(checkSig(senderSig, sender));
require(tx.time >= timeout);
}
}
"#;
let sender_pk = vec![3u8; 32];
let recipient_pk = vec![4u8; 32];
let timeout = 1640000000i64;
let compiled = compile_contract(
source,
&[sender_pk.into(), recipient_pk.into(), timeout.into()],
CompileOptions::default()
)?;
// Build sigscript for multiple entrypoints
let sig = vec![5u8; 64];
// For 'transfer' function (selector = 0)
let transfer_sigscript = compiled.build_sig_script(
"transfer",
vec![sig.clone().into()]
)?;
// transfer_sigscript contains: <signature> <0> (selector 0)
// For 'reclaim' function (selector = 1)
let reclaim_sigscript = compiled.build_sig_script(
"reclaim",
vec![sig.into()]
)?;
// reclaim_sigscript contains: <signature> <1> (selector 1)The build_sig_script method automatically:
- Validates argument count and types
- Encodes arguments properly for the Kaspa script stack
- Appends the function selector for contracts with multiple entrypoints
- Omits the selector for contracts with a single entrypoint
Every SilverScript program defines a single contract. A contract has a name, optional constructor parameters, and one or more functions:
pragma silverscript ^0.1.0;
contract MyContract(int param1, bytes32 param2) {
// Contract constants (optional)
int constant MAX_VALUE = 1000;
// Functions
entrypoint function spend(sig s, pubkey pk) {
require(checkSig(s, pk));
}
}Every contract should start with a pragma directive specifying the SilverScript version:
pragma silverscript ^0.1.0;Version operators:
^0.1.0- Compatible with 0.1.x~0.1.0- Compatible with 0.1.0 only>=0.1.0- Greater than or equal>0.1.0- Greater than<0.2.0- Less than<=0.1.5- Less than or equal=0.1.0- Exactly this version
SilverScript supports the following data types:
| Type | Description | Example |
|---|---|---|
int |
64-bit signed integer | 42, -100, 1000 |
bool |
Boolean value | true, false |
string |
UTF-8 string | "hello", 'world' |
bytes |
Dynamic byte array | 0x1234, 0xabcdef |
byteN |
Fixed-size byte array (N = 1-255) | bytes32, bytes65 |
byte |
Single byte (alias for bytes1) |
byte |
pubkey |
Public key (32 bytes) | pubkey |
sig |
Signature (64 or 65 bytes) | sig |
datasig |
Data signature (64 or 65 bytes) | datasig |
Array Types:
You can create arrays by appending [] to any type:
int[] numbers;
bytes32[] hashes;
pubkey[] publicKeys;Variables must be declared with their type before use:
entrypoint function example() {
// Variable declaration
int myNumber = 42;
bool flag = true;
string message = "Hello World";
bytes data = 0x1234abcd;
// Declaration without initialization
int uninitializedValue;
// Variable reassignment
myNumber = 100;
}SilverScript supports both single-line and multi-line comments:
// This is a single-line comment
/*
* This is a multi-line comment
* It can span multiple lines
*/
int x = 10; // Comments can appear at the end of linesFunctions are defined with the function keyword:
function helper(int x, int y) {
// function body
}Entrypoint functions are callable from outside the contract. Mark them with the entrypoint keyword:
entrypoint function spend(sig s, pubkey pk) {
require(checkSig(s, pk));
}A contract must have at least one entrypoint function. Contracts with multiple entrypoints use function selectors automatically.
Functions can have multiple parameters and return values:
// Function with return type
function add(int a, int b): (int) {
return (a + b);
}
// Multiple return values
function split(bytes32 data): (bytes16, bytes16) {
bytes16 left, bytes16 right = data.split(16);
return (left, right);
}
// Using the return value
entrypoint function example() {
int result = add(5, 10);
require(result == 15);
}int a = 10;
int b = 3;
int sum = a + b; // 13
int difference = a - b; // 7
int product = a * b; // 30
int quotient = a / b; // 3
int remainder = a % b; // 1
int negative = -a; // -10bool eq = (a == b); // false (equality)
bool ne = (a != b); // true (inequality)
bool lt = (a < b); // false (less than)
bool le = (a <= b); // false (less than or equal)
bool gt = (a > b); // true (greater than)
bool ge = (a >= b); // true (greater than or equal)bool t = true;
bool f = false;
bool and = t && f; // false (logical AND)
bool or = t || f; // true (logical OR)
bool not = !t; // false (logical NOT)Note: Bitwise operators require covenant features to be enabled.
int x = 0x0F; // 00001111
int y = 0xF0; // 11110000
int bitAnd = x & y; // 0x00 (bitwise AND)
int bitOr = x | y; // 0xFF (bitwise OR)
int bitXor = x ^ y; // 0xFF (bitwise XOR)Basic if-else structure:
entrypoint function example(int x) {
if (x > 10) {
require(true);
} else if (x < 0) {
require(false);
} else {
require(x == 5);
}
}Single-statement branches don't require braces:
if (x > 0)
require(true);
else
require(false);The require statement enforces conditions. If the condition is false, the contract execution fails:
require(x > 0); // Passes if x > 0, fails otherwise
// With error message
require(x > 0, "x must be positive");Time-based require statements:
// Require transaction time
require(tx.time >= 1640000000);
// Require contract age
require(this.age >= 86400); // 1 day in secondsFor loops iterate over a range of integers. The bounds must be compile-time constants:
contract ForLoop() {
int constant START = 0;
int constant END = 4;
int constant MIN_OUT = 1000;
entrypoint function check() {
for(i, START, END) {
require(tx.outputs[i].value >= MIN_OUT + i);
}
}
}The loop variable i takes values from START to END - 1 (exclusive end).
Integer Literals:
int decimal = 42;
int negative = -100;
int withUnderscore = 1_000_000; // Underscores for readability
int exponential = 1e6; // 1,000,000Boolean Literals:
bool t = true;
bool f = false;String Literals:
string s1 = "Hello World";
string s2 = 'Single quotes work too';
string escaped = "Line 1\nLine 2\tTabbed";
string quote = "He said \"Hello\"";
string apostrophe = 'It\'s working';Hex Literals:
bytes data = 0x1234abcd;
bytes empty = 0x;
bytes pubkeyBytes = 0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef;SilverScript supports convenient number units for values and time:
Value Units:
int amount1 = 1000 litras;
int amount2 = 10 grains;
int amount3 = 1 kas;Time Units:
int time1 = 30 seconds;
int time2 = 5 minutes; // 300 seconds
int time3 = 2 hours; // 7200 seconds
int time4 = 7 days; // 604800 seconds
int time5 = 4 weeks; // 2419200 secondsExample usage:
entrypoint function withdraw() {
require(this.age >= 30 days);
require(tx.outputs[0].value >= 10000 litras);
}Convert ISO 8601 date strings to Unix timestamps:
int timestamp = date("2021-02-17T01:30:00");
require(tx.time >= timestamp);Format: YYYY-MM-DDThh:mm:ss
Arrays must be built dynamically using the .push() method:
// Declare an array
int[] numbers;
bytes32[] hashes;
// Build array with push
numbers.push(1);
numbers.push(2);
numbers.push(3);
numbers.push(4);
numbers.push(5);
// Build bytes32 array
hashes.push(0x1111111111111111111111111111111111111111111111111111111111111111);
hashes.push(0x2222222222222222222222222222222222222222222222222222222222222222);
// Access array elements
int first = numbers[0];
int second = numbers[1];
// Array length
int count = numbers.length;Concatenation:
string hello = "Hello";
string world = "World";
string message = hello + " " + world; // "Hello World"
// Length
int len = message.length; // 11Concatenation:
bytes a = 0x1234;
bytes b = 0x5678;
bytes combined = a + b; // 0x12345678Split:
Split bytes at a specific index:
bytes data = 0x1234567890abcdef;
bytes left = data.split(4)[0]; // 0x12345678
bytes right = data.split(4)[1]; // 0x90abcdefSlice:
Extract a range of bytes:
bytes data = 0x123456789abcdef;
bytes middle = data.slice(2, 5); // bytes from index 2 to 5 (exclusive)Length:
bytes data = 0x1234;
int size = data.length; // 2SilverScript supports explicit type casting:
// Cast to bytes
bytes fromInt = bytes(42);
bytes fromString = bytes("hello");
// Cast to specific byte size
bytes32 hash = bytes32(data);
bytes65 signatureBytes = bytes65(sigBytes);
// Cast to pubkey or sig
pubkey pk = pubkey(keyBytes);
sig signature = sig(signatureBytes);
// Cast to int
int number = int(someData);Example:
entrypoint function example(pubkey pk, bytes65 sigBytes) {
sig s = sig(sigBytes);
require(checkSig(s, pk));
}blake2b(bytes data): bytes32
Compute the BLAKE2b hash of the input:
bytes32 hash = blake2b(data);
bytes32 pkh = blake2b(pk);sha256(bytes data): bytes32
Compute the SHA-256 hash:
bytes32 hash = sha256(data);checkSig(sig signature, pubkey publicKey): bool
Verify a signature against a public key:
require(checkSig(s, pk));bytes(value): bytes
Convert to bytes:
bytes b1 = bytes(42);
bytes b2 = bytes("hello");bytes(int value, int size): bytes
Convert integer to bytes with specific size:
bytes8 b = bytes(1234, 8);int(bool value): int
Convert boolean to integer (true = 1, false = 0):
int x = int(false); // 0length(bytes value): int
Get the length of a byte array:
int size = length(data);Transaction introspection allows contracts to examine the transaction that is spending them.
Nullary Operations (no parameters):
// Current active input index
int inputIdx = this.activeInputIndex;
// Active bytecode (current contract's locking script)
bytes script = this.activeBytecode;
// Number of inputs
int inputCount = tx.inputs.length;
// Number of outputs
int outputCount = tx.outputs.length;
// Transaction version
int version = tx.version;
// Transaction locktime
int locktime = tx.locktime;Time-based Fields:
// Age of the UTXO being spent (in seconds)
require(this.age >= 0);
// Transaction locktime
require(tx.time >= 0);Access properties of transaction inputs:
// Access input at index i
int inputValue = tx.inputs[i].value;
bytes inputScript = tx.inputs[i].lockingBytecode;Example:
entrypoint function spend() {
int currentValue = tx.inputs[this.activeInputIndex].value;
require(currentValue >= 1000);
}Access properties of transaction outputs:
// Access output at index i
int outputValue = tx.outputs[i].value;
bytes lockingScript = tx.outputs[i].lockingBytecode;Example:
entrypoint function transfer() {
// Ensure first output has at least 10000 litras
require(tx.outputs[0].value >= 10000);
}Covenants are contracts that enforce conditions on how funds can be spent. They use transaction introspection to validate outputs.
new LockingBytecodeP2PK(pubkey pk): bytes34
Create a Pay-to-Public-Key locking script:
bytes34 lockScript = new LockingBytecodeP2PK(recipientPubkey);
require(tx.outputs[0].lockingBytecode == lockScript);new LockingBytecodeP2SH(bytes32 scriptHash): bytes35
Create a Pay-to-Script-Hash locking script:
bytes32 redeemScriptHash = blake2b(redeemScript);
bytes35 lockScript = new LockingBytecodeP2SH(redeemScriptHash);
require(tx.outputs[0].lockingBytecode == lockScript);new LockingBytecodeP2SHFromRedeemScript(bytes redeemScript): bytes35
Create P2SH locking script directly from redeem script:
bytes35 lockScript = new LockingBytecodeP2SHFromRedeemScript(redeemScript);Simple Covenant (Send to Specific Address):
pragma silverscript ^0.1.0;
contract SimpleCovenant(pubkey recipient) {
entrypoint function spend() {
// First output must go to the recipient
bytes34 recipientLock = new LockingBytecodeP2PK(recipient);
require(tx.outputs[0].lockingBytecode == recipientLock);
}
}Recurring Payment Covenant:
pragma silverscript ^0.1.0;
contract RecurringPayment(pubkey recipient, int paymentAmount, int period) {
entrypoint function withdraw() {
// Must wait for the period to elapse
require(this.age >= period);
// First output must pay the recipient
bytes34 recipientLock = new LockingBytecodeP2PK(recipient);
require(tx.outputs[0].lockingBytecode == recipientLock);
require(tx.outputs[0].value >= paymentAmount);
// Calculate change
int inputValue = tx.inputs[this.activeInputIndex].value;
int minerFee = 1000;
int changeValue = inputValue - paymentAmount - minerFee;
// If sufficient funds remain, send change back to contract
if (changeValue >= paymentAmount + minerFee) {
bytes changeBytecode = tx.inputs[this.activeInputIndex].lockingBytecode;
require(tx.outputs[1].lockingBytecode == changeBytecode);
require(tx.outputs[1].value == changeValue);
}
}
}Define contract-level constants:
contract MyContract() {
int constant MAX_VALUE = 1000;
int constant MIN_VALUE = 100;
string constant MESSAGE = "hello";
entrypoint function check(int x) {
require(x >= MIN_VALUE);
require(x <= MAX_VALUE);
}
}Constants can also be declared inside functions:
entrypoint function example() {
string constant greeting = "Hello";
require(sha256(greeting) != 0x);
}Unpack multiple values from function returns or split operations:
// Function with multiple returns
function getPair(): (int, int) {
return (10, 20);
}
// Unpack split results and function results
entrypoint function example(bytes32 data) {
bytes16 left, bytes16 right = data.split(16);
(int x, int y) = getPair();
}In Function Parameters:
entrypoint function example(bytes32 data) {
bytes16 x, bytes16 y = data.split(16);
require(x == y);
}Split:
Divide bytes into two parts at a given index:
bytes data = 0x1122334455667788;
// Split at byte 4
bytes left = data.split(4)[0]; // 0x11223344
bytes right = data.split(4)[1]; // 0x55667788
// Direct tuple unpacking with types
bytes4 a, bytes4 b = data.split(4);Slice:
Extract a substring of bytes:
bytes data = 0x1122334455667788;
// Get bytes from index 2 to 5 (exclusive)
bytes middle = data.slice(2, 5); // 0x334455
// Variable indices
int start = 1;
int end = 4;
bytes extracted = data.slice(start, end);pragma silverscript ^0.1.0;
contract P2PK(pubkey pk) {
entrypoint function spend(sig s) {
// Verify the signature
require(checkSig(s, pk));
}
}Constructor arguments:
pk: The recipient's public key
Spend arguments:
s: A signature from the private key corresponding topk
pragma silverscript ^0.1.0;
contract TransferWithTimeout(
pubkey sender,
pubkey recipient,
int timeout
) {
// Recipient can spend at any time
entrypoint function transfer(sig recipientSig) {
require(checkSig(recipientSig, recipient));
}
// Sender can reclaim after timeout
entrypoint function reclaim(sig senderSig) {
require(checkSig(senderSig, sender));
require(tx.time >= timeout);
}
}Constructor arguments:
sender: Public key of the sender (who can reclaim)recipient: Public key of the recipient (who can spend)timeout: Unix timestamp after which sender can reclaim
Spend paths:
- Transfer: Recipient signs to claim funds
- Reclaim: Sender signs after timeout to reclaim funds
A contract that releases periodic payments to a beneficiary:
pragma silverscript ^0.1.0;
contract Mecenas(pubkey recipient, bytes32 funder, int pledge, int period) {
// Periodic payment to recipient
entrypoint function receive() {
// Must wait for the period to elapse
require(this.age >= period);
// Check that the first output sends to the recipient
bytes34 recipientLockingBytecode = new LockingBytecodeP2PK(recipient);
require(tx.outputs[0].lockingBytecode == recipientLockingBytecode);
// Calculate the value that's left
int minerFee = 1000;
int currentValue = tx.inputs[this.activeInputIndex].value;
int changeValue = currentValue - pledge - minerFee;
// If there is not enough left for another pledge after this one,
// send the remainder to the recipient. Otherwise send the
// pledge to the recipient and the change back to the contract
if (changeValue <= pledge + minerFee) {
require(tx.outputs[0].value == currentValue - minerFee);
} else {
require(tx.outputs[0].value == pledge);
bytes changeBytecode = tx.inputs[this.activeInputIndex].lockingBytecode;
require(tx.outputs[1].lockingBytecode == changeBytecode);
require(tx.outputs[1].value == changeValue);
}
}
// Funder can reclaim at any time
entrypoint function reclaim(pubkey pk, sig s) {
require(blake2b(pk) == funder);
require(checkSig(s, pk));
}
}Constructor arguments:
recipient: Public key of the beneficiaryfunder: Hash of the funder's public key (for reclaim)pledge: Amount to pay per periodperiod: Time in seconds between payments
Spend paths:
- Receive: Anyone can trigger a payment after the period elapses
- Reclaim: Funder can reclaim all funds at any time
- Always use pragma directives to specify the language version
- Use descriptive variable and function names for better readability
- Add comments to explain complex logic
- Validate all inputs with
requirestatements - Be mindful of miner fees when calculating output values in covenants
- Test extensively before deploying to mainnet
- Use constants for magic numbers and repeated values
- Keep contracts simple - complexity increases the risk of bugs