Separate P2SH operands from locking script

Motivation

The current implementation of P2SH transactions in A-Block is severely limited by the lack of a separate section for the redeemer of a P2SH output to provide operands to the script. Currently, a P2SH address is the hash of the full script, which effectively reduces the script to a kind of password. This not only makes it impossible to make transactions with complex locking conditions, as all of the stack operands (including any potential signatures or other necessary operands) are baked into the locking script, but also makes them extremely insecure as once a transaction spending the output of a P2SH transaction is broadcast to the network, anyone would be able to redeem any transactions sent to the same P2SH address by simply re-using the same script.

Additionally, a P2SH locking script currently has no mechanism by which to enforce that the message used for signatures is unique to the current transaction. In Bitcoin’s Script language, the opcode for signature verification such as OP_CHECKSIG implicitly use a hash of the current transaction’s inputs and outputs as the message which must be signed, but A-Block’s equivalents require that the signed message be provided as stack operands. In P2PKH transactions, we already push such a hash (as computed by get_tx_in_address_signable_string()) onto the stack implicitly, but P2SH scripts have no mechanism to access this hash. Without a mechanism to get such a transaction-unique message onto the P2SH stack, we cannot make P2SH scripts which are immune to being re-used to redeem different transactions as described above.

Specification

The TxIn format is extended with a new field unlock_script. When verifying a P2SH input, the unlock_script will be prepended to the existing script_signature (which I propose we rename to lock_script). A hash of the OutPoint being redeemed, as well as all of the current transaction’s outputs will also be pushed onto the stack prior to both of the scripts. The full script will be structured as follows:

Bytes(<tx_in_address_signable_string>)
<unlock_script>
<lock_script>

P2SH addresses will be constructed from the hash of the lock_script.

Example

TBD

Considerations

There are some other ways these changes could be implemented:

  • We could add a new pseudo-opcode which serves to separate the unlock_script from the lock_script. The P2SH address would then be computed by hashing only the second part of the script, after the separator opcode. This has the advantage of allowing us to keep using a single script section in the transaction, and could therefore avoid having to modify the TxIn format. However, this advantage is likely irrelevant, as the transaction format is probably going to be drastically changed in the near future anyway, and it would make transaction validation a bit more convoluted.
  • We could implement P2SH transactions the way Bitcoin does, and have the unlock script push the serialized locking script onto the stack after the operands, then deserialize the locking script and continue evaluating. This approach would also allow us to avoid changing the TxIn format, but would make validation much more convoluted
1 Like

I would recommend sticking with the Bitcoin structure for a couple of reason. Primarily the part I mean is that are effectively parameters to another script (like the lock script is a parameter to the P2SH logic) should be wrapped as a binary blob in a PUSHDATA operation as a single stack item. They can then be passed to whatever needs them for parsing and execution.

One big reason for this is that we will likely have several use cases for pushing scripts (that may be either executed or inspected) within other scripts. An example is here. Note that wrapping the script in a PUSHDATA operation replaces the need for the seperator opcode since the data is delimited, and has the side effect of pushing the item onto the stack at the same time. If we are going to use the notion of defining scripts in scripts for multiple use cases it would be good if we have a consistent mechanism across the board. The best way I can see to do that is to keep is as ‘scriptish’ as possible.

I don’t understand the need for the tx_in_signable_string? The locking script would usually include a CHECKSIG operation, if it doesn’t I agree it’s insecure, but that is also true of a non-P2SH script. If we allow those why would we treat P2SH any differently? The attack is the same in both cases, wait until the solution to the hash puzzle is presented in a transaction, suppress the transaction and make a new transaction that spends the coin with the revealed solution.

If implementing it the Bitcoin way one variation I would consider is a more explicit marker. For backward compatibility purposes the “marker” that tell the script engine to use P2SH logic is the script pattern “HASH160 <script_hash> EQUALVERIFY”. We could make this more explicit with an OP code instead e.g. “OP_EXEC_SCRIPT_IF_HASH <script_hash>” (name of op code is open to debate, I was just being explicit about what it’s supposed to do.

If implementing it the Bitcoin way one variation I would consider is a more explicit marker. For backward compatibility purposes the “marker” that tell the script engine to use P2SH logic is the script pattern “HASH160 <script_hash> EQUALVERIFY”. We could make this more explicit with an OP code instead e.g. “OP_EXEC_SCRIPT_IF_HASH <script_hash>” (name of op code is open to debate, I was just being explicit about what it’s supposed to do.

If we do go the Bitcoin route, that sort of output script pattern matching isn’t (currently) necessary at all (or even possible) because transaction outputs do not contain any script. The only type of transaction which is currently functional is P2PKH, in which case the TxOut contains only the public key hash, and the redeeming TxIn contains the full P2PKH script (not only the signature and public key, but also the public key hash and all the verification opcodes). As described in my proposal for improved transaction serialization, my current plan is to to modify TxOuts to use a sort of enum format, so that rather than matching specific script patterns in the TxOut to determine how to interpret it, it’d simply say “This TxOut is P2PKH and the public key has this hash” or “This TxOut is P2SH and the lock script has this hash”. We could do the same for TxIns, so that P2PKH TxIns would just contain a public key and signature, and P2SH TxIns would contain <whatever we decide should go in there>.

I see why having a way to put scripts on the stack would be useful in some cases, but since (as described in my previous paragraph) we do have the flexibility here to add arbitrary new fields, I don’t see the advantage for the P2SH case vs just adding a second script field. To me it just seems like unnecessary additional complexity, because if we take that approach the only thing that we’ll ever end up doing with the serialized lock script is extracting it from the unlock script and deserializing it. Is this (enum-ifying TxIns/Outs) a bad idea? Should I instead focus on changing everything to use the more Bitcoin-like approach of a single script per TxOut and a single script per TxIn?

The tx_in_signable_string is necessary because our OP_CHECKSIG is different than the one in Bitcoin. Bitcoin’s OP_CHECKSIG has two operands (signature, pubkey) and the message which is verified is implicitly generated from the transaction inputs+outputs. Our OP_CHECKSIG has three operands (message, signature, pubkey), so if the script wants to verify that the signature is valid for this specific transaction, it needs to be able to access some kind of data which is unique to a particular transaction. This isn’t unique to P2SH; the current implementation of P2PKH already does this (all P2PKH scripts start off by pushing the tx_in_signable_string onto the stack, and the transaction is considered invalid if the string doesn’t match).

Ahh I see. You’ve effectively combined the functionality of OP_DATASIGVERIFY (Often known as OP_DSV in bitcoin cash circles) and OP_CHECKSIG into one op code which requires you prove it’s a checksig like operation.

Is it possible to separate these into separate op_codes? Then none of that would be necessary, it would also simplify adding more flexible sighash flags to OP_CHECKSIG is this transition was going to happen anyway?

I’d certainly like to separate them, and doing so in a backwards-compatible way wouldn’t even be all too difficult. I’ll have a chat with @andy to see what his thoughts on that are.