[consensus] Add opt-in replay protection for 2x chain (using SIGHASH) #131
Conversation
SIGHASH_2X_REPLAY_PROTECT can be used instead of SIGHASH_SINGLE
I'm concerned that any opt-in replay protection is an exploitation waiting to happen for L2 solutions. This would be compounded when adding a sunset clause (which ideally would be included). The solution would also need to be implemented on Bitcoin Unlimited, Classic and any non-Satoshi nodes. This would would a challenge given the time constraints. My preference would be replay protection that doesn't rely on consensus level changes (e.g. consuming outputs from a 1M-tx). |
Replay protection in this direction is generally not affecting L2 solutions. After all, we just expanded the rule set. Only for the sunset clause this would be an issue. But even then, at least for LN, signatures are explicitly checked, the SIGHASH is not even part of what is transmitted over the wire. (current protocol only transmits 64B, R and S value. Any other value than SIGHASH_ALL would fail the payment channel immediately)
Correct, this is a downside. This change needs to get propagated to any full-validating source code. So far this is a very minimal change though, and we could generalise, such that it could be reused for future forks. |
This patch appears to only affect policy and not consensus. |
Mind to elaborate? :) From my understanding, adding the flag to allow for an additional SIGHASH type in ConnectBlock https://github.com/btc1/bitcoin/pull/131/files#diff-24efdb00bfbe56b140fb006b562cc70bR1738 is a consensus change (making a previously invalid block, one that contains a transaction with a new SIGHASH type, valid). |
Interesting, thanks for the clarification. It does seem like an interesting solution.
I'm wondering if it's even possible to sunset hard-forks? Won't the expanded rule need to remain, even if it's only there to verify pre-sunset transactions? Also, there is software I'd like to use with Bitcoin Cash, but can't because of its expanded rules. This would be an immediate problem for me if this was implemented. See: Rolling out a hard-fork (from the perspective of BU) in such a short time-frame is perhaps the biggest issue though. |
You do modify ConnectBlock to add the flag |
This will sufficiently invalidate the signatures for the 1X chain.
So far this was only a policy rule. With this change, we require bit 8 of nHashType to be set for transactions that have nHashType = SIGHASH_2X_REPLAY_PROTECT. This change is based off of feedback from arubi and apoelstra.
Thanks @apoelstra for the feedback. I changed this proposal to require a specific bit set in the 4B nHashType when the 1B at the end of the signature has a certain value. It's not enough to just allow a new nHashType, as none of them are explicitly forbidden in the first place (I was mistaken here). With this change 1X and 2X will produce an incompatible CheckSig result for SIGHASH_2X, resulting in the ability to replay protect certain transactions. |
Passing in _true_ all times means bitcoind is able to create replay protected outputs post fork. Ideally we have a check for current block height here.
const int nBlockHeight = chainActive.Height(); | ||
if (nBlockHeight >= Params().GetConsensus().SW2XHeight) { | ||
scriptVerifyFlags |= SCRIPT_VERIFY_ALLOW_2X_SIGHASH; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about relaying anti-replay transactions to other nodes once they are in the mempool? If the mempool
RPC returns these transactions to a non-upgraded btc1 node, wouldn't that node get upset because you're sending it invalid transactions?
Perhaps the mempool` RPC call could exclude these transactions until a hardcoded date just before the fork? That gives node operators some time to upgrade.
@petertodd I assume you have another puzzle ready to go? :-) |
Please take a moment to read the thread below before considering adding -ANY- replay protection to SegWit2x. It's important that the Legacy 1x chain does -NOT- survive or continue after the 2MB upgrade. Try to ignore the politics and chatter in the thread as there are some good points made regarding the Legacy 1x chain's nodes mempool flooding/circle of death after the upgrade. 'How is 1x even going to survive a 50/50 hash power stand off?' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still wrapping my head around this. If I understand correctly this only contains code that verifies the transaction. It might be useful to add code to the reference wallet for generating such a transaction.
@@ -205,7 +207,8 @@ bool CheckSignatureEncoding(const vector<unsigned char> &vchSig, unsigned int fl | |||
} else if ((flags & SCRIPT_VERIFY_LOW_S) != 0 && !IsLowDERSignature(vchSig, serror)) { | |||
// serror is set | |||
return false; | |||
} else if ((flags & SCRIPT_VERIFY_STRICTENC) != 0 && !IsDefinedHashtypeSignature(vchSig)) { | |||
} else if ((flags & SCRIPT_VERIFY_STRICTENC) != 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@apoelstra does this mean the replay protection won't work for certain non-standard transactions? And that would cause replay issues if in the future these transactions become standard?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or does everything work fine as long as these future wallets use splitting transactions that are standard
under todays rules?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's fine, it means that transactions which have SIGHASH_2X_REPLAY_PROTECT
become standard so that they'll be propagated on 2x. But these transactions are inherently replay protected by the "set bit 8" rule below (the signature itself can be valid on only one chain), there would be no risk if they became standard or were otherwise communicated directly to miners on the original chain.
@@ -183,12 +183,14 @@ bool static IsLowDERSignature(const valtype &vchSig, ScriptError* serror) { | |||
return true; | |||
} | |||
|
|||
bool static IsDefinedHashtypeSignature(const valtype &vchSig) { | |||
bool static IsDefinedHashtypeSignature(const valtype &vchSig, const bool allowReplay) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused by the variable name allowReplay
. If I understand correctly this boolean will be true
after the fork height is reached. Before this height it's false
, so the OR
statement below is what is was before. Maybe rename it to something like isSighash2xReplayProtectDefined
(but less ugly)?
@@ -1262,6 +1266,11 @@ bool TransactionSignatureChecker::CheckSig(const vector<unsigned char>& vchSigIn | |||
return false; | |||
int nHashType = vchSig.back(); | |||
vchSig.pop_back(); | |||
|
|||
// If nHashType is set to SIGHASH_2X_REPLAY_PROTECT we require bit 8 be set to 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest adding to comment to explain how having bit 8 set makes this transaction invalid for Core nodes. I assume this is different than the policy triggered by SIGHASH_2X_REPLAY_PROTECT
that causes IsDefinedHashtypeSignature
to return false
on Core nodes?
@@ -1262,6 +1266,11 @@ bool TransactionSignatureChecker::CheckSig(const vector<unsigned char>& vchSigIn | |||
return false; | |||
int nHashType = vchSig.back(); | |||
vchSig.pop_back(); | |||
|
|||
// If nHashType is set to SIGHASH_2X_REPLAY_PROTECT we require bit 8 be set to 1 | |||
// Requiring bit 8 to be set to 1 at all times would break backward compatibility |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest adding: "Requiring rather than merely permitting"
// If nHashType is set to SIGHASH_2X_REPLAY_PROTECT we require bit 8 be set to 1 | ||
// Requiring bit 8 to be set to 1 at all times would break backward compatibility | ||
if (fReplayProtection && (nHashType & 0x1f) == SIGHASH_2X_REPLAY_PROTECT) | ||
nHashType |= (1U << 8); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the code guidelines require this to have brackets or be on the same line as the if
statement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this sets bit 8 (0 based, little endian) to 1? This is used when generating / verifying the signature, but it's not actually writing out the transaction? So in that case a Core node wouldn't know about the need to set bit 8, and thus would generate a different signature?
So a wallet that generates a replay protected legacy transaction should not set bit 8 at the end of the serialized transaction? It should only do that during the signature generation?
I found this Stack Overflow answer about legacy transactions useful: "Append the SIGHASH type, serialized as a 32-bit little-endian integer"
It would be nice to add this sort of thing in a comment somewhere :-)
Does this work the same with SegWit transactions (which use BIP143 for the signature pre-image).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to address my own confusion here: 32 bits are used in the transaction pre-image, the thing that's signed / verified in memory. It's not put in the final transaction that's broadcast, only the first byte is. What's still not clear for me, from reading the BIP143 examples, is where the SIGHASH_TYPE type goes for SegWit transactions; but I suppose in the witness, with the first byte appended to the signature as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I posted my last question here as well.
It's interesting to compare this approach to Spoonnet (which is mandatory though):
So the |
Closing - not needed |
This is a WIP branch.
The initial idea is to take something that is currently forbidden in bitcoin, but only checked by full nodes, and allow it. By doing this, we allow wallets to create transactions that are only valid on this chain.
For this PR I've chosen a new SIGHASH id, similar to the approach taken by Bitcoin Cash (except that we don't require this SIGHASH, we barely allow it). We need to assess whether there are any light wallets that do actually do any sanity checks on the SIGHASH field. Unless there are, this change should satisfy the condition, of not breaking consumer wallets.
I am aware that this change adds some technical debt. Ideally we also add a sunset clause, although it would need to have a large time horizon (2 years+).
There are other approaches, as described in #34 (comment) - they might have other merits or downsides, I am open for discussion.
Tests haven't been written/fixed yet, as I first want to see if this approach has consensus.