mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-26 08:15:19 +00:00
Merge pull request #1 from Kava-Labs/add-payment-channels
Add payment channels
This commit is contained in:
commit
8c9406cd35
@ -17,6 +17,7 @@ import (
|
|||||||
//ibccmd "github.com/cosmos/cosmos-sdk/x/ibc/client/cli"
|
//ibccmd "github.com/cosmos/cosmos-sdk/x/ibc/client/cli"
|
||||||
slashingcmd "github.com/cosmos/cosmos-sdk/x/slashing/client/cli"
|
slashingcmd "github.com/cosmos/cosmos-sdk/x/slashing/client/cli"
|
||||||
stakecmd "github.com/cosmos/cosmos-sdk/x/stake/client/cli"
|
stakecmd "github.com/cosmos/cosmos-sdk/x/stake/client/cli"
|
||||||
|
paychancmd "github.com/kava-labs/kava/internal/x/paychan/client/cmd"
|
||||||
|
|
||||||
"github.com/kava-labs/kava/internal/app"
|
"github.com/kava-labs/kava/internal/app"
|
||||||
//"github.com/kava-labs/kava/internal/lcd"
|
//"github.com/kava-labs/kava/internal/lcd"
|
||||||
@ -101,7 +102,7 @@ func main() {
|
|||||||
stakeCmd,
|
stakeCmd,
|
||||||
)
|
)
|
||||||
|
|
||||||
//Add stake commands
|
// Add gov commands
|
||||||
// govCmd := &cobra.Command{
|
// govCmd := &cobra.Command{
|
||||||
// Use: "gov",
|
// Use: "gov",
|
||||||
// Short: "Governance and voting subcommands",
|
// Short: "Governance and voting subcommands",
|
||||||
@ -128,10 +129,27 @@ func main() {
|
|||||||
authcmd.GetAccountCmd("acc", cdc, authcmd.GetAccountDecoder(cdc)),
|
authcmd.GetAccountCmd("acc", cdc, authcmd.GetAccountDecoder(cdc)),
|
||||||
)...)
|
)...)
|
||||||
rootCmd.AddCommand(
|
rootCmd.AddCommand(
|
||||||
client.PostCommands(
|
client.PostCommands( // this just wraps the input cmds with common flags
|
||||||
bankcmd.SendTxCmd(cdc),
|
bankcmd.SendTxCmd(cdc),
|
||||||
)...)
|
)...)
|
||||||
|
|
||||||
|
// Add paychan commands
|
||||||
|
paychanCmd := &cobra.Command{
|
||||||
|
Use: "paychan",
|
||||||
|
Short: "Payment channel subcommand",
|
||||||
|
}
|
||||||
|
paychanCmd.AddCommand(
|
||||||
|
client.PostCommands(
|
||||||
|
paychancmd.CreateChannelCmd(cdc),
|
||||||
|
paychancmd.GetChannelCmd(cdc, "paychan"), // pass in storeKey
|
||||||
|
paychancmd.GeneratePaymentCmd(cdc),
|
||||||
|
paychancmd.VerifyPaymentCmd(cdc, "paychan"), // pass in storeKey
|
||||||
|
paychancmd.SubmitPaymentCmd(cdc),
|
||||||
|
)...)
|
||||||
|
rootCmd.AddCommand(
|
||||||
|
paychanCmd,
|
||||||
|
)
|
||||||
|
|
||||||
// add proxy, version and key info
|
// add proxy, version and key info
|
||||||
rootCmd.AddCommand(
|
rootCmd.AddCommand(
|
||||||
keys.Commands(),
|
keys.Commands(),
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
//"github.com/cosmos/cosmos-sdk/x/ibc"
|
//"github.com/cosmos/cosmos-sdk/x/ibc"
|
||||||
"github.com/cosmos/cosmos-sdk/x/slashing"
|
"github.com/cosmos/cosmos-sdk/x/slashing"
|
||||||
"github.com/cosmos/cosmos-sdk/x/stake"
|
"github.com/cosmos/cosmos-sdk/x/stake"
|
||||||
|
"github.com/kava-labs/kava/internal/x/paychan"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -39,16 +40,19 @@ type KavaApp struct {
|
|||||||
// keys to access the substores
|
// keys to access the substores
|
||||||
keyMain *sdk.KVStoreKey
|
keyMain *sdk.KVStoreKey
|
||||||
keyAccount *sdk.KVStoreKey
|
keyAccount *sdk.KVStoreKey
|
||||||
|
|
||||||
//keyIBC *sdk.KVStoreKey
|
//keyIBC *sdk.KVStoreKey
|
||||||
keyStake *sdk.KVStoreKey
|
keyStake *sdk.KVStoreKey
|
||||||
keySlashing *sdk.KVStoreKey
|
keySlashing *sdk.KVStoreKey
|
||||||
//keyGov *sdk.KVStoreKey
|
//keyGov *sdk.KVStoreKey
|
||||||
keyFeeCollection *sdk.KVStoreKey
|
keyFeeCollection *sdk.KVStoreKey
|
||||||
|
keyPaychan *sdk.KVStoreKey
|
||||||
|
|
||||||
// keepers
|
// keepers
|
||||||
accountMapper auth.AccountMapper
|
accountMapper auth.AccountMapper
|
||||||
feeCollectionKeeper auth.FeeCollectionKeeper
|
feeCollectionKeeper auth.FeeCollectionKeeper
|
||||||
coinKeeper bank.Keeper
|
coinKeeper bank.Keeper
|
||||||
|
paychanKeeper paychan.Keeper
|
||||||
//ibcMapper ibc.Mapper
|
//ibcMapper ibc.Mapper
|
||||||
stakeKeeper stake.Keeper
|
stakeKeeper stake.Keeper
|
||||||
slashingKeeper slashing.Keeper
|
slashingKeeper slashing.Keeper
|
||||||
@ -71,6 +75,7 @@ func NewKavaApp(logger log.Logger, db dbm.DB, traceStore io.Writer, baseAppOptio
|
|||||||
cdc: cdc,
|
cdc: cdc,
|
||||||
keyMain: sdk.NewKVStoreKey("main"),
|
keyMain: sdk.NewKVStoreKey("main"),
|
||||||
keyAccount: sdk.NewKVStoreKey("acc"),
|
keyAccount: sdk.NewKVStoreKey("acc"),
|
||||||
|
keyPaychan: sdk.NewKVStoreKey("paychan"),
|
||||||
//keyIBC: sdk.NewKVStoreKey("ibc"),
|
//keyIBC: sdk.NewKVStoreKey("ibc"),
|
||||||
keyStake: sdk.NewKVStoreKey("stake"),
|
keyStake: sdk.NewKVStoreKey("stake"),
|
||||||
keySlashing: sdk.NewKVStoreKey("slashing"),
|
keySlashing: sdk.NewKVStoreKey("slashing"),
|
||||||
@ -87,6 +92,7 @@ func NewKavaApp(logger log.Logger, db dbm.DB, traceStore io.Writer, baseAppOptio
|
|||||||
|
|
||||||
// Create the keepers
|
// Create the keepers
|
||||||
app.coinKeeper = bank.NewKeeper(app.accountMapper)
|
app.coinKeeper = bank.NewKeeper(app.accountMapper)
|
||||||
|
app.paychanKeeper = paychan.NewKeeper(app.cdc, app.keyPaychan, app.coinKeeper)
|
||||||
//app.ibcMapper = ibc.NewMapper(app.cdc, app.keyIBC, app.RegisterCodespace(ibc.DefaultCodespace))
|
//app.ibcMapper = ibc.NewMapper(app.cdc, app.keyIBC, app.RegisterCodespace(ibc.DefaultCodespace))
|
||||||
app.stakeKeeper = stake.NewKeeper(app.cdc, app.keyStake, app.coinKeeper, app.RegisterCodespace(stake.DefaultCodespace))
|
app.stakeKeeper = stake.NewKeeper(app.cdc, app.keyStake, app.coinKeeper, app.RegisterCodespace(stake.DefaultCodespace))
|
||||||
app.slashingKeeper = slashing.NewKeeper(app.cdc, app.keySlashing, app.stakeKeeper, app.RegisterCodespace(slashing.DefaultCodespace))
|
app.slashingKeeper = slashing.NewKeeper(app.cdc, app.keySlashing, app.stakeKeeper, app.RegisterCodespace(slashing.DefaultCodespace))
|
||||||
@ -98,7 +104,8 @@ func NewKavaApp(logger log.Logger, db dbm.DB, traceStore io.Writer, baseAppOptio
|
|||||||
AddRoute("bank", bank.NewHandler(app.coinKeeper)).
|
AddRoute("bank", bank.NewHandler(app.coinKeeper)).
|
||||||
//AddRoute("ibc", ibc.NewHandler(app.ibcMapper, app.coinKeeper)).
|
//AddRoute("ibc", ibc.NewHandler(app.ibcMapper, app.coinKeeper)).
|
||||||
AddRoute("stake", stake.NewHandler(app.stakeKeeper)).
|
AddRoute("stake", stake.NewHandler(app.stakeKeeper)).
|
||||||
AddRoute("slashing", slashing.NewHandler(app.slashingKeeper))
|
AddRoute("slashing", slashing.NewHandler(app.slashingKeeper)).
|
||||||
|
AddRoute("paychan", paychan.NewHandler(app.paychanKeeper))
|
||||||
//AddRoute("gov", gov.NewHandler(app.govKeeper))
|
//AddRoute("gov", gov.NewHandler(app.govKeeper))
|
||||||
|
|
||||||
// Set func to initialze the chain from appState in genesis file
|
// Set func to initialze the chain from appState in genesis file
|
||||||
@ -110,7 +117,7 @@ func NewKavaApp(logger log.Logger, db dbm.DB, traceStore io.Writer, baseAppOptio
|
|||||||
app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper, app.feeCollectionKeeper))
|
app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper, app.feeCollectionKeeper))
|
||||||
|
|
||||||
// Mount stores
|
// Mount stores
|
||||||
app.MountStoresIAVL(app.keyMain, app.keyAccount, app.keyStake, app.keySlashing, app.keyFeeCollection)
|
app.MountStoresIAVL(app.keyMain, app.keyAccount, app.keyStake, app.keySlashing, app.keyFeeCollection, app.keyPaychan)
|
||||||
err := app.LoadLatestVersion(app.keyMain)
|
err := app.LoadLatestVersion(app.keyMain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cmn.Exit(err.Error())
|
cmn.Exit(err.Error())
|
||||||
@ -121,6 +128,7 @@ func NewKavaApp(logger log.Logger, db dbm.DB, traceStore io.Writer, baseAppOptio
|
|||||||
// Creates a codec for use across the whole app.
|
// Creates a codec for use across the whole app.
|
||||||
func CreateKavaAppCodec() *wire.Codec {
|
func CreateKavaAppCodec() *wire.Codec {
|
||||||
cdc := wire.NewCodec()
|
cdc := wire.NewCodec()
|
||||||
|
paychan.RegisterWire(cdc)
|
||||||
//ibc.RegisterWire(cdc)
|
//ibc.RegisterWire(cdc)
|
||||||
bank.RegisterWire(cdc)
|
bank.RegisterWire(cdc)
|
||||||
stake.RegisterWire(cdc)
|
stake.RegisterWire(cdc)
|
||||||
@ -143,6 +151,7 @@ func (app *KavaApp) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) ab
|
|||||||
|
|
||||||
// The function baseapp runs on receipt of a EndBlock ABCI message
|
// The function baseapp runs on receipt of a EndBlock ABCI message
|
||||||
func (app *KavaApp) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock {
|
func (app *KavaApp) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock {
|
||||||
|
paychan.EndBlocker(ctx, app.paychanKeeper)
|
||||||
validatorUpdates := stake.EndBlocker(ctx, app.stakeKeeper)
|
validatorUpdates := stake.EndBlocker(ctx, app.stakeKeeper)
|
||||||
|
|
||||||
//tags, _ := gov.EndBlocker(ctx, app.govKeeper)
|
//tags, _ := gov.EndBlocker(ctx, app.govKeeper)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
|
/*
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
@ -75,3 +76,4 @@ func TestGenesis(t *testing.T) {
|
|||||||
res1 = bapp.accountMapper.GetAccount(ctx, baseAcc.Address)
|
res1 = bapp.accountMapper.GetAccount(ctx, baseAcc.Address)
|
||||||
assert.Equal(t, acc, res1)
|
assert.Equal(t, acc, res1)
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
57
internal/x/paychan/README.md
Normal file
57
internal/x/paychan/README.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# Unidrectional Payment Channels
|
||||||
|
|
||||||
|
This module implements simple but feature complete unidirectional payment channels. Channels can be opened by a sender and closed immediately by the receiver, or by the sender subject to a dispute period. There are no top-ups or partial withdrawals (yet). Channels support multiple currencies.
|
||||||
|
|
||||||
|
>Note: This module is still a bit rough around the edges. More feature planned. More test cases needed.
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
## Create a channel
|
||||||
|
|
||||||
|
kvcli paychan create --from <your account name> --to <receivers address> --amount 100KVA --chain-id <your chain ID>
|
||||||
|
|
||||||
|
## Send an off-chain payment
|
||||||
|
Send a payment for 10 KVA.
|
||||||
|
|
||||||
|
kvcli paychan pay --from <your account name> --sen-amt 90KVA --rec-amt 10KVA --chan-id <ID of channel> --filename payment.json --chain-id <your chain ID>
|
||||||
|
|
||||||
|
Send the file `payment.json` to your receiver. Then they run the following to verify.
|
||||||
|
|
||||||
|
kvcli paychan verify --filename payment.json
|
||||||
|
|
||||||
|
## Close a channel
|
||||||
|
The receiver can close immediately at any time.
|
||||||
|
|
||||||
|
kvcli paychan submit --from <receiver's account name> --payment payment.json --chain-id <your chain ID>
|
||||||
|
|
||||||
|
The sender can submit a close request, causing the channel will close automatically after a dispute period. During this period a receiver can still close immediately.
|
||||||
|
|
||||||
|
kvcli paychan submit --from <receiver's account name> --payment payment.json --chain-id <your chain ID>
|
||||||
|
|
||||||
|
>Note: The dispute period on the testnet is 30 seconds for ease of testing.
|
||||||
|
|
||||||
|
## Get info on a channel
|
||||||
|
|
||||||
|
kvcli get --chan-id <ID of channel>
|
||||||
|
|
||||||
|
This will print out a channel, if it exists, and any submitted close requests.
|
||||||
|
|
||||||
|
# TODOs
|
||||||
|
|
||||||
|
- in code TODOs
|
||||||
|
- Tidy up - method descriptions, heading comments, remove uneccessary comments, README/docs
|
||||||
|
- Find a better name for Queue - clarify distinction between int slice and abstract queue concept
|
||||||
|
- write some sort of integration test
|
||||||
|
- possible bug in submitting same update repeatedly
|
||||||
|
- find nicer name for payout
|
||||||
|
- add Gas usage
|
||||||
|
- add tags (return channel id on creation)
|
||||||
|
- refactor cmds to be able to test them, then test them
|
||||||
|
- verify doesn’t throw json parsing error on invalid json
|
||||||
|
- can’t submit an update from an unitialised account
|
||||||
|
- pay without a --from returns confusing error
|
||||||
|
- use custom errors instead of using sdk.ErrInternal
|
||||||
|
- split off signatures from update as with txs/msgs - testing easier, code easier to use, doesn't store sigs unecessarily on chain
|
||||||
|
- consider removing pubKey from UpdateSignature - instead let channel module access accountMapper
|
||||||
|
- refactor queue into one object
|
||||||
|
- remove printout during tests caused by mock app initialisation
|
316
internal/x/paychan/client/cmd/cmd.go
Normal file
316
internal/x/paychan/client/cmd/cmd.go
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
|
"github.com/cosmos/cosmos-sdk/client/context"
|
||||||
|
"github.com/cosmos/cosmos-sdk/client/keys"
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/cosmos/cosmos-sdk/wire"
|
||||||
|
authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/internal/x/paychan"
|
||||||
|
)
|
||||||
|
|
||||||
|
// list of functions that return pointers to cobra commands
|
||||||
|
// No local storage needed for cli acting as a sender
|
||||||
|
|
||||||
|
func CreateChannelCmd(cdc *wire.Codec) *cobra.Command {
|
||||||
|
flagTo := "to"
|
||||||
|
flagCoins := "amount"
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "create",
|
||||||
|
Short: "Create a new payment channel",
|
||||||
|
Long: "Create a new unidirectional payment channel from a local address to a remote address, funded with some amount of coins. These coins are removed from the sender account and put into the channel.",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
|
// Create a "client context" stuct populated with info from common flags
|
||||||
|
ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc))
|
||||||
|
// TODO is this needed for channelID
|
||||||
|
// ctx.PrintResponse = true
|
||||||
|
|
||||||
|
// Get sender adress
|
||||||
|
sender, err := ctx.GetFromAddress()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get receiver address
|
||||||
|
toStr := viper.GetString(flagTo)
|
||||||
|
receiver, err := sdk.AccAddressFromBech32(toStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get channel funding amount
|
||||||
|
coinsString := viper.GetString(flagCoins)
|
||||||
|
coins, err := sdk.ParseCoins(coinsString)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the create channel msg to send
|
||||||
|
msg := paychan.MsgCreate{
|
||||||
|
Participants: [2]sdk.AccAddress{sender, receiver},
|
||||||
|
Coins: coins,
|
||||||
|
}
|
||||||
|
err = msg.ValidateBasic()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build and sign the transaction, then broadcast to the blockchain
|
||||||
|
err = ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().String(flagTo, "", "Recipient address of the payment channel.")
|
||||||
|
cmd.Flags().String(flagCoins, "", "Amount of coins to fund the payment channel with.")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func GeneratePaymentCmd(cdc *wire.Codec) *cobra.Command {
|
||||||
|
flagId := "chan-id"
|
||||||
|
flagReceiverAmount := "rec-amt" // amount the receiver should received on closing the channel
|
||||||
|
flagSenderAmount := "sen-amt"
|
||||||
|
flagPaymentFile := "filename"
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "pay",
|
||||||
|
Short: "Generate a new payment.", // TODO descriptions
|
||||||
|
Long: "Generate a payment file (json) to send to the receiver as a payment.",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
|
// Create a "client context" stuct populated with info from common flags
|
||||||
|
ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc))
|
||||||
|
|
||||||
|
// Get the paychan id
|
||||||
|
id := paychan.ChannelID(viper.GetInt64(flagId)) // TODO make this default to pulling id from chain
|
||||||
|
|
||||||
|
// Get channel receiver amount
|
||||||
|
senderCoins, err := sdk.ParseCoins(viper.GetString(flagSenderAmount))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Get channel receiver amount
|
||||||
|
receiverCoins, err := sdk.ParseCoins(viper.GetString(flagReceiverAmount))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// create close paychan msg
|
||||||
|
update := paychan.Update{
|
||||||
|
ChannelID: id,
|
||||||
|
Payout: paychan.Payout{senderCoins, receiverCoins},
|
||||||
|
// empty sigs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign the update as the sender
|
||||||
|
keybase, err := keys.GetKeyBase()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
name := ctx.FromAddressName
|
||||||
|
passphrase, err := ctx.GetPassphraseFromStdin(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bz := update.GetSignBytes()
|
||||||
|
|
||||||
|
sig, pubKey, err := keybase.Sign(name, passphrase, bz)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
update.Sigs = [1]paychan.UpdateSignature{{
|
||||||
|
PubKey: pubKey,
|
||||||
|
CryptoSignature: sig,
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Write out the update
|
||||||
|
jsonUpdate, err := wire.MarshalJSONIndent(cdc, update)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
paymentFile := viper.GetString(flagPaymentFile)
|
||||||
|
err = ioutil.WriteFile(paymentFile, jsonUpdate, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("Written payment out to %v.\n", paymentFile)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().Int(flagId, 0, "ID of the payment channel.")
|
||||||
|
cmd.Flags().String(flagSenderAmount, "", "Total coins to payout to sender on channel close.")
|
||||||
|
cmd.Flags().String(flagReceiverAmount, "", "Total coins to payout to sender on channel close.")
|
||||||
|
cmd.Flags().String(flagPaymentFile, "payment.json", "File name to write the payment into.")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerifyPaymentCmd(cdc *wire.Codec, paychanStoreName string) *cobra.Command {
|
||||||
|
flagPaymentFile := "payment"
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "verify",
|
||||||
|
Short: "Verify a payment file.",
|
||||||
|
Long: "Verify that a received payment can be used to close a channel.",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
|
// Create a "client context" stuct populated with info from common flags
|
||||||
|
ctx := context.NewCoreContextFromViper()
|
||||||
|
|
||||||
|
// read in update
|
||||||
|
bz, err := ioutil.ReadFile(viper.GetString(flagPaymentFile))
|
||||||
|
if err != nil {
|
||||||
|
// TODO add nice message about how to feed in stdin
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// decode json
|
||||||
|
var update paychan.Update
|
||||||
|
cdc.UnmarshalJSON(bz, &update)
|
||||||
|
|
||||||
|
// get the channel from the node
|
||||||
|
res, err := ctx.QueryStore(paychan.GetChannelKey(update.ChannelID), paychanStoreName)
|
||||||
|
if len(res) == 0 || err != nil {
|
||||||
|
return errors.Errorf("channel with ID '%d' does not exist", update.ChannelID)
|
||||||
|
}
|
||||||
|
var channel paychan.Channel
|
||||||
|
cdc.MustUnmarshalBinary(res, &channel)
|
||||||
|
|
||||||
|
//verify
|
||||||
|
verificationError := paychan.VerifyUpdate(channel, update)
|
||||||
|
|
||||||
|
// print result
|
||||||
|
if verificationError == nil {
|
||||||
|
fmt.Printf("Payment is valid for channel '%d'.\n", update.ChannelID)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Payment is NOT valid for channel '%d'.\n", update.ChannelID)
|
||||||
|
fmt.Println(verificationError)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().String(flagPaymentFile, "payment.json", "File name to read the payment from.")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func SubmitPaymentCmd(cdc *wire.Codec) *cobra.Command {
|
||||||
|
flagPaymentFile := "payment"
|
||||||
|
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "submit",
|
||||||
|
Short: "Submit a payment to the blockchain to close the channel.",
|
||||||
|
Long: fmt.Sprintf("Submit a payment to the blockchain to either close a channel immediately (if you are the receiver) or after a dispute period of %d blocks (if you are the sender).", paychan.ChannelDisputeTime),
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
|
// Create a "client context" stuct populated with info from common flags
|
||||||
|
ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc))
|
||||||
|
// ctx.PrintResponse = true TODO is this needed for channelID
|
||||||
|
|
||||||
|
// Get sender adress
|
||||||
|
submitter, err := ctx.GetFromAddress()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// read in update
|
||||||
|
bz, err := ioutil.ReadFile(viper.GetString(flagPaymentFile))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// decode json
|
||||||
|
var update paychan.Update
|
||||||
|
cdc.UnmarshalJSON(bz, &update)
|
||||||
|
|
||||||
|
// Create the create channel msg to send
|
||||||
|
msg := paychan.MsgSubmitUpdate{
|
||||||
|
Update: update,
|
||||||
|
Submitter: submitter,
|
||||||
|
}
|
||||||
|
err = msg.ValidateBasic()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build and sign the transaction, then broadcast to the blockchain
|
||||||
|
err = ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().String(flagPaymentFile, "payment.json", "File to read the payment from.")
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetChannelCmd(cdc *wire.Codec, paychanStoreName string) *cobra.Command {
|
||||||
|
flagId := "chan-id"
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "get",
|
||||||
|
Short: "Get info on a channel.",
|
||||||
|
Long: "Get the details of a non closed channel plus any submitted update waiting to be executed.",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
|
||||||
|
// Create a "client context" stuct populated with info from common flags
|
||||||
|
ctx := context.NewCoreContextFromViper()
|
||||||
|
|
||||||
|
// Get channel ID
|
||||||
|
id := paychan.ChannelID(viper.GetInt64(flagId))
|
||||||
|
|
||||||
|
// Get the channel from the node
|
||||||
|
res, err := ctx.QueryStore(paychan.GetChannelKey(id), paychanStoreName)
|
||||||
|
if len(res) == 0 || err != nil {
|
||||||
|
return errors.Errorf("channel with ID '%d' does not exist", id)
|
||||||
|
}
|
||||||
|
var channel paychan.Channel
|
||||||
|
cdc.MustUnmarshalBinary(res, &channel)
|
||||||
|
|
||||||
|
// Convert the channel to a json object for pretty printing
|
||||||
|
jsonChannel, err := wire.MarshalJSONIndent(cdc, channel)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// print out json channel
|
||||||
|
fmt.Println(string(jsonChannel))
|
||||||
|
|
||||||
|
// Get any submitted updates from the node
|
||||||
|
res, err = ctx.QueryStore(paychan.GetSubmittedUpdateKey(id), paychanStoreName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Print out the submited update if it exsits
|
||||||
|
if len(res) != 0 {
|
||||||
|
var submittedUpdate paychan.SubmittedUpdate
|
||||||
|
cdc.MustUnmarshalBinary(res, &submittedUpdate)
|
||||||
|
|
||||||
|
// Convert the submitted update to a json object for pretty printing
|
||||||
|
jsonSU, err := wire.MarshalJSONIndent(cdc, submittedUpdate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// print out json submitted update
|
||||||
|
fmt.Println(string(jsonSU))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().Int(flagId, 0, "ID of the payment channel.")
|
||||||
|
return cmd
|
||||||
|
}
|
7
internal/x/paychan/client/rest/channel-watcher.go
Normal file
7
internal/x/paychan/client/rest/channel-watcher.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package lcd
|
||||||
|
|
||||||
|
import ()
|
||||||
|
|
||||||
|
// implement a thing to poll blockchain and handles paychan disputes
|
||||||
|
// needs plugged into LCD - add a "background processes" slot in the LCD run function?
|
||||||
|
// eventually LCD could evolve into paychan (network) daemon
|
24
internal/x/paychan/client/rest/rest.go
Normal file
24
internal/x/paychan/client/rest/rest.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package rest
|
||||||
|
|
||||||
|
import (
|
||||||
|
//"github.com/gorilla/mux"
|
||||||
|
//"github.com/tendermint/go-crypto/keys"
|
||||||
|
//"github.com/cosmos/cosmos-sdk/client/context"
|
||||||
|
//"github.com/cosmos/cosmos-sdk/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
// RegisterRoutes registers paychan-related REST handlers to a router
|
||||||
|
func RegisterRoutes(ctx context.CoreContext, r *mux.Router, cdc *wire.Codec, kb keys.Keybase) {
|
||||||
|
//r.HandleFunc("/accounts/{address}/send", SendRequestHandlerFn(cdc, kb, ctx)).Methods("POST")
|
||||||
|
}
|
||||||
|
|
||||||
|
// handler functions ...
|
||||||
|
// create paychan
|
||||||
|
// close paychan
|
||||||
|
// get paychan(s)
|
||||||
|
// send paychan payment
|
||||||
|
// get balance from receiver
|
||||||
|
// get balance from local storage
|
||||||
|
// handle incoming payment
|
||||||
|
*/
|
5
internal/x/paychan/doc.go
Normal file
5
internal/x/paychan/doc.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/*
|
||||||
|
High level package documentation.
|
||||||
|
TODO Explain how the payment channels are implemented.
|
||||||
|
*/
|
||||||
|
package paychan
|
35
internal/x/paychan/endblocker.go
Normal file
35
internal/x/paychan/endblocker.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package paychan
|
||||||
|
|
||||||
|
import (
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func EndBlocker(ctx sdk.Context, k Keeper) sdk.Tags {
|
||||||
|
var err sdk.Error
|
||||||
|
var channelTags sdk.Tags
|
||||||
|
tags := sdk.EmptyTags()
|
||||||
|
|
||||||
|
// Iterate through submittedUpdatesQueue
|
||||||
|
// TODO optimise so it doesn't pull every channel update from DB every block
|
||||||
|
q := k.getSubmittedUpdatesQueue(ctx)
|
||||||
|
var sUpdate SubmittedUpdate
|
||||||
|
var found bool
|
||||||
|
|
||||||
|
for _, id := range q {
|
||||||
|
// close the channel if the update has reached its execution time.
|
||||||
|
// Using >= in case some are somehow missed.
|
||||||
|
sUpdate, found = k.getSubmittedUpdate(ctx, id)
|
||||||
|
if !found {
|
||||||
|
panic("can't find element in queue that should exist")
|
||||||
|
}
|
||||||
|
if ctx.BlockHeight() >= sUpdate.ExecutionTime {
|
||||||
|
k.removeFromSubmittedUpdatesQueue(ctx, sUpdate.ChannelID)
|
||||||
|
channelTags, err = k.closeChannel(ctx, sUpdate.Update)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
tags.AppendTags(channelTags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tags
|
||||||
|
}
|
59
internal/x/paychan/endblocker_test.go
Normal file
59
internal/x/paychan/endblocker_test.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package paychan
|
||||||
|
|
||||||
|
import (
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEndBlocker(t *testing.T) {
|
||||||
|
// TODO test that endBlocker doesn't close channels before the execution time
|
||||||
|
|
||||||
|
// SETUP
|
||||||
|
accountSeeds := []string{"senderSeed", "receiverSeed"}
|
||||||
|
ctx, _, channelKeeper, addrs, _, _, _ := createMockApp(accountSeeds)
|
||||||
|
sender := addrs[0]
|
||||||
|
receiver := addrs[1]
|
||||||
|
coins := sdk.Coins{sdk.NewCoin("KVA", 10)}
|
||||||
|
|
||||||
|
// create new channel
|
||||||
|
channelID := ChannelID(0) // should be 0 as first channel
|
||||||
|
channel := Channel{
|
||||||
|
ID: channelID,
|
||||||
|
Participants: [2]sdk.AccAddress{sender, receiver},
|
||||||
|
Coins: coins,
|
||||||
|
}
|
||||||
|
channelKeeper.setChannel(ctx, channel)
|
||||||
|
|
||||||
|
// create closing update and submittedUpdate
|
||||||
|
payout := Payout{sdk.Coins{sdk.NewCoin("KVA", 3)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}
|
||||||
|
update := Update{
|
||||||
|
ChannelID: channelID,
|
||||||
|
Payout: payout,
|
||||||
|
//Sigs: [1]crypto.Signature{},
|
||||||
|
}
|
||||||
|
sUpdate := SubmittedUpdate{
|
||||||
|
Update: update,
|
||||||
|
ExecutionTime: 0, // current blocktime
|
||||||
|
}
|
||||||
|
// Set empty submittedUpdatesQueue TODO work out proper genesis initialisation
|
||||||
|
channelKeeper.setSubmittedUpdatesQueue(ctx, SubmittedUpdatesQueue{})
|
||||||
|
// flag channel for closure
|
||||||
|
channelKeeper.addToSubmittedUpdatesQueue(ctx, sUpdate)
|
||||||
|
|
||||||
|
// ACTION
|
||||||
|
EndBlocker(ctx, channelKeeper)
|
||||||
|
|
||||||
|
// CHECK RESULTS
|
||||||
|
// ideally just check if keeper.channelClose was called, but can't
|
||||||
|
// writing endBlocker to accept an interface of which keeper is implementation would make this possible
|
||||||
|
// check channel is gone
|
||||||
|
_, found := channelKeeper.getChannel(ctx, channelID)
|
||||||
|
assert.False(t, found)
|
||||||
|
// check queue is empty, NOTE: due to encoding, an empty queue (underneath just an int slice) will be decoded as nil slice rather than an empty slice
|
||||||
|
suq := channelKeeper.getSubmittedUpdatesQueue(ctx)
|
||||||
|
assert.Equal(t, SubmittedUpdatesQueue(nil), suq)
|
||||||
|
// check submittedUpdate is gone
|
||||||
|
_, found = channelKeeper.getSubmittedUpdate(ctx, channelID)
|
||||||
|
assert.False(t, found)
|
||||||
|
}
|
62
internal/x/paychan/handler.go
Normal file
62
internal/x/paychan/handler.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package paychan
|
||||||
|
|
||||||
|
import (
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewHandler returns a handler for "paychan" type messages.
|
||||||
|
// Called when adding routes to a newly created app.
|
||||||
|
func NewHandler(k Keeper) sdk.Handler {
|
||||||
|
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case MsgCreate:
|
||||||
|
return handleMsgCreate(ctx, k, msg)
|
||||||
|
case MsgSubmitUpdate:
|
||||||
|
return handleMsgSubmitUpdate(ctx, k, msg)
|
||||||
|
default:
|
||||||
|
errMsg := "Unrecognized paychan Msg type: " + reflect.TypeOf(msg).Name()
|
||||||
|
return sdk.ErrUnknownRequest(errMsg).Result()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle MsgCreate
|
||||||
|
// Leaves validation to the keeper methods.
|
||||||
|
func handleMsgCreate(ctx sdk.Context, k Keeper, msg MsgCreate) sdk.Result {
|
||||||
|
tags, err := k.CreateChannel(ctx, msg.Participants[0], msg.Participants[len(msg.Participants)-1], msg.Coins)
|
||||||
|
if err != nil {
|
||||||
|
return err.Result()
|
||||||
|
}
|
||||||
|
// TODO any other information that should be returned in Result?
|
||||||
|
return sdk.Result{
|
||||||
|
Tags: tags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle MsgSubmitUpdate
|
||||||
|
// Leaves validation to the keeper methods.
|
||||||
|
func handleMsgSubmitUpdate(ctx sdk.Context, k Keeper, msg MsgSubmitUpdate) sdk.Result {
|
||||||
|
var err sdk.Error
|
||||||
|
tags := sdk.EmptyTags()
|
||||||
|
|
||||||
|
// TODO refactor signer detection - move to keeper or find nicer setup
|
||||||
|
channel, _ := k.getChannel(ctx, msg.Update.ChannelID)
|
||||||
|
participants := channel.Participants
|
||||||
|
|
||||||
|
// if only sender signed
|
||||||
|
if reflect.DeepEqual(msg.Submitter, participants[0]) {
|
||||||
|
tags, err = k.InitCloseChannelBySender(ctx, msg.Update)
|
||||||
|
// else if receiver signed
|
||||||
|
} else if reflect.DeepEqual(msg.Submitter, participants[len(participants)-1]) {
|
||||||
|
tags, err = k.CloseChannelByReceiver(ctx, msg.Update)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err.Result()
|
||||||
|
}
|
||||||
|
// These tags can be used by clients to subscribe to channel close attempts
|
||||||
|
return sdk.Result{
|
||||||
|
Tags: tags,
|
||||||
|
}
|
||||||
|
}
|
375
internal/x/paychan/keeper.go
Normal file
375
internal/x/paychan/keeper.go
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
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(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 := 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(GetSubmittedUpdateKey(channelID))
|
||||||
|
// TODO does this have return values? What happens when key doesn't exist?
|
||||||
|
}
|
||||||
|
func 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")
|
||||||
|
}
|
315
internal/x/paychan/keeper_test.go
Normal file
315
internal/x/paychan/keeper_test.go
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
package paychan
|
||||||
|
|
||||||
|
import (
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKeeper(t *testing.T) {
|
||||||
|
t.Run("CreateChannel", func(t *testing.T) {
|
||||||
|
|
||||||
|
// TODO test for receiver account not existing (OK) and sender not existing (not ok)
|
||||||
|
|
||||||
|
accountSeeds := []string{"senderSeed", "receiverSeed"}
|
||||||
|
const (
|
||||||
|
senderAccountIndex int = 0
|
||||||
|
receiverAccountIndex int = 1
|
||||||
|
)
|
||||||
|
_, addrs, _, _ := createTestGenAccounts(accountSeeds, sdk.Coins{}) // pure function
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
sender sdk.AccAddress
|
||||||
|
receiver sdk.AccAddress
|
||||||
|
coins sdk.Coins
|
||||||
|
shouldCreateChannel bool
|
||||||
|
shouldError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"HappyPath",
|
||||||
|
addrs[senderAccountIndex],
|
||||||
|
addrs[receiverAccountIndex],
|
||||||
|
sdk.Coins{sdk.NewCoin("KVA", 10)},
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NilAddress",
|
||||||
|
sdk.AccAddress{},
|
||||||
|
sdk.AccAddress{},
|
||||||
|
sdk.Coins{sdk.NewCoin("KVA", 10)},
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NilCoins",
|
||||||
|
addrs[senderAccountIndex],
|
||||||
|
addrs[receiverAccountIndex],
|
||||||
|
sdk.Coins{},
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NegativeCoins",
|
||||||
|
addrs[senderAccountIndex],
|
||||||
|
addrs[receiverAccountIndex],
|
||||||
|
sdk.Coins{sdk.NewCoin("KVA", -57)},
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
////// SETUP
|
||||||
|
// create basic mock app
|
||||||
|
ctx, coinKeeper, channelKeeper, addrs, _, _, genAccFunding := createMockApp(accountSeeds)
|
||||||
|
//
|
||||||
|
////// ACTION
|
||||||
|
_, err := channelKeeper.CreateChannel(ctx, testCase.sender, testCase.receiver, testCase.coins)
|
||||||
|
|
||||||
|
//
|
||||||
|
////// CHECK RESULTS
|
||||||
|
// Check error
|
||||||
|
if testCase.shouldError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
// Check if channel exists and is correct
|
||||||
|
channelID := ChannelID(0) // should be 0 as first channel
|
||||||
|
createdChan, found := channelKeeper.getChannel(ctx, channelID)
|
||||||
|
|
||||||
|
if testCase.shouldCreateChannel {
|
||||||
|
expectedChan := Channel{
|
||||||
|
ID: channelID,
|
||||||
|
Participants: [2]sdk.AccAddress{testCase.sender, testCase.receiver},
|
||||||
|
Coins: testCase.coins,
|
||||||
|
}
|
||||||
|
|
||||||
|
// channel exists and correct
|
||||||
|
assert.True(t, found)
|
||||||
|
assert.Equal(t, expectedChan, createdChan)
|
||||||
|
// check coins deducted from sender
|
||||||
|
assert.Equal(t, genAccFunding.Minus(testCase.coins), coinKeeper.GetCoins(ctx, testCase.sender))
|
||||||
|
// check no coins deducted from receiver
|
||||||
|
assert.Equal(t, genAccFunding, coinKeeper.GetCoins(ctx, testCase.receiver))
|
||||||
|
// check next global channelID incremented
|
||||||
|
assert.Equal(t, ChannelID(1), channelKeeper.getNewChannelID(ctx))
|
||||||
|
} else {
|
||||||
|
// channel doesn't exist
|
||||||
|
assert.False(t, found)
|
||||||
|
assert.Equal(t, Channel{}, createdChan)
|
||||||
|
// check no coins deducted from sender
|
||||||
|
assert.Equal(t, genAccFunding, coinKeeper.GetCoins(ctx, addrs[senderAccountIndex]))
|
||||||
|
// check no coins deducted from receiver
|
||||||
|
assert.Equal(t, genAccFunding, coinKeeper.GetCoins(ctx, addrs[receiverAccountIndex]))
|
||||||
|
// check next global channelID not incremented
|
||||||
|
assert.Equal(t, ChannelID(0), channelKeeper.getNewChannelID(ctx))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("CloseChannelByReceiver", func(t *testing.T) {
|
||||||
|
// TODO convert to table driven and add more test cases
|
||||||
|
// channel exists or not (assume channels correct)
|
||||||
|
// various Updates
|
||||||
|
// submittedUpdates existing or not (assume they are valid)
|
||||||
|
|
||||||
|
// SETUP
|
||||||
|
accountSeeds := []string{"senderSeed", "receiverSeed"}
|
||||||
|
const (
|
||||||
|
senderAccountIndex int = 0
|
||||||
|
receiverAccountIndex int = 1
|
||||||
|
)
|
||||||
|
ctx, coinKeeper, channelKeeper, addrs, pubKeys, privKeys, genAccFunding := createMockApp(accountSeeds)
|
||||||
|
|
||||||
|
coins := sdk.Coins{sdk.NewCoin("KVA", 10)}
|
||||||
|
|
||||||
|
// create new channel
|
||||||
|
channelID := ChannelID(0) // should be 0 as first channel
|
||||||
|
channel := Channel{
|
||||||
|
ID: channelID,
|
||||||
|
Participants: [2]sdk.AccAddress{addrs[senderAccountIndex], addrs[receiverAccountIndex]},
|
||||||
|
Coins: coins,
|
||||||
|
}
|
||||||
|
channelKeeper.setChannel(ctx, channel)
|
||||||
|
|
||||||
|
// create closing update
|
||||||
|
payout := Payout{sdk.Coins{sdk.NewCoin("KVA", 3)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}
|
||||||
|
update := Update{
|
||||||
|
ChannelID: channelID,
|
||||||
|
Payout: payout,
|
||||||
|
// empty sig
|
||||||
|
}
|
||||||
|
cryptoSig, _ := privKeys[senderAccountIndex].Sign(update.GetSignBytes())
|
||||||
|
update.Sigs = [1]UpdateSignature{UpdateSignature{
|
||||||
|
PubKey: pubKeys[senderAccountIndex],
|
||||||
|
CryptoSignature: cryptoSig,
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Set empty submittedUpdatesQueue TODO work out proper genesis initialisation
|
||||||
|
channelKeeper.setSubmittedUpdatesQueue(ctx, SubmittedUpdatesQueue{})
|
||||||
|
|
||||||
|
// ACTION
|
||||||
|
_, err := channelKeeper.CloseChannelByReceiver(ctx, update)
|
||||||
|
|
||||||
|
// CHECK RESULTS
|
||||||
|
// no error
|
||||||
|
assert.NoError(t, err)
|
||||||
|
// coins paid out
|
||||||
|
senderPayout := payout[senderAccountIndex]
|
||||||
|
assert.Equal(t, genAccFunding.Plus(senderPayout), coinKeeper.GetCoins(ctx, addrs[senderAccountIndex]))
|
||||||
|
receiverPayout := payout[receiverAccountIndex]
|
||||||
|
assert.Equal(t, genAccFunding.Plus(receiverPayout), coinKeeper.GetCoins(ctx, addrs[receiverAccountIndex]))
|
||||||
|
// channel deleted
|
||||||
|
_, found := channelKeeper.getChannel(ctx, channelID)
|
||||||
|
assert.False(t, found)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InitCloseChannelBySender", func(t *testing.T) {
|
||||||
|
|
||||||
|
// TODO do some documentation here
|
||||||
|
// Ideally this should mock calls to ctx.store.Get/Set - test the side effects without being dependent on implementatino details
|
||||||
|
// TODO test correct behaviour when a submittedUpdate already exists
|
||||||
|
|
||||||
|
accountSeeds := []string{"senderSeed", "receiverSeed", "notInChannelSeed"}
|
||||||
|
const (
|
||||||
|
senderAccountIndex int = 0
|
||||||
|
receiverAccountIndex int = 1
|
||||||
|
otherAccountIndex int = 2
|
||||||
|
)
|
||||||
|
chanID := ChannelID(0)
|
||||||
|
|
||||||
|
type testUpdate struct { // A parameterised version of an Update for use in specifying test cases.
|
||||||
|
channelID ChannelID // channelID of submitted update
|
||||||
|
payout Payout // payout of submitted update
|
||||||
|
pubKeyAccountIndex int // pubkey of signature of submitted update
|
||||||
|
sigAccountIndex int // crypto signature of signature of submitted update
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
setupChannel bool
|
||||||
|
updateToSubmit testUpdate
|
||||||
|
expectedSubmittedUpdate string // "empty" or "sameAsSubmitted"
|
||||||
|
shouldError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"HappyPath",
|
||||||
|
true,
|
||||||
|
testUpdate{chanID, Payout{sdk.Coins{sdk.NewCoin("KVA", 3)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}, senderAccountIndex, senderAccountIndex},
|
||||||
|
"sameAsSubmited",
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NoChannel",
|
||||||
|
false,
|
||||||
|
testUpdate{chanID, Payout{sdk.Coins{sdk.NewCoin("KVA", 3)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}, senderAccountIndex, senderAccountIndex},
|
||||||
|
"empty",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NoCoins",
|
||||||
|
true,
|
||||||
|
testUpdate{chanID, Payout{sdk.Coins{}}, senderAccountIndex, senderAccountIndex},
|
||||||
|
"empty",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NegativeCoins",
|
||||||
|
true,
|
||||||
|
testUpdate{chanID, Payout{sdk.Coins{sdk.NewCoin("KVA", -5)}, sdk.Coins{sdk.NewCoin("KVA", 15)}}, senderAccountIndex, senderAccountIndex},
|
||||||
|
"empty",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"TooManyCoins",
|
||||||
|
true,
|
||||||
|
testUpdate{chanID, Payout{sdk.Coins{sdk.NewCoin("KVA", 100)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}, senderAccountIndex, senderAccountIndex},
|
||||||
|
"empty",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"WrongSignature",
|
||||||
|
true,
|
||||||
|
testUpdate{chanID, Payout{sdk.Coins{sdk.NewCoin("KVA", 3)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}, senderAccountIndex, otherAccountIndex},
|
||||||
|
"empty",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"WrongPubKey",
|
||||||
|
true,
|
||||||
|
testUpdate{chanID, Payout{sdk.Coins{sdk.NewCoin("KVA", 3)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}, otherAccountIndex, senderAccountIndex},
|
||||||
|
"empty",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ReceiverSigned",
|
||||||
|
true,
|
||||||
|
testUpdate{chanID, Payout{sdk.Coins{sdk.NewCoin("KVA", 3)}, sdk.Coins{sdk.NewCoin("KVA", 7)}}, receiverAccountIndex, receiverAccountIndex},
|
||||||
|
"empty",
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
|
||||||
|
// SETUP
|
||||||
|
ctx, _, channelKeeper, addrs, pubKeys, privKeys, _ := createMockApp(accountSeeds)
|
||||||
|
// Set empty submittedUpdatesQueue TODO work out proper genesis initialisation
|
||||||
|
channelKeeper.setSubmittedUpdatesQueue(ctx, SubmittedUpdatesQueue{})
|
||||||
|
// create new channel
|
||||||
|
if testCase.setupChannel {
|
||||||
|
channel := Channel{
|
||||||
|
ID: chanID, // should be 0 as first channel
|
||||||
|
Participants: [2]sdk.AccAddress{addrs[senderAccountIndex], addrs[receiverAccountIndex]},
|
||||||
|
Coins: sdk.Coins{sdk.NewCoin("KVA", 10)},
|
||||||
|
}
|
||||||
|
channelKeeper.setChannel(ctx, channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create update
|
||||||
|
// basic values
|
||||||
|
updateToSubmit := Update{
|
||||||
|
ChannelID: testCase.updateToSubmit.channelID,
|
||||||
|
Payout: testCase.updateToSubmit.payout,
|
||||||
|
// empty sig
|
||||||
|
}
|
||||||
|
// create update's signature
|
||||||
|
cryptoSig, _ := privKeys[testCase.updateToSubmit.sigAccountIndex].Sign(updateToSubmit.GetSignBytes())
|
||||||
|
updateToSubmit.Sigs = [1]UpdateSignature{UpdateSignature{
|
||||||
|
PubKey: pubKeys[testCase.updateToSubmit.pubKeyAccountIndex],
|
||||||
|
CryptoSignature: cryptoSig,
|
||||||
|
}}
|
||||||
|
|
||||||
|
// ACTION
|
||||||
|
_, err := channelKeeper.InitCloseChannelBySender(ctx, updateToSubmit)
|
||||||
|
|
||||||
|
// CHECK RESULTS
|
||||||
|
// Check error
|
||||||
|
if testCase.shouldError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
// Check submittedUpdate
|
||||||
|
su, found := channelKeeper.getSubmittedUpdate(ctx, chanID)
|
||||||
|
switch testCase.expectedSubmittedUpdate {
|
||||||
|
case "empty":
|
||||||
|
assert.False(t, found)
|
||||||
|
assert.Zero(t, su)
|
||||||
|
case "sameAsSubmitted":
|
||||||
|
assert.True(t, found)
|
||||||
|
expectedSU := SubmittedUpdate{updateToSubmit, ChannelDisputeTime}
|
||||||
|
assert.Equal(t, expectedSU, su)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
61
internal/x/paychan/test_common.go
Normal file
61
internal/x/paychan/test_common.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package paychan
|
||||||
|
|
||||||
|
import (
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/mock"
|
||||||
|
//"github.com/stretchr/testify/require"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/auth"
|
||||||
|
"github.com/cosmos/cosmos-sdk/x/bank"
|
||||||
|
abci "github.com/tendermint/tendermint/abci/types"
|
||||||
|
"github.com/tendermint/tendermint/crypto"
|
||||||
|
"github.com/tendermint/tendermint/crypto/ed25519"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Setup an example app with an in memory DB and the required keepers
|
||||||
|
// Also create two accounts with 1000KVA
|
||||||
|
// Could do with refactoring
|
||||||
|
func createMockApp(accountSeeds []string) (sdk.Context, bank.Keeper, Keeper, []sdk.AccAddress, []crypto.PubKey, []crypto.PrivKey, sdk.Coins) {
|
||||||
|
mApp := mock.NewApp() // creates a half complete app
|
||||||
|
coinKeeper := bank.NewKeeper(mApp.AccountMapper)
|
||||||
|
|
||||||
|
// create channel keeper
|
||||||
|
keyChannel := sdk.NewKVStoreKey("channel")
|
||||||
|
channelKeeper := NewKeeper(mApp.Cdc, keyChannel, coinKeeper)
|
||||||
|
// could add router for msg tests
|
||||||
|
//mapp.Router().AddRoute("channel", NewHandler(channelKeeper))
|
||||||
|
|
||||||
|
mApp.CompleteSetup([]*sdk.KVStoreKey{keyChannel})
|
||||||
|
|
||||||
|
// create some accounts
|
||||||
|
genAccFunding := sdk.Coins{sdk.NewCoin("KVA", 1000)}
|
||||||
|
genAccs, addrs, pubKeys, privKeys := createTestGenAccounts(accountSeeds, genAccFunding)
|
||||||
|
|
||||||
|
// initialize the app with these accounts
|
||||||
|
mock.SetGenesis(mApp, genAccs)
|
||||||
|
|
||||||
|
mApp.BeginBlock(abci.RequestBeginBlock{}) // going off other module tests
|
||||||
|
ctx := mApp.BaseApp.NewContext(false, abci.Header{})
|
||||||
|
|
||||||
|
return ctx, coinKeeper, channelKeeper, addrs, pubKeys, privKeys, genAccFunding
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTestGenAccounts deterministically generates genesis accounts loaded with coins, and returns
|
||||||
|
// their addresses, pubkeys, and privkeys.
|
||||||
|
func createTestGenAccounts(accountSeeds []string, genCoins sdk.Coins) (genAccs []auth.Account, addrs []sdk.AccAddress, pubKeys []crypto.PubKey, privKeys []crypto.PrivKey) {
|
||||||
|
for _, seed := range accountSeeds {
|
||||||
|
privKey := ed25519.GenPrivKeyFromSecret([]byte(seed))
|
||||||
|
pubKey := privKey.PubKey()
|
||||||
|
addr := sdk.AccAddress(pubKey.Address())
|
||||||
|
|
||||||
|
genAcc := &auth.BaseAccount{
|
||||||
|
Address: addr,
|
||||||
|
Coins: genCoins,
|
||||||
|
}
|
||||||
|
|
||||||
|
genAccs = append(genAccs, genAcc)
|
||||||
|
privKeys = append(privKeys, privKey)
|
||||||
|
pubKeys = append(pubKeys, pubKey)
|
||||||
|
addrs = append(addrs, addr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
220
internal/x/paychan/types.go
Normal file
220
internal/x/paychan/types.go
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
package paychan
|
||||||
|
|
||||||
|
import (
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/tendermint/tendermint/crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* CHANNEL TYPES */
|
||||||
|
|
||||||
|
// Used to represent a channel in the keeper module.
|
||||||
|
// Participants is limited to two as currently these are unidirectional channels.
|
||||||
|
// Last participant is designated as receiver.
|
||||||
|
type Channel struct {
|
||||||
|
ID ChannelID
|
||||||
|
Participants [2]sdk.AccAddress // [senderAddr, receiverAddr]
|
||||||
|
Coins sdk.Coins
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChannelDisputeTime = int64(6) // measured in blocks TODO pick reasonable time, add to channel or genesis
|
||||||
|
|
||||||
|
type ChannelID int64 // TODO should this be positive only?
|
||||||
|
|
||||||
|
// The data that is passed between participants as payments, and submitted to the blockchain to close a channel.
|
||||||
|
type Update struct {
|
||||||
|
ChannelID ChannelID
|
||||||
|
Payout Payout
|
||||||
|
//Sequence int64 Not needed for unidirectional channels
|
||||||
|
Sigs [1]UpdateSignature // only sender needs to sign in unidirectional
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u Update) GetSignBytes() []byte {
|
||||||
|
bz, err := msgCdc.MarshalJSON(struct {
|
||||||
|
ChannelID ChannelID
|
||||||
|
Payout Payout
|
||||||
|
}{
|
||||||
|
ChannelID: u.ChannelID,
|
||||||
|
Payout: u.Payout})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return sdk.MustSortJSON(bz)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Payout [2]sdk.Coins // a list of coins to be paid to each of Channel.Participants
|
||||||
|
func (p Payout) IsNotNegative() bool {
|
||||||
|
result := true
|
||||||
|
for _, coins := range p {
|
||||||
|
result = result && coins.IsNotNegative()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
func (p Payout) Sum() sdk.Coins {
|
||||||
|
var total sdk.Coins
|
||||||
|
for _, coins := range p {
|
||||||
|
total = total.Plus(coins.Sort())
|
||||||
|
total = total.Sort()
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateSignature struct {
|
||||||
|
PubKey crypto.PubKey
|
||||||
|
CryptoSignature crypto.Signature
|
||||||
|
}
|
||||||
|
|
||||||
|
// An update that has been submitted to the blockchain, but not yet acted on.
|
||||||
|
type SubmittedUpdate struct {
|
||||||
|
Update
|
||||||
|
ExecutionTime int64 // BlockHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubmittedUpdatesQueue []ChannelID // not technically a queue
|
||||||
|
|
||||||
|
// Check if value is in queue
|
||||||
|
func (suq SubmittedUpdatesQueue) Contains(channelID ChannelID) bool {
|
||||||
|
found := false
|
||||||
|
for _, id := range suq {
|
||||||
|
if id == channelID {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all values from queue that match argument
|
||||||
|
func (suq *SubmittedUpdatesQueue) RemoveMatchingElements(channelID ChannelID) {
|
||||||
|
newSUQ := SubmittedUpdatesQueue{}
|
||||||
|
|
||||||
|
for _, id := range *suq {
|
||||||
|
if id != channelID {
|
||||||
|
newSUQ = append(newSUQ, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*suq = newSUQ
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MESSAGE TYPES */
|
||||||
|
/*
|
||||||
|
Message implement the sdk.Msg interface:
|
||||||
|
type Msg interface {
|
||||||
|
|
||||||
|
// Return the message type.
|
||||||
|
// Must be alphanumeric or empty.
|
||||||
|
Type() string
|
||||||
|
|
||||||
|
// Get the canonical byte representation of the Msg.
|
||||||
|
GetSignBytes() []byte
|
||||||
|
|
||||||
|
// ValidateBasic does a simple validation check that
|
||||||
|
// doesn't require access to any other information.
|
||||||
|
ValidateBasic() Error
|
||||||
|
|
||||||
|
// Signers returns the addrs of signers that must sign.
|
||||||
|
// CONTRACT: All signatures must be present to be valid.
|
||||||
|
// CONTRACT: Returns addrs in some deterministic order.
|
||||||
|
GetSigners() []AccAddress
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// A message to create a payment channel.
|
||||||
|
type MsgCreate struct {
|
||||||
|
Participants [2]sdk.AccAddress
|
||||||
|
Coins sdk.Coins
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg MsgCreate) Type() string { return "paychan" }
|
||||||
|
|
||||||
|
func (msg MsgCreate) GetSignBytes() []byte {
|
||||||
|
bz, err := msgCdc.MarshalJSON(msg)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return sdk.MustSortJSON(bz)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg MsgCreate) ValidateBasic() sdk.Error {
|
||||||
|
// Validate msg as an optimisation to avoid all validation going to keeper. It's run before the sigs are checked by the auth module.
|
||||||
|
// Validate without external information (such as account balance)
|
||||||
|
|
||||||
|
//TODO implement
|
||||||
|
|
||||||
|
/* old logic
|
||||||
|
// check if all fields present / not 0 valued
|
||||||
|
if len(msg.Sender) == 0 {
|
||||||
|
return sdk.ErrInvalidAddress(msg.Sender.String())
|
||||||
|
}
|
||||||
|
if len(msg.Receiver) == 0 {
|
||||||
|
return sdk.ErrInvalidAddress(msg.Receiver.String())
|
||||||
|
}
|
||||||
|
if len(msg.Amount) == 0 {
|
||||||
|
return sdk.ErrInvalidCoins(msg.Amount.String())
|
||||||
|
}
|
||||||
|
// Check if coins are sorted, non zero, non negative
|
||||||
|
if !msg.Amount.IsValid() {
|
||||||
|
return sdk.ErrInvalidCoins(msg.Amount.String())
|
||||||
|
}
|
||||||
|
if !msg.Amount.IsPositive() {
|
||||||
|
return sdk.ErrInvalidCoins(msg.Amount.String())
|
||||||
|
}
|
||||||
|
// TODO check if Address valid?
|
||||||
|
*/
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg MsgCreate) GetSigners() []sdk.AccAddress {
|
||||||
|
// Only sender must sign to create a paychan
|
||||||
|
return []sdk.AccAddress{msg.Participants[0]} // select sender address
|
||||||
|
}
|
||||||
|
|
||||||
|
// A message to close a payment channel.
|
||||||
|
type MsgSubmitUpdate struct {
|
||||||
|
Update
|
||||||
|
Submitter sdk.AccAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg MsgSubmitUpdate) Type() string { return "paychan" }
|
||||||
|
|
||||||
|
func (msg MsgSubmitUpdate) GetSignBytes() []byte {
|
||||||
|
bz, err := msgCdc.MarshalJSON(msg)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return sdk.MustSortJSON(bz)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg MsgSubmitUpdate) ValidateBasic() sdk.Error {
|
||||||
|
|
||||||
|
// TODO implement
|
||||||
|
/* old logic
|
||||||
|
// check if all fields present / not 0 valued
|
||||||
|
if len(msg.Sender) == 0 {
|
||||||
|
return sdk.ErrInvalidAddress(msg.Sender.String())
|
||||||
|
}
|
||||||
|
if len(msg.Receiver) == 0 {
|
||||||
|
return sdk.ErrInvalidAddress(msg.Receiver.String())
|
||||||
|
}
|
||||||
|
if len(msg.ReceiverAmount) == 0 {
|
||||||
|
return sdk.ErrInvalidCoins(msg.ReceiverAmount.String())
|
||||||
|
}
|
||||||
|
// check id ≥ 0
|
||||||
|
if msg.Id < 0 {
|
||||||
|
return sdk.ErrInvalidAddress(strconv.Itoa(int(msg.Id))) // TODO implement custom errors
|
||||||
|
}
|
||||||
|
// Check if coins are sorted, non zero, non negative
|
||||||
|
if !msg.ReceiverAmount.IsValid() {
|
||||||
|
return sdk.ErrInvalidCoins(msg.ReceiverAmount.String())
|
||||||
|
}
|
||||||
|
if !msg.ReceiverAmount.IsPositive() {
|
||||||
|
return sdk.ErrInvalidCoins(msg.ReceiverAmount.String())
|
||||||
|
}
|
||||||
|
// TODO check if Address valid?
|
||||||
|
*/
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (msg MsgSubmitUpdate) GetSigners() []sdk.AccAddress {
|
||||||
|
return []sdk.AccAddress{msg.Submitter}
|
||||||
|
}
|
45
internal/x/paychan/types_test.go
Normal file
45
internal/x/paychan/types_test.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package paychan
|
||||||
|
|
||||||
|
import (
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSubmittedUpdatesQueue(t *testing.T) {
|
||||||
|
t.Run("RemoveMatchingElements", func(t *testing.T) {
|
||||||
|
// SETUP
|
||||||
|
q := SubmittedUpdatesQueue{4, 8, 23, 0, 5645657}
|
||||||
|
// ACTION
|
||||||
|
q.RemoveMatchingElements(23)
|
||||||
|
// CHECK RESULTS
|
||||||
|
expectedQ := SubmittedUpdatesQueue{4, 8, 0, 5645657}
|
||||||
|
assert.Equal(t, expectedQ, q)
|
||||||
|
|
||||||
|
// SETUP
|
||||||
|
q = SubmittedUpdatesQueue{0}
|
||||||
|
// ACTION
|
||||||
|
q.RemoveMatchingElements(0)
|
||||||
|
// CHECK RESULTS
|
||||||
|
expectedQ = SubmittedUpdatesQueue{}
|
||||||
|
assert.Equal(t, expectedQ, q)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPayout(t *testing.T) {
|
||||||
|
t.Run("IsNotNegative", func(t *testing.T) {
|
||||||
|
p := Payout{sdk.Coins{sdk.NewCoin("USD", 4), sdk.NewCoin("GBP", 0)}, sdk.Coins{sdk.NewCoin("USD", 129879234), sdk.NewCoin("GBP", 1)}}
|
||||||
|
assert.True(t, p.IsNotNegative())
|
||||||
|
|
||||||
|
p = Payout{sdk.Coins{sdk.NewCoin("USD", -4), sdk.NewCoin("GBP", 0)}, sdk.Coins{sdk.NewCoin("USD", 129879234), sdk.NewCoin("GBP", 1)}}
|
||||||
|
assert.False(t, p.IsNotNegative())
|
||||||
|
})
|
||||||
|
t.Run("Sum", func(t *testing.T) {
|
||||||
|
p := Payout{
|
||||||
|
sdk.Coins{sdk.NewCoin("EUR", 1), sdk.NewCoin("USD", -5)},
|
||||||
|
sdk.Coins{sdk.NewCoin("EUR", 1), sdk.NewCoin("USD", 100), sdk.NewCoin("GBP", 1)},
|
||||||
|
}
|
||||||
|
expected := sdk.Coins{sdk.NewCoin("EUR", 2), sdk.NewCoin("GBP", 1), sdk.NewCoin("USD", 95)}
|
||||||
|
assert.Equal(t, expected, p.Sum())
|
||||||
|
})
|
||||||
|
}
|
18
internal/x/paychan/wire.go
Normal file
18
internal/x/paychan/wire.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package paychan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/cosmos/cosmos-sdk/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterWire(cdc *wire.Codec) {
|
||||||
|
cdc.RegisterConcrete(MsgCreate{}, "paychan/MsgCreate", nil)
|
||||||
|
cdc.RegisterConcrete(MsgSubmitUpdate{}, "paychan/MsgSubmitUpdate", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO move this to near the msg definitions?
|
||||||
|
var msgCdc = wire.NewCodec()
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
wire.RegisterCrypto(msgCdc)
|
||||||
|
RegisterWire(msgCdc)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user