-
Notifications
You must be signed in to change notification settings - Fork 14
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
feat: handle prewitness deposits #4698
Conversation
ec5ddf5
to
a1560db
Compare
@@ -90,6 +90,7 @@ impl<T: Config<Instance3, TargetChain = Bitcoin>> OnRuntimeUpgrade for Migration | |||
expires_at: old_channel.expires_at, | |||
action: old_channel.action, | |||
boost_fee: 0, | |||
boost_status: BoostStatus::NotBoosted, |
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.
Do we need these migrations still? I added boost status to get the code to compile, but it would be more correct for this to have a separate (e.g. DepositChannelDetails_v3) structs since this migration only updates from v2 to v3. Easier to just delete these of course if we don't need them. Similarly, I see that boost_fee
was added here too, but boost_fee
was only introduced in v5.
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.
Yeah I think so. The idea with not merging migrations like this is a) it's easier to see the correctness of a migration if it's not mixed with another and b) it allows us to easily slice the commit history. And say we're releasing at some earlier commit. If the migration was merged into this, then we'd have to rewrite it in that case.
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 discussed this with @dandanlen yesterday and agreed that we can already remove these migrations (since we aready have a branch for 1.3), which makes things easy. It is something we should keep in mind in the future though (Dan suggested we should more proactively delete already applied migrations to avoid this).
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.
@dandanlen are you going to remove stale migrations separately, or do you want this to be done in this PR (I don't think it would be a big deal to merge as is and remove them later IMO).
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.
Yep I'll deal with it.
@@ -453,7 +454,7 @@ macro_rules! assets { | |||
} | |||
}; | |||
|
|||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Encode, Decode, TypeInfo, MaxEncodedLen, Hash, Serialize, Deserialize)] | |||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Encode, Decode, TypeInfo, MaxEncodedLen, Hash, Serialize, Deserialize, EnumIter)] |
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 you add and use a fn all()
as above.
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 can do that, but because I have generic TargetChainAsset
types (unlike the any::Asset
you are referring to), I would have to add a new trait with fn all()
to set it as a bound and then use it where I'm currently using strum::IntoEnumIterator
, which doesn't seem much different from using strum::IntoEnumIterator
directly... Is there another reason you prefer fn all()
and do you still think it is worth adding?
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.
Still reviewing, but here're some initial comments
@@ -0,0 +1,424 @@ | |||
use super::*; |
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.
Just general comment on these tests, I think they're good candidates for fuzzing. We could add ranges of values in some tests rather than using one set of specific values per test too.
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.
Are you talking about just adding a some tests with randomized values, and running them in some loop, or actually integrating some kind of third-party fuzzing framework/library? To clearify, is this a requirement before merging this PR, or just a nice to have that we can do later?
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 some tests with randomized values is going to get us 80% of the way there - made some references to such specific situations in some other comments. I would say there are some cases we should do in this PR, like the boost_pool logic, to ensure rounding etc. is correct, but other cases can wait.
use sp_std::collections::btree_set::BTreeSet; | ||
|
||
type AccountId = u32; | ||
type TestPool = BoostPool<AccountId, Ethereum>; |
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.
feel like we should be able to write a macro, or generic function so that we can have each test for each chain without repetition, ik we don't do this many other places but I think it's a nice thing to start doing more often
let mut pool = TestPool::new(100); | ||
pool.add_funds(BOOSTER_1, 1000); | ||
|
||
assert_eq!(pool.provide_funds_for_boosting(BOOST_1, 1010), Ok((1010, 10))); |
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.
Is this right? We only have 1000, but we need to provide 1010 in boost funds? The boosted amount can only be a maximum of the amount in the pool? How can we provide more than we have?
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.
Maybe the terminology could be improved here, but I (indirectly) use "provide" to mean amount deducted + fee
. "Boosted amount" is probably more accurate, but not sure how that would work with the function name (I don't like boost_funds
since that would sound more like the entire amount is to be boosted by that pool IMO).
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.
maybe returning a struct that can have named params would help to clarify it here?
37dea3d
to
4aa1ac7
Compare
Update: added migrations and benchmarks. Added corresponding sections to the PRs description. @kylezs |
} | ||
|
||
#[benchmark] | ||
fn boost_finalised() { |
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.
just "deposit_finalised"? We use it on process_deposits, and it's not necessarily the case we have a boost
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.
Are you asking why this doesn't also contain code for processing deposit normally? Or just about renaming it? I use boost_finalised
as benchmark for deposit_finalised
because it is (by far) the more expensive code path. (Is there a better technique for benchmarking code with branches?) I use boost_finalised
as the name to better reflect the code that's actually being benchmarked.
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.
was just suggesting renaming it. usually the benchmarks are named after their extrinsic, I can see this case is a little unique though, we're benchmarking the boost pathway through this extrinsic - so guess I'm indifferent will leave it to you
state-chain/pallets/cf-ingress-egress/src/migrations/deposit_channels_with_boost_fee.rs
Show resolved
Hide resolved
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 main thing left to investigate here is the prewitness deposit ids and assess if we actually need them now. We could simplify a couple of things by removing them, so worth considering.
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.
Chatted to Max yesterday, we can look at factoring out the prewitness ids in another PR. Would await @dandanlen 's review before merging.
Nice one 👍
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.
LGTM, great work.
I've added some suggestions. The only one I feel little strongly about is the comment about the type arg on ScaledAmount.Everything else is subject to discussion.
Up to you @kylezs if we address any of this before merging.
} | ||
|
||
// Manually implementing Default because deriving didn't work due to generic parameter: | ||
impl<C: Chain> Default for ScaledAmount<C> { |
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.
For future reference - Substrate provides DefaultNoBound
(amongst others like CloneNoBound, DebugNoBound etc.) to help with this.
struct ScaledAmount<C: Chain> { | ||
val: u128, |
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 C: Chain
constraint is overkill here.
We could just use ScaledAmount<A: AtLeast32BitUnsigned>
. This will also work nicer with scale metadata as there's less indirection.
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.
The reason I chose Chain
is for extra type safety, i.e. to ensure that if I convert from EthAmount
, then we can only convert back to EthAmount
. Admittedly, the code would probably be hard to misuse even without this trait bound, but you could make this argument about other uses of C::ChainAmount
. I'm not sure about the implication for the scale metadata though, so if you think it is important, I can remove C: Chain
. (Not sure if AtLeast32BitUnsigned
would be needed here though, what would A
be constraining in that case?).
if let Some(pending_deposits) = self.pending_withdrawals.get_mut(booster_id) { | ||
if !pending_deposits.remove(&boost_id) { | ||
log::warn!("Withdrawing booster contributed to boost {boost_id}, but it is not in pending withdrawals"); | ||
} | ||
|
||
if pending_deposits.is_empty() { | ||
self.pending_withdrawals.remove(booster_id); | ||
} |
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 is mostly repeated above (in on_finalised_deposit), there's some scope for encapsulation here. Maybe BTreeMap<Account, Balance>
could be abstracted out into its own BoostPoolFragment
type or something with some well-defined primitives like split/withdraw/combine. This might make it easier to reason about the code too.
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.
Tbh, I'm not sure what's being suggested here. Not sure how BoostPoolFragment
wtih, say, splitting would come into play. Are you talking about amounts
rather than pending_withdrawals
?
The two methods here do look quite similar, but we are also talking about 10 lines of code or so, where the body of the loop is mostly different. I could maybe create a function remove_prewitnessed_deposit
which would return (somewhat unexpectedly) a bool indicating whether the booster is "withdrawing", which we could use to do different things in on_finalised_deposit
and on_lost_deposit
, but that doesn't look like a good/clear interface. It seems to me that in this case the (cognitive) cost of indirection isn't worth marginal benefit from deduplicating a few lines of code. We can probably make equally good arguments either way TBH. Maybe I'm misunderstanding your suggestion?
&mut self, | ||
boost_id: BoostId, | ||
amount_to_boost: C::ChainAmount, | ||
) -> Result<(C::ChainAmount, C::ChainAmount), &'static str> { |
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.
Some doc comments would be nice, in particular to explain what the return amounts are.
Some of my comments got lost inside VS Code - adding them here for posterity. I think it would help if we break up the BoostPool struct into two items: The BoostPool (with the fee rate and amounts are available for boosts) and an individual Boost (with the pending boosts and withdrawals). Then when we boost we have a method like I would also suggest including the fee tiers in the BoostPool instead of having a separate pool per fee tier. Seems like it might make testing simpler, and makes it easier to iterate etc. Not sure about this. I'm not really a fan of on_* naming for things that are not hooks. By hooks I mean some trait with methods that we expect to be called at well-defined lifecylce events. But in this case, there is no indirection, and only one implementation, so I feel like a name that describes what happens is more useful. |
…-validator * origin/main: fix: broker flakiness on bouncer CI (#4736) fix: migration for earned fees (#4733) feat: handle prewitness deposits (#4698) Update Sdk with Arbitrum support (#4720) chore: remove insta deprecated warning (#4734) refactor: move vanity names to account roles pallet (#4719) Arbitrum backup rpc (#4730) # Conflicts: # state-chain/pallets/cf-validator/src/lib.rs # state-chain/pallets/cf-validator/src/mock.rs # state-chain/pallets/cf-validator/src/tests.rs
I gave it a quick try, and I don't think this is a clear improvement. Most BoostPool's methods need to access at least two, but usually three out of the three fields:
So yeah, we can put Seems like there will be some downsides to splitting the struct too (apart from the time it would take). Storing them as separate storage items would mean more storage api quieries (unless we are talking about storing them together in yet another struct). For tests it also seems more convenient to have a single struct. |
I'm also not sure about this. Having a single boost tier as a struct boundary seems to make sense (even conceptually we think of them as separate entities). If we combine them, we'd have to update all fields to be BTreeMaps where values are the current fields (some of which are already BTreeMaps of BTreeMaps)... this would make the code unwieldy and more error prone. Maybe you are talking about keeping the existing |
I see your point regarding on_* methods and I generally agree. I used these names as a placeholder initially, but better names never came to me. At the same time, while something like How about |
About this - it's not just about what is accessed, it's also about communicating ownership and control flow. I can access the same data in a method call, but it makes a big difference whether it's an input or and output, and what the lifetimes are. For boost, for example, we seem to be working with a couple of higher-level primitives: We have a pool. We can 'withdraw' some portion thereof. The pool does not have access to withdrawn funds: ownership and control has been transferred. The funds can then be returned to the pool later, or might be lost. In the former case, ownership returns to the pool, in the latter case Whether we store these separately or within the same struct is besides the point. I just think that the types and method signatures could more accurately communicate the flow of control and ownership and it would be easier to tell at a glance that, for example, certain logic errors just aren't possible. To be clear, I didn't actually attempt the refactor and am likely over-simplifying (in particular re. how this interacts with stop_boosting), but my impression while reviewing was that I had to keep a lot of context in my head when reading the method implementations. Also I'm not saying we have to go and change this all right now, or indeed ever. It's fine as it is. Re. the naming thing - again, it doesn't have to be addressed, but I do want to make my point 😄 : As a general rule, I don't think a function implementation should make any assumptions about 'when it should be called'. That's for the caller to decide. There are exceptions to this (ie. there are times when |
Pull Request
Closes: PRO-1145
Checklist
Please conduct a thorough self-review before opening the PR.
Summary
Pre-witnessing
When a deposit is pre-witnessed, we check if it can be boosted. Specifically, a deposit is boosted if:
If the deposit is boosted:
amount = deposit_amount - ingress_fee - boost fee
.DepositBoosted
event is emittedFull witnessing/finalisation
When a deposit is finalised, we check if the channel is boosted (for the matching amount), and if so, we now credit the booster pool instead of executing the channel action.
booster_contributions
recordsDepositFinalised
event is emitted (renamed fromDepositReceived
) with action:DepositAction::BoostersCredited
.New extrinsic: add_boost_funds
New extrinsic: stop_boosting
Other Changes
perform_channel_action
as a function, so that it can be called from two places: on boosting, and on finalisation if not boosted (as before).Questions:
DepositBoosted
event? I have addedboost_fee
which is the total boost fee, but do we need to break that down by pools? Along with the amounts contributed from each pool?BoostersCredited
?Migrations
Benchmarks
on_lost_deposit
, which depends on the number of boosters (who participated in the boost). Updated existingon_idle
hook to additionally use the weight ofon_lost_deposit
for any lost deposit.(For extrinsics, I wasn't sure if it is possible/appropriate to read storage items in the pallet::weight annotation to return an accurate weight. For some benchmarks, I ended up picking some reasonable values instead, leaning towards overestimating the weight.)
Added a benchmark for
add_boost_funds
(this one was straightforward)Added a benchmark for
stop_boosting
(here the weight depends on the number of pending boosts, I chose 100, which is probably an overestimate, at least at this stage)Added benchmarks for boosting and finalising deposits. Because both of them are called from
process_deposits
, I chose to usefinalise_deposit
here because it significantly "heavier". In fact, I wonder if we could fine tune the parameters a bit (I chose 100 boosters, with 30% of which are withdrawing resulting in a DB read/write for each to update their balance). Or perhaps there is a way to make this benchmark use real data at execution time?