Welcome to Spedn’s documentation!¶
Spedn is a high level smart contracts language for Bitcoin Cash. It is designed for explicitness and safety:
- It is statically typed - detects many errors at compile time
- It is explicitly typed - no guessing what the expression is supposed to return
- It is purely-functional - free of side effects, the common source of bugs
- It has a familiar syntax resembling C# or Rust
Warning
Spedn is an experimental tool. It is not recommended to be used on mainnet yet. Or at least do a lot of tests on testnet first.
Quick start guide¶
Build from sources¶
Intsall Haskell Tool Stack.
Download Spedn sources.
$ git clone https://bitbucket.org/o-studio/spedn.git
Build and install Spedn.
$ cd spedn/spedn $ stack install
Installation from npm¶
Alternatively, you can install a JavaScript version from npmjs repository:
$ npm i -g @spedn/cli
Your first contract¶
Create a file mycontract.spedn with a following content:
contract ExpiringTip(Ripemd160 alice, Ripemd160 bob) {
challenge receive(Sig sig, PubKey pubKey) {
verify hash160(pubKey) == bob;
verify checkSig(sig, pubKey);
}
challenge revoke(Sig sig, PubKey pubKey) {
verify checkSequence(7d);
verify hash160(pubKey) == alice;
verify checkSig(sig, pubKey);
}
}
Compile with command:
$ spedn compile -c mycontract.spedn
You should get a compiled contract template similar to this:
<alice> <bob> 2 PICK TRUE EQUAL IF 3 PICK HASH160 OVER EQUALVERIFY (...)
Understanding Script¶
Before developing contracts with Spedn it is worth understanding what they are compiled to and how Bitcoin Cash transactions internally work.
There is no spoon…¶
From a user perspective it’s convenient to perceive a Bitcoin Cash address as a kind of account with a balance. But this is just a nice abstraction over a mechanism that works in slightly more complicated way.
There is no account. Every transaction contains inputs and outputs. An output consists of an amount of bitcoins
and a script (often called scriptPubKey
) specifying some spending conditions for that amount.
An input is a reference to some output of a previous transaction and some script (called scriptSig
)
satisfying the spending condition from scriptPubKey
.
In a typical transaction, scriptPubKey
contains a public key of a coin owner
and scriptSig
contains a signature matching that public key - hence the names.
An output that is not yet referenced by any other transaction is called Unspent Transaction Output (UTXO).
A UTXO can be perceived as a lockbox containing a single coin.
An address is a user readable representation of a standard scriptPubKey. There can be many UTXOs with the same address.
Kinds of boxes¶
You can spot two kinds of addresses in Bitcoin Cash:
Pay To Public Key Hash (P2PKH)¶
This is an “ordinary” address representing a very simple script that checks two condidtions:
- If the public key provided in scriptSig matches the hash in scriptPubKey when hashed with SHA-256 and then RIPEMD-160.
- If the signature provided in scriptSig is valid for that key.
Pay To Script Hash (P2SH)¶
This is a “smart contract” address.
Instead of public key hash it cointains a hash of an entire script that is called a redeem script
.
The scriptSig is supposed to provide the actual script that matches this hash and arguments to it.
Making fancy boxes¶
All those scripts are bytecode that run in a stack machine.
A human readable representation (assembly language) of this bytecode is called… Script.
Script is a FORTH-like, stack oriented language containing numerous opcodes, some generic (like OP_ADD
),
some very Bitcoin-specific (like OP_CHECKSIG
).
It intentionally lacks support for recursion what guarantees that all scripts finish
(and even do so in deterministic time).
Writing scripts in Script is quite hard. This is why Spedn was created. It’s a high level language that compiles to Script. Contracts written in Spedn represent redeem scripts for P2SH addresses.
Migration Guide¶
Migrationg from v0.3 to v0.3.1¶
Javascript SDK in this release has been decoupled into several smaller components.
The Compiler stays in SDK library which moved to @spedn/sdk
. However, you won’t need it anymore
if you just want to use already compiled contracts. In that case, install RTS (Runtime System) library with backend of choice:
- use
@spedn/rts-bchjs
if you want to build the app on top of BCH-JS. - use
@spedn/rts-bitbox
if you want to build the app on top of BITBOX-SDK.
To use RTS, import apropriate adapter:
import { BchjsRts } from "@spedn/rts-bchjs";
import BCHJS from "@chris.troutner/bch-js";
const rts = new BchjsRts("mainnet")
const testRts = new BchjsRts("testnet", new BCHJS({ restURL: "https://tapi.fullstack.cash/v3/" }));
To allow RTS to work without compiler, the Portable format has been introduced.
When using the CLI, you can request the compiler to output it with -f portable
switch.
When using the Spedn service from SDK, this format will be returned by default.
To turn this JSON into contract classes as before, use rts.load(portable)
.
const mod = await using(new Spedn(), async compiler => await compiler.compileFile("./BlindEscrow.spedn"));
fs.writeFileSync("./blind_escrow.json", JSON.stringify(mod));
// ...
const { BlindEscrow } = rts.load(JSON.parse(fs.readFileSync("./blind_escrow.json");
You’ll also need RTS to create TxBuilder:
import { TxBuilder } from "@spedn/rts";
const builder = new TxBuilder(rts);
Static factory methods in P2PKH class can now be found in P2PKHFactory, that requires RTS:
import { P2PKHFactory } from "@spedn/rts";
const factory = new P2PKHFactory(rts);
const addr = factory.fromKeyPair(key);
Migrating from v0.1 to v0.2¶
There are several syntax changes that might cause your contract compiled for v0.1 version of Spedn doesn’t compile anymore. Here’s how to fix it:
The
bin
type has been replced by[byte]
. Just replace all occurences. This will be good enough but consider being more strict by providing the exact size of the byte array, like[byte;5]
.Tuple destructuring has a new syntax. Instead of
bin [a, b]
, use([byte] a, [byte] b)
. As before, consider being more strict, ex.([byte;4] a, [byte;28] b)
.With the November 2019 BCH protcol upgrade,
OP_CHECKMULTISIG
started to support Schnorr signatures but using this requires providing a checkbits argument instead of null dummy. Spedn 0.2 supports this mode exclusively so you’ll have to add a checkbits argument.Code:
challenge(Sig a, Sig b) { verify checkMultiSig([a, b], [k1, k2, k3]); }
becomes:
challenge([bit;3] checkbits, Sig a, Sig b) { verify checkMultiSig(checkbits, [a, b], [k1, k2, k3]); }
A single code file can now contain multiple contracts therefore the compiler in Spedn TypeScript SDK returns a new data structure called module instead of a single contract template. Instead of
const MyContract = await compiler.compileFile("./MyContract.spedn");
useconst { MyContract } = await compiler.compileFile("./MyContract.spedn");
.
Syntax overview¶
Module¶
A single code file is called a module. It can contain any number of type definitions and contract templates.
Typed should be defined first, followed by contract templates.
Single and multiline comments are supported with //
and /* */
delimiters respectively.
Contract Templates¶
A contract template in Spedn represents a template for generating a P2SH address and corresponding redeem script. It can be parametrized. Contract parameters have to be specified to instantiate it, that is - to generate a particular contract with an address. Contract name should start with a capital letter.
You can perceive a contract template as a specification of a pin tumbler lock mechanism while a contract is a particular lock and parameters are pin lengths in it.
Syntax:
contract ContractName ( [type paramName [, …]] ) { }
Example:
contract SomeContract(Ripemd160 pubKeyHash, int x) {
// challenges
}
Challenges¶
A challenge is a set of conditions that have to be met to redeem a coin locked in a contract.
Challenges specify arguments that will be expected to be pushed in scriptSig
when redeeming the coin.
A contract must contain at least one challenge and a challenge must define at least one argument.
Challenges must have unique names.
A challenge introduces a lexical scope so two different challenges can define an argument with the same name.
When redeeming a coin, a redeemer must choose one of the challenges and satisfy its conditions. On the assemby level its done by pushing challenge’s number (indexed from 1) after arguments. If there is only one challenge, only arguments are pushed.
You can perceive a challenge as a keyhole in a lock and arguments as keys.
Syntax:
challenge name ( type argName [, …] **) statement **
Example:
challenge someChallenge(PubKey pubKey, Sig signature) {
// statements...
}
Statements¶
A challenge can contain any number of statements. To be precise - it contains a single statement but this can be a block statement which can contain any number of statements.
There are the following kinds of statements:
Verification¶
The most important statement and often the only one needed. It evaluates an expression and fails the script if the result is false.
Syntax:
verify expr ;
Example:
verify hash1 == hash2;
Variable binding¶
You can define a local variable that will be accessible down in the same lexical scope and nested scopes.
Syntax:
type name = expr ;
Example:
int a = b + c;
There is also a possibility to deconstruct a tuple into many variables (like in case of using the split operator). If some of the results is unnecessary, you can ignore them with a low dash operator.
Syntax:
( type1 name1 , type2 name2 ) = expr1 @ expr2 ;
(_, type2 name2 ) = expr1 @ expr2 ;
( type name , _) = expr1 @ expr2 ;
Example:
([byte;4] prefix, _) = secret @ 4;
Conditional¶
You can conditionaly execute a branch of code. A branch introduces a new lexical scope and it can be a verification, block or another conditional.
Syntax:
if ( condition ) statement [ else statement ]
Example:
if (num % 2 == 1)
verify checkSig(sig, alice);
else
verify checkSig(sig, bob);
Fail¶
To immediately fail the execution just type fail;
- it will compile to OP_RETURN
.
if (num % 2 = 1)
verify checkSig(sig, alice);
else
fail;
Separator¶
The separator;
statement compiles to OP_CODESEPARATOR
.
It affects the way the tx preimage used in checkSig
is generated so that only the code after the separator
is included. Might by useful for reducing the size of a preimage used in covenant-style contracts.
Block¶
A block is a statement that groups several statements for sequential execution. A block introduces a lexical scope. The last statement must be a verification or conditional.
Syntax:
{ [ statements… ] }
Example:
if (num % 2 = 1) {
verify checkSig(sig, alice);
} else {
verify checkSig(sig, bob);
verify checkSequence(5d);
}
Loop¶
There are no loops, it’s Bitcoin.
Type Definitions¶
You can defile a type alias. The name of the new type must start with a capital letter.
Syntax:
type Name = other type ;
Example:
type Message = [byte;7];
Once defined you can declare variables of the new type and use a type constructor for casting a raw type to an alias.
Message msg = Message("abcdefg");
Lexical scopes¶
Spedn creates common, nested lexical scopes for parameters, arguments, variables and functions. There can be no 2 identical names within the same scope. Also - name shadowing is prohibited so a nested scope cannot redefine a name present in its parent scope.
There are following scopes in the nesting order:
- Module scope - contains predefined functions and type definitions
- Contract scope - introduced by the contract, contains contract parameters
- Challenge scope - introduced by the challenge, contains challenge arguments and local variables
- Local scope - introduced by if/else/block statements, contains local variables
Exhaustive example:
// a global scope, names like checkSig, min, max are reserved.
type Msg = [byte;15];
// contract scope begins
contract X(int a, int b) { // names a, b are defined
// challenge scope begins
challenge a( // it's OK for the challenge to be named a because challenge names don't occupy the name table.
int c // name c is defined
/* int a // BAD - already defined in contract scope */)
{
verify a >= b;
/* verify a == d // BAD - d is not yet defined */
int d = a + b; // name d is defined
if (d > 0)
// if scope begins
{
int e = d % c;
verify e == 0;
}
// if scope ends; e is gone.
else
// else scope begins
verify a == b;
// else scope ends
/* verify e == 1 // BAD - e is gone */
}
// challenge scope ends; c, d are gone
// challenge scope begins
challenge b(int c, int d) // names c, d are defined
{
verify c == d;
}
// challenge scope ends; c, d are gone
}
// contract scope ends; a, b are gone
Types¶
Basic Types¶
Basic types reflect types Script operates on.
bool - a boolean value. Can be either
true
orfalse
.verify
andif
statements expect an expression returning this type.bit - a binary flag. Only arrays of this type can be created, see below. Array of bits is expected by
checkMultiSig
function.int - a 32-bit signed integer. Literals of this type can be specified in dec or hex.
int a = -1234; int b = 0xff00i; // notice `i` suffix
byte - a single byte. Literals of this type are specified in hex (with
0x
prefix) or as UTF-8 strings (in doubel quotes).byte x = 0x11; byte y = "a";
Arrays¶
Arrays are series of values with the same type.
Overall syntax of an array type is:
[ element_type ; size ]
In case of bit
and byte
the array will mean a single byte vector on the stack in Bitcoin Virtual Machine terms.
In case of other types the array will mean a number of stack elements with element 0 on the bottom.
You can create bit array literals with 0b
prefix, byte arrays with 0x
prefix or double quotes
and any other arrays with comma-separated lists in brackets.
[bit;4] checkbits = 0b1011; [byte;3] a = 0x11ab33; [byte; 3] b = "abc"; [int;5] c = [1, 2, 3, 4, 5]; [[byte;2];3] d = ["ab", "cd", "ef"];
It is also possible to define a byte list if the array size is unknown at the compile time.
[byte] str = expr;
A byte array can be implicitly casted to a byte list but not the other way. It is recommended though to use explicit sizes as much as possible to leverage the static type checking of the size.
([byte] left, [byte] right) = expr @ 3; // okay ([byte;3] left, [byte] right) = expr @ 3; // better ([byte;2] left, [byte] right) = expr @ 3; // type error
With an exception of bit arrays the array elements can be accessed by an index starting from 0.
[byte] arr = "abcd" byte c = arr[2]; byte x = arr[(i + 1) % 4];
Tuples¶
Tuples are similar to arrays but the values can vary in types.
Overall syntax of a tuple type is:
( type1 , type2 [, …])
You can create tuple literals with a comma separated list of values in parentheses. Tuples can be deconstructed into separate variables with a deconstructing assgnment syntax:
( type1 name1 , type2 name2 [, …]) = expr ;
Elements of 2-tuples can be extracted with fst
and snd
functions.
Examples:
([byte;4], [byte]) tuple1 = x @ 4; ([byte;4] left, [byte] right) = tuple1; [byte;4] first = fst(tuple1); [byte] second = snd(tuple1); (int, [byte], [bit;3]) tuple2 = (a, b, c);
Domain-Spcecific Types¶
To increase safety, Spedn introduces meaningful types that help with catching semantic errors at compile time.
Numeric types¶
These types add meaning to a raw int
.
They must be explicitly casted from int
with a type constructor.
They cannot be casted back to int
.
- Time - represents an absolute time.
Can be expressed as a Unix Timestamp or a Block Height and variously defined.
Time x = `2018-10-13 21:37:00`; // defined with a time literal Time y = TimeStamp(1539466620); // conversion from `int` interpreted as Unix Timestamp Time z = TimeStamp(584834); // conversion from `int` interpreted as Block Height
TimeSpan - represents a relative time period. Can be expressed as a number of blocks or 512-seconds periods.
TimeSpan x = 1d 2h 3m 4s; // Time units literal. Be awre that the number will be rounded down to full 512s periods TimeSpan y = 10b; // Blocks literal. TimeSpan z = Blocks(10); // Conversion from `int`
Binary types¶
These types add meaning to a raw byte arrays.
They can be implicitly casted to [byte]
.
They must be explicitly casted from [byte]
with a type constructor.
PubKey - represents a public key.
PubKey alice = PubKey(0x11223344556677889900aabbccddeeff);
Sig - represents a tx signature (which can be checked with
checkSig
).Sig alice = Sig(0x11223344556677889900aabbccddeeff); verify checkSig(alice, alicePubKey);
DataSig - represents a data signature (which can be checked with
checkDataSig
).DataSig alice = DataSig(0x11223344556677889900aabbccddee); verify checkDataSig(alice, preimageHash, alicePubKey);
Ripemd160 - represents a result of RIPEMD-160 hash.
Ripemd160 h = hash160(pubKey);
Sha1 - represents a result of SHA-1 hash.
Sha1 x = sha1(secret);
Sha256 - represents a result of SHA-256 hash.
Sha256 x = hash256(secret);
Preimage - represents a raw transaction preimage. You can break it down to components with
parse
function.Preimage preimage = Preimage(0xaabc45......); TxState tx = parse(preimage);
TxState - a 10-tuple containing preimage components. They can be accessed by tuple deconstruction.
- NVersion - nVersion of the transaction (4-byte little endian)
- Sha256 - hashPrevouts (32-byte hash)
- Sha256 - hashSequence (32-byte hash)
- Outpoint - outpoint (32-byte hash + 4-byte little endian)
- ScriptCode - scriptCode of the input (serialized as scripts inside CTxOuts)
- Value - value of the output spent by this input (8-byte little endian)
- NSequence - nSequence of the input (4-byte little endian)
- Sha256 - hashOutputs (32-byte hash)
- NLocktime - nLocktime of the transaction (4-byte little endian)
- Sighash - sighash type of the signature (4-byte little endian)
(NVersion v, Sha256 hp, Sha256 hs, Outpoint o, ScriptCode code, Value val, NSequence ns, Sha256 ho, NLocktime l, Sighash sh) = parse(preimage)
Alternatively you can use functions extrancing a single component.
NVersion v = nVesrion(preimage); Sha256 hp = hashPrevouts(preimage); Sha256 hs = hashSequence(preimage); Outpoint o = outpoint(preimage); ScriptCode code = scriptCode(preimage); Value val = value(preimage); NSequence s = nSequence(preimage); Sha256 ho = hashOutputs(preimage); NLocktime l = nLocktime(preimage); Sighash sh = sighash(preimage);
Custom types¶
You can defile a type alias. The name of the new type must start with a capital letter. The types have to be defined before contracts in the code file. Syntax is:
type Name = other type ;
Once defined you can declare variables of the new type and use a type constructor for casting a raw type to an alias. It behaves the same way as constructors of of domain-specific types described in the previous sections. Actually, all of these are defined as type aliases internally.
type Message = [byte;42]; ... Message msg = Message(str);
Operators¶
Precedence | Operator | Description | Associativity |
---|---|---|---|
1 | -a |
Unary minus | right to left |
1 | !a |
Logical NOT | right to left |
2 | a / b |
Integer division | left to right |
2 | a % b |
Modulo | left to right |
3 | a + b |
Integer addition | left to right |
3 | a - b |
Integer subtraction | left to right |
4 | a . b |
bytes arrays concatenation | left to right |
5 | a < b |
Less than | left to right |
5 | a <= b |
Less than or equal | left to right |
5 | a > b |
Greater than | left to right |
5 | a >= b |
Greater than or equal | left to right |
6 | a == b |
Equal | left to right |
6 | a != b |
Not equal | left to right |
6 | a === b |
Numeric and equal | left to right |
6 | a !== b |
Numeric and not equal | left to right |
7 | a & b |
Bitwise AND | left to right |
8 | a ^ b |
Bitwise XOR | left to right |
9 | a | b |
Bitwise OR | left to right |
10 | a && b |
Bolean AND Note: Both a and b are always evaluated. |
left to right |
11 | a || b |
Boolean OR Note: Both a and b are always evaluated. |
left to right |
12 | a @ b |
Split bytes array a at position b . |
none |
13 | cond ? t : f |
If cond is true returns t , otherwise f . |
right to left |
Functions¶
Math Functions¶
int abs(int a)
Returns an absolute value of the argument.
int min(int a, int b)
Returns the smaller argument.
int max(int a, int b)
Returns the larger argument.
bool within(int x, int min, int max)
Returns
true
ifx >= min && x < max
.
Hashing Functions¶
Ripemd160 ripemd160([byte] bytes)
Returns a RIPEMD-160 hash of the argument.
Sha1 sha1([byte] bytes)
Returns a SHA-1 hash of the argument.
Sha256 sha256([byte] bytes)
Returns a SHA-256 hash of the argument.
Ripemd160 hash160([byte] bytes)
Returns RIPEMD-160 hash of SHA-256 hash of the argument.
Sha256 hash256([byte] bytes)
Returns double SHA-256 hash of the argument.
Cryptographic Checks¶
bool checkSig(Sig sig, PubKey pk)
Validates a transaction signature
sig
againnst a public keypk
.bool checkMultiSig(List<Sig> sigs, List<PubKey> pks)
Validates the set of signatures against the set of public keys.
bool checkDataSig(DataSig sig, [byte] msg, PubKey pk)
Validates a signature
sig
of an arbitrary messagemsg
against a public keypk
.
Timelock Checks¶
Verification checkLockTime(Time t)
Validates whether the spending transaction occurs after time
t
, expressed as a block height or a timestamp.Verification checkSequence(TimeSpan duration)
Validates whether the spending transaction happens after
duration
relative to the locking transaction, expressed as a number of blocks or number of 512 seconds-long periods.
Array Operations¶
[byte] num2bin(int num, int size)
Converts a number
num
into a bytes list. Bytes remain little-endian and are padded with zeros up tosize
.[byte] Bytes(int num)
Reinterprets a number
num
as a bytes list without affecting the byte order or size.int bin2num([byte] data)
Converts a bytes list
data
to an integer. The list is treated as little-endian. The result is minimally encoded.[byte] reverseBytes([byte] data)
[byte;n] reverseBytes([byte;n] data)
Returns an array with bytes in reverse order.
[byte;3] rev = reverseBytes(0xabcdef); // rev == 0xefcdab
int size([byte] data)
Returns the length of
data
.int checkSize([byte; x] data)
Returns true it the runtime size of the byte array matches the declared size
x
.bin fst([bin, bin] data)
Returns the first element of a tuple (result of
@
operator).[byte] left = fst(0xaabbccdd @ 2); // left == 0xaabb
bin snd([bin, bin] data)
Returns the second element of a tuple (result of
@
operator).[byte] right = snd(0xaabbccdd @ 2); // right == 0xccdd
DataSig toDataSig(Sig data)
Converts a signature suitable for
checkSig
function (with a sighash flag) to a signature suitable forcheckDataSig
function (without a sighash flag).verify checkSig(sig, pubKey); verify checkDataSig(toDataSig(sig), preimageHash, pubKey);
Covenant Introspection¶
TxState parse(Preimage p)
Returns a 10-tuple of preimage components.
NVersion nVersion(Preimage p)
Returns nVersion of the transaction (4-byte little endian).
Sha256 hashPrevouts(Preimage p)
Returns hashPrevouts.
Sha256 hashSequence(Preimage p)
Returns hashSequence.
Outpoint outpoint(Preimage p)
Returns outpoint (32-byte hash + 4-byte little endian).
ScriptCode scriptCode(Preimage p)
Returns scriptCode of the input (serialized as scripts inside CTxOuts).
Value value(Preimage p)
Returns value of the output spent by this input (8-byte little endian).
NSequence nSequence(Preimage p)
Returns nSequence of the input (4-byte little endian).
Sha256 hashOutputs(Preimage p)
Returns hashOutputs.
NLocktime nLocktime(Preimage p)
Returns nLocktime of the transaction.
Sighash sighash(Preimage p)
Returns sighash type of the signature (4-byte little endian).
Type Constructors¶
PubKey PubKey(bin data)
Ripemd160 Ripemd160(bin data)
Sha1 Sha1(bin data)
Sha256 Sha256(bin data)
Sig Sig(bin data)
DataSig DataSig(bin data)
Time TimeStamp(int timestamp)
Time TimeStamp(int blockHeight)
TimeSpan Blocks(int number)
Command-line Interface¶
The general syntax is:
$ spedn COMMAND args
Compiling¶
To compile a contract to opcodes, use:
$ spedn compile -c MyContract.spedn
If the contract contains parameters, a template with placeholders will be generated.
To instantiate the contract with particular parameter values, provide them as key=value
pairs after --
.
For example, assuming MyContract has alicePHK
parameter of type Ripemd160
and delay
parameter of type TimeSpan
, you can use the following:
$ spedn compile -c MyContract.spedn -- alicePKH=0xb08f0f859f53873e8f02f6c0a8290a53e76a2e0a delay=1d1h
To compile a contract to a hex representation, use:
$ spedn compile -f hex -c MyContract.spedn -- alicePKH=0xb08f0f859f53873e8f02f6c0a8290a53e76a2e0a delay=1d1h
Note that in this case, the contract must be fully instantiated (all parameters values must be provided).
Portable¶
Spedn defines custom Script representation called Portable. It’s a JSON-based format containing both opcodes and necessary reflection data allowing it to be instantiated in runtime libraries.
To compile a contract to Spedn Portable format, use:
$ spedn compile -f portable -c MyContract.spedn > MyCompiledContract.json
BCH-JS Integration¶
Spedn is available for NodeJS developers as set of libraries extending capabilities of BCH-JS or other libraries. TypeScript type definitions are provided out of the box.
Spedn provides 3 components installed separately:
- CLI (Command Line Interface) - allows to compile Spedn contracts.
- RTS (Runtime System) - allows to use compiled contracts in your app.
- SDK (Software Development Kit) - allows to compile Spedn contracts in your app.
Installation¶
To install Spedn RTS with BITBOX integration in your JS project, type:
npm i @spedn/rts-bchjs
# or
yarn add @spedn/rts-bchjs
To install Spedn SDK in your JS project, type:
npm i @spedn/sdk
# or
yarn add @spedn/sdk
NodeJS v11 or newer is required for SDK. You can also use v10 but then Worker Threads feature
has to be explicitly enabled by --experimental-worker
flag.
Compiler service¶
Spedn compiler runs as a service in a worker thread that you can start, use and dispose with Spedn
class.
import { Spedn } from "@spedn/sdk";
async function main() {
const compiler = new Spedn();
/* use compiler */
compiler.dispose();
}
main();
Instead of manually disposing the service you can also wrap the code in the using
function inspired by some languages,
which guarantees automatic disposal of a resource also in case of exceptions. It can also return a value
import { Spedn, using } from "@spedn/sdk";
async main() {
await using(new Spedn(), async compiler => {
/* use compiler */
});
}
main();
Runtime¶
Spedn RTS provides a way to interact with compiled smart contracts and with the blockchain via an adapter of an external library, in this case - BCH-JS.
You can use the default BCHJS instance for mainnet or provide a customized one.
import { BchjsRts } from "@spedn/rts-bchjs";
import BCHJS from "@chris.troutner/bch-js";
const rts = new BchjsRts("mainnet");
const testRts = new BchjsRts("testnet", new BCHJS({ restURL: "https://tapi.fullstack.cash/v3/" }));
Compiling modules¶
To compile a source file use compileFile
method, optionally providing an rts.
To compile source code in a string, use compileCode
.
const BlindEscrowPortableModule = await compiler.compileFile("./BlindEscrow.spedn");
const { ExpiringTip } = await compiler.compileCode(::`
contract ExpiringTip(Ripemd160 alice, Ripemd160 bob) {
challenge receive(Sig sig, PubKey pubKey) {
verify hash160(pubKey) == bob;
verify checkSig(sig, pubKey);
}
challenge revoke(Sig sig, PubKey pubKey) {
verify checkSequence(7d);
verify hash160(pubKey) == alice;
verify checkSig(sig, pubKey);
}
}
`, rts);
The output of those methods depends on whether you provided RTS or not.
If so, you’ll get an activated module containing smart contract classes ready to instantiate.
Static field params
in the class describes what parameters are required to instantiate it.
console.log(ExpiringTip.params);
// Object {alice: "Ripemd160", bob: "Ripemd160"}
If RTS is not provided, the method returns a portable module that can be saved to a JSON file or activated later.
import { readFileSync, writeFileSync } from "fs";
writeFileSync("blind_escrow.json", JSON.stringify(BlindEscrowPortableModule));
const mod = rts.load(BlindEscrowPortableModule);
Instantiating contracts¶
To instantiate the template, just create an object of the contract class, providing parameters values.
Parameters are passed as an object literal explicitly assigning values by names. Values of bool
and int
Spedn type can be passed as ordinary JS booleans and numbers. Time
and TimeSpan
are also passed as numbers
(see BIP65 and BIP112 for value interpretation details).
All the other types should be passed as JS Buffer
.
In case of ExpiringTip
you’ll need 2 public keys which you can generate with BCH-JS.
import BCHJS from "@chris.troutner/bch-js";
const bchjs = new BCHJS({ restURL: "https://tapi.fullstack.cash/v3/" });
const mnemonic = "draw parade crater busy book swim soldier tragic exit feel top civil";
const wallet = bchjs.HDNode.fromSeed(await bchjs.Mnemonic.toSeed(mnemonic), "testnet");
const alice = bchjs.HDNode.derivePath(wallet, "m/44'/145'/0'/0/0");
const bob = bchjs.HDNode.derivePath(wallet, "m/44'/145'/1'/0/0");
const tip = new ExpiringTip({
alice: alice.toIdentifier(), // Ripemd160 hash of Alice's public key
bob: bob.toIdentifier() // Ripemd160 hash of Bob's public key
});
Once created, you can read the contract funding address and lookup for UTXOs (coins) that are locked in it.
Also, a field challengeSpecs
contains definitions of challenges and their parameters.
console.log(tip.getAddress("mainnet"));
// bitcoincash:pppvx30pcylxzhewr6puknpuvz7gjjtl4sdw4ezcnp
const coins = await tip.findCoins("mainnet");
// Array(2) [.....]
console.log(tip.challengeSpecs);
// Object {receive: Object, revoke: Object}
console.log(tip.challengeSpecs.receive);
// Object {sig: "Sig", pubKey: "PubKey"}
Spending coins¶
To spend coins, use TxBuilder
. Provide tx inputs with from
method and outputs with to
method.
Optionally, set a timelock with withTimelock
.
To send the transaction to the network use broadcast
method.
If you just want to build the transaction without broadcasting it, use build
method.
from
method accept a single coin or an array of coins as a first parameter.
Because you can’t (in most cases) sign the input without defining all the inputs and outputs first,
from
method does not simply accept scriptSig parameter. Instead, it accepts a SigningCallback
function
and the actual signing is deferred to the moment of calling build
/broadcast
.
SigningCallback
accepts 2 parameters. The first one is an object containing contract challenges.
The second one is a SigningContext
which provides methods necessary for signing:
sign(keyPair, hashType)
- generates a siggnature valid forOP_CHECKSIG
.signData(keyPair, data)
- generates a signature valid forOP_CHECKDATASIG
.preimage(hashType)
- generates the same preimage as one used bysign(keyPair, hashType)
(useful forOP_CHECKDATASIG
covenants).
Note that methods accepting hashType
always add SIGHASH_FORKID
flag so you don’t need to specify it
explicitly.
to
method accepts an address or a scriptPubKey buffer as its first argument and an amount (in satoshis)
as the second one. You can also omit the amount at a single output - in this case, TxBuilder
will
treat this output as a change address and automatically calculate its amount choosing optimal transaction fee.
In the following example, all the previously found coins are spent using receive
challenge but 5mBCH goes to
Bob’s new address and the rest goes back to Alice.
import { SigHash } from "@spedn/rts";
const txid = await new TxBuilder(rts)
.from(coins, (input, context) =>
input.receive({
sig: context.sign(bob.keyPair, SigHash.SIGHASH_ALL),
pubKey: bob.toPublicKey()
})
)
.to("bitcoincash:qrc2jhalczuka8q3dvk0g8mnkqx79wxp9gvvqvg7qt", 500000)
.to(alice.getAddress())
.withTimelock(567654)
.broadcast();
Spending ordinary P2PKH¶
Spedn SDK provides also a class P2PKH
which is a representation of an ordinary Pay to Public Key Hash address.
You can instantiate it with a public key hash buffer or several factory methods:
import { P2PKH, P2PKHFactory } from "@spedn/rts";
const factory = new P2PKHFactory(rts);
let addr = new P2PKH(rts, bob.getIdentifier());
addr = factory.fromKeyPair(bob.keyPair);
addr = factory.fromPubKey(bob.toPublicKey());
addr = factory.fromAddress(bob.toCashAddress());
// all the above are equivalent
P2PKH contracts can be spent just like any other contract - they have spend({sig, pubKey})
challenge,
but you can also replace the whole signing callback with a convenient helper signWith(keyPair)
.
Let’s modify the previous example to spend additional input.
import { signWith } from "@spedn/rts";
const bobsCoins = await addr.findCoins("mainnet");
const txid = await new TxBuilder("mainnet")
.from(coins, (input, context) =>
input.receive({
sig: context.sign(bob.keyPair, SigHash.SIGHASH_ALL),
pubKey: bob.tpPublicKey()
})
)
.from(bobsCoins[14], signWith(bob.keyPair))
.to("bitcoincash:qrc2jhalczuka8q3dvk0g8mnkqx79wxp9gvvqvg7qt", 500000)
.to(alice.toCashAddress())
.withTimelock(567654)
.broadcast();
Spending generic P2SH¶
Spedn SDK provides also a class GenericP2SH
for interoperability with any Pay to Script Hash contract
created without Spedn. To work with that kind of contract, you just need to know its redeemScript and
what arguments it expects. The generated class will have a single challenge spend
with parameter
requirements as specified in the constructor.
import { GenericP2SH } from "@spedn/rts";
const contract = new GenericP2SH(redeemScriptBuffer, { sig: "Sig", someNumber: "int" });
BITBOX Integration¶
Spedn is available for NodeJS developers as set of libraries extending capabilities of BITBOX SDK or other libraries. TypeScript type definitions are provided out of the box.
Spedn provides 3 components installed separately:
- CLI (Command Line Interface) - allows to compile Spedn contracts.
- RTS (Runtime System) - allows to use compiled contracts in your app.
- SDK (Software Development Kit) - allows to compile Spedn contracts in your app.
Installation¶
To install Spedn RTS with BITBOX integration in your JS project, type:
npm i @spedn/rts-bitbox
# or
yarn add @spedn/rts-bitbox
To install Spedn SDK in your JS project, type:
npm i @spedn/sdk
# or
yarn add @spedn/sdk
NodeJS v11 or newer is required for SDK. You can also use v10 but then Worker Threads feature
has to be explicitly enabled by --experimental-worker
flag.
Compiler service¶
Spedn compiler runs as a service in a worker thread that you can start, use and dispose with Spedn
class.
import { Spedn } from "@spedn/sdk";
async function main() {
const compiler = new Spedn();
/* use compiler */
compiler.dispose();
}
main();
Instead of manually disposing the service you can also wrap the code in the using
function inspired by some languages,
which guarantees automatic disposal of a resource also in case of exceptions. It can also return a value
import { Spedn, using } from "@spedn/sdk";
async main() {
await using(new Spedn(), async compiler => {
/* use compiler */
});
}
main();
Runtime¶
Spedn RTS provides a way to interact with compiled smart contracts and with the blockchain via an adapter of an external library, in this case - BITBOX-SDK.
You can use the default BITBOX instance for mainnet or provide a customized one.
import { BitboxRts } from "@spedn/rts-bitbox";
import { BITBOX } from "bitbox-sdk";
const rts = new BitboxRts("mainnet");
const testRts = new BitboxRts("testnet", new BITBOX({ restURL: "https://tapi.fullstack.cash/v3/" }));
Compiling modules¶
To compile a source file use compileFile
method, optionally providing an rts.
To compile source code in a string, use compileCode
.
const BlindEscrowPortableModule = await compiler.compileFile("./BlindEscrow.spedn");
const { ExpiringTip } = await compiler.compileCode(::`
contract ExpiringTip(Ripemd160 alice, Ripemd160 bob) {
challenge receive(Sig sig, PubKey pubKey) {
verify hash160(pubKey) == bob;
verify checkSig(sig, pubKey);
}
challenge revoke(Sig sig, PubKey pubKey) {
verify checkSequence(7d);
verify hash160(pubKey) == alice;
verify checkSig(sig, pubKey);
}
}
`, rts);
The output of those methods depends on whether you provided RTS or not.
If so, you’ll get an activated module containing smart contract classes ready to instantiate.
Static field params
in the class describes what parameters are required to instantiate it.
console.log(ExpiringTip.params);
// Object {alice: "Ripemd160", bob: "Ripemd160"}
If RTS is not provided, the method returns a portable module that can be saved to a JSON file or activated later.
import { readFileSync, writeFileSync } from "fs";
writeFileSync("blind_escrow.json", JSON.stringify(BlindEscrowPortableModule));
const mod = rts.load(BlindEscrowPortableModule);
Instantiating contracts¶
To instantiate the template, just create an object of the contract class, providing parameters values.
Parameters are passed as an object literal explicitly assigning values by names. Values of bool
and int
Spedn type can be passed as ordinary JS booleans and numbers. Time
and TimeSpan
are also passed as numbers
(see BIP65 and BIP112 for value interpretation details).
All the other types should be passed as JS Buffer
.
In case of ExpiringTip
you’ll need 2 public keys which you can generate with BITBOX.
import { BITBOX } from "bitbox-sdk";
const bitbox = new BITBOX();
const mnemonic = "draw parade crater busy book swim soldier tragic exit feel top civil";
const wallet = bitbox.HDNode.fromSeed(bitbox.Mnemonic.toSeed(mnemonic));
const alice = bitbox.HDNode.derivePath(wallet, "m/44'/145'/0'/0/0");
const bob = bitbox.HDNode.derivePath(wallet, "m/44'/145'/1'/0/0");
const tip = new ExpiringTip({
alice: alice.getIdentifier(), // Ripemd160 hash of Alice's public key
bob: bob.getIdentifier() // Ripemd160 hash of Bob's public key
});
Once created, you can read the contract funding address and lookup for UTXOs (coins) that are locked in it.
Also, a field challengeSpecs
contains definitions of challenges and their parameters.
console.log(tip.getAddress("mainnet"));
// bitcoincash:pppvx30pcylxzhewr6puknpuvz7gjjtl4sdw4ezcnp
const coins = await tip.findCoins("mainnet");
// Array(2) [.....]
console.log(tip.challengeSpecs);
// Object {receive: Object, revoke: Object}
console.log(tip.challengeSpecs.receive);
// Object {sig: "Sig", pubKey: "PubKey"}
Spending coins¶
To spend coins, use TxBuilder
. Provide tx inputs with from
method and outputs with to
method.
Optionally, set a timelock with withTimelock
.
To send the transaction to the network use broadcast
method.
If you just want to build the transaction without broadcasting it, use build
method.
from
method accept a single coin or an array of coins as a first parameter.
Because you can’t (in most cases) sign the input without defining all the inputs and outputs first,
from
method does not simply accept scriptSig parameter. Instead, it accepts a SigningCallback
function
and the actual signing is deferred to the moment of calling build
/broadcast
.
SigningCallback
accepts 2 parameters. The first one is an object containing contract challenges.
The second one is a SigningContext
which provides methods necessary for signing:
sign(keyPair, hashType)
- generates a siggnature valid forOP_CHECKSIG
.signData(keyPair, data)
- generates a signature valid forOP_CHECKDATASIG
.preimage(hashType)
- generates the same preimage as one used bysign(keyPair, hashType)
(useful forOP_CHECKDATASIG
covenants).
Note that methods accepting hashType
always add SIGHASH_FORKID
flag so you don’t need to specify it
explicitly.
to
method accepts an address or a scriptPubKey buffer as its first argument and an amount (in satoshis)
as the second one. You can also omit the amount at a single output - in this case, TxBuilder
will
treat this output as a change address and automatically calculate its amount choosing optimal transaction fee.
In the following example, all the previously found coins are spent using receive
challenge but 5mBCH goes to
Bob’s new address and the rest goes back to Alice.
import { SigHash } from "@spedn/rts";
const txid = await new TxBuilder(rts)
.from(coins, (input, context) =>
input.receive({
sig: context.sign(bob.keyPair, SigHash.SIGHASH_ALL),
pubKey: bob.getPublicKeyBuffer()
})
)
.to("bitcoincash:qrc2jhalczuka8q3dvk0g8mnkqx79wxp9gvvqvg7qt", 500000)
.to(alice.getAddress())
.withTimelock(567654)
.broadcast();
Spending ordinary P2PKH¶
Spedn SDK provides also a class P2PKH
which is a representation of an ordinary Pay to Public Key Hash address.
You can instantiate it with a public key hash buffer or several factory methods:
import { P2PKH, P2PKHFactory } from "@spedn/rts";
const factory = new P2PKHFactory(rts);
let addr = new P2PKH(rts, bob.getIdentifier());
addr = factory.fromKeyPair(bob.keyPair);
addr = factory.fromPubKey(bob.toPublicKey());
addr = factory.fromAddress(bob.toCashAddress());
// all the above are equivalent
P2PKH contracts can be spent just like any other contract - they have spend({sig, pubKey})
challenge,
but you can also replace the whole signing callback with a convenient helper signWith(keyPair)
.
Let’s modify the previous example to spend additional input.
import { signWith } from "@spedn/rts";
const bobsCoins = await addr.findCoins("mainnet");
const txid = await new TxBuilder("mainnet")
.from(coins, (input, context) =>
input.receive({
sig: context.sign(bob.keyPair, SigHash.SIGHASH_ALL),
pubKey: bob.getPublicKeyBuffer()
})
)
.from(bobsCoins[14], signWith(bob.keyPair))
.to("bitcoincash:qrc2jhalczuka8q3dvk0g8mnkqx79wxp9gvvqvg7qt", 500000)
.to(alice.getAddress())
.withTimelock(567654)
.broadcast();
Spending generic P2SH¶
Spedn SDK provides also a class GenericP2SH
for interoperability with any Pay to Script Hash contract
created without Spedn. To work with that kind of contract, you just need to know its redeemScript and
what arguments it expects. The generated class will have a single challenge spend
with parameter
requirements as specified in the constructor.
import { GenericP2SH } from "@spedn/rts";
const contract = new GenericP2SH(redeemScriptBuffer, { sig: "Sig", someNumber: "int" });
Zero Conf Forfeits¶
This example is based on /u/awemany’s proposal for securing 0-conf transactions. In addition to a regular payment output and a change output we create also a forfeit output. The forfeit can be ordinarily spent by the customer which would be nonsensical if he also wanted to doublespend. If the doublespend is actually attempted then the miner can spend the forfeit by presenting a proof of that.
Read the details here or watch a presentation.
contract Forfeit(
Ripemd160 inputPKH, // a public key hash used to redeem the input in the payment tx
Ripemd160 customerPKH // a public key hash to be used to redeem the forfeit
) {
// This challenge is used by the customer to reclaim the forfeit.
// Basically, a typical P2PKH.
challenge ok(PubKey pubKey, Sig sig) {
verify hash160(pubKey) == customerPKH;
verify checkSig(sig, pubKey);
}
// This challenge can be used by a miner to claim the forfeit
// if he can prove there was a doublespend attempt.
challenge fraud(
DataSig paymentSig, // A signature used in payment transaction
[byte] paymentPayload, // Signed data from the transaction
DataSig doublespendSig, // Another signature taken from the doublespend attempt
[byte] doublespendPayload, // Signed data from the doublespend
PubKey pubKey // Public Key matching both signatures
) {
// If the provided PK matches the one from the payment input...
if (hash160(pubKey) == inputPKH) {
// verify the signature provided in that payment...
verify checkDataSig(paymentSig, paymentPayload, pubKey);
// and that there was seen some other transaction which also validly signed that input...
verify checkDataSig(doublespendSig, doublespendPayload, pubKey);
} else {
// otherwise don't allow to spend it
fail;
}
}
}
ChainBet Protocol¶
ChainBet is a proposed Bitcoin Cash protocol to enable on-chain betting. You can read the details here.
The flow of the bet consists of several steps that can be expressed in Spedn.
Escrow Preparation¶
Alice Escrow Address¶
The main purpose of Alice’s escrow address is to reveal Alice’s Secret A when spent. It will require both Alice and Bob’s signature plus the secret. By requiring the secret, it reveals it to Bob, thus fulfilling that part of the commitment scheme.
Alternatively, Alice can retrieve the funds unilaterally after 8 confirmations in the situation when Bob abandonds the betting process.
contract ChainBetAliceEscrow(PubKey alicePK, PubKey bobPK, Ripemd160 commitment) {
challenge cancel(Sig aliceSig) {
verify checkSequence(8b);
verify checkSig(aliceSig, alicePK);
}
challenge proceed(Sig aliceSig, Sig bobSig, bin secret) {
verify hash160(secret) == commitment;
verify checkMultiSig(0b11, [aliceSig, bobSig], [alicePK, bobPK]);
}
}
Bob Escrow Address¶
The main purpose of Bob’s escrow address is to prevent Bob from double spending. Once the funding transaction is created, Alice’s secret will be revealed. If Bob sees that he has a loss, he could theoretically attempt to double spend his input to the funding transaction, thereby invalidating it.
By first moving the funds into escrow and requiring Alice’s signature in addition to Bob’s to spend, Bob cannot on his own attempt a doublespend.
Of course, it is necessary for the transaction that funds the escrow account to have at least 1 confirmation before the funding transaction is attempted, because otherwise Bob could doublespend that, invalidating both itself and the child transaction (the funding transaction).
Alternatively, Bob can also retrieve his own funds unilaterally after 8 confirmations in the situation when Alice abandonds the betting process.
contract ChainBetBobEscrow(PubKey alicePK, PubKey bobPK) {
challenge cancel(Sig bobSig) {
verify checkSequence(8b);
verify checkSig(bobSig, bobPK);
}
challenge proceed(Sig aliceSig, Sig bobSig) {
verify checkMultiSig([aliceSig, bobSig], [alicePK, bobPK]);
}
}
Phase 5: Funding Transaction¶
Alice should now have both of Bob’s signatures, so she can spend from both escrow addresses to create the (main) funding transaction. lice should wait until both escrow transactions have at least one confirmation before broadcasting the funding transaction. Otherwise, she risks a double spend attack where Bob learns her secret, discovers he has lost the bet, and then tries to double spend the input to the Bob escrow account.
Using a shorthand notation where Alice’s Secret is “A” and the hash is “HASH_A”, and Bob’s Secret is “B” and its hash is “HASH_B”, then we can say that the main P2SH address is a script that allows the funds to be spent if:
Alice can sign for her public key AND Hash(A)= HASH_A AND Hash(B)=HASH_B AND A+B is an odd number.
…or if Bob can sign for his public key AND Hash(A)= HASH_A AND Hash(B)=HASH_B AND A+B is an even number.
…or if Alice can sign for her public key and the transaction is more than 4 blocks old.
contract Bet(
Ripemd160 aliceCommitment,
Ripemd160 bobCommitment,
PubKey alicePK,
PubKey bobPK) {
challenge odd([byte] aliceSecret, [byte] bobSecret, Sig aliceSig, bool cancel) {
if (!cancel) {
verify hash160(aliceSecret) == aliceCommitment;
verify hash160(bobSecret) == bobCommitment;
([byte;4] a, _) = aliceSecret @ 4;
([byte;4] b, _) = bobSecret @ 4;
verify (bin2num(a) + bin2num(b)) % 2 == 1;
}
else verify checkSequence(8b);
verify checkSig(aliceSig, alicePK);
}
challenge even([byte] aliceSecret, [byte] bobSecret, Sig bobSig) {
verify hash160(aliceSecret) == aliceCommitment;
verify hash160(bobSecret) == bobCommitment;
([byte;4] a, _) = aliceSecret @ 4;
([byte;4] b, _) = bobSecret @ 4;
verify (bin2num(a) + bin2num(b)) % 2 == 0;
verify checkSig(bobSig, bobPK);
}
}
Contributing¶
Every kind of contribution is appreciated, especially:
- Syntax ideas and other features suggestions
- Code review
- Unit tests
- Bug reports
- Usage examples and docs improvement