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

Deep freeze (#5187) #5187

Open
wants to merge 18 commits into
base: develop
Choose a base branch
from
Open

Deep freeze (#5187) #5187

wants to merge 18 commits into from

Conversation

vvysokikh1
Copy link
Collaborator

@vvysokikh1 vvysokikh1 commented Nov 11, 2024

High Level Overview of Change

This PR implements Deep Freeze feature described in this specification: XLS-77d

Context of Change

Added new flags and functionality allowing trust lines to be deep-frozen.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (non-breaking change that only restructures code)
  • Performance (increase or change in throughput and/or latency)
  • Tests (you added tests for code that already exists, or your new feature included in this PR)
  • Documentation update
  • Chore (no impact to binary, e.g. .gitignore, formatting, dropping support for older tooling)
  • Release

API Impact

  • Public API: New feature (new methods and/or new fields)
  • Public API: Breaking change (in general, breaking changes should only impact the next api_version)
  • libxrpl change (any change that may affect libxrpl or dependents of libxrpl)
  • Peer protocol change (must be backward compatible or bump the peer protocol version)

Comment on lines 52 to 61
{
return terNO_LINE;
}
if (view.rules().enabled(featureDeepFreeze))
{
if (sle->isFlag(lsfHighDeepFreeze) || sle->isFlag(lsfLowDeepFreeze))
{
return terNO_LINE;
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

In the original code, the if (sle->isFlag((dst > src) ? lsfHighFreeze : lsfLowFreeze)) is used to check whether the high or low freeze flag is set based on whether the dst Account ID is numerically larger or smaller than the src Account ID.

In the newly added code, since there are differences in behavior between which party (account) sets the deep freeze flag, shouldn't the same logic be used here as above, i.e. if (sle->isFlag((dst > src) ? lsfHighDeepFreeze : lsfLowDeepFreeze))?

Or, since the spec states:

When an issuer enacts Deep-freeze, the following rules apply to the tokens in that trust line:

  • ...
  • The counterparty can neither send nor receive from others on the deep-frozen trustline
  • ...

and:

An individual address can deep-freeze its trust line to an issuer or financial institution ...

  • It prevents other addresses from sending that financial institution's tokens to the individual address.
  • It also prevents the individual address from sending the token to other non-issuer addresses.

then if either party deep freezes the trust line then terNO_LINE should be returned, in which case the current code is fine as-is?

Copy link
Collaborator

@shawnxie999 shawnxie999 Nov 14, 2024

Choose a reason for hiding this comment

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

applying the same logic could work. But i don't believe that's necessary because, if either low or high deep freeze flag is enabled, the trustline balance cannot be changed regardless which party enacted the deepfreeze (other than the scenario where there is a direct payment between issuer and holder)

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'll share a comment I wrote elsewhere on this topic:

I'm actually having a hard time wrapping my head around the idea that if either deep freeze flag is set, then the line is deep frozen.
The way I'm trying to reconcile it is that the only reason for the two flags is to be able to keep track of which side of the trust line set it, and thus can clear it.

It would probably be worth adding an explicit comment here and anywhere else relevant that, unlike regular freeze (or really any of the other flags), a deep frozen trustline acts the same regardless of which side froze it. The only reason for two flags is to track which side(s) froze the line.

Comment on lines 416 to 443
bool willSetFreeze = false;
if (bSetFreeze && !bClearFreeze && !sle->isFlag(lsfNoFreeze))
{
uFlagsOut |= (bHigh ? lsfHighFreeze : lsfLowFreeze);
willSetFreeze = true;
}
else if (bClearFreeze && !bSetFreeze)
{
uFlagsOut &= ~(bHigh ? lsfHighFreeze : lsfLowFreeze);
}

bool const alreadyFrozen =
bHigh ? sle->isFlag(lsfHighFreeze) : sle->isFlag(lsfLowFreeze);
if (bSetDeepFreeze && !bClearDeepFreeze &&
(willSetFreeze || alreadyFrozen))
{
if (ctx_.view().rules().enabled(featureDeepFreeze))
uFlagsOut |= (bHigh ? lsfHighDeepFreeze : lsfLowDeepFreeze);
else
return temMALFORMED;
}
else if (bClearDeepFreeze && !bSetDeepFreeze)
{
if (ctx_.view().rules().enabled(featureDeepFreeze))
uFlagsOut &= ~(bHigh ? lsfHighDeepFreeze : lsfLowDeepFreeze);
else
return temMALFORMED;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Wouldn't the following be easier to follow?

Suggested change
bool willSetFreeze = false;
if (bSetFreeze && !bClearFreeze && !sle->isFlag(lsfNoFreeze))
{
uFlagsOut |= (bHigh ? lsfHighFreeze : lsfLowFreeze);
willSetFreeze = true;
}
else if (bClearFreeze && !bSetFreeze)
{
uFlagsOut &= ~(bHigh ? lsfHighFreeze : lsfLowFreeze);
}
bool const alreadyFrozen =
bHigh ? sle->isFlag(lsfHighFreeze) : sle->isFlag(lsfLowFreeze);
if (bSetDeepFreeze && !bClearDeepFreeze &&
(willSetFreeze || alreadyFrozen))
{
if (ctx_.view().rules().enabled(featureDeepFreeze))
uFlagsOut |= (bHigh ? lsfHighDeepFreeze : lsfLowDeepFreeze);
else
return temMALFORMED;
}
else if (bClearDeepFreeze && !bSetDeepFreeze)
{
if (ctx_.view().rules().enabled(featureDeepFreeze))
uFlagsOut &= ~(bHigh ? lsfHighDeepFreeze : lsfLowDeepFreeze);
else
return temMALFORMED;
}
if (bSetFreeze && !bClearFreeze && !sle->isFlag(lsfNoFreeze))
{
uFlagsOut |= (bHigh ? lsfHighFreeze : lsfLowFreeze);
}
else if (bClearFreeze && !bSetFreeze)
{
uFlagsOut &= ~(bHigh ? lsfHighFreeze : lsfLowFreeze);
}
// First (un)set the deep freeze flag independently of the regular freeze flag.
if (bSetDeepFreeze && !bClearDeepFreeze && !sle->isFlag(lsfNoFreeze))
{
uFlagsOut |= (bHigh ? lsfHighDeepFreeze : lsfLowDeepFreeze);
}
else if (bClearDeepFreeze && !bSetDeepFreeze)
{
uFlagsOut &= ~(bHigh ? lsfHighDeepFreeze : lsfLowDeepFreeze);
}
// Next ensure the deep freeze flag can only be set when the regular freeze flag is set.
if ((!(uFlagsOut & lsfLowFreeze) && (uFlagsOut & lsfLowDeepFreeze)) ||
(!(uFlagsOut & lsfHighFreeze) && (uFlagsOut & lsfHighDeepFreeze)))
{
return temMALFORMED;
}

If you wish you can gate the deep freeze section by if (bDeepFreezeEnabled), but since the set and clear flags will be zero when deep freeze is disabled, this wouldn't be absolutely necessary. There'd be a tiny performance hit as long as deep freeze is disabled (namely, extra conditional statements that would always be no-ops), while gaining performance when it is enabled (namely, not performing the deep freeze enabled 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.

I was looking into this and I have a strong feeling that we miss the correct check on ClearFreeze. It should also include checking if the trust line is not deep frozen.

isFrozen(view, account, currency, issuer))
((zeroIfFrozen == fhZERO_IF_FROZEN) &&
isFrozen(view, account, currency, issuer)) ||
isDeepFrozen(view, account, currency, issuer))
Copy link
Collaborator

Choose a reason for hiding this comment

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

This code change, by adding an additional isDeepFrozen check, should be unnecessary. Namely, deep freezing a trust line is only possible when it is already regularly frozen, while having deep freeze without regular freeze is not allowed; hence, the regular freeze check is sufficient to capture both cases.

After reverting this change, the newly introduced isDeepFreeze function can then also be removed as it's not used anywhere else.

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 the isDeepFrozen check is necessary because either side can deep freeze an account. It's possible that the account set the deep freeze flag. isFrozen would return false in that case, because "normal" freeze only stops the other side of the line from sending.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Good point - I stand corrected.

Copy link
Collaborator

@ximinez ximinez left a comment

Choose a reason for hiding this comment

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

Partial review. I didn't go through all the tests.

src/xrpld/app/tx/detail/CreateOffer.cpp Outdated Show resolved Hide resolved
src/xrpld/app/tx/detail/SetTrust.cpp Outdated Show resolved Hide resolved
src/xrpld/ledger/detail/View.cpp Outdated Show resolved Hide resolved
isFrozen(view, account, currency, issuer))
((zeroIfFrozen == fhZERO_IF_FROZEN) &&
isFrozen(view, account, currency, issuer)) ||
isDeepFrozen(view, account, currency, issuer))
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 the isDeepFrozen check is necessary because either side can deep freeze an account. It's possible that the account set the deep freeze flag. isFrozen would return false in that case, because "normal" freeze only stops the other side of the line from sending.

Comment on lines 314 to 316
((zeroIfFrozen == fhZERO_IF_FROZEN) &&
isFrozen(view, account, currency, issuer)) ||
isDeepFrozen(view, account, currency, issuer))
Copy link
Collaborator

Choose a reason for hiding this comment

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

However, I do think there is a more subtle mistake here, which is that the check for isDeepFrozen ignores the zeroIfFrozen flag. My understanding of fhIGNORE_FREEZE is that it should ignore both types of freezes because the caller needs the true balance of the trust line at that point.

Suggested change
((zeroIfFrozen == fhZERO_IF_FROZEN) &&
isFrozen(view, account, currency, issuer)) ||
isDeepFrozen(view, account, currency, issuer))
(zeroIfFrozen == fhZERO_IF_FROZEN) &&
(isFrozen(view, account, currency, issuer) ||
isDeepFrozen(view, account, currency, issuer)))

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 wonder if that is actually a mistake. This function is used to check if this account is allowed to spend. So this code:

zeroIfFrozen == fhZERO_IF_FROZEN) &&
	         (isFrozen(view, account, currency, issuer)

verifies that this trust line is frozen by issuer? If fhIGNORE_FREEZE set that means that it was frozen by currency holder and he can still spend if he wants?

In this case the deep freeze should not look at the flag and not allow spending in any case.

Please advise if you think this is not the correct understanding

Copy link
Collaborator Author

@vvysokikh1 vvysokikh1 Nov 18, 2024

Choose a reason for hiding this comment

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

EDITED: changed to handle both freeze and deep freeze only if fhZERO_IF_FROZEN is set
I suggest this refactoring to make it a bit more readable:

    auto const sle = view.read(keylet::line(account, issuer, currency));
    auto const allowBalance = [&]() {
        if (!sle)
        {
            return false;
        }

        if (zeroIfFrozen == fhZERO_IF_FROZEN)
        {
            if (isFrozen(view, account, currency, issuer) ||
                isDeepFrozen(view, account, currency, issuer))
            {
                return false;
            }
        }

        return true;
    }();

    if (allowBalance)
    {
        amount = sle->getFieldAmount(sfBalance);
        if (account > issuer)
        {
            // Put balance in account terms.
            amount.negate();
        }
        amount.setIssuer(issuer);
    }
    else
    {
        amount.clear(Issue{currency, issuer});
    }

src/xrpld/app/tx/detail/SetTrust.cpp Outdated Show resolved Hide resolved
Copy link
Collaborator

@ximinez ximinez left a comment

Choose a reason for hiding this comment

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

Partial review

src/xrpld/app/paths/detail/StepChecks.h Outdated Show resolved Hide resolved
src/xrpld/app/tx/detail/SetTrust.cpp Outdated Show resolved Hide resolved
src/xrpld/app/tx/detail/SetTrust.cpp Outdated Show resolved Hide resolved
include/xrpl/protocol/LedgerFormats.h Outdated Show resolved Hide resolved
src/xrpld/app/tx/detail/SetTrust.cpp Outdated Show resolved Hide resolved
include/xrpl/protocol/detail/features.macro Outdated Show resolved Hide resolved
src/xrpld/ledger/detail/View.cpp Outdated Show resolved Hide resolved
src/xrpld/ledger/detail/View.cpp Show resolved Hide resolved
src/test/app/AMMExtended_test.cpp Outdated Show resolved Hide resolved
@@ -4397,6 +4397,48 @@ struct AMM_test : public jtx::AMMTest
0,
std::nullopt,
{features});

