Cosmos-sdk: Subscription Payments Module

Created on 28 Jun 2019  路  12Comments  路  Source: cosmos/cosmos-sdk

Summary

Allow accounts to create time-based payments to other accounts. This would allow applications to offer services in exchange for a subscription that deducts a certain amount of funds from user's account on a regular interval.

Problem Definition

Allows for more complex applications that want to define a different payment model than paying-per-transaction. Subscriptions are a well-understood and popular payment model for Internet applications, makes sense to allow this functionality on the SDK.

Since the payment is not being triggered by a transaction, the subscription payment processing would most likely be dealt with in EndBlocker. Though service provider could send transaction that triggers all subscribers to pay if they haven't paid for current interval. Careful thought is needed to make sure we can iterate over all paying subscribers efficiently.

Proposal

Create subscription module:

New msgs to subscribe and unsubscribe from payments and associated handlers.

Will have to determine what is the best way of triggering the payments; either in EndBlocker or have service provider submit CollectPayments transaction on every interval.

Determine how to store subscriptions so that they are efficiently iterable.

Will subscriptions be time intervalled, block intervalled, or either?


For Admin Use

  • [ ] Not duplicate issue
  • [ ] Appropriate labels applied
  • [ ] Appropriate contributors tagged
  • [ ] Contributor assigned/self-assigned

All 12 comments

A module such as this would be a good candidate for discussion for what belongs in the SDK and what doesn't 馃憤

A module such as this would be a good candidate for discussion for what belongs in the SDK and what doesn't 馃憤

I think this should live on the SDK as it'd be a general-purpose module, in contrast with the uniswap module for eg.

Careful thought is needed to make sure we can iterate over all paying subscribers efficiently.

Indeed. I don't think iterating over all subscriptions and deducting payments in EndBlock is viable for obvious performance reasons. Have you thought of better alternatives to this @AdityaSripal?

A FIFO queue where subscriptions are sorted by remaining duration to next payment. Then on the EndBlock only query the first item鈥檚 time and check if it鈥檚 due or not

I don't think we should do EndBlock either since its possible that arbitrary number of subscriptions could be ready for payment. My preference is to use a FIFO queue to track which subscriptions are due. Then the service provider can send a CollectPayments message that will use whatever gas he submits to iterate over the front of the queue and collect its payments until it reaches first undue subscription.

We emit an event that tells service provider whether he has collected all due payments or not. If he hasn't he can always submit a second tx to collect the remaining payments.

I was thinking more of something in these lines:

func EndBlock(...) {
    queue := k.GetPaymentsQueue
    for ctx.BlockTime() < paymentsQueue[0].dueTime {
        paymentsQueue[0].dueTime = paymentsQueue[0].dueTime.Add(paymentsQueue[0].paymentPeriod)
        // reduce each the items index by 1 and 
        // insert the item back to the sorted queue
    }
}

@AdityaSripal I like the idea of having a proprietary message. FIFO queue also makes sense. However, the iteration over the queue based on provided gas isn't clear to me. I don't see how such an iteration capped by gas will work. It seems the simpler approach of just iterating over all of them until the 1st undue one is the easier approach. It will be up to the provider to provide enough gas (they can run simulation first).

@fedekunze we want to avoid any unnecessary iteration in EndBlock (not sure if that is what you intended).

Well what I mean by iteration capped by gas is that if the service provider provides only enough gas to process 5 subscription payments but there are 6 due subscription payments in the FIFO queue, then the handler should just process the 5 payments and return leaving 1 due subscription payment left in the FIFO queue to be collected in a future msg. Opposed to the default behavior of trying to process all due payments, and panicing with Out of Gas Exception on the 6th iteration.

The reason I think this is useful is because I think we should design the module in mind to handle extremely popular subscription services that might end up being on an SDK chain. It's possible that a popular service has so many due payments on a given block, that trying to process all of them will consume more than the BlockGasLimit. If that's the case, then all those due payments cannot get collected since any attempt to do so in a single tx will either get rejected because the GasProvided is above BlockGasLimit or will panic because the gas provided was too low.
Even if surpassing the BlockGasLimit is unlikely, its possible that trying to submit a super-high gas tx is suboptimal compared to submitting multiple tx's over multiple blocks.

I don't think this would be too hard to implement. If we simply have an upper-bound on the amount of Gas needed to execute a loop, we can do this like so:

func Handler(...) {
    queue := k.GetPaymentsQueue
    while queue[0].IsDue() and ctx.GasRemaining > CostPerIteration {
        k.CollectPayment(queue[0])
        queue.pop()
    }
}

The CostPerIteration could be a hardcoded constant that gets initially calculated based on chain-params

Yes ofc, that makes sense. Still not clear on the gas mechanics though. Namely, CostPerIteration. How is this defined? The gas consumption based on the logic in the branch/each iteration would have to be constant. I'm not sure we can guarantee this, can we?

Yea so I think CostPerIteration would be calculated before entering the loop.
I think we can hardcode the number of times we retrieve from store or do any gas-consuming operation, we can retrieve the gas-costs/op from the params and then calculate the total gas cost per loop.

I believe since we're popping off FIFO queue it should be constant. But this doesn't need to be constant, nor does the calculation need to be exact. So long as we define a constant upper bound on how much gas is charged on each iteration, we should be fine.

@AdityaSripal what are your thoughts on initially taking the simpler approach and having the client/message explicitly specify the number of subscriptions to process? The handler would still emit an event with the total # of remaining due subscriptions.

Yea i like that idea.

Rather than iterating to see total remaining number, we could just send a boolean event to see if all due subscriptions have been paid or not.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

faboweb picture faboweb  路  3Comments

ValarDragon picture ValarDragon  路  3Comments

mossid picture mossid  路  3Comments

johnmcdowall picture johnmcdowall  路  3Comments

ValarDragon picture ValarDragon  路  3Comments