0g-chain/internal/x/paychan/keeper.go
2018-08-30 13:43:15 -04:00

418 lines
13 KiB
Go

package paychan
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/wire"
"github.com/cosmos/cosmos-sdk/x/bank"
)
// Keeper of the paychan store
// Handles validation internally. Does not rely on calling code to do validation.
// Aim to keep public methods safe, private ones not necessaily.
type Keeper struct {
storeKey sdk.StoreKey
cdc *wire.Codec // needed to serialize objects before putting them in the store
coinKeeper bank.Keeper
// TODO investigate codespace
//codespace sdk.CodespaceType
}
// Called when creating new app.
func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper) Keeper {
keeper := Keeper{
storeKey: key,
cdc: cdc,
coinKeeper: ck,
//codespace: codespace,
}
return keeper
}
// ============================================== Main Business Logic
// Create a new payment channel and lock up sender funds.
func (k Keeper) CreateChannel(ctx sdk.Context, sender sdk.AccAddress, receiver sdk.AccAddress, coins sdk.Coins) (sdk.Tags, sdk.Error) {
// TODO do validation and maybe move somewhere nicer
/*
// args present
if len(sender) == 0 {
return nil, sdk.ErrInvalidAddress(sender.String())
}
if len(receiver) == 0 {
return nil, sdk.ErrInvalidAddress(receiver.String())
}
if len(amount) == 0 {
return nil, sdk.ErrInvalidCoins(amount.String())
}
// Check if coins are sorted, non zero, positive
if !amount.IsValid() {
return nil, sdk.ErrInvalidCoins(amount.String())
}
if !amount.IsPositive() {
return nil, sdk.ErrInvalidCoins(amount.String())
}
// sender should exist already as they had to sign.
// receiver address exists. am is the account mapper in the coin keeper.
// TODO automatically create account if not present?
// TODO remove as account mapper not available to this pkg
//if k.coinKeeper.am.GetAccount(ctx, receiver) == nil {
// return nil, sdk.ErrUnknownAddress(receiver.String())
//}
// sender has enough coins - done in Subtract method
// TODO check if sender and receiver different?
*/
// subtract coins from sender
_, tags, err := k.coinKeeper.SubtractCoins(ctx, sender, coins)
if err != nil {
return nil, err
}
// Calculate next id
id := k.getNewChannelID(ctx)
// create new Paychan struct
channel := Channel{
ID: id,
Participants: [2]sdk.AccAddress{sender, receiver},
Coins: coins,
}
// save to db
k.setChannel(ctx, channel)
// TODO add to tags
return tags, err
}
func (k Keeper) InitCloseChannelBySender(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) {
// This is roughly the default path for non unidirectional channels
// TODO Validate update - e.g. check signed by sender
q, found := k.getSubmittedUpdatesQueue(ctx)
if !found {
panic("SubmittedUpdatesQueue not found.") // TODO nicer custom errors
}
if q.Contains(update.ChannelID) {
// Someone has previously tried to update channel
// In bidirectional channels the new update is compared against existing and replaces it if it has a higher sequence number.
// existingSUpdate, found := k.getSubmittedUpdate(ctx, update.ChannelID)
// if !found {
// panic("can't find element in queue that should exist")
// }
// k.addToSubmittedUpdatesQueue(ctx, k.applyNewUpdate(existingSUpdate, update))
// However in unidirectional case, only the sender can close a channel this way. No clear need for them to be able to submit an update replacing a previous one they sent, so don't allow it.
// TODO tags
// TODO custom errors return sdk.EmptyTags(), sdk.NewError("Sender can't submit an update for channel if one has already been submitted.")
panic("Sender can't submit an update for channel if one has already been submitted.")
} else {
// No one has tried to update channel
submittedUpdate := SubmittedUpdate{
Update: update,
ExecutionTime: ctx.BlockHeight() + ChannelDisputeTime, //TODO check what exactly BlockHeight refers to
}
k.addToSubmittedUpdatesQueue(ctx, submittedUpdate)
}
tags := sdk.EmptyTags() // TODO tags
return tags, nil
}
func (k Keeper) CloseChannelByReceiver(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) {
// TODO Validate update
// Check if there is an update in the queue already
q, found := k.getSubmittedUpdatesQueue(ctx)
if !found {
panic("SubmittedUpdatesQueue not found.") // TODO nicer custom errors
}
if q.Contains(update.ChannelID) {
// Someone has previously tried to update channel but receiver has final say
k.removeFromSubmittedUpdatesQueue(ctx, update.ChannelID)
}
tags, err := k.closeChannel(ctx, update)
return tags, err
}
// Main function that compare updates against each other.
// Pure function
// Not needed in unidirectional case.
// func (k Keeper) applyNewUpdate(existingSUpdate SubmittedUpdate, proposedUpdate Update) SubmittedUpdate {
// var returnUpdate SubmittedUpdate
// if existingSUpdate.Sequence > proposedUpdate.Sequence {
// // update accepted
// returnUpdate = SubmittedUpdate{
// Update: proposedUpdate,
// ExecutionTime: existingSUpdate.ExecutionTime, // FIXME any new update proposal should be subject to full dispute period from submission
// }
// } else {
// // update rejected
// returnUpdate = existingSUpdate
// }
// return returnUpdate
// }
// unsafe close channel - doesn't check if update matches existing channel TODO make safer?
func (k Keeper) closeChannel(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) {
var err sdk.Error
var tags sdk.Tags
// Add coins to sender and receiver
// TODO check for possible errors first to avoid coins being half paid out?
for _, payout := range update.Payouts {
// TODO check somewhere if coins are not negative?
_, tags, err = k.coinKeeper.AddCoins(ctx, payout.Address, payout.Coins)
if err != nil {
panic(err)
}
}
k.deleteChannel(ctx, update.ChannelID)
return tags, nil
}
// =========================================== QUEUE
func (k Keeper) addToSubmittedUpdatesQueue(ctx sdk.Context, sUpdate SubmittedUpdate) {
// always overwrite prexisting values - leave paychan logic to higher levels
// get current queue
q, found := k.getSubmittedUpdatesQueue(ctx)
if !found {
panic("SubmittedUpdatesQueue not found.")
}
// append ID to queue
if !q.Contains(sUpdate.ChannelID) {
q = append(q, sUpdate.ChannelID)
}
// set queue
k.setSubmittedUpdatesQueue(ctx, q)
// store submittedUpdate
k.setSubmittedUpdate(ctx, sUpdate)
}
func (k Keeper) removeFromSubmittedUpdatesQueue(ctx sdk.Context, channelID ChannelID) {
// get current queue
q, found := k.getSubmittedUpdatesQueue(ctx)
if !found {
panic("SubmittedUpdatesQueue not found.")
}
// remove id
q.RemoveMatchingElements(channelID)
// set queue
k.setSubmittedUpdatesQueue(ctx, q)
// delete submittedUpdate
k.deleteSubmittedUpdate(ctx, channelID)
}
func (k Keeper) getSubmittedUpdatesQueue(ctx sdk.Context) (SubmittedUpdatesQueue, bool) {
// load from DB
store := ctx.KVStore(k.storeKey)
bz := store.Get(k.getSubmittedUpdatesQueueKey())
var suq SubmittedUpdatesQueue
if bz == nil {
return suq, false // TODO maybe create custom error to pass up here
}
// unmarshal
k.cdc.MustUnmarshalBinary(bz, &suq)
// return
return suq, true
}
func (k Keeper) setSubmittedUpdatesQueue(ctx sdk.Context, suq SubmittedUpdatesQueue) {
store := ctx.KVStore(k.storeKey)
// marshal
bz := k.cdc.MustMarshalBinary(suq)
// write to db
key := k.getSubmittedUpdatesQueueKey()
store.Set(key, bz)
}
func (k Keeper) getSubmittedUpdatesQueueKey() []byte {
return []byte("submittedUpdatesQueue")
}
// ============= SUBMITTED UPDATES
// These are keyed by the IDs of thei associated Channels
// This section deals with only setting and getting
func (k Keeper) getSubmittedUpdate(ctx sdk.Context, channelID ChannelID) (SubmittedUpdate, bool) {
// load from DB
store := ctx.KVStore(k.storeKey)
bz := store.Get(k.getSubmittedUpdateKey(channelID))
var sUpdate SubmittedUpdate
if bz == nil {
return sUpdate, false
}
// unmarshal
k.cdc.MustUnmarshalBinary(bz, &sUpdate)
// return
return sUpdate, true
}
// Store payment channel struct in blockchain store.
func (k Keeper) setSubmittedUpdate(ctx sdk.Context, sUpdate SubmittedUpdate) {
store := ctx.KVStore(k.storeKey)
// marshal
bz := k.cdc.MustMarshalBinary(sUpdate) // panics if something goes wrong
// write to db
key := k.getSubmittedUpdateKey(sUpdate.ChannelID)
store.Set(key, bz) // panics if something goes wrong
}
func (k Keeper) deleteSubmittedUpdate(ctx sdk.Context, channelID ChannelID) {
store := ctx.KVStore(k.storeKey)
store.Delete(k.getSubmittedUpdateKey(channelID))
// TODO does this have return values? What happens when key doesn't exist?
}
func (k Keeper) getSubmittedUpdateKey(channelID ChannelID) []byte {
return []byte(fmt.Sprintf("submittedUpdate:%d", channelID))
}
// ========================================== CHANNELS
// Reteive a payment channel struct from the blockchain store.
func (k Keeper) getChannel(ctx sdk.Context, channelID ChannelID) (Channel, bool) {
// load from DB
store := ctx.KVStore(k.storeKey)
bz := store.Get(k.getChannelKey(channelID))
var channel Channel
if bz == nil {
return channel, false
}
// unmarshal
k.cdc.MustUnmarshalBinary(bz, &channel)
// return
return channel, true
}
// Store payment channel struct in blockchain store.
func (k Keeper) setChannel(ctx sdk.Context, channel Channel) {
store := ctx.KVStore(k.storeKey)
// marshal
bz := k.cdc.MustMarshalBinary(channel) // panics if something goes wrong
// write to db
key := k.getChannelKey(channel.ID)
store.Set(key, bz) // panics if something goes wrong
}
func (k Keeper) deleteChannel(ctx sdk.Context, channelID ChannelID) {
store := ctx.KVStore(k.storeKey)
store.Delete(k.getChannelKey(channelID))
// TODO does this have return values? What happens when key doesn't exist?
}
func (k Keeper) getNewChannelID(ctx sdk.Context) ChannelID {
// get last channel ID
var lastID ChannelID
store := ctx.KVStore(k.storeKey)
bz := store.Get(k.getLastChannelIDKey())
if bz == nil {
lastID = -1 // TODO is just setting to zero if uninitialized ok?
} else {
k.cdc.MustUnmarshalBinary(bz, &lastID)
}
// increment to create new one
newID := lastID + 1
bz = k.cdc.MustMarshalBinary(newID)
// set last channel id again
store.Set(k.getLastChannelIDKey(), bz)
// return
return newID
}
func (k Keeper) getChannelKey(channelID ChannelID) []byte {
return []byte(fmt.Sprintf("channel:%d", channelID))
}
func (k Keeper) getLastChannelIDKey() []byte {
return []byte("lastChannelID")
}
/*
// Close a payment channel and distribute funds to participants.
func (k Keeper) ClosePaychan(ctx sdk.Context, sender sdk.Address, receiver sdk.Address, id int64, receiverAmount sdk.Coins) (sdk.Tags, sdk.Error) {
if len(sender) == 0 {
return nil, sdk.ErrInvalidAddress(sender.String())
}
if len(receiver) == 0 {
return nil, sdk.ErrInvalidAddress(receiver.String())
}
if len(receiverAmount) == 0 {
return nil, sdk.ErrInvalidCoins(receiverAmount.String())
}
// check id ≥ 0
if id < 0 {
return nil, sdk.ErrInvalidAddress(strconv.Itoa(int(id))) // TODO implement custom errors
}
// Check if coins are sorted, non zero, non negative
if !receiverAmount.IsValid() {
return nil, sdk.ErrInvalidCoins(receiverAmount.String())
}
if !receiverAmount.IsPositive() {
return nil, sdk.ErrInvalidCoins(receiverAmount.String())
}
store := ctx.KVStore(k.storeKey)
pych, exists := k.GetPaychan(ctx, sender, receiver, id)
if !exists {
return nil, sdk.ErrUnknownAddress("paychan not found") // TODO implement custom errors
}
// compute coin distribution
senderAmount := pych.Balance.Minus(receiverAmount) // Minus sdk.Coins method
// check that receiverAmt not greater than paychan balance
if !senderAmount.IsNotNegative() {
return nil, sdk.ErrInsufficientFunds(pych.Balance.String())
}
// add coins to sender
// creating account if it doesn't exist
k.coinKeeper.AddCoins(ctx, sender, senderAmount)
// add coins to receiver
k.coinKeeper.AddCoins(ctx, receiver, receiverAmount)
// delete paychan from db
pychKey := paychanKey(pych.Sender, pych.Receiver, pych.Id)
store.Delete(pychKey)
// TODO create tags
//sdk.NewTags(
// "action", []byte("channel closure"),
// "receiver", receiver.Bytes(),
// "sender", sender.Bytes(),
// "id", ??)
tags := sdk.NewTags()
return tags, nil
}
// Creates a key to reference a paychan in the blockchain store.
func paychanKey(sender sdk.Address, receiver sdk.Address, id int64) []byte {
//sdk.Address is just a slice of bytes under a different name
//convert id to string then to byte slice
idAsBytes := []byte(strconv.Itoa(int(id)))
// concat sender and receiver and integer ID
key := append(sender.Bytes(), receiver.Bytes()...)
key = append(key, idAsBytes...)
return key
}
// Get all paychans between a given sender and receiver.
func (k Keeper) GetPaychans(sender sdk.Address, receiver sdk.Address) []Paychan {
var paychans []Paychan
// TODO Implement this
return paychans
}
// maybe getAllPaychans(sender sdk.address) []Paychan
*/