Discussion:
Generalised Replay Protection for Future Hard Forks
Add Reply
Mats Jerratsch via bitcoin-dev
2017-11-05 23:48:43 UTC
Reply
Permalink
Raw Message
Presented is a generalised way of providing replay protection for future hard forks. On top of replay protection, this schema also allows for fork-distinct addresses and potentially a way to opt-out of replay protection of any fork, where deemed necessary (can be beneficial for some L2 applications).

## Rationale

Currently when a hard fork happens, there is ad-hoc replay protection built within days with little review at best, or no replay protection at all. Often this is either resource problem, where not enough time and developers are available to sufficiently address replay protection, or the idea that not breaking compatibility is favourable. Furthermore, this is potentially a recurring problem with no generally accepted solution yet. Services that want to deal in multiple forks are expected to closely follow all projects. Since there is no standard, the solutions differ for each project, requiring custom code for every fork. By integrating replay protection into the protocol, we advocate the notion of non-hostile forks.

Users are protected against accidentally sending coins on the wrong chain through the introduction of a fork-specific incompatible address space. The coin/token type is encoded in the address itself, removing some of the importance around the question _What is Bitcoin?_. By giving someone an address, it is explicitly stated _I will only honour a payment of token X_, enforcing the idea of validating the payment under the rules chosen by the payee.

## Iterative Forks

In this schema, any hard fork is given an incremented id, `nForkId`. `nForkId` starts at `1`, with `0` being reserved as a wildcard. When project X decides to make an incompatible change to the protocol, it will get assigned a new unique `nForkId` for this fork. A similar approach like for BIP43 can be taken here. Potentially `nForkId` can be reused if a project has not gained any amount of traction.

When preparing the transaction for signing or validation, `nForkId` is appended to the final template as a 4B integer (similar to [1]). Amending BIP143, this would result in

```
Double SHA256 of the serialization of:
1. nVersion of the transaction (4-byte little endian)
2. hashPrevouts (32-byte hash)
3. hashSequence (32-byte hash)
4. outpoint (32-byte hash + 4-byte little endian)
5. scriptCode of the input (serialized as scripts inside CTxOuts)
6. value of the output spent by this input (8-byte little endian)
7. nSequence of the input (4-byte little endian)
8. hashOutputs (32-byte hash)
9. nLocktime of the transaction (4-byte little endian)
10. sighash type of the signature (4-byte little endian)
11. nForkId (4-byte little endian)
```


For `nForkId=0` this step is ommitted. This will immediately invalidate signatures for any other branch of the blockchain than this specific fork. To distinguish between `nForkId=0` and `nForkId` hardcoded into the software, another bit has to be set in the 1B SigHashId present at the end of signatures.

To make this approach more generic, payment addresses will contain the fork id, depending on which tokens a payee expects payments in. This would require a change on bech32 addresses, maybe to use a similar format used in lightning-rfc [2]. A wallet will parse the address, it will extract `nForkId`, and it displays which token the user is about to spend. When signing the transaction, it will use `nForkId`, such that the transaction is only valid for this specific token. This can be generalised in software to the point where replay protection *and* a new address space can be introduced for forks without breaking existing clients.

For light clients, this can be extended by enforcing the coinbase/block header to contain the `nForkId` of the block. Then the client can distinguish between different chains and tokens it received on each. Alternatively, a new P2P message type for sending transactions could be introduced, where prevOut and `nForkId` is transmitted, such that the lite client can check for himself, which token he received.

Allowing signatures with `nForkId=1` can be achieved with a soft fork by incrementing the script version of SegWit, making this a fully backwards compatible change.

[1]
https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-February/013542.html <https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-February/013542.html>
[2]
https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md <https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md>
Jacob Eliosoff via bitcoin-dev
2017-11-06 19:21:28 UTC
Reply
Permalink
Raw Message
Thanks Mats, this proposal makes sense to me (especially the idea of
fork-specific addresses). It prevents replay across forks, and makes it
easy for client software, and thus potentially users, to specify which fork
a tx is for. But, like other (rougher) past proposals I've seen, it does
little to prevent users from accidentally sending on the wrong fork.

Take the specific and common case of non-upgraded wallet software. Suppose
a HF happens, and becomes the network used by 90% of users. Will old
wallets still default to the old nForkId (10% legacy chain)? If so, I'd
expect a lot of accidental mis-sends on that chain.

