RFC: Swappable Signature Verification Protocol Spec
Historically, a signature verification algorithm is tightly coupled with other transaction validation logic in a CKB lock script. The anyone-can-pay lock script serves such as example. One obvious reason for doing this, is to simplify the task of writing a CKB script. In the early days, you definitely want to limit the scope you are dealing with, so as to make sure the script you build is secure enough.
As time grows, we learn more and more about how to build CKB scripts. At the same time, the problem brought by bundling signature verification algorithm with lock script logic, is slowly gaining attention: assume we have N signature verification algorithm, and M specific lock script logics, it will take N*M lock scripts to fulfill all the combinations. This brings significant maintainence burdens as well as on-chain storage waste. Is there a solution around this problem?
This RFC tries to address this problem: by defining a common interface for signature verification algorithm, we shall be able to decouple a signature verification library, from a typical lock script. When executing, the lock script can first load the signature verification library via dynamic linking, then calls functions provided by the library to perform the actual signature verification path.
Note there is already some attempt at solving this problem, here we have an example, where the actual lock script is separate from the secp256k1 verification library.
The spec here consists of 2 parts: a common library API that is shared by all signature verification libraries; and a unified workflow shared by all lock scripts that are leveraging external signature verification libraries.
We propose every signature verification library following the spec be compiled into a dynamic linked ELF library with the following 2 exposed functions(in C ABI):
int load_prefilled_data(void *data, size_t *len); int validate_signature(void *prefilled_data, const uint8_t *signature_buffer, size_t signature_size, const uint8_t *message_buffer, size_t message_size, uint8_t *identity, size_t *identity_len);
The 2 functions here, serve different purposes:
load_prefilled_data is used when the library requires initializating certain constant data, such as multiplication table for speedups. In the full lifetime of a CKB script, it is expected to only call this function once to initialize the data. All latter invocations can share the same prefilled data.
This function should support 2 invocation modes:
lenis an address for a variable with value 0, the function is expected to fill in the length required by the prefilled data into the address denoted by
len. This can be used by the caller to allocate enough prefilled data for the library.
datais not NULL, and the variable denoted by
lencontains enough length, the function is expected to fill prefilled data in the memory buffer started from
data, and then fill the actual length of the prefilled data in
In either mode, a return value of 0 denoting success, other values denote failures, and should immediately trigger a script failure.
This is a function that does the actual signature verification work. It takes the prefilled data generated earlier, a variable length signature buffer, and a variable length message buffer as input. It then runs the signature verification logic, and then generate data into the identity buffer. Identity, can thus be used to compare with fields denoting user identity(for example: public key hash stored in script args). This interface is carefully designed to work for many cases:
- For a recoverable signature algorithm, the signature buffer shall contain the recoverable signature. Public key, or public key hash depending on the needs, will be generated and filled to the identity buffer. A lock script only needs to validate that the generated public key matches the one specified, most likely in script args.
- For a non-recoverable signature algorithm, one can put both the actual signature and the public key into the signature buffer for verification. In this case, the library shall perform the signature verification, then generate the actual identity from public key.
In either case, a return value of 0 denoting success, other values denote failures, and should immediately trigger a script failure.
Lock Script Workflow
For a lock script confronting to the spec here, it is expected to keep 2 pieces of information somwhere, most likely in the script args part:
- A hash denoting the signature verification library to use.
- A piece of optional user identification information. For example, non-recoverable signature algorithm can leverage this place for inserting public key.
- A piece of identity information to match against with signature verification identity. Depending on different algorithm, this might be of any length or even missing: in the case of secp256k1 recoverable signature, this will be of 65 bytes long; in the case of non-recoverable signature, this will be missing.
When executing, the lock script shall perform the following steps:
- It loads the correct signature verification library denoted by the hash mentioned above;
- It invokes
load_prefilled_datato generate data required by the signature verification library;
- It extracts signature data from witness. As mentioned above, although this is named signature;
- It extracts user identification information, if exists.
- Signature data is created from simply concatenating signature data, with user identification information(if exists)
- It calculates the signing message, based on its own specific logic;
- It extracts identity information.
- It then invokes
validate_signaturefrom the signature verification library with signature data and message. If the function fails, the script also fails;
- Now it tries to match the identity generated by
validate_signature, with the identity information. If they are identical(meaning if one is missing, the other must also be missing), the script succeeds, otherwise, the script fails.
One example of a signature verification library following the above spec, can be found at here.