Skip to content

Standard Simplicity Script Pubkey

Andrew Poelstra edited this page Jul 18, 2023 · 13 revisions

We can specify a simple Simplicity program that checks a signature on the sigAllHash transaction digest for a fixed public key.

(Word <32-byte x-only-public-key> ▵ disconnect iden (Jet sigAllHash)) ▵ witness <64-byte BIP-0340-signature> ; Jet checkSigVerify : 𝟙 ⊢ 𝟙

The signature is part of a witness expression, meaning it’s value is excluded from the program’s address.

Similarly, the use of disconnect means the sigAllHash is also excluded from the program’s address. As we shall see, the hash of the sigAllHash program as part of the signed message. This feature lets use chose a “sigHash mode” at the time of signing from amoung several jets. Users can even define an arbitrary simplicity expression to create their own custom sigHash mode.

DAGs

On chain, a Simplicity program is represented by a sequence of nodes representing a DAG (directed acyclic graph) where each node can only reference previous nodes in the sequence. The last node in the sequence becomes the root of the DAG.

Each node is a Simplicity combinator, and each combinator references nodes that make up their sub-expressions (if any).

The disassembled DAG of the standard Simplicity program for a single public key reads as follows.

e0 := const <32-byte x-only-public-key>    : 𝟙 ⊢ 𝟚^256
e1 := iden                                 : 𝟚^256 × 𝟙 ⊢ 𝟚^256 × 𝟙
e2 := jet_sig_all_hash                     : 𝟙 ⊢ 𝟚^256
e3 := disconnect e1 e2                     : 𝟙 ⊢ 𝟚^256 × 𝟚^256
e4 := pair e0 e3                           : 𝟙 ⊢ 𝟚^256 × 𝟚^512
e5 := witness <64-byte BIP-0340-signature> : 𝟙 ⊢ 𝟚^512
e6 := pair e4 e5                           : 𝟙 ⊢ (𝟚^256 × 𝟚^512) × 𝟚^512
e7 := jet_check_sig_verify                 : (𝟚^256 × 𝟚^512) × 𝟚^512 ⊢ 𝟙

main := comp e6 e7 : 𝟙 ⊢ 𝟙

For each subexpression above we have a comment showing the inferred type of that expression.

Let’s go through this program line by line.

e0

e0 := const <32-byte x-only-public-key>    : 𝟙 ⊢ 𝟚^256

This expression is a constant function that outputs a user’s 256-bit public key. This will be the first component of the data that will be passed to the checkSigVerify jet in e7.

e1

e1 := iden                                 : 𝟚^256 × 𝟙 ⊢ 𝟚^256 × 𝟙

This expression is passed to disconnect in e3. It receives the Commitment Merkle Root (CMR) of the hash mode expression (see e2), and any other input to the disconnect function. In this example there is no other input, so we just have a unit type here. Normally this expression would determine if such a CMR is authorized. However in this case we simply pass the CMR along where it will be included in the message generated by disconnect in e3 for signature verification.

e2

e2 := jet_sig_all_hash                     : 𝟙 ⊢ 𝟚^256

This is a jet that computes a transaction hash of all transaction data. It is analogous to Bitcoin Script’s SIGHASH_ALL digest, but differs in its exact implementation.

e3

e3 := disconnect e1 e2                     : 𝟙 ⊢ 𝟚^256 × 𝟚^256

This expression assembles the 512-bit message that will make up the message component of the data that will be passed to the checkSigVerify jet in e7. The disconnect combinator passes the CMR of e2 as an input to e1, which in turn simply passes it back and it ends up as the first component of disconnect’s output. The second component of the output is the output of e2 itself, which is a 256-bit transaction digest.

The important property of disconnect is that its CMR excludes e2. This means that the expression e2 is only decided at redemption time, and the user can replace the digest expression with any other transaction digest jet, or even make up their own digest expression themselves.

However, because the CMR of e2 becomes part of the signed message, it cannot be altered after it is signed without creating a new signature.

e4

e4 := pair e0 e3                           : 𝟙 ⊢ 𝟚^256 × 𝟚^512

This expression pairs up the output of e0, the user’s public key, with the 512-bit message generated by e3.

e5

e5 := witness <64-byte BIP-0340-signature> : 𝟙 ⊢ 𝟚^512

witness expressions denote constant functions that output Simplicity values. In this example we output a 512-bit BIP-0340 signature value.

The key difference between witness expressions and Word expressions like in e0 is that the CMR of witness excludes the witness’s value. Similar to disconnect, this means that the witness’s value is not decided until redemption time.

Naturally, signature values cannot be forged by third parties who do not know the private key corresponding to the fixed public key in e0.

e6

e6 := pair e4 e5                           : 𝟙 ⊢ (𝟚^256 × 𝟚^512) × 𝟚^512

This expression pairs up the output of e4 and e5, creating a nested tuple with

  1. the user’s public key
  2. a 512-bit message consisting of the CMR of the chosen hash mode expression, and the output of that hash mode expression.
  3. a 512-bit BIP-0340 signature value.

e7

e7 := jet_check_sig_verify                 : (𝟚^256 × 𝟚^512) × 𝟚^512 ⊢ 𝟙

This is a jet that verifies a signature on a message with a given public key. The message is additionally tagged with an application specific tag for Simplicity signatures.

This jet has no output. It either succeeds or aborts. This makes this jet to be compatible with batch verification, allowing an implementation to optimistically succeed and continue, while validating the signature in batch at a later time.

Root

main := comp e6 e7 : 𝟙 ⊢ 𝟙

The expression root simply composes e6, which generates the public key, message and signature, with e7 which consumes them and validates the signature.

This expression has type 𝟙 ⊢ 𝟙 which is the type that is required by all Simplicity programs.