Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sign method - verify accurate encoding #1026

Merged
merged 7 commits into from
Aug 16, 2019

Conversation

intelliot
Copy link
Collaborator

When signing a transaction, we should decode the signedTransaction blob and check the integrity (accuracy) of the data.

Copy link
Collaborator

@mDuo13 mDuo13 left a comment

Choose a reason for hiding this comment

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

I'm not sure how to test that this works as intended but I do wonder how many edge cases like with Flags might break it.

const decoded = binary.decode(serialized)

// ...And ensure it is equal to the original tx, except:
// - It must have a TxnSignature or txSigners (multisign).
Copy link
Collaborator

Choose a reason for hiding this comment

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

What about flags? The "standard" way of serializing a transaction usually adds tfFullyCanonicalSig even if the user didn't specify it. I could see that feature accidentally tripping this check.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's a great question. I think that if the user didn't specify tfFullyCanonicalSig then ripple-lib's sign method does not add it. However, the prepare* methods -- including prepareTransaction -- do: https://github.com/ripple/ripple-lib/blob/develop/src/transaction/utils.ts#L67

Copy link
Collaborator

Choose a reason for hiding this comment

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

I thought if you left Flags off entirely it did provide it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

  • I've verified that signing a transaction with ripple-lib's sign method (which also serializes it) does not add tfFullyCanonicalSig, regardless of whether the user specified Flags or not. But note that the prepare* methods do add it.
  • I've added a test that ensures that no Flags are added: c5714de

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think that despite the prepare* methods, the sign method should also add Flags if none are specified, mainly because our documentation says we do: https://xrpl.org/transaction-malleability.html

To further protect users, Ripple has configured its code to enable the tfFullyCanonicalSig flag by default where possible. Ripple strongly encourages third-party implementations of XRP Ledger software to generate only fully-canonical signatures, and enable tfFullyCanonicalSig on transactions by default.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@ximinez I think that's beyond the scope of this PR, but I've created a new PR that does that: #1028

@intelliot
Copy link
Collaborator Author

As far as I'm currently aware, the only case that this will catch occurs when an amount of XRP drops contains a decimal point; and this will be caught ahead of time with ripple-binary-codec 0.2.2. (This PR also updates ripple-lib to use ripple-binary-codec 0.2.2.) This PR incorporates the secondary check as "defense in depth". Here's how I tested it:

  1. Created unit tests.
  2. I verified that the unit tests pass with the secondary check alone (using the older ripple-binary-codec).
  3. I removed the secondary check and verified that the tests fail.
  4. I updated ripple-binary-codec and verified that the tests pass (with a small change to the tests: the error message that the test expects).
  5. I added the secondary check back and verified that the tests still pass.

I also verified that the decoded transaction's Fee is checked (to ensure it's not higher than maxFeeXRP) by temporarily commenting out the first Fee check (which checks the original txJSON prior to signing/encoding).

@@ -1,3 +1,4 @@
import * as isEqual from '../common/js/lodash.isequal'
import * as utils from './utils'
import keypairs = require('ripple-keypairs')
import binary = require('ripple-binary-codec')
Copy link
Contributor

Choose a reason for hiding this comment

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

optional: Consider rebasing this import to binaryCodec because it makes the code below easier to read IMO:

const decoded = binary.decode(serialized)

vs

const decoded = binaryCodec.decode(serialized)

// - It must have a TxnSignature or txSigners (multisign).
if (!decoded.TxnSignature && !tx.Signers) {
throw new utils.common.errors.ValidationError(
'Serialized signed transaction is missing "TxnSignature" property'
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider rephrasing to line up with the check you're actually performing:
"Serialized transaction must have a TxnSignature or Signers property."

const decoded = binary.decode(serialized)

// ...And ensure it is equal to the original tx, except:
// - It must have a TxnSignature or txSigners (multisign).
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Refer to this property the same way you refer to it in code: 'Signers' vs txSigners, or at the very least 'tx.Signers'.

Or, if you think it should be called txSigners, you could refactor the code to this property name.

}

// - We know that the original tx did not have Signers, so if it exists, we should delete it:
delete decoded.Signers
Copy link
Contributor

Choose a reason for hiding this comment

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

You've already deleted this on line 66.

@keefertaylor
Copy link
Contributor

Some initial (mostly stylistic) comments.

Copy link
Contributor

@keefertaylor keefertaylor left a comment

Choose a reason for hiding this comment

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

LGTM - Please wait for another reviewer to stamp as well since I'm still relatively new to these flows.

test/api-test.js Show resolved Hide resolved
@intelliot
Copy link
Collaborator Author

intelliot commented Aug 16, 2019

Thanks to @mDuo13 for taking another look! I've also done some additional manual testing myself, and everything looks good.

@intelliot intelliot merged commit 5e138b9 into develop Aug 16, 2019
@intelliot intelliot deleted the verify-encoding-for-sign-method branch August 19, 2019 18:14
@jnr101
Copy link
Collaborator

jnr101 commented Sep 23, 2019

Hi,

I am using values with standard amount of digits after the '.' like e.g. "3.141000". It turns out that this fails the verification. It was easy for me to fix, but if you feel like values with zeros should be accepted, then you might want to change it.

The following code will show the issue:

const {RippleAPI} = require('ripple-lib')

api = new RippleAPI({ server: 'wss://s1.ripple.com' });

const address = "rrrrrrrrrrrrrrrrrNAMEtxvNvQ"
const secret = "shNs3GgHxoNPRWL6chhwH5s5A97T5"

const order = {
  "direction": "sell",
  "quantity": {
    "currency": "USD",
    "counterparty": 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B',
    "value": "3.140000"
  },
  "totalPrice": {
    "currency": "XRP",
    "value": "31415"
  }
};

(async () => {
  await api.connect()
  const accountInfo = await api.getAccountInfo(address)
  const prepared = await api.prepareOrder(address, order, { "sequence": accountInfo.sequence } )
  const {signedTransaction} = api.sign(prepared.txJSON, secret);
  api.disconnect()
})().catch(err => {
  console.error(err.message, '\n', JSON.stringify(err.data, null, 2))
})

@intelliot
Copy link
Collaborator Author

intelliot commented Sep 23, 2019

@jnr101 Thanks for pointing that out! I put some thought into it and decided to keep the current behavior because it's risky (and potentially unexpected) for the encoded transaction to contain different values than were originally provided to the sign method. In the case of trailing zeros, you are probably doing the right thing by trimming those off prior to asking ripple-lib to sign.

To make this behavior more discoverable, I improved the error message and added a diff so that users can quickly see where the discrepancy lies: #1037

@jnr101
Copy link
Collaborator

jnr101 commented Sep 23, 2019

Hi @intelliot, sounds good to me. I have run the tests and looks good 👍

@yxxyun
Copy link

yxxyun commented Oct 3, 2019

Screenshot_20191004-004846_Utoken
after this update, is this right behavior ?

@intelliot
Copy link
Collaborator Author

@yxxyun Correct. The underlying transaction format is in drops and thus does not allow more than 6 decimal places for XRP amounts. It would be risky for the library to silently/automatically do rounding or truncation, so apps/clients must do that prior to passing the amount to ripple-lib. Depending on the context/use case, different rounding policies are possible. Hope this helps!

@yxxyun
Copy link

yxxyun commented Oct 4, 2019

edited,
fixed the bug in utoken.

@intelliot
Copy link
Collaborator Author

@yxxyun Could you create a new issue describing the problem?

Note that you can specify TakerGets and TakerPays by using OfferCreate JSON with ripple-lib's prepareTransaction method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants