The first thing to remember here is that the entire script that is executed comes in two parts. The first part comes in the output that is being spent. This script fragment is incomplete - if you run it in the script interpreter by itself, you will get an error. The second part comes in the input that spends the output. When verifying a transaction, the input script and the output script are essentially concatenated and executed together as a single script (they aren't actually concatenated but it's easier to think of it that way).
The next thing to remember is that the script interpreter is relatively dumb. It's a simple machine whose only memory is the stack and executes operations as it sees them. It does not know about standard script patterns and so cannot assign any special meaning or treatment to specific script patterns. It also does not know what kind of data a stack element is before it executes an opcode. It will take whatever is on the stack and interpret it as needed for the opcode, even if it's the wrong data type.
If your output script is OP_HASH160 <push(hash(P1))> OP_EQUALVERIFY OP_CHECKSIG
, this is just a script fragment that requires an input script to provide the necessary data to satisfy it. The input script you propose would be <push(sig with P1)> <push(P1)> <push(P1)>
which results in a combined script of <push(sig with P1)> <push(P1)> <push(P1)> OP_HASH160 <push(hash(P1))> OP_EQUALVERIFY OP_CHECKSIG
.
To make it easier to follow the script execution, I'm going to notate the P1
s as P1_a
and P1_b
.
We can walk through how this would verify:
Remaining Script |
Current Operation |
Resulting Stack (top is first) |
<push(P1_a)> <push(P1_b)> OP_HASH160 <push(hash(P1))> OP_EQUALVERIFY OP_CHECKSIG |
<push(sig with P1)> |
<sig with P1> |
<push(P1_b)> OP_HASH160 <push(hash(P1))> OP_EQUALVERIFY OP_CHECKSIG |
<push(P1_a)> |
<P1_a> <sig with P1> |
OP_HASH160 <push(hash(P1))> OP_EQUALVERIFY OP_CHECKSIG |
<push(P1_b)> |
<P1_b> <P1_a> <sig with P1> |
<push(hash(P1))> OP_EQUALVERIFY OP_CHECKSIG |
OP_HASH160 |
<hash(P1_b)> <P1_a> <sig with P1> |
OP_EQUALVERIFY OP_CHECKSIG |
<push(hash(P1))> |
<hash(P1)> <hash(P1_b)> <P1_a> <sig with P1> |
OP_CHECKSIG |
OP_EQUALVERIFY |
<P1_a> <sig with P1> |
|
OP_CHECKSIG |
<true> |
As you can see, we end with P1_a
and sig with P1
as the two stack elements that go into the OP_CHECKSIG
. These two are provided by the person trying to spend the transaction. As mentioned in the previous answer, an input script of <sig with P2> <P2> <P1>
can satisfy this output script as well. Let's walk through that to see how it would work:
Remaining Script |
Current Operation |
Resulting Stack (top is first) |
<push(P2)> <push(P1)> OP_HASH160 <push(hash(P1))> OP_EQUALVERIFY OP_CHECKSIG |
<push(sig with P2)> |
<sig with P2> |
<push(P1)> OP_HASH160 <push(hash(P1))> OP_EQUALVERIFY OP_CHECKSIG |
<push(P2)> |
<P2> <sig with P2> |
OP_HASH160 <push(hash(P1))> OP_EQUALVERIFY OP_CHECKSIG |
<push(P1)> |
<P1> <P2> <sig with P2> |
<push(hash(P1))> OP_EQUALVERIFY OP_CHECKSIG |
OP_HASH160 |
<hash(P1)> <P2> <sig with P2> |
OP_EQUALVERIFY OP_CHECKSIG |
<push(hash(P1))> |
<hash(P1)> <hash(P1)> <P2> <sig with P2> |
OP_CHECKSIG |
OP_EQUALVERIFY |
<P2> <sig with P2> |
|
OP_CHECKSIG |
<true> |
This results in success as well, hence your script is vulnerable. The reason it works is because P2
is never verified to be the public key that is specified by the hash. The script interpreter does not know or care that this could be a standard script, so it does not know to enforce that P2
has to be the same as P1
. The script itself needs to do that, and this script fails to do that because it does not tell the interpreter to check that the two stack elements are equal.
As for how this could be exploited, anyone who observes the transaction with your intended input script would learn what P1
is. Then anyone can copy that P1
and produce a transaction that has the second script with P2
.
Lastly, let's look at how the actual script with OP_DUP
works. The output script is OP_DUP OP_HASH160 <push(hash(P1))> OP_EQUALVERIFY OP_CHECKSIG
and the input script is <sig with P1> <P1>
Remaining Script |
Current Operation |
Resulting Stack (top is first) |
<push(P1)> OP_DUP OP_HASH160 <push(hash(P1))> OP_EQUALVERIFY OP_CHECKSIG |
<push(sig with P1)> |
<sig with P1> |
OP_DUP OP_HASH160 <push(hash(P1))> OP_EQUALVERIFY OP_CHECKSIG |
<push(P1)> |
<P1> <sig with P1> |
OP_HASH160 <push(hash(P1))> OP_EQUALVERIFY OP_CHECKSIG |
OP_DUP |
<P1_dup> <P1> <sig with P1> |
<push(hash(P1))> OP_EQUALVERIFY OP_CHECKSIG |
OP_HASH160 |
<hash(P1_dup)> <P1> <sig with P1> |
OP_EQUALVERIFY OP_CHECKSIG |
<push(hash(P1))> |
<hash(P1)> <hash(P1_dup)> <P1> <sig with P1> |
OP_CHECKSIG |
OP_EQUALVERIFY |
<P1> <sig with P1> |
|
OP_CHECKSIG |
<true> |
This is secure because we duplicate P1
exactly and then hash it and check the hashes. Because we duplicated it, we know that it must be the same and if the hashes match, then the original also has the same hash. This ensures that the public key that we use in the OP_CHECKSIG
is the public key that was intended.
Of course this assumes that OP_DUP
duplicates exactly as we expect it to. This is a reasonable assumption - we're assuming that all of the other opcodes work as we expect them to as well. If we didn't, then any script could verify because the opcodes could be doing anything.
Lastly, the script interpreter could have been written to give your script special treatment. It could have been written to behave differently if it saw this particular script template and verify that the top two stack elements were identical. Then this would not be vulnerable. However it is not written that way, and so we cannot rely on this behavior. Furthermore, the script construction that you have described is also inefficient. It's a harder template to match, and it requires actually providing duplicated public keys, which for some transactions, can result in a significant portion of their size. This will then result in increased transaction fees and less block space. So overall, the DUP method is more concise and safer.
It should also be noted that Segwit does introduce a simple single key construction that doesn't actually rely on script and is really just template matching. This is likely more in line with what you are suggesting.