Skip to content
This repository has been archived by the owner on Apr 6, 2020. It is now read-only.

Simplify transaction's constructor and EIP155 handling #153

Merged
merged 10 commits into from
May 21, 2019

Conversation

alcuadrado
Copy link
Member

@alcuadrado alcuadrado commented May 17, 2019

This PR implements the changes discussed in the TS migration PR and in fixes #149.

A summary of the changes:

  1. chainId can't be provided in the data object.
  2. Only opts.common, or opts.chain and opts.hardfork can be used.
  3. The default hardfork is now 'petersburg'.
  4. The chain id is always obtained from the internal common object now. It isn't computed in the constructor.
  5. The constructor throws if the v value is present, indicates that EIP155 was enabled, and the chain id it indicates doesn't match the one of the internal common object.
  6. No default v is set. If a transaction isn't signed, it would be an empty buffer.
  7. Transactions are always signed with EIP155 after Spurios Dragon.
  8. If v is changed after construction, and it doesn't match the one of the internal common object, we throw a clear message in tx.verifySignature() instead of just letting it return false. its value is validated in its setter.

I think (7) and (8) require some extra comments:

(7) This is different from the previous implementation, where by default EIP155 wasn't used. We'd need an extra param to implement that. That would also make the logic to decide when to use EIP155 would be more complex, and it's already fairly complex. IMO it's better to use EIP155 by default, giving the users replay-protection by default. This EIP has been active for a long time now.

(8) Maybe this should be done in the v setter. What do you think? It would require a call to Object.defineProperty in the constructor, as anything in the prototype is shadowed by defineProperties. Update: See next comment

@alcuadrado
Copy link
Member Author

alcuadrado commented May 18, 2019

(8) Maybe this should be done in the v setter. What do you think? It would require a call to Object.defineProperty in the constructor, as anything in the prototype is shadowed by defineProperties.

I just updated this PR with an implementation of this logic. I think it's better to validate v in its setter than throwing a possible unexpected error in a boolean function.

To implement this I had to change the hash method because it was temporarily modifying the transaction with invalid values. Now it's a pure function.

In doing the modification to hash, I noticed that all the fields have allowLess: true except v. I'm not sure where to check if this is correct or just an error, but I'm pretty sure it's wrong. Also, as we have already discussed in #151, this is RLP-related logic, which IMO shouldn't be part of the internal representation of these objects.

@alcuadrado
Copy link
Member Author

Update: I fixed a bug when signing already signed txs. If it was previously signed without EIP155, tx.hash(false) would not generate an EIP155 hash, but sign would assume it did.

@holgerd77
Copy link
Member

Thanks for the super solid work and the great summary, I would generally go along with all changes proposed/implemented, just as some first estimation. 😄

Copy link
Member

@holgerd77 holgerd77 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One change request on a comment, another question on v setting, otherwise to be continued...

src/transaction.ts Show resolved Hide resolved
src/transaction.ts Show resolved Hide resolved
src/transaction.ts Show resolved Hide resolved
src/transaction.ts Show resolved Hide resolved
src/transaction.ts Show resolved Hide resolved
@alcuadrado
Copy link
Member Author

alcuadrado commented May 19, 2019

Update: I fixed a bug when signing already signed txs. If it was previously signed without EIP155, tx.hash(false) would not generate an EIP155 hash, but sign would assume it did.

Woke up thinking that this change isn't right. Signing signed and unsigned transactions should have the same logic to decide whether to use EIP155 or not, but _implementsEIP155 works differently for signed and unsigned transactions.

I see two ways of fixing this:

  1. Split the hash method in three: one to get the tx id, one to get the message to sign, and finally one to get the message to verify the signature.
  2. Remove any previous signature before signing.

I'll make a commit with the second alternative, as the first one needs further discussion, and can be addressed as part of #151.

UPDATE: Done.
UPDATE2: I implemented (1) in #154 to get an idea of how it would look.

@alcuadrado
Copy link
Member Author

I'll make a commit with the second alternative, as the first one needs further discussion, and can be addressed as part of #151.