This is just a gap in your proposal, not a flaw, but it's worth thinking
about less hazard-prone ways wallets could default nForkId. Perhaps they
could listen to all forks, and default to the one whose last (recent) block
had the highest difficulty? Or just check those blocks to see if multiple
forks are (nontrivially) active, and if so warn the user and force them to
confirm? Something like that.


On Nov 6, 2017 7:05 AM, "Mats Jerratsch via bitcoin-dev" <
bitcoin-***@lists.linuxfoundation.org> wrote:


Presented is a generalised way of providing replay protection for future
hard forks. On top of replay protection, this schema also allows for
fork-distinct addresses and potentially a way to opt-out of replay
protection of any fork, where deemed necessary (can be beneficial for some
L2 applications).

## Rationale

Currently when a hard fork happens, there is ad-hoc replay protection built
within days with little review at best, or no replay protection at all.
Often this is either resource problem, where not enough time and developers
are available to sufficiently address replay protection, or the idea that
not breaking compatibility is favourable. Furthermore, this is potentially
a recurring problem with no generally accepted solution yet. Services that
want to deal in multiple forks are expected to closely follow all projects.
Since there is no standard, the solutions differ for each project,
requiring custom code for every fork. By integrating replay protection into
the protocol, we advocate the notion of non-hostile forks.

Users are protected against accidentally sending coins on the wrong chain
through the introduction of a fork-specific incompatible address space. The
coin/token type is encoded in the address itself, removing some of the
importance around the question _What is Bitcoin?_. By giving someone an
address, it is explicitly stated _I will only honour a payment of token X_,
enforcing the idea of validating the payment under the rules chosen by the
payee.

## Iterative Forks

In this schema, any hard fork is given an incremented id, `nForkId`.
`nForkId` starts at `1`, with `0` being reserved as a wildcard. When
project X decides to make an incompatible change to the protocol, it will
get assigned a new unique `nForkId` for this fork. A similar approach like
for BIP43 can be taken here. Potentially `nForkId` can be reused if a
project has not gained any amount of traction.

When preparing the transaction for signing or validation, `nForkId` is
appended to the final template as a 4B integer (similar to [1]). Amending
BIP143, this would result in

```
Double SHA256 of the serialization of:
1. nVersion of the transaction (4-byte little endian)
2. hashPrevouts (32-byte hash)
3. hashSequence (32-byte hash)
4. outpoint (32-byte hash + 4-byte little endian)
5. scriptCode of the input (serialized as scripts inside CTxOuts)
6. value of the output spent by this input (8-byte little endian)
7. nSequence of the input (4-byte little endian)
8. hashOutputs (32-byte hash)
9. nLocktime of the transaction (4-byte little endian)
10. sighash type of the signature (4-byte little endian)
11. nForkId (4-byte little endian)
```


For `nForkId=0` this step is ommitted. This will immediately invalidate
signatures for any other branch of the blockchain than this specific fork.
To distinguish between `nForkId=0` and `nForkId` hardcoded into the
software, another bit has to be set in the 1B SigHashId present at the end
of signatures.

To make this approach more generic, payment addresses will contain the fork
id, depending on which tokens a payee expects payments in. This would
require a change on bech32 addresses, maybe to use a similar format used in
lightning-rfc [2]. A wallet will parse the address, it will extract
`nForkId`, and it displays which token the user is about to spend. When
signing the transaction, it will use `nForkId`, such that the transaction
is only valid for this specific token. This can be generalised in software
to the point where replay protection *and* a new address space can be
introduced for forks without breaking existing clients.

For light clients, this can be extended by enforcing the coinbase/block
header to contain the `nForkId` of the block. Then the client can
distinguish between different chains and tokens it received on each.
Alternatively, a new P2P message type for sending transactions could be
introduced, where prevOut and `nForkId` is transmitted, such that the lite
client can check for himself, which token he received.

Allowing signatures with `nForkId=1` can be achieved with a soft fork by
incrementing the script version of SegWit, making this a fully backwards
compatible change.

[1]
https://lists.linuxfoundation.org/pipermail/bitcoin-dev/
2017-February/013542.html

