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:
pragma nexscript ^1.2.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". The SDK computes them when you instantiate a Contract, and encodeData() / encodeNumber() build them inside a contract.
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 functions can be useful for stateful contracts. The constraint script of a contract is its hidden constructor parameters pushed in order, and the visible parameters are pushed in order too, so a contract can rebuild the locking bytecode of a new instance of itself with changed parameters:
contract StatefulContract(bytes variable, int version, string visible message) {
function mutate() {
// The template stays the same
bytes20 templateHash = hash160(this.activeBytecode);
// The new hidden parameters make up the new constraint script
bytes20 constraintHash = hash160(encodeData(variable + 0xbeef) + encodeNumber(version + 1));
// The new visible parameters
bytes visibleParameters = encodeData(bytes(message + "hello"));
// Check that the funds are sent to the new instance of this contract
require(tx.outputs[0].lockingBytecode == new LockingBytecodeP2ST(templateHash, constraintHash, visibleParameters));
}
}
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. Another example is the "streaming Mecenas" contract in the covenants guide.