Use Code Separators
How Code Separators Work
In a Bitcoin signature, what is signed is serialized ScriptContext, whose format is as follows:
Part 5, scriptCode
, usually contains the entire smart contract, i.e., locking script. The only exception is when there is OP_CODESEPARATOR (OCS) in it. When the signature is being verified by checkSig, scriptCode
is the locking script but removing everything up to and including the last executed OCS.
If multiple instances of OP_CODESEPARATOR
are present, a subsequent checkSig
will only use the part of the locking script after the most recent occurrence of OP_CODESEPARATOR
as the scriptCode
.
How to Insert Code Separators
To insert an OP_CODESEPARATOR
in place, simply invoke insertCodeSeparator().
export class CodeSeparator extends SmartContract {
@prop()
readonly addresses: FixedArray<Addr, 3>;
constructor(addresses: FixedArray<Addr, 3>) {
super(...arguments);
this.addresses = addresses;
}
@method()
public unlock(sigs: FixedArray<Sig, 3>, pubKeys: FixedArray<PubKey, 3>) {
assert(pubKey2Addr(pubKeys[0]) == this.addresses[0]);
this.insertCodeSeparator()
assert(this.checkSig(sigs[0], pubKeys[0]));
this.insertCodeSeparator()
assert(pubKey2Addr(pubKeys[1]) == this.addresses[1]);
assert(this.checkSig(sigs[1], pubKeys[1]));
this.insertCodeSeparator()
assert(pubKey2Addr(pubKeys[2]) == this.addresses[2]);
assert(this.checkSig(sigs[2], pubKeys[2]));
}
}
In the above example, the unlock
method calls insertCodeSeparator
. Each invocation of checkSig
will use the code below the most recent invocation of insertCodeSeparator
in the signature verification process. Multiple OP_CODESEPARATOR
s can be inserted, each affecting the checkSig
right after it.
Generate a Signature
When OP_CODESEPARATOR
is used, we need to change the way to get signatures.
This is because conventionally, the signature covers the entire locking script, instead of a subscript with everything before OCS removed.
We can achieve this by passing the index of insertCodeSeparator
as a method call parameter, to specify which OP_CODESEPARATOR
divides the locking script.
Let's take a look at an example for the smart contract above:
// Create array of signature options, each for a separate public key.
const pubKeyOrAddrToSign: SignaturesOption = []
for (let i = 0; i < publicKeys.length; i++) {
const pubKey = publicKeys[i]
pubKeyOrAddrToSign.push({
pubKeyOrAddr: pubKey, // The public key for which a signature will be created.
csIdx: i // Index of the `insertCodeSeparator` call, starting from 0
// I.e., if csIdx = 1, then only the code starting from and including
// the second occurence of `insertCodeSeparator` will be signed.
})
}
const callContract = async () => await demo.methods.unlock(
(sigResps) => {
// Inside the signature responses we can observe,
// which instance of the `insertCodeSeparator` the signature
// takes into account:
console.log(sigResps)
return findSigs(sigResps, publicKeys)
},
publicKeys.map((publicKey) => PubKey(toHex(publicKey))) as FixedArray<PubKey, 3>,
{
pubKeyOrAddrToSign
} as MethodCallOptions<CodeSeparator>
)
expect(callContract()).not.throw