[2]
https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-
encoding.md
Mats Jerratsch via bitcoin-dev
2017-11-08 16:45:01 UTC
Reply
Permalink
Raw Message
Hey Jacob!
Take the specific and common case of non-upgraded wallet software. Suppose a HF happens, and becomes the network used by 90% of users. Will old wallets still default to the old nForkId (10% legacy chain)? If so, I'd expect a lot of accidental mis-sends on that chain.
With this proposal implemented, a 'mis-send' is fundamentally impossible. The address contains the identifier of the token that should be sent.

If anything, it's possible to 'mis-receive'.
That is, the receiving wallet was not aware of a newer chain, and the receiver actually wanted to receive the newer token, but instead his wallet created an address for the old token. It is the responsibility of the receiver to write a correct invoice. This is the case everywhere else in the world too, so this seems like a reasonable trade-off.

I would even argue that this should hold in a legal case, where the receiver cannot claim that he was expecting a payment in another token (contrary to how it is today, like when users send BTC to a BCH address, losing their funds with potentially no legal right for reimbursement). If I sent someone an invoice over 100€, I cannot later proclaim that I actually expected $100.

With this proposal, wallets are finally able to distinguish between different tokens. With this ability, I expect to see different implementations, some wallets which advertise staying conservative, following a strict ruleset, and other wallets being more experimental, following hashing rate or other metrics.
Jacob Eliosoff via bitcoin-dev
2017-11-09 20:45:43 UTC
Reply
Permalink
Raw Message
OK, I see. On the whole this is the best replay protection solution I've
seen. In particular, I hope developers of Bech32 and other new address
formats will take a close look at incorporating a fork ID this way.

As I understand you, a private key in cold storage would (of course) remain
valid across HFs, but an *address* would be valid only for the nForkId it
was generated for. There may be cold-storage-type cases where it's
important for an address to be valid across all chains, ie, to
intentionally allow replay? But I guess this could just be a special
nForkId value, say -1?
Post by Mats Jerratsch via bitcoin-dev
Hey Jacob!
Post by Jacob Eliosoff via bitcoin-dev
Take the specific and common case of non-upgraded wallet software.
Suppose a HF happens, and becomes the network used by 90% of users. Will
old wallets still default to the old nForkId (10% legacy chain)? If so,
I'd expect a lot of accidental mis-sends on that chain.
With this proposal implemented, a 'mis-send' is fundamentally impossible.
The address contains the identifier of the token that should be sent.
If anything, it's possible to 'mis-receive'.
That is, the receiving wallet was not aware of a newer chain, and the
receiver actually wanted to receive the newer token, but instead his wallet
created an address for the old token. It is the responsibility of the
receiver to write a correct invoice. This is the case everywhere else in
the world too, so this seems like a reasonable trade-off.
I would even argue that this should hold in a legal case, where the
receiver cannot claim that he was expecting a payment in another token
(contrary to how it is today, like when users send BTC to a BCH address,
losing their funds with potentially no legal right for reimbursement). If I
sent someone an invoice over 100€, I cannot later proclaim that I actually
expected $100.
With this proposal, wallets are finally able to distinguish between
different tokens. With this ability, I expect to see different
implementations, some wallets which advertise staying conservative,
following a strict ruleset, and other wallets being more experimental,
following hashing rate or other metrics.
Sjors Provoost via bitcoin-dev
2017-11-09 21:01:10 UTC
Reply
Permalink
Raw Message
As I understand you, a private key in cold storage would (of course) remain valid across HFs, but an address would be valid only for the nForkId it was generated for. There may be cold-storage-type cases where it's important for an address to be valid across all chains, ie, to intentionally allow replay? But I guess this could just be a special nForkId value, say -1?
If I understand the proposal correctly, you can always spend coins; it's the next transaction that is replay protected.

I like the idea of specifying the fork in bech32 [0]. On the other hand, the standard already has a human readable part. Perhaps the human readable part can be used as the fork id?

Note that in your currently proposal nForkId is only in the transaction signature pre-image. It's not in the serialized transaction, so a node would just have to try to see if the signature is valid. I don't know if that's a problem.
Allowing signatures with `nForkId=1` can be achieved with a soft fork by incrementing the script version of SegWit, making this a fully backwards compatible change.
What's the purpose of nForkId 1?
potentially a way to opt-out of replay protection of any fork, where deemed necessary (can be beneficial for some L2 applications).
Can you give an example of where this opt-out would be useful? Why wouldn't it be enough to just sign one transaction for each fork?

