Motivation
To design a protocol for Open Tx
, we need to follow some basic principles, such as signature rules, combination rules and security concerns. In this post, I wanna talk about these issues.
Signature rules
In general, every meaningful lock script
needs an asymmetric cryptographical signature to unlock. For default SIGHASH_ALL
scheme, it first calculates the full TX’s hash, then sign the hash result. For OTX, to confirm the ownership, it’s signature must cover every OTX’s inputs and the fields it concerns.
As demostrated in the above image, the full TX is combined by two open TXs, OTX#0
and OTX#1
. If this full TX is a legacy one, and suppose all the inputs share the same lock script, it’s hash should cover all the data of the TX. But for open TX scheme, these two OTXs have their own signatures to cover their constrains about the full TX.
Let’s take a simplest scenario as an example. Alice has 100 USDT stored in input#0
, and she sends 20 USDT to Bob with 80 USDT change saved in output #1
. This OTX#0
is invalid because 1) it generates an extra output cell, which requires extra CKBytes to create (say 100 CKBytes) 2) it doesn’t provide any mining fee. Someone else combines OTX#1
with OTX#0
to firm a valid TX, it contains 1000 CKBytes as inputs, and 890 CKBytes as output. So OTX#1
provides 100 CKBytes for output#1
and 10 CKBytes for mining fee.
In this demo, the signaure of OTX#0
should cover input#0
, output#0
and output#1
, and that of OTX#1
should cover input#1
and output#2
. The signature here has two purposes, 1) assert the ownership of inputs, and 2) ensure the integrity of a TX/OTX. To achieve these, signatures should covers the consuming inputs and the components of the TX that the signer concerns. There are four different fineness levels for components’ scope definition.
Scope Level 0, TX
It means the signature covers all data of a TX. In other words, this level of signature is the same as SIGHASH_ALL
in Bitcoin. It loads all data from the TX, hash it and sign. No one can modify the TX, and thus it cannot support open transaction scheme.
Scope Level 1, Cells
On this level, OTX’s signature covers specific cells. Like the demostration above, the first OTX signs some inputs and some outputs, the second OTX signs others, and they can finally be combined with each other to firm a valid TX.
This mode requires a well definition method for an OTX to identify the cells it concerns. For example, each signature follows a cell index bitmap as parameters (in witnesses fields) to describe the cells it covers.
Scope Level 2, Fields
On this level, an OTX defines which fields in TX’ cells it concerns. For example, it constrains output#0
's type script
, data
, and code hash
of lock script
field, and leave the args
of lock script
alone.
Scope Level 3, Logics
This level gives more flexible constrains to an OTX. Such as marking the capacity
of output#0
must greater than 1000, or requesting the data
field of an output must equals to the hash of its lock script.
Composability
There are two composability requirements in OTX protocol, the first one is combination between different OTXs, the second one is combination between OTX protocol scripts and user defined scripts.
Multiple OTXs
When initializing an OTX, one does not known how many other OTXs will be attached to this OTX, and what lock scripts / type scripts of OTX they have. So different OTXs should keep composable under the same protocol.
The key infomation underlying here is that the signature rules that refer to target cells must use some relative index, or they will be mess up in different stack sequence.
OTX feature as an extention
To support OTX features, input cells’ lock script
should follow OTX rules. To make things easy and different implementation compatible with each other, it’s better to create a universal OTX lib, and every lock script
invoke it.
/* pseudo-code: OTX ready SECP256K1 lock script,
note that the demo code cannot achieve scope level 3 (logic) constrans.
*/
int main() {
// load lock script args
lock_args = load_script_args(CKB_SOURCE_GROUP_INPUT);
// load OTX lib
otx_handle = load_lib(OTX_LIB_HASH);
// load OTX parameters in witness, which defines the signature coverage
otx_parameters = load_witness(0, CKB_SOURCE_GROUP_INPUT);
// hash the items defined in otx_parameters,
// including all inputs with the same lock_script
hash_init(digest);
while (message = load_tx_input(CKB_SOURCE_GROUP_INPUT)) {
hash_update(digest, message);
}
// extract items in TX
TX = load_tx();
items = otx_handle.invoke(TX, otx_parameters);
// hash the items
for (item in items) {
message = load_tx_item(item);
hash_update(digest, message);
}
// load signature
signature = load_witness(1, CKB_SOURCE_GROUP_INPUT);
// recover pk
pubkey = secp256k1_ecdsa_recover(digest, signature);
// calculate pkhash
pk_hash = hash(pubkey);
// return 0 if pkhash matches lock_args
return match(pk_hash, lock_args);
}
Recap
An OTX must identify the constrains, and there are 4 levels of constrain details. To make the constrains immutable, we use signatures cover them. We also need OTXs composable and code reusable, so we’d better provide a shared library to help lock scripts aquire OTX features.