-
Notifications
You must be signed in to change notification settings - Fork 0
Vault Language Proposal
Vault Language Proposal
Author : Joonmo Yang<[email protected]>
Created: 2019-10-02
Writing a complex script for CodeChain Virtual Machine(CCVM) is difficult. Although it is powerful enough to express most of the common use cases for UTXOs, it’s too low-level for programmers to manage. Bitcoin had a similar problem for their script system, and Miniscript was proposed recently to address this situation. We propose Vault, a simple and human-readable script language inspired by Miniscript to provide a better experience for writing scripts for CodeChain VM.
pk(A)
pkh(H_A)
pk(A) || pk(B)
success = pk(A) && pk(B);
burn = pkh(C) && before(100s);
pk(2, A, B, C)
thresh(2, pkh(H_A), pk(B), after(100s))
pk(A) && after(1000blk) || pkh(B) && hash(X)
Top level structure:
success = expr ;
burn = expr ;
If success = expr ;
is omitted, success = false;
is inserted as a default.
If burn = expr ;
is omitted, burn = false;
is inserted as a default.
If burn = expr ;
is omitted, success =
and the semicolon can be omitted from the success condition as well.
Base expressions:
-
pk(A)
: true if the unlock script provides a signature of public keyA
-
pkh(H_A)
: true if the unlock script provides a signature and a public key of the public key hashH_A
-
after(time)
: true if the asset is unlocked aftertime
.time
can be specified in seconds (s) or a block number (blk) -
before(time)
: true if the asset is unlocked aftertime
.time
can be specified in seconds (s) or a block number (blk) -
hash(X)
: true if the unlock script provides a preimage of hashX
Complex expressions:
-
A && B
=thresh(2, A, B)
: true if both A and B are true -
A || B
=thresh(1, A, B)
: true if either A or B is true -
thresh(n, A, B, C, …)
: true if exactlyn
(>0) expressions inA
,B
,C
, … are true
A Vault script is compiled into two values: the lock script and the parameters. When creating an asset in CodeChain, the creator should attach the parameters and the hash of the compiled lock script to the transaction.
When unlocking an asset locked with a Vault script, the required unlock script can be generated from the compiler. The compiler receives a Vault script, list of signatures and preimages, expected execution time, and a flag for whether to burn or unlock this asset. The unlock script will be generated only if the conditions specified by the Vault script can be satisfied with the given values and the given time. If there are multiple combinations of values that can satisfy the lock condition, the generated unlock script could be different depending on the implementation. With the generated unlock script, the asset owner can unlock and spend the asset by attaching it to a transaction.
The following is a example Javascript-like pseudocode that locks and unlocks an asset with the Vault script pk(A) || after(100s)
. The details might be changed in the actual implementation.
// Creating an asset
const pubkey = "SOME_PUBLIC_KEY";
const timelock = 100;
const script = `pk(${pubkey}) || after(${timelock}s)`;
const [lockScript, parameters] = compileVault(script);
const lockScriptHash = blake160(lockScript);
const createTransaction = new TransferAssetTransaction({
...someOtherArguments1,
outputs: [{ lockScriptHash, parameters }]
);
<Example pseudocode for creating an asset with Vault>
// Unlocking an asset
const pubkey = "SOME_PUBLIC_KEY";
const timelock = 100;
const script = `pk(${pubkey}) || after(${timelock}s)`;
const tag = createTag(ALL_OUTPUTS, ALL_INPUTS);
const signatures = { [pubkey]: [tag, "VALID_SIGNATURE"] };
const confirmTime = 120;
const [lockScript, unlockScript] = unlockVault(script, {
signatures,
time: confirmTime
});
const unlockTransaction = new TransferAssetTransaction({
...someOtherArguments2,
inputs: [{
utxo: createTransaction.output[0],
lockScript,
unlockScript
}]
});
<Example pseudocode for unlocking an asset created with Vault>
A Vault script consists of two parts: the success condition and the burn condition. The order of the conditions does not matter. Examples of top-level structures are as follows:
Condition type | Syntax | Default |
---|---|---|
success | success = expr ; |
success = false; |
burn | burn = expr ; |
burn = false; |
If a condition is omitted, the default condition is automatically inserted. Furthermore, if the burn condition is omitted, the success =
part and the semicolon can be omitted as well.
The following are examples of how the omitted conditions are treated:
Script | Fully expanded |
---|---|
success = pk(A); |
success = pk(A); |
success = pk(A); |
success = pk(A); |
burn = after(100s); |
success = false; |
pk(A) |
success = pk(A); |
Expressions accept one or more parameters, and each of the parameters must have the correct type that the expression expects. The type check is performed at compile-time and does not affect the run-time behavior of an expression. The following types are used in the expressions:
Name | Description | Syntax | Example |
---|---|---|---|
U64 |
64bit unsigned integer | 0 [1-9][0-9]* |
12345 |
H160 |
160bit hexadecimal value | 0x[0-9a-fA-F]{20} |
0x0123456789abcdef0123 |
H256 |
256bit hexadecimal value | 0x[0-9a-fA-F]{32} |
0x (0123 repeated 8 times) |
H512 |
512bit hexadecimal value | 0x[0-9a-fA-F]{64} |
0x (0123 repeated 16 times) |
Time |
Length of time | (U64 value)s ( U64 value)blk
|
300s 128blk
|
Expression | Parameter Type | Description |
---|---|---|
true |
Always true | |
false |
Always false | |
pk(pub) |
pub: H512 |
True if a signature of public key pub is provided. |
pk(m, pub1, …, pubn) |
m: U64 pub: H512
|
True if m signatures of pub1 , …, pubn are provided. |
pkh(hash) |
hash: H160 |
True if a public key that hashes to hash and the signature of that public key is provided. Blake160 is used as the hash function. |
after(time) |
time: Time |
True if it’s executed after time . |
before(time) |
time: Time |
True if it’s executed before time . |
blake256(hash) |
hash: H256 |
True if the blake256 preimage of hash is provided. |
sha256(hash) |
hash: H256 |
True if the sha256 preimage of hash is provided. |
ripemd160(hash) |
hash: H160 |
True if the ripemd160 preimage for hash is provided. |
keccak256(hash) |
hash: H256 |
True if the keccak256 preimage for hash is provided. |
blake160(hash) |
hash: H160 |
True if the blake160 preimage for hash is provided. |
The thresh
expression is the only way to combine different expressions. The syntax is as follows:
thresh(m, e1, …, en) (m: U64 > 0, e1,…,en: Expression)
The thresh
expression above is true when exactly m
expressions in subexpressions e1
, …, en
are true. The evaluation order of the subexpressions is from left to right, and if the number of true expressions becomes m
, the remaining expressions are not executed. In other words, if a user can satisfy m
expressions in e1
, …, ek
, then ek+1
, …, en
will not be executed.
To provide a better user experience, the following syntactic sugars are provided:
-
expr1 || expr2
=thresh(1, expr1, expr2)
-
expr1 && expr2
=thresh(2, expr1, expr2)
These syntactic sugars are expanded to the thresh
expression by the compiler before compiling them to the CCVM instructions. Note that if &&
and ||
are used together, &&
has a higher precedence over ||
.
Base expressions are compiled into the CodeChain script according to the following translation rules:
Expression | Parameters | Script |
---|---|---|
true |
PUSH 1 |
|
false |
PUSH 0 |
|
pk(pub) |
pub |
CHKSIG |
pk(m, pub1, …, pubn) |
n pub1 … pubn m |
CHKMULTISIG |
pkh(hash) |
hash |
COPY 1 BLAKE160 EQ JZ 2 PUSH 0 JMP 1 CHKSIG |
after(time) |
time |
RELTIME GT |
before(time) |
time |
RELTIME LT |
blake256(hash) |
hash |
SWAP BLAKE256 EQ |
sha256(hash) |
hash |
SWAP SHA256 EQ |
ripemd160(hash) |
hash |
SWAP RIPEMD160 EQ |
keccak256(hash) |
hash |
SWAP KECCAK256 EQ |
blake160(hash) |
hash |
SWAP BLAKE160 EQ |
A thresh
expression executes its subexpressions from left to right, and stops the execution when the number of true subexpressions becomes m
(>0). This is implemented by pushing a special value counter to the stack and incrementing it if the execution result of a subexpression is true. When the counter becomes m
, the remaining subexpressions are not executed and the parameters of those subexpressions are removed from the stack. This ensures that the stack only contains the parameters of the remaining expressions.
The parameters of the thresh(m, e1, …, en)
are defined as follows:
…param(e1), …, …param(en), m
At the start of the execution, a special value counter is created for tracking the number of true subexpressions in the current thresh
expression. It is initialized by the following script:
PUSH 0
Before executing the subexpressions, the parameters for that expression is lifted to the top of the stack. If the subexpression is a base expression, the required number of values from the unlock script are lifted too. Note that these lifted values will be consumed and removed from the stack after executing the subexpression.
After executing a subexpression, the stack will look like the following table:
Stack | Description |
---|---|
0/1 |
Execution result of the subexpression |
0 |
counter |
... | Parameters of the remaining subexpressions |
m |
Number of required true expressions |
... | ... |
The result is added to the counter and is compared with the threshold value(m
) in the stack. If the counter is equal to m
, all the remaining subexpressions are skipped. It can be expressed as the following script:
ADD DUP COPY (index of m in the stack) EQ JNZ (distance to the POP instructions)
Instruction | Stack (left is the stack top) | |||||
---|---|---|---|---|---|---|
0/1 (result) |
counter |
…params
|
m |
… | ||
ADD |
new_counter |
…params
|
m |
… | ||
DUP |
new_counter |
new_counter |
…params
|
m |
… | |
COPY |
m |
new_counter |
new_counter |
…params
|
m |
… |
EQ |
new_counter == m |
new_counter |
…params
|
m |
… | |
JNZ |
new_counter |
…params
|
m |
… | ||
<Execution example> |
When the subexpressions are skipped, all the remaining parameters for those subexpressions are dropped and 1 is pushed to the stack. It can be expressed as the following script:
POP POP … POP PUSH 1
For optimization purposes, the counter handling instructions for the last subexpression is different from the other instructions. When the last subexpression is executed, we don’t have to maintain the counter because we don’t have any more subexpression to execute. Thus, we can push counter + result == m
to the stack as a result. It can be expressed as the following script:
ADD EQ JMP (distance to the end of the script)
Instruction | Stack | ||||
---|---|---|---|---|---|
0/1 (result) |
counter |
m |
… | ||
ADD |
m |
… | |||
EQ |
new_counter == m |
… | |||
JMP |
new_counter == m |
… | |||
<Execution example for thresh> |
So the compilation result of thresh(m, e1, e2, …, en)
looks like this:
PUSH 0
(…LIFT values for e1)
(…instructions for e1)
ADD DUP COPY (index of m) EQ JNZ (distance to POPs)
(…LIFT values for e2)
(…instructions for e2)
ADD DUP COPY (index of m) EQ JNZ (distance to POPs)
…
(…LIFT values for en)
(…instructions for en)
ADD EQ JMP (distance to the end of the script)
POP … POP PUSH 1
The final compilation result depends on the success expression and the burn expression. There are three cases we should consider:
The compilation result of the expression for the success condition is used as the final result.
In this case, the script must burn the asset if the burn expression is satisfied. Thus, the compilation result of the burn expression is used and the following instructions are added at the end:
JZ 1 BURN
In this case, the compilation result requires an additional value to be provided by the unlock script. If the provided value is zero, the success condition is executed. Otherwise, the burn condition is executed.
Vault script: success = pk(A);
Parameters: A
Lock script: CHKSIG
Vault script: success = pkh(H_A);
Parameters: H_A
Lock script: COPY 1 BLAKE160 EQ JZ 2 PUSH 0 JMP 1 CHKSIG
Vault script: success = pk(A) || pk(B);
Parameters: A B 1
Lock script:
PUSH 0
LIFT 5 LIFT 5 LIFT 3
CHKSIG
ADD DUP COPY 3 EQ JNZ 7
LIFT 4 LIFT 4 LIFT 3
CHKSIG
ADD EQ JMP 4
POP POP POP PUSH 1
Vault script:
success = pk(A) && pk(B);
burn = pkh(H_C) && before(100s);
Parameters: H_C 100 2 A B 2
Lock script:
JZ 30
DROP 3 DROP 3 DROP 3
PUSH 0
LIFT 5 LIFT 5 LIFT 3
COPY 1 BLAKE160 EQ JZ 2 PUSH 0 JMP 1 CHKSIG
ADD DUP COPY 3 EQ JNZ 6
LIFT 1
RELTIME LT
ADD EQ JMP 3
POP POP PUSH 1
JZ 25 BURN
POP POP POP
PUSH 0
LIFT 5 LIFT 5 LIFT 3
CHKSIG
ADD DUP COPY 3 EQ JNZ 7
LIFT 4 LIFT 4 LIFT 3
CHKSIG
ADD EQ JMP 4
POP POP POP PUSH 1
Vault script: success = pk(2, A, B, C);
Parameters: 3 A B C 2
Lock script: CHKMULTISIG
Vault script: thresh(2, pkh(H_A), pk(B), after(100s))
Parameters: A B 100 2
Lock script:
PUSH 0
LIFT 7 LIFT 7 LIFT 7 LIFT 4
COPY 1 BLAKE160 EQ JZ 2 PUSH 0 JMP 1 CHKSIG
ADD DUP COPY 4 EQ JNZ 15
LIFT 5 LIFT 5 LIFT 3
CHKSIG
ADD DUP COPY 3 EQ JNZ 7
LIFT 1
RELTIME GT
ADD EQ JMP 5
POP POP POP POP PUSH 1
Vault script: pk(A) && after(1000blk) || pkh(B) && hash(X)
Parameters: A 1000 2 B X 2 1
Lock script:
PUSH 0
LIFT 3 LIFT 3 LIFT 3
PUSH 0
LIFT 10 LIFT 10 LIFT 3
CHKSIG
ADD DUP COPY 3 EQ JNZ 6
LIFT 1
RELTIME GT
ADD EQ JMP 4
POP POP POP PUSH 1
ADD DUP COPY 6 EQ JNZ 38
LIFT 4 LIFT 4 LIFT 4 LIFT 4
PUSH 0
LIFT 8 LIFT 8 LIFT 8 LIFT 4
COPY 1 BLAKE160 EQ JZ 2 PUSH 0 JMP 1 CHKSIG
ADD DUP COPY 3 EQ JNZ 8
LIFT 5 LIFT 2
SWAP BLAKE160 EQ
ADD EQ JMP 4
POP POP POP PUSH 1
ADD EQ JMP 6
POP POP POP POP POP PUSH 1
The compiler is also responsible for generating an unlock script corresponding to a Vault script when the correct set of values are given.
For the assets locked with a Vault script, the structure of the unlock script differs a lot depending on the conditions that can be satisfied. For example, for the expression thresh(2, e0, e1, e2)
, if we are only aware of the values that can satisfy e0
and e2
, we have to provide some values that can make the e1
’s result false. However, if we know the values that can satisfy e0
and e1
, we don’t have to provide any more values.
Thus, the list of expressions to be satisfied should be decided before generating an unlock script for a Vault script. The compiler receives values that can be used for this decision, and automatically derives the list of expressions from them. The list of values that the compiler receives is as follows:
- Map of (public key, (tag, signature)) (default: empty map)
- Map of (hash, preimage) (default: empty map)
- Expected age of an asset at the time of execution in terms of seconds
- Expected age of an asset at the time of execution in terms of blocks
- Flag for deciding whether to unlock or burn.
burn
andsuccess
are allowed. (default:success
)
All the values are optional, but the execution time must be provided if the Vault script contains after(…s)
or before(…s)
and the execution block number must be provided if the Vault script contains after(…blk)
or before(…blk)
.
The resulting list of expressions must satisfy the following rules:
- If the flag is
success
, the top level expression of the success condition must be selected. - If the flag is
burn
, the top level expression of the burn condition must be selected. - If
thresh(m, e0, …, en)
is selected, exactlym
expressions ine0
, …,en
must be selected. - If a base expression is selected, the condition in the table below must be satisfied.
Expression | Satisfaction condition |
---|---|
true |
Always |
false |
Never |
pk(pub) |
The signature table contains an entry for public key pub
|
pk(m, pub1, …, pubn) |
The signature table contains m entries in n public keys pub1 , …, pubn The signature table’s entries for pub1 , …, pubn must have the same value for tag |
pkh(hash) |
The preimage table contains an entry for hash .The preimage of the hash has a length of 64.The signature table contains an entry for the preimage of hash
|
after(times) |
The expected age of an asset in second is larger than time . |
after(timeblk) |
The expected age of an asset in block is larger than time . |
before(times) |
The expected age of an asset in second is less than time . |
before(timeblk) |
The expected age of an asset in block is less than time . |
blake256(hash) |
The preimage table contains an entry for hash . |
sha256(hash) |
The preimage table contains an entry for hash . |
ripemd160(hash) |
The preimage table contains an entry for hash . |
keccak256(hash) |
The preimage table contains an entry for hash . |
blake160(hash) |
The preimage table contains an entry for hash . |
<Base expression satisfaction table> |
If the top level expression couldn’t be selected, the compiler throws an error. If there were multiple possible ways to select a list of expressions, the actual selected list of expressions depends on the implementation.
There are some cases (e.g. in thresh
expression) where we have to intentionally make the script fail. To generate such unlock scripts, dummy values that are expected to never satisfy the condition is used.
Unlock scripts that {do, do not} satisfy the condition for the base expressions are as follows:
Expression | Used values | Unlock script that satisfies the condition | Unlock script that fails to satisfy the condition |
---|---|---|---|
true |
Always possible | Impossible | |
false |
Impossible | Always possible | |
pk(pub) |
(tag , sig ) = SignatureMap[pub ] |
PUSHB 65 sig PUSH tag |
PUSHB 65 0x0…0 PUSH 0 |
pk(m, pub1, …, pubn) |
sig1 , sig2 , … = signatures of m public keys in pub1 , …, pubn
|
PUSH tag PUSHB 65 sig1 PUSHB 65 sig2, … |
PUSH 0 PUSHB 65 sig1 PUSHB 65 sig2, … |
pkh(hash) |
pub = PreimageMap[hash ]( tag , sig ) = SignatureMap[pub ] |
PUSHB 65 sig PUSH tag PUSHB 64 pub |
PUSHB 65 0x0…0 PUSH 0 PUSHB 64 0x0…0 |
after(time) |
Always possible if selected Impossible if not selected |
Always possible if not selected Impossible if selected |
|
before(time) |
Always possible if selected Impossible if not selected |
Always possible if not selected Impossible if selected |
|
blake256(hash) |
preimage = PreimageMap[hash ] |
PUSHB len(preimage) preimage |
PUSH 0 if blake256(0) != hash PUSH 1 if blake256(0) == hash
|
sha256(hash) |
preimage = PreimageMap[hash ] |
PUSHB len(preimage) preimage |
PUSH 0 if sha256(0) != hash PUSH 1 if sha256(0) == hash
|
ripemd160(hash) |
preimage = PreimageMap[hash ] |
PUSHB len(preimage) preimage |
PUSH 0 if ripemd160(0) != hash PUSH 1 if ripemd160(0) == hash
|
keccak256(hash) |
preimage = PreimageMap[hash ] |
PUSHB len(preimage) preimage |
PUSH 0 if keccak256(0) != hash PUSH 1 if keccak256(0) == hash
|
blake160(hash) |
preimage = PreimageMap[hash ] |
PUSHB len(preimage) preimage |
PUSH 0 if blake160(0) != hash PUSH 1 if blake160(0) == hash
|
As described in 4.3.2, the thresh(m, e1, …, en)
expression executes the subexpressions from left to right, and skips the remaining subexpressions if the number of accumulated true expressions is equal to m
. If ek
is the rightmost subexpression in the selected subexpressions, ek
is the last executed subexpression because the number of selected subexpressions is equal to m
. Thus, the generated unlock script should provide inputs for all the subexpressions until ek
, and should not provide inputs for the subexpressions after ek
. If there are any subexpressions in e1
, …, ek
that are not selected, the generated unlock script should provide the inputs that can make that subexpression’s result false.
The generated unlock script for thresh(m, e1, …, en)
is as follows:
…unlock(ek, is_selected(ek)), …, …unlock(e1, is_selected(e1))
where
-
ek
= rightmost selected expression ine1
, …,en
-
is_selected(e)
= true ife
is selected, false otherwise -
unlock(e, true)
= Unlock script that makese
succeed -
unlock(e, false)
= Unlock script that makese
fail
To make a thresh
expression fail, all the unlock scripts for the subexpressions must fail as follows:
…unlock(en, false), …, …unlock(e1, false)
where
-
unlock(e, false)
= Unlock script that makese
fail
After generating the unlock script for the selected expressions, we make final changes to the result for the burn/success selection.
The generated unlock script of the success expression is used as the final result.
The generated unlock script of the burn expression is used as the final result.
In this case, the lock script requires an additional value as specified in 5.3.3. If the flag provided to the compiler was burn
, PUSH 1
is appended at the end of the unlock script. If the flag was success
, PUSH 0
is appended.
Vault script: success = pk(A);
Unlock script: PUSHB 65 <sig> PUSH 3
Vault script: success = pkh(H_A);
Unlock script: PUSHB 65 <sig> PUSH 3 PUSHB 64 <pub>
Vault script: success = pk(A) || pk(B);
Unlock script: PUSHB 65 <sigA> PUSH 3
Vault script:
success = pk(A) && pk(B);
burn = pkh(H_C) && before(100s);
Unlock script: PUSHB 65 <sigC> PUSH 3 PUSHB 64 <pubC> PUSH 1
Vault script: success = pk(2, A, B, C);
Unlock script: PUSH 3 PUSHB 65 <sigA> PUSHB 65 <sigB>
Vault script: thresh(2, pkh(H_A), pk(B), after(100s))
Unlock script:
PUSHB 65 <0x0…0> PUSH 0
PUSHB 65 <sigA> PUSH 3 PUSHB 65 <pubA>
Vault script: pk(A) && after(1000blk) || pkh(B) && hash(X)
Unlock script:
PUSHB 32 <preX>
PUSHB 65 <sigB> PUSH 3 PUSHB 64 <pubB>
PUSHB 65 <0x0…0> PUSH 0
The LIFT
opcode used in this document doesn’t exist in the CCVM now. Its behavior is "lifting” the value in the stack to the top. In other words, LIFT n
removes the stack item at the n
th index and pushes the removed value to the top of the stack. Its behavior is the same as COPY n DROP n+1
, but since it’s repeated too much, it’d be better to make a new opcode for this.
The current implementation of the timelock uses the "timelock" field on the AssetTransferInput data structure. This allows only one timelock condition in an asset, so composable timelocks such as after(100s) && before(200s) && pk(A)
cannot exist. One way of solving this is by introducing opcodes for fetching the current (absolute and relative) timestamp of the transaction. We can compare the desired time with the value retrieved from the opcode to check if the timelock is satisfied. The RELTIME
opcode in this document is used for this purpose, representing the opcode for pushing the relative time value (in seconds) since the asset was created.
The ADD
opcode is used for accumulating the number of satisfied expressions in thresh
expressions. Also, GT
and LT
opcodes are used for checking the timelock. Since we do not have such opcodes in our current CCVM spec, we need to add these instructions.
jmyang I had a presentation about this proposal at CodeChain Techtalk on 10/16. Here's the link for the slides. https://drive.google.com/open?id=1y7D8W3irJPR0bp9hWJldz2HhEb8o5j1LfkpI3Egz_cU