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

Pallet services-payment #77

Merged
merged 22 commits into from
May 25, 2023
Merged

Pallet services-payment #77

merged 22 commits into from
May 25, 2023

Conversation

notlesh
Copy link
Contributor

@notlesh notlesh commented Apr 26, 2023

This PR implements a basic fee payment mechanism for Tanssi. Should solve #10.

This initial PR starts with a bare-bones implementation which relies on:

  • payment made on the orchestratorChain itself
  • only native orchestratorChain currency accepted
  • purchased credits are stored in a simple mapping of para_id -> num_credits.

The fee charged for purchasing a credit is runtime-configurable via the OnChargeForBlockCredit trait. This could, for example, implement a dynamic fee mechanism.

To prevent excessive hoarding of cheaply-purchased credits, a runtime-configurable maximum (T::MaxCreditsStored) is applied.

Future work: Whenever a block is created for the given para, burn_credit_for_para() can be called to erase one of the credits. This errors if there are no credits available, and so should be called before work is done to create a block if possible.

@notlesh notlesh mentioned this pull request Apr 27, 2023
@girazoki
Copy link
Contributor

Thanks for starting the PR @notlesh. I think the way I imagine this happening is by having something like this:

  • An associated type that contains how much block production costs: BlockProductionCost.
  • A storage item (probably a mapping) paraId->BlockCredit
  • An extrinsic that allows the caller to purchase block production credit for a particular paraId. The caller for now does not need to be the parachain-manager. The extrinsic would try to burn BlockProductionCost * block_credits_to_be_purchased. If succesful, then this block credit is added for the paraId

What do you think¿

@notlesh
Copy link
Contributor Author

notlesh commented Apr 27, 2023

  • An associated type that contains how much block production costs: BlockProductionCost.

Shouldn't this be variable? Or is there another layer of fees for containerChain txns themselves?

  • A storage item (probably a mapping) paraId->BlockCredit

Seems reasonable, but I have a couple thoughts:

  1. This adds storage, I was going to tentatively avoid that if possible. Not a big deal, but worth mentioning.
  2. It implies that some future block can be valued and purchased now. Pre-paying is fine, but I think block costs should be variable. At the very least, future supply/demand of block space may be very different than it is now. This point could be mitigated by capping the number of block credits that can be stockpiled, but that comes at the cost of UX.
  • An extrinsic that allows the caller to purchase block production credit for a particular paraId. The caller for now does not need to be the parachain-manager. The extrinsic would try to burn BlockProductionCost * block_credits_to_be_purchased. If succesful, then this block credit is added for the paraId

This is in line with my design ideas so far, except that I was trying to prevent valuating block production until the latest point possible (again, I think block production cost should be variable and I expect volatility).

Thanks for the initial feedback.

Another idea I had, that I may not have articulated well, is the idea that containerChain block production could somehow implicitly pay for itself. This could be something like a special XCM instruction that's reserved for this purpose and executed immediately on block creation, or it could be something like a proof that the containerChain did some storage change that is effectively the payment itself (e.g. the orchestratorChain's sovereign account grew). The main advantage of this approach is that it makes payment and block creation an atomic pair of operations.

@notlesh
Copy link
Contributor Author

notlesh commented Apr 28, 2023

Ok, I've incorporated some of your suggestions:

  • Blocks are pre-paid and credits are tracked per paraId
  • A flexible per-block cost can be calculated on the fly when credits are purchased
  • The max number of pre-paid credits that can be accumulated is limited (MaxCreditsStored). I think this should help prevent chains from stockpiling too many credits when fees are cheap.

@tmpolaczyk
Copy link
Contributor

Payment doesn't necessarily need to be made per block. Something more like a long-term lease similar to Polkadot could be used, and payment could be made in lump-sums.

I like this idea, and given that we expect some container chain to only produce blocks occasionally (think of price feeds that only update if the price changes significantly), there should be some way for container chains to pay a priority fee to ensure that they can produce a block as soon as possible.

Haven't thought about this in much detail, but it seems to me that the problem of whether to produce a block for container chain A or container chain B is very similar to the problem of whether to include a transaction in a block. So we could benefit from implementing some state-of-the-art features such as a base fee, dynamic fees, etc.

@notlesh
Copy link
Contributor Author

notlesh commented Apr 28, 2023

there should be some way for container chains to pay a priority fee to ensure that they can produce a block as soon as possible.

Good point, I haven't given much thought to this yet. A credit system like the one currently implemented is not a good choice when we want to support some kind of priority tip, and I think we should.

but it seems to me that the problem of whether to produce a block for container chain A or container chain B is very similar to the problem of whether to include a transaction in a block