In Spoonnet, the version number is added to the SIGHASH_TYPE in the pre-image. Your solution of just adding another field seems easier, but maybe there's a downside?

Sjors

[0] https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#Bech32
Mats Jerratsch via bitcoin-dev
2017-11-10 11:28:06 UTC
Reply
Permalink
Raw Message
I guess I wasn't clear on the wildcard, `nForkId=0`

This proposal puts Bitcoin at `nForkId=1`, with the purpose of having `nForkId=0` valid on *all* future forks. This means you can create a `nLockTime` transaction, delete the private key and still be assured to not lose potential future tokens.

In theory `nForkId=0` could be used for an address too, the sending wallet should display a warning message about unknown side effects though. This address would be future-safe, and you can put it into a safe-deposit box (even though I see little reason to back up an _address_. You would always back up a _private key_, which translates into funds on any fork.)

Furthermore, `nForkId=0` can be used for L2 applications. Let's say Alice and Bob open a payment channel. One week later, project X decides to fork the network into a new token, implementing a custom way of providing strong two-way replay protection. The protocol Alice and Bob use for the payment channel has not implemented this new form of replay protection. Alice and Bob now have to make a choice:

(1) Ignore this new token. This comes with an evaluation of how much this new token could be worth in the future. They will continue normal channel operation, knowing that their funds on the other branch will be locked up until eternity. When they close their payment channel, the closing transaction will get rejected from the other network, because it's not following the format for replay protected transactions.

(2) Close the payment channel before the fork. The transaction, which closes the payment channel has to be mined before the fork, potentially paying a higher-than-normal fee.

With this proposal implemented, there are two additional choices

(3) Create the commitment transactions with `nForkId=0`. This ensures that when the channel gets closed, funds on other chains are released accordingly. This also means that after the fork, payments on the channel move both, the original token and the new token. Potentially, Alice and Bob want to wait before further transacting on the channel, to see if the token has substantial value. If it has, they can *then* close the channel and open a new channel again. (Note: The funding transaction can use a specific `nForkId`, preventing you from locking up multiple coins when funding the channel, but you can choose to settle with `nForkId=0` to not lock up future coins)

(4) Make the protocol aware of different `nForkId`. After the fork, the participants can chose to *only* close the payment channel on the new token, making the payment channel Bitcoin-only again. This is the preferred option, as it means no disruption to the original network.
Post by Sjors Provoost via bitcoin-dev
I like the idea of specifying the fork in bech32 [0]. On the other hand, the standard already has a human readable part. Perhaps the human readable part can be used as the fork id?
I was considering this too. On the other hand, it's only _human readable_ because thy bytes used currently encode 'bc'. For future forks, this would just be two random letters than, but potentially acceptable.
Jacob Eliosoff via bitcoin-dev
2017-11-11 05:18:11 UTC
Reply
Permalink
Raw Message
OK, so nForkId 0 is exactly the "valid on all chains" specifier I was
asking about, cool. And your LN example (and nLockTime txs in general)
illustrate why it's preferable to implement a generic replay protection
scheme like yours *in advance*, rather than before each fork: all ad hoc RP
schemes I know of break old txs on one of the chains, even when that's not
desirable - ie, they offer no wildcard like nForkId 0.

