RFC: Swappable Signature Verification Protocol Spec

RFC: Swappable Signature Verification Protocol Spec

Introduction

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.

Spec

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.

Common API

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 *signer_buffer,
    size_t signer_size, const uint8_t *message_buffer, size_t message_size,
    uint8_t *output, size_t *output_len);

The 2 functions here, serve different purposes:

load_prefilled_data

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:

  • When data is NULL, and len is 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.
  • When data is not NULL, and the variable denoted by len contains 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 len field.

In either mode, a return value of 0 denoting success, other values denote failures, and should immediately trigger a script failure.

validate_signature

This is a function that does the actual signature verification work. It takes the prefilled data generated earlier, a variable length signerbuffer, and a variable length message buffer as input. It then runs the signature verification logic, and if needed, generate data into the output buffer. This interface is carefully designed to work for many cases:

  • For a recoverable signature algorithm, the signer buffer shall contain the recoverable signature. Public key, or public key hash depending on the needs, will be generated and filled to the output 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 signer buffer for verification. In this case, no output will be generated.

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 output information to match against with signature verification output. 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_data to 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.
  • Signer 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 output information.
  • It then invokes validate_signature from the signature verification library with signer data and message. If the function fails, the script also fails;
  • Now it tries to match the output generated by validate_signature, with the output information. If they are identical(meaning if one is missing, the other must also be missing), the script succeeds, otherwise, the script fails.

Examples

One example of a signature verification library following the above spec, can be found at here.

9 Likes

I have been expecting for this for a while.

Thank you Xuejie!

1 Like

Will len be changed here?

The description here does not mention what to put in the message buffer. The case 2 does not say how to put the two fields into one buffer.

It’s better to add some diagrams and a concrete example here to help readers understand the specificaiton.

Better adding a diagram for the whole workflow.

Since signature verification always requires singer information, the third parameter can be renamed into signer, which is input or output parameter depending on the signature algorithm.

All comments except the diagram part has been addressed.

It seems that my comment is confusing. I really mean the third buffer parameter output can be renamed into singer. :joy:

For recoverable signature

validate_signature(prefilled_data, signature, message, out_signer)

out_signer is the output variable which will store the restored public key.

For normal signature, caller pass the public key as input in the signer buffer:

validate_signature(prefilled_data, signature, message, signer)