Skip to content

Instantly share code, notes, and snippets.

@prestwich
Last active May 4, 2024 19:16
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save prestwich/009986c2c5321434758fc17c37861f58 to your computer and use it in GitHub Desktop.
Save prestwich/009986c2c5321434758fc17c37861f58 to your computer and use it in GitHub Desktop.
But what is a scriptsig?

What is a ScriptSig anyway?

Bitcoin legacy transactions (and Zcash transparent sends) use "ScriptSigs" to prove authorization. A ScriptSig contains the user's signature andm maybe some other information. They let the protocol check that the tx came from someone allowed to spend those Bitcoin. Most users never interact with them directly, and with the advent of SegWit, they're slowly being phased out of Bitcoin. But it's still worth knowing how they work.

Basics

Money in Bitcoin and Zcash is locked to a small program called the "PubkeyScript." The PubkeyScript sets rules for the spender. In order to spend funds, you have to provide a ScriptSig that passes the PubkeyScript program.

Usually these programs are very simple. But they can get more complex.

Decoding Scripts with Riemann

from riemann.script import serialization

script_text = 'OP_DUP OP_HASH160 deadbeefdeadbeefdeadbeefdeadbeefdeadbeef OP_EQUALVERIFY OP_CHECKSIG'

script_bytes = serialization.serialize(script_text)
serialization.deserialize(script_bytes)

script_hex = serialization.hex_serialize(script_text)

Single-Signer Address

Here's a ScriptSig from a real Bitcoin tx (Zcash ScriptSigs are identical):

4830450221009cf0cc5bd315e5df07da6920482ecd6651f5eadadf2db91b697202b6d0320695022042138453d50658851ec5520a66a326f54b5cb41db53fa3b443d81e2f1be7ad510121030ffc466f7127be5bccf305c8ca58aa5c1cc1531d4f8ad23d87640bb64de34b34

It's kinda ugly.

As the name implies, a ScriptSig is a Script. So we deserialize it using the rules of Bitcoin Script:

PUSH(30450221009cf0cc5bd315e5df07da6920482ecd6651f5eadadf2db91b697202b6d0320695022042138453d50658851ec5520a66a326f54b5cb41db53fa3b443d81e2f1be7ad5101)
PUSH(030ffc466f7127be5bccf305c8ca58aa5c1cc1531d4f8ad23d87640bb64de34b34)

The first push is a signature. The second push is a public key. These are checked against the PubkeyScript that owns the coins. Bitcoin's 1... legacy addresses, and Zcash's t1... transparent addresses use a standard PubkeyScript called "pay to pubkey hash" or "P2PKH." That standard script is

OP_DUP
OP_HASH160
PUSH({pubkeyhash})
OP_EQUALVERIFY
OP_CHECKSIG

So we duplicate the provided public key, then we hash the duplicate. Then we check that the hash of the provided public key is the pubkeyhash we expect. If it is, we check the signature. If that passes, then the ScriptSig satisfies the ScriptPubkey.

More Complex Scripts

The second kind of standard address (Bitcoin's 3 addresses and Zcash's t3) uses an extra validation step. These are called "pay to script hash" or "P2SH" addresses, because instead of including the hash of a public key, the funds are held by the hash of a script. This means that the spender must provide that script hash.

The standard p2sh script is:

OP_HASH160
PUSH(scripthash)
OP_EQUAL

And here's a parsed ScriptSig that matches it:

OP_0 
PUSH(30440220567cce18178b8ecd88178b2d7df906e1e479517b8d81314c3f2da34a8769f79b022063f8da1eb62aae2dc9c88ea90097e83032d04a119f56e41b8af4f1d63a63d37b01)
PUSH(3045022100b6e1fc63c7024a9f14ebe2a0efd6985e8829842652a40e46002f089232a340e4022040934d70b30b56c7c4637789b20b9324071ce8ca8f57e94260a7b19c9812d2f601)
PUSH(52210246ccf4de0c54cc7f3354cdd993c2c50cf965fd82238b89659fbd73a1b4bf05a121024fc59f72272a897fe43803374969f396058152fe4765a8d15216f94624257b1b21022593bc69ecbf3bbcc3c58082267cb49dadaf4ca8dbf1b2297338a9d628c4297653ae)

You might recognize those data pushes starting with 30 as signatures. But what is that OP_0 doing there? And what is that big blob with 52? Well, get ready for this because the answer is weird.

The 52... is a Script within a Script. We call it the RedeemScript because sure, Gavin decided to for some reason. The standard P2SH behavior is to hash the RedeemScript, and then check it against the scripthash in the PubkeyScript. Then, if the hash is correct, we execute the RedeemScript with the rest of the ScriptSig as arguments.

So let's unpack that RedeemScript:

OP_2 
PUSH(0246ccf4de0c54cc7f3354cdd993c2c50cf965fd82238b89659fbd73a1b4bf05a1)
PUSH(024fc59f72272a897fe43803374969f396058152fe4765a8d15216f94624257b1b)
PUSH(022593bc69ecbf3bbcc3c58082267cb49dadaf4ca8dbf1b2297338a9d628c42976)
OP_3 
OP_CHECKMULTISIG

Oh hey that looks familiar. That's a standard multisig script with 3 pubkeys! This one requires 2 signers. So that's why we have 2 signatures up above. Funnily enough, the OP_0 is because Satoshi wrote an off-by-one bug and OP_CHECKMULTISIG needs one more argument than it's supposed to. So we just pass it a useless 0 because it's easy.

Conclusion

Bitcoin and ZEC are held by PubkeyScripts that can be encoded as Addreses. The PubkeyScript is used to validate the ScriptSig. For P2SH, the ScriptSig must contain a RedeemScript that also gets run. For P2PKH, there's no RedeemScript. All of this is legacy code, and yes, it is a mess. We improved it with SegWit, which is much cleaner.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment