-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
base: develop
Are you sure you want to change the base?
Deep freeze (#5187) #5187
Conversation
Specification: XRPLF/XRPL-Standards#220
to accepting the amendment.
{ | ||
return terNO_LINE; | ||
} | ||
if (view.rules().enabled(featureDeepFreeze)) | ||
{ | ||
if (sle->isFlag(lsfHighDeepFreeze) || sle->isFlag(lsfLowDeepFreeze)) | ||
{ | ||
return terNO_LINE; | ||
} | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
src/xrpld/app/tx/detail/SetTrust.cpp
Outdated
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; | ||
} |
There was a problem hiding this comment.
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?
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).
There was a problem hiding this comment.
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.
src/xrpld/ledger/detail/View.cpp
Outdated
isFrozen(view, account, currency, issuer)) | ||
((zeroIfFrozen == fhZERO_IF_FROZEN) && | ||
isFrozen(view, account, currency, issuer)) || | ||
isDeepFrozen(view, account, currency, issuer)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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/ledger/detail/View.cpp
Outdated
isFrozen(view, account, currency, issuer)) | ||
((zeroIfFrozen == fhZERO_IF_FROZEN) && | ||
isFrozen(view, account, currency, issuer)) || | ||
isDeepFrozen(view, account, currency, issuer)) |
There was a problem hiding this comment.
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.
src/xrpld/ledger/detail/View.cpp
Outdated
((zeroIfFrozen == fhZERO_IF_FROZEN) && | ||
isFrozen(view, account, currency, issuer)) || | ||
isDeepFrozen(view, account, currency, issuer)) |
There was a problem hiding this comment.
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.
((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))) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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});
}
9421c37
to
c7c26ac
Compare
ba87028
to
de58a26
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Partial review
Co-authored-by: Ed Hennis <ed@ripple.com>
@@ -4397,6 +4397,48 @@ struct AMM_test : public jtx::AMMTest | |||
0, | |||
std::nullopt, | |||
{features}); | |||
|
|||
// Individually deep frozen account | |||
if (features[featureDeepFreeze]) |
There was a problem hiding this comment.
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);
There was a problem hiding this comment.
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
.
There was a problem hiding this 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) |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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.
auto const sle = view.read(keylet::line(account, issuer, currency)); | ||
return sle && | ||
(sle->isFlag(lsfHighDeepFreeze) || sle->isFlag(lsfLowDeepFreeze)); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this 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) |
There was a problem hiding this comment.
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))); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]) | ||
{ |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)); | ||
} |
There was a problem hiding this comment.
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(); | ||
} | ||
} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
- alice is deep frozen on USD
- bob creates a USD check
- 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
testRippleState(sa - featureDeepFreeze); | ||
testGlobalFreeze(sa - featureDeepFreeze); | ||
testOffersWhenFrozen(sa - featureDeepFreeze); | ||
|
||
testRippleState(sa); | ||
testGlobalFreeze(sa); | ||
testOffersWhenFrozen(sa); |
There was a problem hiding this comment.
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);
}
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
.gitignore
, formatting, dropping support for older tooling)API Impact
libxrpl
change (any change that may affectlibxrpl
or dependents oflibxrpl
)