Eval: Move congestion monitoring and fee escalation on-chain#6400
Eval: Move congestion monitoring and fee escalation on-chain#6400jannotti wants to merge 25 commits intoalgorand:masterfrom
Conversation
dd908fe to
f17cee2
Compare
b5f1d18 to
68c0b54
Compare
53acaf3 to
574aefd
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #6400 +/- ##
==========================================
- Coverage 47.90% 47.70% -0.20%
==========================================
Files 662 654 -8
Lines 87991 87982 -9
==========================================
- Hits 42149 41975 -174
- Misses 43085 43238 +153
- Partials 2757 2769 +12 ☔ View full report in Codecov by Sentry. |
87fa07d to
8b6d6ee
Compare
8b6d6ee to
498d8b1
Compare
| GenesisID: "test", | ||
| GenesisHash: crypto.Digest{0x02, 0x02}, | ||
| Load: 0, | ||
| CongestionTax: 2_000_000, // Weird, but allowed (maybe there was a downgrade?) |
There was a problem hiding this comment.
Okay, now CongestionTax comparison in PreCheck is not consensus gated.
As I understand random values in CongestionTax not really allowed in pre-congestion consensus? - Looks like need one more test case with prev.CongestionTax = 0 and current.CongestionTax != 0 both pre-congestion consensus, and have PreCheck to fail?
There was a problem hiding this comment.
I can do that if you want, but in a sense, there is a stronger check already present.
By testing this:
prev := BlockHeader{
Round: 1,
GenesisID: "test",
GenesisHash: crypto.Digest{0x02, 0x02},
Load: 0,
CongestionTax: 2_000_000, // Weird, but allowed (maybe there was a downgrade?)
}
prev.CurrentProtocol = protoNoCongestion
current := BlockHeader{
Round: prev.Round + 1,
GenesisID: prev.GenesisID,
GenesisHash: prev.GenesisHash,
Branch: prev.Hash(),
Branch512: prev.Hash512(),
Load: 0,
CongestionTax: 1_700_000, // Tapers from above. (3*.9)-1=1.7
}
current.CurrentProtocol = protoNoCongestion
We show that the CongestionTax must be correct, because we show it goes from 2.0 to 1.7. If it must be correct, then it must be 0, since all pre-upgrade Loads are 0.
There was a problem hiding this comment.
I do think I should show that the 1_700_000 is required to be exact. I forgot to do that.
I'll make that change, let me know if you want the 0s too.
There was a problem hiding this comment.
I agree exact values make sense but this test does not show reality since it is impossible to have non-zero CongestionTax when prev Load is zero (given non-congested consensus). I think we still need to have prev zero and current non-zero error case to make sure such blocks are not really possible.
There was a problem hiding this comment.
Sure, I'll put that in.
This test does reflect reality if we ever turn off LoadTracking in the future. The CongestionTax might be non-zero when that happens, and so this will happen and the tax will slowly get back down to zero.
| groupPaid = basics.AddSaturate(groupPaid, ptxn.Txn.Fee.Raw) | ||
| // Check fees in the existing group first. Allows fee pooling in inner groups. | ||
| usage, groupPaid, _ := transactions.SummarizeFees(cx.subtxns) // tip does not appear in inner | ||
| usage = basics.AddSaturate(usage, 1e6) // +1e6 because we're adding a txn |
There was a problem hiding this comment.
This 1e6 constant is because inner txns can't have a non-standard FeeFactor ... I guess that will be true forever but might want to call that out
There was a problem hiding this comment.
Not exactly. Yes, it is true that currently there are no legal non-minfee transactions, but in the PR that adds them as outers (for big notes, programs, etc) I did not intend to block them here.
But notice that this is when we first add a transaction to an inner group. It's currently blank. No type. No notes, nothing. So all we can do is estimate the eventual cost. We presume it is one minFee here only for the purpose of trying to set the newly created txn's Fee. We're trying to find out if the current fee surplus is enough, and if not, we default to the Fee to the amount needed. This can't be perfect for a future with non minFee txns. But:
- It doesn't have to be. Since the user can still adjust the fee anyway, the real place where it has to be correct is
opItxnSubmitwhere we decide if the inner group really has enough. At that point, we will know if any inners are special and need to cost more. - We probably don't even want it to be perfect. By being a lower estimate of the Fee required, we prevent apps from being tricked into paying for expensive inners using the default fee mechanism. They will have to explicitly increment the fee if they want to use big features.
- Almost nobody uses this default mechanism. They zero out the Fee as a way to demand that the existing fee surplus is enough to pay for the txns. The model people have gravitated to is "Top-level transactions need to send enough surplus to make inners work".
| if err != nil { | ||
| return 0 | ||
| } | ||
| return latestHdr.CongestionTax |
There was a problem hiding this comment.
If we want to give the API user advice for how to get into the next block should it be NextCongestionTax(latest.Load, latest.CongestionTax)?
There was a problem hiding this comment.
For now, yes, it should be:
txn.Tip = NextCongestionTax(latest.Load, latest.CongestionTax)
We may want to export something like NextCongestionTax to the SDK. Maybe something that takes a number of blocks, and presumes 10% further growth?
In the future, the will want to set txn.Tax to a comfortable over estimate (they won't spend it if it's not needed). And they'll set Tip only if the want priority.
There was a problem hiding this comment.
I thought the SDK is getting / is expecting to get NextCongestionTax via this transaction params REST API implementation method here in node.go?
There was a problem hiding this comment.
The API will provide the current CongestionTax, but a caller may well prefer to assume further growth. They will never be able to get their transaction into exactly the same next block that they observe with an API call.
| for range pool.numPendingWholeBlocks { | ||
| tax = bookkeeping.NextCongestionTax(1e6, tax) | ||
| } | ||
| if tax > tip { |
There was a problem hiding this comment.
I forget, how do heartbeats and stateproof txns get past this check now?
There was a problem hiding this comment.
I second this, both SP and HB txns must have zero Tip per WellFormed.
This check efficiently prevents SP and HB txn to be accepted into the pool
There was a problem hiding this comment.
Seems like a problem, thank you for catching. I think my fix will be something like
if tax > tip && usage > 0 where I get the usage with SummarizeFees, so that any free transaction automatically avoids needing a tip.
There was a problem hiding this comment.
I did something simpler, but also avoided repeat checks. I simply allow singleton transactions with no fee through. As described in the comment, that is safe, and avoids repeating the same checks in multiple places.
fbc33eb
| // ConstructPayment fills in the suggested fee when fee=0. But if the user actually used --fee=0 on the | ||
| // commandline, we ought to do what they asked (especially now that zero or low fees make sense in | ||
| // combination with other txns that cover the groups's fee. | ||
| // combination with other txns that cover the groups's fee). |
There was a problem hiding this comment.
Do we need an explicit tip flag too? So you can manually set tip in addition to fee? If you don't pass the fee flag you will get the auto-suggested tip+fee but if you do pass fee flag you will get 0 tip + fee passed in? Or is anyone who would want to use this stuff going to use an SDK instead of clerk probably
There was a problem hiding this comment.
Added in 5a85fdf
Check it for the four cases of using or not using fee and tip, see if you agree. (I think the only one that may be contentious is using --fee but not setting --tip.
|
Suggestion: temporary enable congestion fees in current consensus to see what will fail. |
| // the tip to specify to allow entry into a congestion algod. | ||
| func suggestedFee(tx transactions.Transaction, suggested model.TransactionParametersResponse) (basics.MicroAlgos, basics.Micros) { | ||
| // Default to the suggested tax rate, if the caller didn't supply it | ||
| return basics.MicroAlgos{Raw: suggested.MinFee}, nilToZero(suggested.CongestionTax) |
There was a problem hiding this comment.
should it return Fee = suggested .MinFee * (1 + nilToZero(suggested.CongestionTax)), nilToZero(suggested.CongestionTax) ?
Otherwise ConstructPayment above produces a txn with too low Fee if chain is actually congested so that txn will be rejected.
There was a problem hiding this comment.
Yes, that's right. I will try to think how we can test this stuff better. We should certainly have higher level tests that operate under congestion.
There was a problem hiding this comment.
Maybe make a custom proto with low MaxTxnBytesPerBlock so that even few txns per block create congestion condition
update ClassifyTxPoolError for on-chain-congestion
| // We don't bother to check the type or other details because | ||
| // eval.TransactionGroup is never going to let a zero fee singleton | ||
| // through that isn't allowed. We're trying not to sprinkle the same | ||
| // checks in multiple places. |
There was a problem hiding this comment.
agreed with not having multiple fee checks, but maybe we could add a Txn.Fee > params.MinFee or non-zero Fee check in WellFormed somewhere? AFAICT a singleton pay transaction with zero fee would pass WellFormed, then use up crypto signature verification CPU, then get admitted to the pool here, then finally die in Remember but maybe we could block 0 fee fields for all but stateproof & heartbeat txns in WellFormed?
There was a problem hiding this comment.
it is tricky because you really only want it for singleton 0-fee txns so maybe WellFormed is not the right place
There was a problem hiding this comment.
Are you concerned about intentional or unintentional 0-fee txns?
If the intent is to waste our time, they could just use an empty account, but put in a big fee.
If it's a mistake, I can't imagine it being a persistent problem.
There was a problem hiding this comment.
oh good point .. i was trying to think of reasons to add a transaction type check here but it's true there are tons of other ways to get past WellFormed and this spot.
There was a problem hiding this comment.
Take note, by the way, of what's below. We're not checking any fees here. At most, we're checking the Tip is set properly.
|
Closing this, since we decided to go with a smaller PR that just adds the header fields for now, no change to congestion handling/dropping. |
Currently, congestion is monitored locally, by each
algodand is not a part of consensus. So while individualalgods may require higher fees, transaction processing is unaware of that requirement. Therefore, the extra fee can end up being "double spent" to execute inner transactions. This PR introduces two header fields,Loadwhich indicates how full a particular block is, andCongestionTaxwhich grows when theLoadexceeds 50% and returns to 0 with less full blocks. The long term plan is that groups must paynumTxns * MinFee * (1+CongestionTax)to succeed.As of this PR, however, groups merely state their willingness to pay extra by setting a
Tipfield on one transaction, and ensure the sum of theirFeefields (both outer and inner) is more than the number of txns (both outer and inner) * MinFee * (1 +Tip). At ingress time,algodwill confirm thatTipexceeds the current block'sCongestionTax, and at eval-time the protocol will ensure the Fees exceed the tipped value.Note that the
Feefields are not grown byTip. InsteadTipmultiplies the costs of the group, and theFeefields must be big enough to pay that higher price. They are not scaled, to avoid "tricking" existing code, especially logicsigs. Users that prepare groups should obtain the current congestion level, set theTip, and adjust theirFeefields accordingly.After this PR, transactions will be dropped when they arrive at
algodif the current block'sCongestionTaxis higher than the submitted group'sTip, but groups will not be dropped at evaluation time unless they failed to include enoughFeefor their statedTip. The result of this policy is that once a group is admitted to the transaction queue, it will not be dropped if congestion increases before the group gets into a block. However, groups that included aTipto get admitted will still be held to that standard.This means that congestion is still outside the protocol in some ways (proposers could include a transaction with no
Tip, even when the previous blocks have high load, andCongestionTaxis high), but the groundwork was been laid to first track congestion in protocol, and eventually enforce it at evaluation time. Since we expect allalgods to reject lowTipsduring congestion, only proposers can take advantage of this "loophole". Transactions will not propagate withTip < CongestionTax.Why don't we enforce
CongestionTaxduring evaluation with this update? Because it would lead to an ugly pathology. Suppose there is no congestion. Lots of transactions arrive withMinFeefees, and noTip. As soon as one block is created that's more than 1/2 full, the next block would have a positiveCongestionTaxso all those pending transactions would be dropped upon re-evaluation. We'd have an empty block, congestion would go down, and then a bunch ofMinFeetransactions would be sent again.Avoid that problem requires some sort of "pay-as-you-go" or "refund" model. Transactions will need to specify that they are willing to pay more, but only if there's congestion that demands it. If congestion is less than they have indicated their willingness to pay, they will keep this part of their fee.
A Phase 2 PR is described below to help explain some of the unusual aspects of this PR
To avoid backward incompatible changes, we will leave
Feeto mean, "I always pay this fee", andTipwill always mean "I am willing to be charged this extra amount". There will need to be two new transaction field, perhaps calledRefundableFeeandTax.RefundableFeeis an additional amount (inMicroAlgos) which does not necessarily go to the fee sink.Taxrepresents a factor that the transaction is willing to pay if it is included in block withCongestionTax, but otherwise will not. With these fields added, then we can confirm at evaluation time thatTax + Tipis at leastCongestionTax. But onlymin(Tax, CongestionTax) + Tipwill be charged. There are several issues with that change, so prudence leads us to put that off a bit. Which transactions receive the refund, if there areRefundableFees in multiple transactions of a group? Maybe only one transactions can specify aRefundableFee? Can more that one transaction specifyTax? Or does it apply to all? How can logicsigs be protected from unintentional spend (probably,RefundableFeeis prohibited for them)? Do apps need similar protection? Maybe inners can't specify these new fields.So: the short term solution for this PR and the next consensus release is to only enforce the changing congestion value at ingress, then lock that choice in for checking during evaluation even if the congestion level changes. This gives the community time to understand the new congestion model, since it will appear in the block. Wallets and such can begin to offer recommended fees. This consensus change will report the current
CongestionTaxin the/v2/transactions/paramscall to allow wallets to make such recommendations.In a related cleanup,
EnableFeePoolingis eliminated as a consensus parameter. Since it is strictly more permissive, we can simply assume it has always been true.Dependent PR
algorand/go-algorand-sdk#761 will be needed to sync the new fields and consensus params
Summary
Test Plan