package paychan import ( "bytes" "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. // Keepers contain main business logic of the module. type Keeper struct { storeKey sdk.StoreKey cdc *wire.Codec // needed to serialize objects before putting them in the store coinKeeper bank.Keeper //codespace sdk.CodespaceType TODO custom errors } // 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 } // 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 checks) 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()) } // 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 } // Initiate the close of a payment channel, subject to dispute period. func (k Keeper) InitCloseChannelBySender(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) { // This is roughly the default path for non unidirectional channels // get the channel channel, found := k.getChannel(ctx, update.ChannelID) if !found { return nil, sdk.ErrInternal("Channel doesn't exist") } err := VerifyUpdate(channel, update) if err != nil { return nil, err } q := k.getSubmittedUpdatesQueue(ctx) 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 sdk.ErrInternal("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 } // Immediately close a channel. func (k Keeper) CloseChannelByReceiver(ctx sdk.Context, update Update) (sdk.Tags, sdk.Error) { // get the channel channel, found := k.getChannel(ctx, update.ChannelID) if !found { return nil, sdk.ErrInternal("Channel doesn't exist") } err := VerifyUpdate(channel, update) if err != nil { return nil, err } // Check if there is an update in the queue already q := k.getSubmittedUpdatesQueue(ctx) 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 // } func VerifyUpdate(channel Channel, update Update) sdk.Error { // 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 !verifySignatures(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) { var err sdk.Error var tags sdk.Tags channel, _ := k.getChannel(ctx, update.ChannelID) // TODO check channel exists and participants matches update payout length // 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 { _, tags, err = k.coinKeeper.AddCoins(ctx, channel.Participants[i], coins) if err != nil { panic(err) } } k.deleteChannel(ctx, update.ChannelID) return tags, nil } func verifySignatures(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 } // =========================================== SUBMITTED UPDATES QUEUE func (k Keeper) addToSubmittedUpdatesQueue(ctx sdk.Context, sUpdate SubmittedUpdate) { // always overwrite prexisting values - leave paychan logic to higher levels // get current queue q := k.getSubmittedUpdatesQueue(ctx) // 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 := k.getSubmittedUpdatesQueue(ctx) // 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 { // load from DB store := ctx.KVStore(k.storeKey) bz := store.Get(k.getSubmittedUpdatesQueueKey()) var suq SubmittedUpdatesQueue // if the submittedUpdatesQueue not found then return an empty one if bz != nil { // unmarshal k.cdc.MustUnmarshalBinary(bz, &suq) } return suq } 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 their 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(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 := 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(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(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(getLastChannelIDKey(), bz) // return return newID } func GetChannelKey(channelID ChannelID) []byte { return []byte(fmt.Sprintf("channel:%d", channelID)) } func getLastChannelIDKey() []byte { return []byte("lastChannelID") }