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

cap28 - remove pre auth signers on signature verification failure #2379

Merged
merged 3 commits into from
Apr 3, 2020

Conversation

sisuresh
Copy link
Contributor

@sisuresh sisuresh commented Jan 9, 2020

Description

Implements https://github.com/stellar/stellar-protocol/blob/master/core/cap-0028.md

Checklist

  • Reviewed the contributing document
  • Rebased on top of master (no merge commits)
  • Ran clang-format v5.0.0 (via make format or the Visual Studio extension)
  • Compiles
  • Ran all tests
  • If change impacts performance, include supporting evidence per the performance document

Copy link
Contributor

@jonjove jonjove left a comment

Choose a reason for hiding this comment

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

This change looks correct (other than the fact that it doesn't account for txBAD_SEQ as we discussed), but I feel like it should be possible to do this more efficiently and in a more "obvious" way.

src/transactions/OperationFrame.cpp Outdated Show resolved Hide resolved
src/transactions/TransactionFrame.cpp Outdated Show resolved Hide resolved
@sisuresh sisuresh force-pushed the cap_28 branch 2 times, most recently from 8b4c293 to a8634fb Compare February 21, 2020 23:52
Copy link
Contributor

@jonjove jonjove left a comment

Choose a reason for hiding this comment

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

I think from a PR clarity perspective, it would probably be much better to make the change that simplifies how one-time signers are removed (eg. remove mUsedOneTimeSigners and move everything into TransactionFrame) before the protocol change. That would make it possible to review the refactor using today's semantics. Then the actual protocol change should be just a few lines on top of that.

I had a difficult time reviewing because the two changes are kind of blended together.

src/transactions/TransactionFrame.cpp Outdated Show resolved Hide resolved
src/transactions/TransactionFrame.h Outdated Show resolved Hide resolved
src/transactions/TransactionFrame.cpp Outdated Show resolved Hide resolved
src/transactions/TransactionFrame.cpp Outdated Show resolved Hide resolved
src/transactions/TransactionFrame.cpp Outdated Show resolved Hide resolved
src/transactions/TransactionFrame.h Outdated Show resolved Hide resolved
@sisuresh sisuresh force-pushed the cap_28 branch 2 times, most recently from 420b53d to 45643a2 Compare March 6, 2020 01:03
Copy link
Contributor

@jonjove jonjove left a comment

Choose a reason for hiding this comment

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

This is getting close: found another bug, and a few nitpicks. This is way easier to review since you re-ordered the commits, but can you re-write the timestamps so GitHub displays them in the right order?

src/transactions/TransactionFrame.cpp Outdated Show resolved Hide resolved
src/transactions/TransactionFrame.h Outdated Show resolved Hide resolved
@@ -684,6 +692,12 @@ TransactionFrame::apply(Application& app, AbstractLedgerTxn& ltx,
auto signaturesValid = cv >= (ValidationType::kInvalidPostAuth) &&
processSignatures(signatureChecker, ltxTx);

if (cv >= ValidationType::kInvalidRemoveOneTimeSigners &&
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we should instead modify processSignatures to deal with this (it's signature related, after all): this code was hard to follow, and this makes it harder.

src/transactions/TransactionFrame.cpp Outdated Show resolved Hide resolved
return; // probably account was removed due to merge operation
}

if (removeAccountSigner(header, account, signerKey))
Copy link
Contributor

Choose a reason for hiding this comment

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

we could clean this up too:

  1. I'd expect normalizeSigners to be called from removeAccountSigner (there is no reason to split this as a an account entry with signers unsorted is illegal)
  2. as removing a signer doesn't change the sort order, we don't need to normalize
  3. basically, we can just remove the call to normalizeSigners
  4. we already check that signers are sorted with the LedgerEntryIsValid invariant (so no need to add an extra assert)

removeUsedOneTimeSignerKeys(signatureChecker, ltxOuter);
if (ledgerVersion < 13)
{
removeOneTimeSignerFromAllSourceAccounts(ltxOuter);
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe we could add a bool property to signatureChecker (set where we used to add to the map of used one time signatures) to know if one time signers were used at all to decide if we want to call that function?
The new code is always going to loop over all accounts / all signers (so loading up to 101 accounts and perform 101*20=2020 extra checks for no reason)

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this makes sense. In the event that we aren't checking signatures (eg. cv < ValidationType::kInvalidPostAuth) then we have to loop over all accounts and signers, since they haven't been checked by SignatureChecker. In the event that we are checking signatures (eg. cv >= ValidationType::kInvalidPostAuth) then we have already loaded the accounts in the SignatureChecker so they are in memory and looping over the accounts and signers requires no I/O.

In the worst case, we have to do the full loop including I/O. In all other cases, we have already done the I/O so it shouldn't be expensive. If at a future date we assess that this is an unacceptable amount of overhead, let's optimize and make the code more complicated at that point.

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess this was more a comment on the fact that in the previous version we had 0 overhead when not using one time signers to an implementation that bears the overhead all the time (all that for a feature that is virtually never used).

I am quite not sure what the overhead of this is: maintaining LedgerTxn is not free, we had examples in the past where the overhead was managing maps.

I guess what we can do is perform some performance test comparison of this new implementation to compare it against 12.5.0, replaying protocol 12 ledger ranges with version 13.0.0 both with protocol 12 and 13.

@sisuresh sisuresh force-pushed the cap_28 branch 3 times, most recently from df7a018 to d2e2bb6 Compare March 13, 2020 18:03
processSignatures(signatureChecker, ltxTx);
auto signaturesValid =
cv >= (ValidationType::kMaybeValidCheckSignatures) &&
processSignatures(signatureChecker, ltxTx);
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand the name kMaybeValidCheckSignatures: it's invalid for sure as it failed with txINSUFFICIENT_BALANCE.

If we want to rename something, I'd rename kFullyValid that is kMaybeValid as we didn't check for signatures yet.

Also, per my previous comment, the only reason we're calling processSignature in this case is to remove one time signers, so it would be a lot clearer to move the call to removeOneTimeSignerFromAllSourceAccounts into that processSignature function.

I would change the contract of processSignatures to return if the transaction is valid.
So this code would instead look like

valid = processSignatures(cv, signatureChecker, ltxTx);

(and we'd remove the valid = signaturesValid && (cv == ValidationType::kFullyValid); later)

As we make this change, I think it's better to "fast fail" with the proper error code in protocol 13. This way the transaction will fail with txINSUFFICIENT_BALANCE instead of (potentially) some other error like txBAD_AUTH_EXTRA.

the actual function then becomes:

bool
TransactionFrame::processSignatures(ValidationType cv, SignatureChecker& signatureChecker,
                                    AbstractLedgerTxn& ltxOuter)
{
    auto maybeValid = (cv == ValidationType::kMaybeValid);
    auto allOpsValid = true;
    uint32_t ledgerVersion;
    LedgerTxn ltx(ltxOuter);
    ledgerVersion = ltx.loadHeader().current().ledgerVersion;
    if (ledgerVersion < 10)
    {
        return maybeValid;
    }

    // check if we need to fast fail and use the original error code
    if (ledgerVersion >= 13 && !maybeValid)
    {
        removeOneTimeSignerFromAllSourceAccounts(ltx);
        ltx.commit();
        return false;
    }
    // older versions of the protocol only fast fail in a subset of cases
    if (ledgerVersion < 13 && cv < ValidationType::kInvalidPostAuth)
    {
        return false;
    }

    {
        // scope here to avoid potential side effects of loading source accounts
        LedgerTxn ltx2(ltx);            
        for (auto& op : mOperations)
        {
            if (!op->checkSignature(signatureChecker, ltx2, false))
            {
                allOpsValid = false;
            }
        }
    }

    removeOneTimeSignerFromAllSourceAccounts(ltx);

    ltx.commit();

    if (!allOpsValid)
    {
        markResultFailed();
        return false;
    }

    if (!signatureChecker.checkAllSignaturesUsed())
    {
        getResult().result.code(txBAD_AUTH_EXTRA);
        return false;
    }

    return maybeValid;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

As I added a comment on the scope around LedgerTxn ltx2(ltx), I realized that:

  • either this scope is not needed (pretty much sure it is)
  • or we have a potential bug with the new removeOneTimeSignerFromAllSourceAccounts that loads all source accounts - it should only load & modify the ones that get changed

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand the name kMaybeValidCheckSignatures: it's invalid for sure as it failed with txINSUFFICIENT_BALANCE.

As we make this change, I think it's better to "fast fail" with the proper error code in protocol 13. This way the transaction will fail with txINSUFFICIENT_BALANCE instead of (potentially) some other error like txBAD_AUTH_EXTRA.

Yes, you're absolutely right about this. When I wrote that comment I didn't notice that we check the balance then check the signatures regardless of the result of the balance check. In an ideal world, we would never have even checked signatures in this case. Let's make this change in protocol 13. Let's also rename kFullyValid to kMaybeValid as you suggest.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also in the code @MonsieurNicolas suggested, I don't believe that LedgerTxn ltx(ltxOuter); (line 8) is required. However, the scope with LedgerTxn ltx2(ltx); is required (but let's just call it ltx now).

You are correct that removeOneTimeSignerFromAllSourceAccounts is currently wrong, it should only load and modify the ones that get changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I modified removeOneTimeSignerFromAllSourceAccounts to first look for the signer through loadAccountWithoutRecord.

Something to scrutinize here is that I use the iterator derived from the loadAccountWithoutRecord account signers to erase from the loadAccount account signers to avoid a second search. This should be fine since the LedgerEntry from the first load should be cached, but I'm wondering if it might be safer and cleaner to just do the second search if we get a match on the signer (which isn't a common scenario). @jonjove

I also added a check in TxTests.cpp that verifies any delta from an early failure post V13 must have a signature removed.

Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't safe. Every time a new LedgerEntry is loaded it produces a copy. So the vector of signers for the mutable LedgerTxnEntry will be a copy of your initial vector from the ConstLedgerTxnEntry; the initial vector may even have been destructed.

That being said, you don't actually need to search again. The signers are sorted which guarantees they won't change order, so you can calculate the index of the relevant signer. Then you can just index into the new vector directly (I would recommend adding an assert here to check that you actually found the right signer).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@sisuresh sisuresh force-pushed the cap_28 branch 2 times, most recently from 5281757 to bd253bb Compare March 16, 2020 20:02
Copy link
Contributor

@jonjove jonjove left a comment

Choose a reason for hiding this comment

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

This looks very good now. I had couple of very minor nitpicks, and one slightly more important comment to make this easier/safer to maintain in the future.

src/transactions/TransactionFrame.cpp Outdated Show resolved Hide resolved
src/transactions/TransactionFrame.cpp Outdated Show resolved Hide resolved
src/transactions/TransactionFrame.cpp Outdated Show resolved Hide resolved
src/transactions/TransactionFrame.cpp Outdated Show resolved Hide resolved
src/transactions/TransactionFrame.cpp Outdated Show resolved Hide resolved
Copy link
Contributor

@jonjove jonjove left a comment

Choose a reason for hiding this comment

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

This mostly looks good. The main issue is that it doesn't pass tests (because of an error in the tests, not the implementation). The other three comments are all about making the code easier to maintain and reason about. I spent an unusually long time trying to understand how "merge source account before payment" works, so it is probably worth simplifying and adding comments.

src/test/TxTests.cpp Show resolved Hide resolved
src/transactions/TransactionFrame.cpp Outdated Show resolved Hide resolved
src/transactions/test/TxEnvelopeTests.cpp Outdated Show resolved Hide resolved
src/transactions/test/TxEnvelopeTests.cpp Outdated Show resolved Hide resolved
@jonjove
Copy link
Contributor

jonjove commented Apr 1, 2020

This is ready to merge once you squash.

@MonsieurNicolas
Copy link
Contributor

r+ 70878d5

@latobarita latobarita merged commit 467666f into stellar:master Apr 3, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants