Skip to main content

Script Templates

In Nexa, coins are held in Script Templates which dictate the requirements for how those coins can be spent. Script Templates are a generalization of the Pay-To-Script-Hash (P2SH) concept on Bitcoin. Just like P2SH, Pay-To-Script-Template (P2ST) hashes the script code to present it in a concise way. A big advantage of using templates is that they can be standardized, these are called "Well-Known" Script Templates. The 1st well-known Script Template, with script number 1, is Pay-To-Publickey-Template (P2PKT). Because of this standardization the template doesn't need to be provided but can just be replaced by the script number 1.

P2PKT explained

The most basic transaction is a Pay-To-Publickey-Template transaction, where only a private key corresponding to the public key can create a valid signature for the transaction. In Bitcoin the default transaction type is variation on this: Pay-To-PublickeyHash (P2PKH) so the public key itself is not revealed. In Nexa this explicit hashing of the public key is not necessary for privacy & security reasons because it happens behind the 'constraint hash'! This brings us to the second important concept after templates: constraints!

Template & Constraint Script

The template of paying to a public key is standardized, but the constraint is unique to each public key used in a P2PKT transaction. This "Constraint Script" is the part that specifies data to be used in a template that is specific to the entity who creates the address. Most commonly this would be a public key, but it can be any data. Similar to the Template Script, the Constraint Script is also hashed when making a transaction, this means that only the hash of the public key push script is visible and not the public key itself!

Recreating P2PKT

For learning purposes, we'll recreate P2PKT by just using the Pay-To-Script-Template without the well-known standardization. A NexScript implementation for it would look the following:

ExampleContract.nex
pragma nexscript ^0.1.0;

contract P2PKT(pubkey recipient) {
// Allow the recipient to claim their received money
function transfer(sig recipientSig) {
require(checkSig(recipientSig, recipient));
}
}

First, the version that is used for the Nexscript contract is declared. Next the contract keyword is used followed by the chosen name for the Nexscript contract, here we chose P2PKT. Next up we have the Nexscript parameters for the constraints which are added as the constraint script, as explained here is just a simple script pushing the public key. Then, between braces, there is the Nexscript code for the template script. A Nexscript contract consists of one or multiple functions, each with its own name (called transfer here). Each function has round brackets where it can accept function arguments which need to be provided when spending the from the contract, in the case of our P2PKT contract this is the recipientSig. Then, between braces again, there's the actual requirements imposed by the function, here it is simply that the provided signature matches the public key provided in the constraints.

Advantages of Templates

We saw that one of the advantages of templates was that popular templates like P2PKT could be efficiently standardized with a unique number. Using templates has other use cases: if a smart contract mutates and has a different Constraint Script or Visible Parameters, then the template will still remain the same only the constraintHash or visibleParameters will change. This is considered a more advanced usage of script but can easily be used through Nexscript with the expression LockingBytecodeP2ST:

new LockingBytecodeP2ST(bytes20 templateHash, bytes constraintHash, bytes visibleParameters): bytes

The LockingBytecodeP2ST expression takes 3 arguments, the templateHash, the constraintHash, and encoded visible contract parameters visibleParameters when mutating the state of a self-replicating contract for example, the template will remain the same and the state update will be hashed into the Constraint Script or be reflected in visible parameters. Visible parameters should already be encoded as script of push-only operations, minimally encoded. E.g. two params: number "1" and data blob "0xbeef" will be encoded as "5102beef". See nexcore-lib's Script class to build bitcoin scripts.

An example of a local state contract is the "streaming Mecenas" contract in the covenant guide. The relevant part of the code is:

    // Extract the template hash from the lockingbytecode
bytes templateHash = hash160(this.activeBytecode);

// Create the new constraintScript
bytes newConstrainScript = 0x08 + bytes8(tx.locktime);
bytes20 constraintHash = hash160(newConstrainScript);

// Create the locking bytecode for the new contract and check that
// the change output sends to that contract
bytes newContractLock = new LockingBytecodeP2ST(templateHash, constraintHash, 0x);
require(tx.outputs[1].lockingBytecode == newContractLock);

This setup has the advantage that the templateHash always remains the same so it's easy to see the money is sent to a different instance of the same contract. constructing the newConstrainScript can be more complex if you keep multiple arguments in the local state or if the arguments have a variable length.

Some contracts on Nexa allow for empty constraint hash, e.g. in the case when a NexScript contract has no constructor parameters or when all constructor parameters are visible. To account for that the following syntax shall be used (note the use of 0x for constraint hash):

new LockingBytecodeP2ST(templateHash, 0x, 0x);

If constraintHash is stored in a variable and is not the length of 20 bytes the contract will be created but it will be invalid. For the case of it being wrong-sized hex literal such as 0xbeef, the contract will fail to compile.

When constructing constraintHash and visibleParameters the encodeNumber and encodeData can be useful for stateful contracts:

contract statefulContract(bytes variable, int version, string visible message) {
function mutate() {
variable = variable + 0xbeef;
version = version + 1;
message = message + "hello"

// Extract the template hash from the lockingbytecode
bytes templateHash = hash160(encodeData(variable) + encodeNumber(version));

// Create the new constraintScript
bytes20 constraintHash = hash160(newConstrainScript);

// Create the new visibleArgs
bytes visibleArgs = encodeData(bytes(message));

// Create the locking bytecode for the new contract and check that
// the change output sends to that contract
bytes newContractLock = new LockingBytecodeP2ST(templateHash, constraintHash, visibleArgs);
require(tx.outputs[1].lockingBytecode == newContractLock);
}
}