You can think of a "locking script" as function definition, and the "unlocking script" as function arguments that will make the function evaluate to true.
The commonly used P2PKH address is really a little program attached to the UTXO which has a function definition like f_p2pkh(public_key, signature, sighash_flags)
. An UTXO keeps some BTC locked with that function. The spender then creates a transaction which references a particular instance of the function f_p2pkh
and provides the 3 arguments public_key
, signature
, and sighash_flags
.
Here the function definition hard-codes the public key hash so you can't sign with any key but must use the specific key defined as a constant in the function itself.
But what "message" does the signature sign? The function will call a native function (OP_CHECKSIG) which constructs a message from the transaction context it's being spent in using the instruction encoded in the "sighash_flag" argument. This lets the spender not just unlock the UTXO but also decide exactly where the unlocked BTC can move to (new outputs).
The signature "message" is constructed from the transaction body according to supported recipes, encoded by sighash flags. With P2PKH, the spender gets to choose the recipe - which allows for some flexibility in what outputs to require: SIGHASH_ALL, SIGHASH_NONE, SIGHASH_SINGLE, and also gives signer the option to allow others to add more inputs by using SIGHASH_ANYONECANPAY.
Each UTXO is like a little box that holds some BTC and locked with a smart lock, and our function definition is the lock's firmware, and the firmware can "see" other parts of the transaction. We spend by feeding the smart lock the information required to open, which then releases the BTC and moves it to some new boxes.