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

Speed up BTC signing #2507

Merged
merged 8 commits into from
Oct 17, 2022
Merged

Speed up BTC signing #2507

merged 8 commits into from
Oct 17, 2022

Conversation

andrewkozlik
Copy link
Contributor

@andrewkozlik andrewkozlik commented Sep 10, 2022

Summary

This PR should reduce the singing time of a large CoinJoin transaction from 220 seconds to 44 seconds. All time measurements were made on a physical device using trezorlib to provide the transaction. The changes are outlined below:

Added a large CoinJoin transaction to the device tests

Added a CoinJoin transaction with 400 inputs and 1200 outputs of which 10 and 30, respectively, are internal. This is the largest transaction that Wasabi will produce under default settings.

Added no_serialize option to SignTx

  • Implemented in TT only.
  • Reduces the signing time by about one third.
  • What about T1? I don't see a strong reason to implement it in T1. We want it to save time in signing extra-large transactions like CoinJoins, where time is of the essence. T1 doesn't even implement external inputs, much the less CoinJoins. Is it OK to ignore the option in T1 or should it return an error if the option is set to True? @matejcik
  • Naming. I'd prefer serialize, but setting [default=true] in the protobuf definition doesn't make the protobuf parser automatically assign true, when the option is not set. So I wonder, do we always want the default to be false in protobuf? Is this a bug in the parser? @matejcik

Verify ownership proofs before transaction approval in BTC signing

This allows us to completely skip Step 3 in CoinJoin signing. Reduces the signing time by about 7 % more, because we don't have to stream the external inputs again.

Use C implementation of Bech32 decode

Reduces the signing time by almost 40 % more. I can't explain why this is. I tried to optimize the Python implementation, but it didn't help and I couldn't locate any particular operation that is responsible for the huge time difference. Maybe someone with hardware expertise can do some profiling to figure out what's going on.

Improve Bitcoin signing progress display

I decided to split the progress wheel into two phases:

  • Phase 1: Loading transaction. Steps 1 and 2 of the SignTx process, i.e. everything that happens before final user confirmation. Previously we didn't show any progress for Phase 1.
  • Phase 2: Signing transaction. This consists of the remaining steps of the SignTx process: prevtx verification, verification of presigned external inputs, the actual signing operations and transaction serialization.

During phase 1 we discover important information about the transaction: taproot_only, number of SegWit inputs and external inputs. This is critical for giving a good estimate of the number of operations in Phase 2.

For example, in a typical CoinJoin, Phase 1 takes about 95% of the time. Phase 2 is fast, but we wouldn't be able to tell without knowing that the transaction is Taproot-only and that most inputs are external. So if we relied only on the total number of inputs and outputs as sources of information for the time estimate, then we would grossly overestimate the overall time.

On the other hand, in a non-Taproot transaction, Phase 2 may take 95 % of the time, because the previous transactions need to be verified. The user is unlikely to even notice the "Loading transaction" progress dialog, unless the number of inputs is large.

Furthermore, differentiating between loading and signing may be useful in interactive signing to indicate to the user that no more confirmations are expected from them in the "Signing transaction" phase.

In debug mode the firmware will now check that the progress wheel completed exactly the predicted number of steps. If not, then it will return a FirmwareError. The only scenario that I know of, which won't be predicted correctly, is transaction replacement with mixed legacy + SegWit input script types. This can be fixed easily (add segwit_count to OriginalTxInfo), but I think it's not worth the extra code, because Suite doesn't allow this scenario.

Other improvements I tried

I tried reverting 2cedc68 to enable the BIP32 cache. This decreased the processing time by 22ms per internal input or output, which improves the overall time by about one second. (Not included in this PR.)

Review

@matejcik please review overall.
@onvej-sl please look at these two commits:

  • d54cae0 I suggest to double-check for buffer overflow and out of bounds read.
  • de951d2 this changes the BTC signing flow slightly, so let's have two reviewers check this commit for security issues.

@andrewkozlik andrewkozlik force-pushed the andrewkozlik/coinjoin-speed branch from 671dabd to c94d265 Compare September 10, 2022 19:02
@andrewkozlik andrewkozlik added core Trezor Core firmware. Runs on Trezor Model T and T2B1. bitcoin Bitcoin related code Code improvements R&D Research and development team related labels Sep 14, 2022
@andrewkozlik andrewkozlik force-pushed the andrewkozlik/coinjoin-speed branch 3 times, most recently from 70c3a01 to d54cae0 Compare September 16, 2022 12:42
@andrewkozlik andrewkozlik marked this pull request as ready for review September 16, 2022 13:33
@andrewkozlik andrewkozlik removed the request for review from prusnak September 16, 2022 13:33
@andrewkozlik
Copy link
Contributor Author

andrewkozlik commented Sep 19, 2022

In 87bc82b I tried changing no_serialize to serialize and the default to true. This change breaks device tests as well as unit tests. There seems to be something very wrong with the handling of default values in our code base.

If I don't specify serialize in SignTx, then pytest ... -vv log shows this:

[2022-09-19 18:42:28,612] trezorlib.client DEBUG: sending message: SignTx
SignTx (13 bytes) {
    coin_name: 'Testnet',
    inputs_count: 2,
    outputs_count: 1,
}

Trezor log shows this:

58574977910 trezor.wire DEBUG received message contents:
SignTx {
    inputs_count: 2
    lock_time: 0
    coin_name: 'Testnet'
    version: 1
    amount_unit: 0
    outputs_count: 1
    decred_staking_ticket: False
    timestamp: None
    branch_id: None
    version_group_id: None
    expiry: None
    serialize: False
}

This would indicate two bugs, neither of which I can explain:

  • It's as if the __init__() method of SignTx in trezorlib here isn't getting called, so True is never assigned to the serialized parameter on the trezorlib side.
  • At the same time on the Trezor side the serialized parameter is assigned False instead of True as the default.

@matejcik
Copy link
Contributor

  • It's as if the __init__() method of SignTx in trezorlib here isn't getting called, so True is never assigned to the serialized parameter on the trezorlib side.

It's not, and that is by design. (though very confusing)
The generated code is better thought of as a fakery for type-checking purposes than actual code; when we drop python 3.6 compatibility, the generated constructors will fly out the window.

  • At the same time on the Trezor side the serialized parameter is assigned False instead of True as the default.

Indeed, there seems to be a bug in pb2py where it expects the value to be a capital-T True. I'd need to check but I guess that shoud either be lowercase, or maybe a case-insensitive match?

@matejcik
Copy link
Contributor

It's not, and that is by design. (though very confusing)

except that it does not matter and the functionality should be there: #2527

@andrewkozlik
Copy link
Contributor Author

Indeed, there seems to be a bug in pb2py where it expects the value to be a capital-T True. I'd need to check but I guess that shoud either be lowercase, or maybe a case-insensitive match?

I think it should be lowercase. The change seems to work. Waiting for the CI to complete. BTW, I had to run make clean then make build_unix for the fix to take effect, which indicates a missing dependency.

for i in range(self.tx_info.tx.inputs_count):
if i in self.external:
progress.advance()
txi = await helpers.request_tx_input(self.tx_req, i, self.coin)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It seems to me that we can easily avoid streaming the inputs that have an ownership proof here. See this draft. Does it make sense?

The reason why I chose to use the has_presigned boolean value over a set of presigned indices is because it seemed like a premature optimization to use a set. It would optimize the signing speed only when all of the following conditions are satisfied:

  • There is at least one presigned input.
  • There are many external inputs with an ownership proof.
  • All internal inputs are taproot.

So far we don't have a use-case for that. But it would be useful for CoinJoins where some participants don't have the ability to produce an ownership proof for their input and produce a presigned input. I am not against the optimization. I guess using a set vs. bool here won't make a relevant difference in terms of RAM.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can add this commit to the PR: 2700aa2

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@onvej-sl?

I added the commit to this PR as 0c24201.

@matejcik matejcik requested review from mmilata and grdddj October 6, 2022 12:22
Copy link
Contributor

@grdddj grdddj left a comment

Choose a reason for hiding this comment

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

Just a small review/some ideas, I cannot judge the signing details very much

core/src/apps/bitcoin/sign_tx/progress.py Outdated Show resolved Hide resolved
core/src/apps/bitcoin/sign_tx/progress.py Show resolved Hide resolved
@andrewkozlik andrewkozlik force-pushed the andrewkozlik/coinjoin-speed branch from 2366402 to 76e451d Compare October 12, 2022 07:48
Copy link
Member

@mmilata mmilata left a comment

Choose a reason for hiding this comment

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

Changes to signing flow look good as far as my limited understanding of it goes. Stared a bit at the C changes and did not find any issues. Good job with the speedup!

core/src/trezor/crypto/bech32.py Show resolved Hide resolved
core/src/apps/bitcoin/sign_tx/progress.py Outdated Show resolved Hide resolved
@matejcik
Copy link
Contributor

Is it OK to ignore the option in T1

Yes. This doesn't break compatibility in any way either, the caller is free to ignore the serialized chunk.

Naming. I'd prefer serialize

this is resolved now, right?

@matejcik
Copy link
Contributor

I tried to optimize the Python implementation, but it didn't help and I couldn't locate any particular operation that is responsible for the huge time difference.

could it be the checksum calculation? native ints are only like 28 bits IIRC, so anything bigger is a mpz bigint

@andrewkozlik andrewkozlik force-pushed the andrewkozlik/coinjoin-speed branch from b4e4326 to e1316fa Compare October 14, 2022 13:56
@andrewkozlik
Copy link
Contributor Author

I tried to optimize the Python implementation, but it didn't help and I couldn't locate any particular operation that is responsible for the huge time difference.

could it be the checksum calculation? native ints are only like 28 bits IIRC, so anything bigger is a mpz bigint

I suppose it could be a part of the problem. When I was researching the speedup, I couldn't localize the time-demanding part to any particular step inside bech32.decode().

@andrewkozlik andrewkozlik merged commit ee8c596 into master Oct 17, 2022
@andrewkozlik andrewkozlik deleted the andrewkozlik/coinjoin-speed branch October 17, 2022 13:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bitcoin Bitcoin related code Code improvements core Trezor Core firmware. Runs on Trezor Model T and T2B1. R&D Research and development team related
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

5 participants