While testing this I realized that toJSON wasn't included in the TS migration, so I added it back.

I didn't reimplement its behavior, as defineProperties will overwrite it anyway. I'm not happy with that though, but duplicating its logic didn't seem right either. I think I just don't like defineProperties 😅

@holgerd77
Copy link
Member

Signing signed and unsigned transactions should have the same logic to decide whether to use EIP155 or not

Would agree here, taking 2. should be a valid fix for now.

toJSON wasn't included in the TS migration, so I added it back

Ok.

holgerd77
holgerd77 previously approved these changes May 21, 2019
Copy link
Member

@holgerd77 holgerd77 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alcuadrado Ok, I think I can give this a go now, thanks for the great work! 😄

Please nevertheless answer the one outstanding question.

st.equal(
pt.serialize().toString('hex'),
'ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080',
'ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080808080',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I likely don't have the complete picture yet, but I am not getting atm why the serialization now differs from the example from the EIP page linked above in the comment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, is this the RLP empty buffer case?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it's because of that.

Note that what the EIP calls "signing data" isn't the output of serialize(), it's the preimage of the hash returned by hash(false). We don't have a getter for such value in this class.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an explanation about this in the test

@@ -137,7 +136,7 @@ tape('[FakeTransaction]: Basic functions', function(t) {
})

t.test('should sign', st => {
const tx = new FakeTransaction(txData)
const tx = new FakeTransaction(txData, { hardfork: 'tangerineWhistle' })
tx.sign(Buffer.from('164122e5d39e9814ca723a749253663bafb07f6af91704d9754c361eb315f0c1', 'hex'))
st.plan(3)
st.equal(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New test cases sufficiently cover the changed EIP-155 and v value setting behavior, good.

this._senderPubKey = ecrecover(
msgHash,
v,
this.r,
this.s,
useChainIdWhileRecoveringPubKey ? this._chainId : undefined,
useChainIdWhileRecoveringPubKey ? this.getChainId() : undefined,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All verifySignature() changes unproblematic, ok.

const unsigned = !this.r.length && !this.s.length
const seeksReplayProtection = this._chainId > 0

if ((unsigned && seeksReplayProtection) || (!unsigned && meetsAllEIP155Conditions)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the heaviest code logic change in the PR and I get that this upper part has been moved into this._implementsEIP155() but I am not completely sure how to read this conditional simplification.

Could you give a short reasoning for the two cases in the if clause? That has likely already been done implicitly in your other explanations on the overall changes, but just want to make sure we are not missing anything here.

Won't make this a blocker request though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, they aren't completely equivalent. Not for unsigned transactions.

The decision to use EIP155 or not when signing was previously based on the _chainId field, and its complex initialization logic in the constructor. It's simpler now: if the hardfork is spurious dragon or later, we use EIP155.

If a transaction is already signed and we are validating it, the logic is the same. It should be in spurious dragon or later and have an EIP155-encoded chain id as v to enable EIP155 validation.

}

Object.assign(this, sig)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes in sign() are ok.

@holgerd77
Copy link
Member

@s1na @alcuadrado After this has been merged (@alcuadrado feel free to merge) I think we are in a good working state on the library. I would very much tend to do now do a major release on this, I think the changes already done are heavy enough and further refactoring/simplification work started and discussed by you guys is still a bit far off respectively shouldn't be overrushed and we should take our time here and do a subsequent likely major release on this once ready.

Are you going along with this?

@alcuadrado
Copy link
Member Author

@s1na @alcuadrado After this has been merged (@alcuadrado feel free to merge) I think we are in a good working state on the library. I would very much tend to do now do a major release on this, I think the changes already done are heavy enough and further refactoring/simplification work started and discussed by you guys is still a bit far off respectively shouldn't be overrushed and we should take our time here and do a subsequent likely major release on this once ready.

Are you going along with this?

Completely agree. I created #154 as a place to explore different ideas, not to be merged and released now.

Copy link
Member

@holgerd77 holgerd77 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thanks for the explanations.

@alcuadrado alcuadrado merged commit b564c15 into master May 21, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Simplify Transaction constructor's options
2 participants