One comment on your LN example: users would have to take note that nForkId
0 txs would be valid not only on future forks, but on *past* forks too.
Eg, if BCH had been deployed with nForkId 2, then a user setting up BTC LN
txs now with nForkId 0 would have to be aware that those txs would be valid
for BCH too. Of course the user could avoid this by funding from a
BTC-only address, but it is a potential minor pitfall of nForkId 0. (Which
I don't see any clean way around.)
Post by Mats Jerratsch via bitcoin-dev
I guess I wasn't clear on the wildcard, `nForkId=0`
This proposal puts Bitcoin at `nForkId=1`, with the purpose of having
`nForkId=0` valid on *all* future forks. This means you can create a
`nLockTime` transaction, delete the private key and still be assured to not
lose potential future tokens.
In theory `nForkId=0` could be used for an address too, the sending wallet
should display a warning message about unknown side effects though. This
address would be future-safe, and you can put it into a safe-deposit box
(even though I see little reason to back up an _address_. You would always
back up a _private key_, which translates into funds on any fork.)
Furthermore, `nForkId=0` can be used for L2 applications. Let's say Alice
and Bob open a payment channel. One week later, project X decides to fork
the network into a new token, implementing a custom way of providing strong
two-way replay protection. The protocol Alice and Bob use for the payment
channel has not implemented this new form of replay protection. Alice and
(1) Ignore this new token. This comes with an evaluation of how much this
new token could be worth in the future. They will continue normal channel
operation, knowing that their funds on the other branch will be locked up
until eternity. When they close their payment channel, the closing
transaction will get rejected from the other network, because it's not
following the format for replay protected transactions.
(2) Close the payment channel before the fork. The transaction, which
closes the payment channel has to be mined before the fork, potentially
paying a higher-than-normal fee.
With this proposal implemented, there are two additional choices
(3) Create the commitment transactions with `nForkId=0`. This ensures that
when the channel gets closed, funds on other chains are released
accordingly. This also means that after the fork, payments on the channel
move both, the original token and the new token. Potentially, Alice and Bob
want to wait before further transacting on the channel, to see if the token
has substantial value. If it has, they can *then* close the channel and
open a new channel again. (Note: The funding transaction can use a specific
`nForkId`, preventing you from locking up multiple coins when funding the
channel, but you can choose to settle with `nForkId=0` to not lock up
future coins)
(4) Make the protocol aware of different `nForkId`. After the fork, the
participants can chose to *only* close the payment channel on the new
token, making the payment channel Bitcoin-only again. This is the preferred
option, as it means no disruption to the original network.
Post by Sjors Provoost via bitcoin-dev
I like the idea of specifying the fork in bech32 [0]. On the other hand,
the standard already has a human readable part. Perhaps the human readable
part can be used as the fork id?
I was considering this too. On the other hand, it's only _human readable_
because thy bytes used currently encode 'bc'. For future forks, this would
just be two random letters than, but potentially acceptable.
Mats Jerratsch via bitcoin-dev
2017-11-14 13:49:56 UTC
Reply
Permalink
Raw Message
But I like the 'old' idea of putting the hash of a block that MUST be on the chain that this txn can eventually be added to. If the hash is not a valid block on the chain, the txn can't be added.
It means you can choose exactly which forks you want to allow your txn on, pre-fork for both, post-fork for only one, and gets round the issue of who gets to decide the nForkid value.. since you don't need one. Also, all the old outputs work fine, and LN not an issue.
I'm missing why this scheme would be better ?
I do agree that solutions like `SIGHASH_BLOCKCOMMIT` are superior in the sense that they are very difficult to circumvent. However, a fork could also follow the original chain in SPV mode and allow transactions protected with these mechanism. Since it's fundamentally impossible to disallow transactions in future projects, the goal shouldn't be to make this overly complicated.

Furthermore, this schema is not just adding replay protection. It makes transacting safer overall (due to a dedicated address format per fork) and allows light clients to differentiate between multiple forks. In the past three months, at least $600k has been lost by users sending BCH to a BTC address [1].
Thanks for the clarification. How would a tx specify a constraint like "nForkId>=1"? I was thinking of it just as a number set on the tx.
Whether the transaction is replay protected or not is specified by setting a bit in the `SigHashId`. If this bit is set, then the signature *preimage* MUST have `nForkId` appended. `nForkId` is not part of the final transaction, someone who wants to verify the transaction must know which `nForkId` it was created with.

If the bit isn't set, it means `nForkId=0`, which allows other forks to validate the signature.
Also note that since forks form a partial order, but IDs (numbers) form a total order, ">=" will miss some cases. Eg, suppose BCH had forked with nForkId 2, and then you set up a LN funding tx on BCH with nForkId>=2, and then Segwit2x forked (from BTC!) with nForkId 3. The BCH funding tx would be valid on Segwit2x. This is more of a fundamental problem than a bug - to avoid it you'd have to get into stuff like making each fork reference its parent-fork's first block or something, someone has written about this...
Sorry, I was careless with the use of `>=` there. You are correct, forks form a tree. For this proposal, every leaf must be assigned a unique `nForkId`. The relationship between `nForkId` is irrelevant (e.g. which number is bigger), as long as they are unique. Transactions are only valid IFF `nForkId` matches exactly the `nForkId` of the software validating it. As described above, the transaction doesn't even contain `nForkId`, and the node surely is not starting to guess which one it could be.

[1]
https://twitter.com/khannib/status/930223617744437253 <https://twitter.com/khannib/status/930223617744437253>
Jacob Eliosoff via bitcoin-dev
2017-11-15 05:02:48 UTC
Reply
Permalink
Raw Message
Post by Mats Jerratsch via bitcoin-dev
Sorry, I was careless with the use of `>=` there. You are correct, forks
form a tree. For this proposal, every leaf must be assigned a unique
`nForkId`. The relationship between `nForkId` is irrelevant (e.g. which
number is bigger), as long as they are unique. Transactions are only valid
IFF `nForkId` matches exactly the `nForkId` of the software validating it.
As described above, the transaction doesn't even contain `nForkId`, and the
node surely is not starting to guess which one it could be.
OK, but then it seems to me you have a dilemma for, eg, your LN commitment
tx. You either give it the specific nForkId of the fork it's created on -
making it invalid on *all* other forks (eg, any future "non-contentious
upgrade" HF that replaces that fork). Or you give it nForkId 0 - which has
the "BCH tx valid on Segwit2x (& vice versa)" flaw.

It may make sense to revise your proposal to incorporate Luke's
OP_CHECKBLOCKATHEIGHT
<https://github.com/bitcoin/bips/blob/master/bip-0115.mediawiki>, and make
the fork ID a (block height, hash) pair rather than just a number. But I
still think the idea of fork-specific addresses is a keeper!
Post by Mats Jerratsch via bitcoin-dev
But I like the 'old' idea of putting the hash of a block that MUST be on
the chain that this txn can eventually be added to. If the hash is not a
valid block on the chain, the txn can't be added.
It means you can choose exactly which forks you want to allow your txn on,
pre-fork for both, post-fork for only one, and gets round the issue of who
gets to decide the nForkid value.. since you don't need one. Also, all the
old outputs work fine, and LN not an issue.
I'm missing why this scheme would be better ?
I do agree that solutions like `SIGHASH_BLOCKCOMMIT` are superior in the
sense that they are very difficult to circumvent. However, a fork could
also follow the original chain in SPV mode and allow transactions protected
with these mechanism. Since it's fundamentally impossible to disallow
transactions in future projects, the goal shouldn't be to make this overly
complicated.
Furthermore, this schema is not just adding replay protection. It makes
transacting safer overall (due to a dedicated address format per fork) and
allows light clients to differentiate between multiple forks. In the past
three months, at least $600k has been lost by users sending BCH to a BTC
address [1].
Thanks for the clarification. How would a tx specify a constraint like
"nForkId>=1"? I was thinking of it just as a number set on the tx.
Whether the transaction is replay protected or not is specified by setting
a bit in the `SigHashId`. If this bit is set, then the signature *preimage*
MUST have `nForkId` appended. `nForkId` is not part of the final
transaction, someone who wants to verify the transaction must know which
`nForkId` it was created with.
If the bit isn't set, it means `nForkId=0`, which allows other forks to
validate the signature.
Also note that since forks form a partial order, but IDs (numbers) form a
total order, ">=" will miss some cases. Eg, suppose BCH had forked with
nForkId 2, and then you set up a LN funding tx on BCH with nForkId>=2, and
then Segwit2x forked (from BTC!) with nForkId 3. The BCH funding tx would
be valid on Segwit2x. This is more of a fundamental problem than a bug -
to avoid it you'd have to get into stuff like making each fork reference
its parent-fork's first block or something, someone has written about
this...
Sorry, I was careless with the use of `>=` there. You are correct, forks
form a tree. For this proposal, every leaf must be assigned a unique
`nForkId`. The relationship between `nForkId` is irrelevant (e.g. which
number is bigger), as long as they are unique. Transactions are only valid
IFF `nForkId` matches exactly the `nForkId` of the software validating it.
As described above, the transaction doesn't even contain `nForkId`, and the
node surely is not starting to guess which one it could be.
[1]
https://twitter.com/khannib/status/930223617744437253
Loading...