// Individually deep frozen account
if (features[featureDeepFreeze])
Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't need to repeat the code. Just add a conditional error:

auto const err = features[featureDeepFreeze] ? ter(tecPATH_DRY) : ter(tesSUCCESS)

And then use it in pay:

env(pay(alice, carol, USD(1)),
            path(~USD),
            sendmax(XRP(10)),
            txflags(tfNoRippleDirect | tfPartialPayment),
            err);

Copy link
Collaborator

Choose a reason for hiding this comment

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

Actually, you shouldn't test here and other updated unit-tests if the feature is disabled because trust is going to fail. You need a separate test for disabled feature in SetTrust and also add to SetTrust tests for invalid combination of the flags. Also need to add OfferCreate and offer crossing, if not added yet, to Offer.

src/test/app/AMMExtended_test.cpp Outdated Show resolved Hide resolved
src/xrpld/app/tx/detail/SetTrust.cpp Outdated Show resolved Hide resolved
src/xrpld/app/paths/detail/StepChecks.h Outdated Show resolved Hide resolved
@vvysokikh1 vvysokikh1 changed the title [WIP] Deep freeze Deep freeze (#5187) Dec 10, 2024
@vvysokikh1 vvysokikh1 marked this pull request as ready for review December 10, 2024 15:41
Copy link
Collaborator

@gregtatcam gregtatcam left a comment

Choose a reason for hiding this comment

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

I left a few comments.
Check unit-tests are failing most likely because there is no deep freeze check in CreateCheck transactor.
I still have to review the unit-tests.

bool const bSetFreeze = (uTxFlags & tfSetFreeze);
bool const bSetDeepFreeze = (uTxFlags & tfSetDeepFreeze);

if (bNoFreeze)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't need nested if's.

{
permRmOffer(entry->key());
JLOG(j_.trace())
<< "Removing deep frozen unfunded offer " << entry->key();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should add offer_ = TOffer<TIn, TOut>{};

// able to create a check for USD to alice.
env(trust(gw1, alice["USD"](0), tfSetFreeze | tfSetDeepFreeze));
env.close();
env(check::create(alice, bob, USD(50)), ter(tecFROZEN));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should add deep freeze check to CreateCheck transactor.

Comment on lines +286 to +288
auto const sle = view.read(keylet::line(account, issuer, currency));
return sle &&
(sle->isFlag(lsfHighDeepFreeze) || sle->isFlag(lsfLowDeepFreeze));
Copy link
Collaborator

Choose a reason for hiding this comment

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

personally i find it easier to read as:

 auto const sle = view.read(keylet::line(account, issuer, currency));
 if (!sle)
   return false;

 return sle->isFlag(lsfHighDeepFreeze) || sle->isFlag(lsfLowDeepFreeze);

@@ -857,6 +893,7 @@ trustCreate(
const bool bAuth, // --> authorize account.
const bool bNoRipple, // --> others cannot ripple through
const bool bFreeze, // --> funds cannot leave
bool bDeepFreeze, // --> can neither receive nor send funds
Copy link
Collaborator

Choose a reason for hiding this comment

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

should this be const bool?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

not really, unless we have to follow the same approach which is kind of pointless.

There's no real benefit of making bool const here and some people consider that bad practice altogether.

Copy link
Collaborator

@gregtatcam gregtatcam left a comment

Choose a reason for hiding this comment

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

I'd like to see:

  • more tests where account deep freezes the trustline as opposed to the issuer and update all unit-tests for this scenario.
  • side by-side tests (if feature {} else {}), where applicable, to show a difference between regular freeze and deep freeze
  • more payment tests with AMM and general payment tests with more steps in the path, where the frozen path is in the middle.
  • should test the rippling via an account; i.e. an account is in the path.

@@ -186,6 +186,181 @@ class Freeze_test : public beast::unit_test::suite
}
}

void
testDeepFreeze(FeatureBitset features)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should add tests where an account freezes a trustline as opposed to an issuer. All tests - freeze, offer, payment should have these tests. I think check has these tests.

env.close();
env(pay(alice, bob, USD(1)), ter(tecPATH_DRY));
env.close();
env(check::create(bob, alice, USD(50)));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't this fail on check create? Alice is not allowed to receive anyways.

Copy link
Collaborator

Choose a reason for hiding this comment

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

from my understanding we would always allow check creation, it would fail only when someone tries to cash it


// Individually deep frozen account
if (features[featureDeepFreeze])
{
Copy link
Collaborator

Choose a reason for hiding this comment

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

AMM Create, Deposit, Withdraw are not impacted in any way by deep freeze, correct? Just want to check.

Copy link
Collaborator

Choose a reason for hiding this comment

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

from design perspective it should not, deep freeze imposes a superset of restrictions of the existing regular freeze feature. so any restrictions in these transactions should have already been implemented through the freeze feature already.

auto let =
affected[0u][sfModifiedNode.fieldName][sfLedgerEntryType.fieldName];
BEAST_EXPECT(let == jss::AccountRoot);
// test: cannot deep freeze already frozen line
Copy link
Collaborator

Choose a reason for hiding this comment

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

Wrong comment? It's not already frozen, it can't be frozen.

Copy link
Collaborator Author

@vvysokikh1 vvysokikh1 Jan 6, 2025

Choose a reason for hiding this comment

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

It's in the context of no freeze, when no freeze is enacted we can't deep freeze already frozen line

Edit: global freeze -> no freeze

env.fund(XRP(10000), G1, A1, A2, A3);
env.close();

env.trust(G1["USD"](1000), A1, A2, A3);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Consider using IOU type instead of operator[]. IMHO it's a bit easier to read:

auto const USD = G1["USD"];

env.trust(USD(1000), A1, A2, A3);

balance(A2, G1["USD"](998)),
offers(A1, 0));

// test: cannot create passive sell offer
Copy link
Collaborator

Choose a reason for hiding this comment

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

should tfPassive be set?

env(trust(G1, A1["USD"](0), tfClearFreeze | tfClearDeepFreeze));
env.close();
env.require(balance(A1, G1["USD"](1001)), offers(A1, 0));
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should the same tests be implemented without the DeepFreeze to show the difference if any?

env(trust(G1, A1["USD"](0), tfClearFreeze | tfClearDeepFreeze));
env.close();
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should the same tests be implemented without the DeepFreeze to show the difference if any?

Copy link
Collaborator

Choose a reason for hiding this comment

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

seems to missing some test when cashing a check. Example:

  1. alice is deep frozen on USD
  2. bob creates a USD check
  3. alice tries to cash the check (which should fail)

for these tests we should also add pre/post amendment behavior as Greg noted, would be easier to compare the difference in behavior

Comment on lines +4132 to 4138
testRippleState(sa - featureDeepFreeze);
testGlobalFreeze(sa - featureDeepFreeze);
testOffersWhenFrozen(sa - featureDeepFreeze);

testRippleState(sa);
testGlobalFreeze(sa);
testOffersWhenFrozen(sa);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can be refactored

        for (auto const features : {sa, sa - featureDeepFreeze})
        {
            testRippleState(features);
            testGlobalFreeze(features);
            testOffersWhenFrozen(features);
        }

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.

6 participants