One idea I had was to implement the block production as a transaction/extrinsic. This would let us leverage existing fee mechanisms and txpool logic. I think its downside is that it wouldn't work well for chains that need more regular block production, but I'm not sure how much we want to cater to this or not.

I think one thing we should keep in mind in our design is that a given containerChain may rely significantly or entirely on the fees they collect from their own transactions in order to pay fees on the orchestratorChain. So our fee design may heavily influence theirs, for better or worse.

@girazoki
Copy link
Contributor

girazoki commented May 2, 2023

I like this idea, and given that we expect some container chain to only produce blocks occasionally (think of price feeds that only update if the price changes significantly), there should be some way for container chains to pay a priority fee to ensure that they can produce a block as soon as possible.

The ability to produce blocks as fast as possible will come from the relay side of things (which for now we have not discussed how to support). This will have to do more with DOT than our own token. Essentially, there will be an auction for this. And its also more for parathreads (future work).

Note that distinct collators will be assigned for container chain A or B in the case of parachains. The case of parathreads has to be better thought, so I would not spend that much time figuring that out (specially until we know how the relay will handle auctions for slots, etc)

And for parachains, we only need them to have block credit to produce blocks for now. I would keep this incremental given the whole complexity and unknowns of the issue (let's make sure we support the parachain usecase, then we will migrate to whatever comes)

@girazoki
Copy link
Contributor

girazoki commented May 2, 2023

2. It implies that some future block can be valued and purchased now. Pre-paying is fine, but I think block costs should be variable. At the very least, future supply/demand of block space may be very different than it is now. This point could be mitigated by capping the number of block credits that can be stockpiled, but that comes at the cost of UX.

I like this capped limit for now. BTW yes costs are expected to be variable (with usage likely), but we can do this in a separate PR.

@girazoki
Copy link
Contributor

girazoki commented May 2, 2023

One idea I had was to implement the block production as a transaction/extrinsic. This would let us leverage existing fee mechanisms and txpool logic. I think its downside is that it wouldn't work well for chains that need more regular block production, but I'm not sure how much we want to cater to this or not.

I thought about this and for the parachain usecase it really does defeat the purpose. It is probably a good option for parathreads though. But as said, we can evaluate later based on our needs. One thing we need to make sure is that Tanssi cannot receive the whole block or anything substantial as an input to an extrinsic, as this would add limit the capacity by the Tanssi capacity. The whole idea of the system is that container-chains are only limited by the weight and pov associated to their relay chain slot, and not any other parachain-slot.

/// Provider of a block cost which can adjust from block to block
type ProvideBlockProductionCost: ProvideBlockProductionCost<Self>;
/// The maximum number of credits that can be accumulated
type MaxCreditsStored: Get<u64>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
type MaxCreditsStored: Get<u64>;
type MaxCreditsStored: Get<Self::BlockNumber>;

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 did this conceptually simple change in 2dc5fdb, but it was messy. It makes sense to constrain credits stored to the same type as BlockNumber, but this bled into many other types. It would bleed into UX, too, which makes me hesitate.

I'll let you decide if you like it (or make suggestions).

@girazoki
Copy link
Contributor

girazoki commented May 2, 2023

I like the shape the PR is getting. What do you think about implementing inside the pallet in some module a couple of potential options for OnChargeForBlockCredit? One could be burning those tokens, the other could be locking them up (I am thinking about using the first, but just in case we want to change our opinion). Also a few tests would be great

/// Handler for fee charging. This will be invoked when fees need to be deducted from the fee
/// account for a given paraId.
pub trait OnChargeForBlockCredit<T: Config> {
fn withdraw_fee(
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
fn withdraw_fee(
fn charge_block_production_credit(

I feel withdraw_fee implies already burning tokens, while I think the pallet is well designed to be more modular at this point. So I think we should use a more generic term here (although I am not the best with naming)

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 went with charge_credit in 3da6f36 to be terse, lmk what you think

@notlesh
Copy link
Contributor Author

notlesh commented May 2, 2023

I like the shape the PR is getting.

Thanks again for the feedback. I'm comfortable with the design given the early nature of the project, but I still have concerns. I think we could come back to those later, though.

One I haven't voiced yet is the relationship with collator assignment and floating fees. In a txn world, there's always an opportunity cost associated with including a txn. In Ethereum, they call(ed) that an uncle fee -- it's the risk that including a txn is just enough extra work that someone else beats you to block authoring, leaving your block as an uncle. In Moonbeam, it's similar but not as pronounced since authoring slots are well-known.

What I'm getting at is: when a collator gets assigned to a container parachain, it has no control over the opportunity cost for that assignment. In addition, it's expected to make blocks consistently, so paying a tip makes no sense. It could be that its assignment is really shitty and it does a lot of work for little reward, or maybe that this parachain ends up not keeping up with its block credits and so it never gets to author for them and so it receives no rewards. ...and I bring this up because: can we design this with some economic incentives that improve such a situation? I honestly like the parathread model better than the parachain model so for for this reason -- it's better for the orchestrator chain in general. Specifically, it has more room for the block author to decide which work is worth doing and which isn't.

We can call this "off topic" for now, so thanks for thinking through it with me.

What do you think about implementing inside the pallet in some module a couple of potential options for OnChargeForBlockCredit? One could be burning those tokens, the other could be locking them up (I am thinking about using the first, but just in case we want to change our opinion).

Yes, this could be good, and great for testing / early dev 👍

Also a few tests would be great

Yes, tests will come, I just wanted us to settle a bit on the design before doing so. I'm content with this PR as a first round implementation, so I can get started on tests if no one wants to make any big changes.

@girazoki
Copy link
Contributor

girazoki commented May 3, 2023

What I'm getting at is: when a collator gets assigned to a container parachain, it has no control over the opportunity cost for that assignment. In addition, it's expected to make blocks consistently, so paying a tip makes no sense. It could be that its assignment is really shitty and it does a lot of work for little reward, or maybe that this parachain ends up not keeping up with its block credits and so it never gets to author for them and so it receives no rewards. ...and I bring this up because: can we design this with some economic incentives that improve such a situation? I honestly like the parathread model better than the parachain model so for for this reason -- it's better for the orchestrator chain in general. Specifically, it has more room for the block author to decide which work is worth doing and which isn't.

I am happy to re-consider the design of the pallet if we find a common solution that might work with how we think parathreads will work in the future and parachains. But very likely we need to start serving parachains.

One thing we can do is require that in order for collators to get assigned to parachains, parachains need to have credits at least for two sessions of block production, assuming a block production of, e.g., 12 seconds. This way a collator knows the parachain will have credit for serving blocks. And I am expecting parachain container-chains to have a similar block production rate than tanssi. One thing I did not mention is that the initial idea is that there is no distinction between rewards for Tanssi collators or container-chain collators, i.e., producing a block in a container-chain will count towards the rewards in Tanssi as much as if the block was produced in Tanssi.

For the case of parachains, which have a constant block production, collators will likely be unable to choose whether they collate in one parachain or the other, they will simply get assigned to one parachain every session (and likely, they will rotate). The parathread model can be a bit different, in which we have a pool of collators that can choose to produce blocks based, among other things, on the priority, fee, tip or other variables.

At the end it might well be that we have different service-payment constraints for both models, and even different collator-models. But this is hard to anticipate. I am open to suggestions if we find a common model satisfying both though

{
#[pallet::call_index(0)]
#[pallet::weight(0)] // TODO
pub fn purchase_credits(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It might be desirable to be able to provide a maximum fee the caller is willing to pay per credit here...

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I think it might be useful. Can you add that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added in 3a91bcf as an Optional limit. LMK what you think.


let existing_credits = BlockProductionCredits::<T>::get(para_id).unwrap_or(T::BlockNumber::zero());
let updated_credits = existing_credits.saturating_add(credits);
ensure!(
Copy link
Contributor

Choose a reason for hiding this comment

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

We can maybe buy as many credit as we can to fill the credit buffer here instead of erroring.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added in 188e2d2.

One detail to note: if there are 0 purchases possible (even if 0 were requested) the extrinsic succeeds and a CreditsPurchased event will be emitted with credits_purchased == 0.

This case is a bit ambiguous to me, but this seemed like the most graceful thing to do. We could bloat the event with one more field for clarity: credits_requested.

Copy link
Contributor

@girazoki girazoki 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 the pallet is good enough as it is for now if we fix the couple of comments that I have. @nanocryk @tmpolaczyk can you take a look at this?

#[pallet::genesis_build]
impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {
fn build(&self) {
todo!();
Copy link
Contributor

Choose a reason for hiding this comment

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

Won't this panic on genesis? If we don't want to implement it just make it a noop.

Suggested change
todo!();
// TODO: implement

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, fixed in a4b0d36

@tmpolaczyk
Copy link
Contributor

Also run cargo fmt.

@notlesh
Copy link
Contributor Author

notlesh commented May 23, 2023

I believe this PR is ready to go (feedback addressed, CI is green). PTAL and feel free to merge, @girazoki.

@girazoki girazoki merged commit db63985 into master May 25, 2023
@girazoki girazoki deleted the notlesh-services-payment branch May 25, 2023 08:10
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.

3 participants