0g-chain/internal/x/paychan/keeper.go

382 lines
12 KiB
Go
Raw Normal View History

2018-07-08 22:09:07 +00:00
package paychan
2018-07-09 18:46:51 +00:00
import (
"bytes"
"fmt"
2018-07-10 13:56:04 +00:00
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/wire"
2018-07-13 13:27:55 +00:00
"github.com/cosmos/cosmos-sdk/x/bank"
2018-07-09 18:46:51 +00:00
)
2018-08-24 23:18:41 +00:00
// Keeper of the paychan store
2018-07-11 21:02:07 +00:00
// Handles validation internally. Does not rely on calling code to do validation.
2018-08-24 23:18:41 +00:00
// Aim to keep public methods safe, private ones not necessaily.
2018-07-08 22:09:07 +00:00
type Keeper struct {
2018-07-13 13:27:55 +00:00
storeKey sdk.StoreKey
2018-07-10 13:56:04 +00:00
cdc *wire.Codec // needed to serialize objects before putting them in the store
2018-07-08 22:09:07 +00:00
coinKeeper bank.Keeper
2018-08-24 23:18:41 +00:00
//codespace sdk.CodespaceType
2018-07-08 22:09:07 +00:00
}
2018-07-09 18:46:51 +00:00
// Called when creating new app.
2018-07-10 13:56:04 +00:00
func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper) Keeper {
2018-07-08 22:09:07 +00:00
keeper := Keeper{
2018-07-13 13:27:55 +00:00
storeKey: key,
2018-07-10 13:56:04 +00:00
cdc: cdc,
2018-07-08 22:09:07 +00:00
coinKeeper: ck,
2018-07-09 18:46:51 +00:00
//codespace: codespace,
2018-07-08 22:09:07 +00:00
}
return keeper
}
2018-08-27 21:58:58 +00:00
// ============================================== Main Business Logic
2018-07-08 22:09:07 +00:00
2018-07-09 18:46:51 +00:00
// 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) {
// Check addresses valid (Technicaly don't need to check sender address is valid as SubtractCoins does that)
if len(sender) == 0 {
return nil, sdk.ErrInvalidAddress(sender.String())
}
if len(receiver) == 0 {
return nil, sdk.ErrInvalidAddress(receiver.String())
}
// check coins are sorted and positive (disallow channels with zero balance)
if !coins.IsValid() {
return nil, sdk.ErrInvalidCoins(coins.String())
}
if !coins.IsPositive() {
return nil, sdk.ErrInvalidCoins(coins.String())
}
2018-08-24 23:18:41 +00:00
2018-07-08 22:09:07 +00:00
// subtract coins from sender
2018-08-24 23:18:41 +00:00
_, tags, err := k.coinKeeper.SubtractCoins(ctx, sender, coins)
2018-07-12 13:32:36 +00:00
if err != nil {
return nil, err
}
// Calculate next id
id := k.getNewChannelID(ctx)
2018-07-12 13:32:36 +00:00
// create new Paychan struct
2018-08-24 23:18:41 +00:00
channel := Channel{
ID: id,
Participants: [2]sdk.AccAddress{sender, receiver},
Coins: coins,
2018-07-13 13:27:55 +00:00
}
2018-07-08 22:09:07 +00:00
// save to db
2018-08-24 23:18:41 +00:00
k.setChannel(ctx, channel)
2018-07-10 13:56:04 +00:00
2018-08-27 21:58:58 +00:00
// TODO add to tags
2018-07-10 13:56:04 +00:00
return tags, err
2018-07-08 22:09:07 +00:00
}
func (k Keeper) InitCloseChannelBySender(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) {
2018-08-27 21:58:58 +00:00
// This is roughly the default path for non unidirectional channels
err := k.validateUpdate(ctx, update)
if err != nil {
return nil, err
}
2018-08-27 21:58:58 +00:00
q, found := k.getSubmittedUpdatesQueue(ctx)
if !found {
panic("SubmittedUpdatesQueue not found.") // TODO nicer custom errors
}
2018-08-27 21:58:58 +00:00
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.")
sdk.ErrInternal("Sender can't submit an update for channel if one has already been submitted.")
2018-08-27 21:58:58 +00:00
} else {
// No one has tried to update channel
2018-08-27 21:58:58 +00:00
submittedUpdate := SubmittedUpdate{
Update: update,
ExecutionTime: ctx.BlockHeight() + ChannelDisputeTime, //TODO check what exactly BlockHeight refers to
2018-08-27 21:58:58 +00:00
}
k.addToSubmittedUpdatesQueue(ctx, submittedUpdate)
2018-08-24 23:18:41 +00:00
}
tags := sdk.EmptyTags() // TODO tags
return tags, nil
2018-08-27 21:58:58 +00:00
}
2018-08-24 23:18:41 +00:00
func (k Keeper) CloseChannelByReceiver(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) {
err := k.validateUpdate(ctx, update)
if err != nil {
return nil, err
}
2018-08-27 21:58:58 +00:00
// 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
}
2018-08-27 21:58:58 +00:00
if q.Contains(update.ChannelID) {
// Someone has previously tried to update channel but receiver has final say
k.removeFromSubmittedUpdatesQueue(ctx, update.ChannelID)
2018-08-27 21:58:58 +00:00
}
tags, err := k.closeChannel(ctx, update)
return tags, err
2018-08-24 23:18:41 +00:00
}
2018-08-27 21:58:58 +00:00
// 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
// }
2018-08-24 23:18:41 +00:00
func (k Keeper) validateUpdate(ctx sdk.Context, update Update) sdk.Error {
// Check that channel exists
channel, found := k.getChannel(ctx, update.ChannelID)
if !found {
return sdk.ErrInternal("Channel doesn't exist")
}
// Check the num of payout participants match channel participants
if len(update.Payout) != len(channel.Participants) {
return sdk.ErrInternal("Payout doesn't match number of channel participants")
}
// Check each coins are valid
for _, coins := range update.Payout {
if !coins.IsValid() {
return sdk.ErrInternal("Payout coins aren't formatted correctly")
}
}
// Check payout coins are each not negative (can be zero though)
if !update.Payout.IsNotNegative() {
return sdk.ErrInternal("Payout cannot be negative")
}
// Check payout sums to match channel.Coins
if !channel.Coins.IsEqual(update.Payout.Sum()) {
return sdk.ErrInternal("Payout amount doesn't match channel amount")
}
// Check sender signature is OK
if !k.verifySignatures(ctx, channel, update) {
return sdk.ErrInternal("Signature on update not valid")
}
return nil
}
// 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) {
2018-08-29 03:45:26 +00:00
var err sdk.Error
var tags sdk.Tags
2018-08-27 21:58:58 +00:00
channel, _ := k.getChannel(ctx, update.ChannelID)
// TODO check channel exists and participants matches update payout length
2018-08-24 23:18:41 +00:00
// Add coins to sender and receiver
// TODO check for possible errors first to avoid coins being half paid out?
for i, coins := range update.Payout {
2018-08-29 03:45:26 +00:00
// TODO check somewhere if coins are not negative?
_, tags, err = k.coinKeeper.AddCoins(ctx, channel.Participants[i], coins)
if err != nil {
panic(err)
}
2018-08-27 21:58:58 +00:00
}
k.deleteChannel(ctx, update.ChannelID)
2018-08-27 21:58:58 +00:00
return tags, nil
}
2018-08-27 21:58:58 +00:00
func (k Keeper) verifySignatures(ctx sdk.Context, channel Channel, update Update) bool {
// In non unidirectional channels there will be more than one signature to check
signBytes := update.GetSignBytes()
address := channel.Participants[0]
pubKey := update.Sigs[0].PubKey
cryptoSig := update.Sigs[0].CryptoSignature
// Check public key submitted with update signature matches the account address
valid := bytes.Equal(pubKey.Address(), address) &&
// Check the signature is correct
pubKey.VerifyBytes(signBytes, cryptoSig)
return valid
}
2018-08-27 21:58:58 +00:00
// =========================================== 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.")
}
2018-08-27 21:58:58 +00:00
// append ID to queue
if !q.Contains(sUpdate.ChannelID) {
2018-08-27 21:58:58 +00:00
q = append(q, sUpdate.ChannelID)
}
// set queue
k.setSubmittedUpdatesQueue(ctx, q)
2018-08-27 21:58:58 +00:00
// store submittedUpdate
k.setSubmittedUpdate(ctx, sUpdate)
}
func (k Keeper) removeFromSubmittedUpdatesQueue(ctx sdk.Context, channelID ChannelID) {
2018-08-27 21:58:58 +00:00
// get current queue
q, found := k.getSubmittedUpdatesQueue(ctx)
if !found {
panic("SubmittedUpdatesQueue not found.")
}
2018-08-27 21:58:58 +00:00
// remove id
q.RemoveMatchingElements(channelID)
// set queue
k.setSubmittedUpdatesQueue(ctx, q)
2018-08-27 21:58:58 +00:00
// delete submittedUpdate
k.deleteSubmittedUpdate(ctx, channelID)
}
func (k Keeper) getSubmittedUpdatesQueue(ctx sdk.Context) (SubmittedUpdatesQueue, bool) {
2018-08-27 21:58:58 +00:00
// load from DB
store := ctx.KVStore(k.storeKey)
bz := store.Get(k.getSubmittedUpdatesQueueKey())
var suq SubmittedUpdatesQueue
2018-08-27 21:58:58 +00:00
if bz == nil {
return suq, false // TODO maybe create custom error to pass up here
2018-08-27 21:58:58 +00:00
}
// unmarshal
k.cdc.MustUnmarshalBinary(bz, &suq)
2018-08-27 21:58:58 +00:00
// return
return suq, true
2018-08-27 21:58:58 +00:00
}
2018-08-29 03:45:26 +00:00
func (k Keeper) setSubmittedUpdatesQueue(ctx sdk.Context, suq SubmittedUpdatesQueue) {
2018-08-27 21:58:58 +00:00
store := ctx.KVStore(k.storeKey)
// marshal
2018-08-29 03:45:26 +00:00
bz := k.cdc.MustMarshalBinary(suq)
2018-08-27 21:58:58 +00:00
// 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 their associated Channels
2018-08-27 21:58:58 +00:00
// This section deals with only setting and getting
func (k Keeper) getSubmittedUpdate(ctx sdk.Context, channelID ChannelID) (SubmittedUpdate, bool) {
2018-08-27 21:58:58 +00:00
// 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)
2018-08-27 21:58:58 +00:00
store.Set(key, bz) // panics if something goes wrong
}
func (k Keeper) deleteSubmittedUpdate(ctx sdk.Context, channelID ChannelID) {
2018-08-27 21:58:58 +00:00
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)
2018-08-27 21:58:58 +00:00
store.Set(key, bz) // panics if something goes wrong
}
func (k Keeper) deleteChannel(ctx sdk.Context, channelID ChannelID) {
2018-08-27 21:58:58 +00:00
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 {
2018-08-27 21:58:58 +00:00
// get last channel ID
var lastID ChannelID
store := ctx.KVStore(k.storeKey)
2018-08-27 21:58:58 +00:00
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)
2018-08-27 21:58:58 +00:00
}
// increment to create new one
newID := lastID + 1
2018-08-27 21:58:58 +00:00
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))
2018-08-27 21:58:58 +00:00
}
func (k Keeper) getLastChannelIDKey() []byte {
return []byte("lastChannelID")
2018-08-24 23:18:41 +00:00
}