From 0c18e6720f384bd485da5f83e00d03843826fa8b Mon Sep 17 00:00:00 2001 From: Heyang Zhou Date: Tue, 6 Aug 2019 13:53:59 +0800 Subject: [PATCH] Preserve globals of smart contracts into account state. (#151) * Revert "Changed to urfave/cli for the shell, added autocompletion, some 80 col changes. (#121)" This reverts commit 67033d27517041c670725bfc5b2505906088a4ce. * sys/const, contract, db: Preserve globals for contracts. --- cmd/wavelet/cli.go | 245 --------------- cmd/wavelet/completion.go | 41 --- cmd/wavelet/main.go | 34 +- cmd/wavelet/{actions.go => shell.go} | 452 ++++++++++++++------------- common.go | 5 +- contract.go | 55 +++- db.go | 9 + go.mod | 1 - log/console.go | 1 - sys/const.go | 7 + 10 files changed, 320 insertions(+), 530 deletions(-) delete mode 100644 cmd/wavelet/cli.go delete mode 100644 cmd/wavelet/completion.go rename cmd/wavelet/{actions.go => shell.go} (51%) diff --git a/cmd/wavelet/cli.go b/cmd/wavelet/cli.go deleted file mode 100644 index 73a9accc..00000000 --- a/cmd/wavelet/cli.go +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright (c) 2019 Perlin -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -package main - -import ( - "encoding/csv" - "fmt" - "io" - "strings" - "text/tabwriter" - - "github.com/chzyer/readline" - "github.com/perlin-network/noise/skademlia" - "github.com/perlin-network/wavelet" - "github.com/perlin-network/wavelet/log" - "github.com/rs/zerolog" - "github.com/urfave/cli" -) - -const ( - vtRed = "\033[31m" - vtReset = "\033[39m" - prompt = "»»»" -) - -type CLI struct { - app *cli.App - rl *readline.Instance - client *skademlia.Client - ledger *wavelet.Ledger - logger zerolog.Logger - keys *skademlia.Keypair - - completion []string -} - -func NewCLI(client *skademlia.Client, ledger *wavelet.Ledger, keys *skademlia.Keypair) (*CLI, error) { - c := &CLI{ - client: client, - ledger: ledger, - logger: log.Node(), - keys: keys, - app: cli.NewApp(), - } - - c.app.Name = "wavelet" - c.app.HideVersion = true - c.app.UsageText = "command [arguments...]" - c.app.CommandNotFound = func(ctx *cli.Context, s string) { - c.logger.Error(). - Msg("Unknown command: " + s) - } - - // List of commands and their actions - c.app.Commands = []cli.Command{ - { - Name: "status", - Aliases: []string{"l"}, - Action: a(c.status), - Description: "print out information about your node", - }, - { - Name: "pay", - Aliases: []string{"p"}, - Action: a(c.pay), - Description: "pay the address an amount of PERLs", - }, - { - Name: "call", - Aliases: []string{"c"}, - Action: a(c.call), - Description: "invoke a function on a smart contract", - }, - { - Name: "find", - Aliases: []string{"f"}, - Action: a(c.find), - Description: "search for any wallet/smart contract/transaction", - }, - { - Name: "spawn", - Aliases: []string{"s"}, - Action: a(c.spawn), - Description: "test deploy a smart contract", - }, - { - Name: "deposit-gas", - Aliases: []string{"g"}, - Action: a(c.depositGas), - Description: "deposit gas to a smart contract", - }, - { - Name: "place-stake", - Aliases: []string{"ps"}, - Action: a(c.placeStake), - Description: "deposit a stake of PERLs into the network", - }, - { - Name: "withdraw-stake", - Aliases: []string{"ws"}, - Action: a(c.withdrawStake), - Description: "withdraw stake and diminish voting power", - }, - { - Name: "withdraw-reward", - Aliases: []string{"wr"}, - Action: a(c.withdrawReward), - Description: "withdraw rewards into PERLs", - }, - { - Name: "exit", - Aliases: []string{"quit", ":q"}, - Action: a(c.exit), - }, - } - - // Generate the help message - s := strings.Builder{} - s.WriteString("Commands:\n") - w := tabwriter.NewWriter(&s, 0, 0, 1, ' ', 0) - - for _, c := range c.app.VisibleCommands() { - fmt.Fprintf(w, - " %s (%s) %s\t%s\n", - c.Name, strings.Join(c.Aliases, ", "), c.Usage, - c.Description, - ) - } - - w.Flush() - c.app.CustomAppHelpTemplate = s.String() - - // Add in autocompletion - var completers = make( - []readline.PrefixCompleterInterface, - 0, len(c.app.Commands)*2+1, - ) - - for _, cmd := range c.app.Commands { - completers = append(completers, readline.PcItem( - cmd.Name, c.getCompleter(), - )) - - for _, alias := range cmd.Aliases { - completers = append(completers, readline.PcItem( - alias, c.getCompleter(), - )) - } - } - - var completer = readline.NewPrefixCompleter(completers...) - - // Make a new readline struct - rl, err := readline.NewEx(&readline.Config{ - Prompt: vtRed + prompt + vtReset + " ", - AutoComplete: completer, - HistoryFile: "/tmp/wavelet-history.tmp", - InterruptPrompt: "^C", - EOFPrompt: "exit", - HistorySearchFold: true, - }) - - if err != nil { - return nil, err - } - - c.rl = rl - - log.SetWriter( - log.LoggerWavelet, - log.NewConsoleWriter(rl.Stdout(), log.FilterFor( - log.ModuleNode, - log.ModuleNetwork, - log.ModuleSync, - log.ModuleConsensus, - log.ModuleContract, - )), - ) - - return c, nil -} - -func (cli *CLI) Start() { -ReadLoop: - for { - line, err := cli.rl.Readline() - switch err { - case readline.ErrInterrupt: - if len(line) == 0 { - break ReadLoop - } - - continue ReadLoop - - case io.EOF: - break ReadLoop - } - - r := csv.NewReader(strings.NewReader(line)) - r.Comma = ' ' - - s, err := r.Read() - if err != nil { - s = strings.Fields(line) - } - - // Add an app name as $0 - s = append([]string{cli.app.Name}, s...) - - if err := cli.app.Run(s); err != nil { - cli.logger.Error().Err(err). - Msg("Failed to run command.") - } - } - - cli.rl.Close() -} - -func (cli *CLI) exit(ctx *cli.Context) { - cli.rl.Close() -} - -func a(f func(*cli.Context)) func(*cli.Context) error { - return func(ctx *cli.Context) error { - f(ctx) - return nil - } -} diff --git a/cmd/wavelet/completion.go b/cmd/wavelet/completion.go deleted file mode 100644 index 370a0103..00000000 --- a/cmd/wavelet/completion.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2019 Perlin -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -package main - -import "github.com/chzyer/readline" - -func (cli *CLI) getCompleter() *readline.PrefixCompleter { - return readline.PcItemDynamic(func(string) []string { - return cli.completion - }) -} - -func (cli *CLI) addCompletion(ids ...string) { -MainLoop: - for _, id := range ids { - for _, c := range cli.completion { - if c == id { - continue MainLoop - } - } - - cli.completion = append(cli.completion, id) - } -} diff --git a/cmd/wavelet/main.go b/cmd/wavelet/main.go index c53e48b8..46c2cadf 100644 --- a/cmd/wavelet/main.go +++ b/cmd/wavelet/main.go @@ -61,17 +61,7 @@ type Config struct { } func main() { - log.SetWriter( - log.LoggerWavelet, - log.NewConsoleWriter(nil, log.FilterFor( - log.ModuleNode, - log.ModuleNetwork, - log.ModuleSync, - log.ModuleConsensus, - log.ModuleContract, - )), - ) - + log.SetWriter(log.LoggerWavelet, log.NewConsoleWriter(nil, log.FilterFor(log.ModuleNode, log.ModuleNetwork, log.ModuleSync, log.ModuleConsensus, log.ModuleContract))) logger := log.Node() app := cli.NewApp() @@ -176,15 +166,13 @@ func main() { } // apply the toml before processing the flags - app.Before = altsrc.InitInputSourceWithContext( - app.Flags, func(c *cli.Context) (altsrc.InputSourceContext, error) { - filePath := c.String("config") - if len(filePath) > 0 { - return altsrc.NewTomlSourceFromFile(filePath) - } - return &altsrc.MapInputSource{}, nil - }, - ) + app.Before = altsrc.InitInputSourceWithContext(app.Flags, func(c *cli.Context) (altsrc.InputSourceContext, error) { + filePath := c.String("config") + if len(filePath) > 0 { + return altsrc.NewTomlSourceFromFile(filePath) + } + return &altsrc.MapInputSource{}, nil + }) cli.VersionPrinter = func(c *cli.Context) { fmt.Printf("Version: %s\n", c.App.Version) @@ -228,9 +216,9 @@ func main() { sort.Sort(cli.FlagsByName(app.Flags)) sort.Sort(cli.CommandsByName(app.Commands)) - if err := app.Run(os.Args); err != nil { - logger.Fatal().Err(err). - Msg("Failed to parse configuration/command-line arguments.") + err := app.Run(os.Args) + if err != nil { + logger.Fatal().Err(err).Msg("Failed to parse configuration/command-line arguments.") } } diff --git a/cmd/wavelet/actions.go b/cmd/wavelet/shell.go similarity index 51% rename from cmd/wavelet/actions.go rename to cmd/wavelet/shell.go index 48913560..5d853bd0 100644 --- a/cmd/wavelet/actions.go +++ b/cmd/wavelet/shell.go @@ -24,16 +24,147 @@ import ( "encoding/binary" "encoding/hex" "fmt" + "io" "io/ioutil" "strconv" + "strings" + "github.com/chzyer/readline" + "github.com/perlin-network/noise/skademlia" "github.com/perlin-network/wavelet" + "github.com/perlin-network/wavelet/log" "github.com/perlin-network/wavelet/sys" "github.com/pkg/errors" - "github.com/urfave/cli" + "github.com/rs/zerolog" ) -func (cli *CLI) status(ctx *cli.Context) { +type CLI struct { + rl *readline.Instance + client *skademlia.Client + ledger *wavelet.Ledger + logger zerolog.Logger + keys *skademlia.Keypair + tree string +} + +func NewCLI(client *skademlia.Client, ledger *wavelet.Ledger, keys *skademlia.Keypair) (*CLI, error) { + completer := readline.NewPrefixCompleter( + readline.PcItem("l"), readline.PcItem("status"), + readline.PcItem("p"), readline.PcItem("pay"), + readline.PcItem("c"), readline.PcItem("call"), + readline.PcItem("f"), readline.PcItem("find"), + readline.PcItem("g"), readline.PcItem("deposit-gas"), + readline.PcItem("s"), readline.PcItem("spawn"), + readline.PcItem("ps"), readline.PcItem("place-stake"), + readline.PcItem("ws"), readline.PcItem("withdraw-stake"), + readline.PcItem("wr"), readline.PcItem("withdraw-reward"), + readline.PcItem("help"), + ) + + rl, err := readline.NewEx( + &readline.Config{ + Prompt: "\033[31m»»»\033[0m ", + AutoComplete: completer, + HistoryFile: "/tmp/readline.tmp", + InterruptPrompt: "^C", + EOFPrompt: "exit", + HistorySearchFold: true, + }, + ) + if err != nil { + return nil, err + } + + log.SetWriter(log.LoggerWavelet, log.NewConsoleWriter(rl.Stderr(), log.FilterFor(log.ModuleNode, log.ModuleNetwork, log.ModuleSync, log.ModuleConsensus, log.ModuleContract))) + + return &CLI{ + rl: rl, + client: client, + ledger: ledger, + logger: log.Node(), + tree: completer.Tree(" "), + keys: keys, + }, nil +} + +func toCMD(in string, n int) []string { + in = strings.TrimSpace(in[n:]) + if in == "" { + return []string{} + } + + return strings.Split(in, " ") +} + +func (cli *CLI) Start() { + defer func() { + _ = cli.rl.Close() + }() + + for { + line, err := cli.rl.Readline() + switch err { + case readline.ErrInterrupt: + if len(line) == 0 { + return + } else { + continue + } + case io.EOF: + return + } + + switch { + case line == "l" || line == "status": + cli.status() + case strings.HasPrefix(line, "p "): + cli.pay(toCMD(line, 2)) + case strings.HasPrefix(line, "pay "): + cli.pay(toCMD(line, 4)) + case strings.HasPrefix(line, "c "): + cli.call(toCMD(line, 2)) + case strings.HasPrefix(line, "call "): + cli.call(toCMD(line, 5)) + case strings.HasPrefix(line, "f "): + cli.find(toCMD(line, 2)) + case strings.HasPrefix(line, "find "): + cli.find(toCMD(line, 5)) + case strings.HasPrefix(line, "g "): + cli.depositGas(toCMD(line, 2)) + case strings.HasPrefix(line, "deposit-gas"): + cli.depositGas(toCMD(line, 11)) + case strings.HasPrefix(line, "s "): + cli.spawn(toCMD(line, 2)) + case strings.HasPrefix(line, "spawn "): + cli.spawn(toCMD(line, 5)) + case strings.HasPrefix(line, "ps "): + cli.placeStake(toCMD(line, 3)) + case strings.HasPrefix(line, "place-stake "): + cli.placeStake(toCMD(line, 12)) + case strings.HasPrefix(line, "ws "): + cli.withdrawStake(toCMD(line, 3)) + case strings.HasPrefix(line, "withdraw-stake "): + cli.withdrawStake(toCMD(line, 15)) + case strings.HasPrefix(line, "wr "): + cli.withdrawReward(toCMD(line, 3)) + case strings.HasPrefix(line, "withdraw-reward "): + cli.withdrawReward(toCMD(line, 16)) + case line == "": + fallthrough + case line == "help": + cli.usage() + default: + fmt.Printf("unrecognised command :'%s'\n", line) + } + } +} + +func (cli *CLI) usage() { + _, _ = io.WriteString(cli.rl.Stderr(), "commands:\n") + _, _ = io.WriteString(cli.rl.Stderr(), cli.tree) +} + +func (cli *CLI) status() { preferredID := "N/A" if preferred := cli.ledger.Finalizer().Preferred(); preferred != nil { @@ -63,9 +194,7 @@ func (cli *CLI) status(ctx *cli.Context) { } cli.logger.Info(). - Uint8("difficulty", round.ExpectedDifficulty( - sys.MinDifficulty, sys.DifficultyScaleFactor, - )). + Uint8("difficulty", round.ExpectedDifficulty(sys.MinDifficulty, sys.DifficultyScaleFactor)). Uint64("round", round.Index). Hex("root_id", round.End.ID[:]). Uint64("height", cli.ledger.Graph().Height()). @@ -84,32 +213,76 @@ func (cli *CLI) status(ctx *cli.Context) { Msg("Here is the current status of your node.") } -func (cli *CLI) pay(ctx *cli.Context) { - var cmd = ctx.Args() +func (cli *CLI) depositGas(cmd []string) { + if len(cmd) != 2 { + fmt.Println("deposit-gas ") + return + } - if len(cmd) < 2 { - cli.logger.Error(). - Msg("Invalid usage: pay ") + recipient, err := hex.DecodeString(cmd[0]) + if err != nil { + cli.logger.Error().Err(err).Msg("The recipient you specified is invalid.") + return + } + + if len(recipient) != wavelet.SizeAccountID { + cli.logger.Error().Int("length", len(recipient)).Msg("You have specified an invalid account ID to find.") + return + } + + amount, err := strconv.ParseUint(cmd[1], 10, 64) + if err != nil { + cli.logger.Error().Err(err).Msg("Failed to convert payment amount to a uint64.") + return + } + + var payload wavelet.Transfer + copy(payload.Recipient[:], recipient) + payload.GasDeposit = amount + + snapshot := cli.ledger.Snapshot() + + balance, _ := wavelet.ReadAccountBalance(snapshot, cli.keys.PublicKey()) + _, codeAvailable := wavelet.ReadAccountContractCode(snapshot, payload.Recipient) + + if balance < amount+sys.TransactionFeeAmount { + cli.logger.Error().Uint64("your_balance", balance).Uint64("amount_to_send", amount).Msg("You do not have enough PERLs to deposit into the smart contract.") + return + } + + if !codeAvailable { + cli.logger.Error().Hex("recipient_id", recipient).Msg("The recipient you specified is not a smart contract.") + return + } + + tx, err := cli.sendTransaction(wavelet.NewTransaction(cli.keys, sys.TagTransfer, payload.Marshal())) + if err != nil { + return + } + + cli.logger.Info().Msgf("Success! Your gas deposit transaction ID: %x", tx.ID) +} + +func (cli *CLI) pay(cmd []string) { + if len(cmd) != 2 { + fmt.Println("pay ") return } recipient, err := hex.DecodeString(cmd[0]) if err != nil { - cli.logger.Error().Err(err). - Msg("The recipient you specified is invalid.") + cli.logger.Error().Err(err).Msg("The recipient you specified is invalid.") return } if len(recipient) != wavelet.SizeAccountID { - cli.logger.Error().Int("length", len(recipient)). - Msg("You have specified an invalid account ID to find.") + cli.logger.Error().Int("length", len(recipient)).Msg("You have specified an invalid account ID to find.") return } amount, err := strconv.ParseUint(cmd[1], 10, 64) if err != nil { - cli.logger.Error().Err(err). - Msg("Failed to convert payment amount to a uint64.") + cli.logger.Error().Err(err).Msg("Failed to convert payment amount to a uint64.") return } @@ -122,53 +295,39 @@ func (cli *CLI) pay(ctx *cli.Context) { balance, _ := wavelet.ReadAccountBalance(snapshot, cli.keys.PublicKey()) if balance < amount+sys.TransactionFeeAmount { - cli.logger.Error(). - Uint64("your_balance", balance). - Uint64("amount_to_send", amount). - Msg("You do not have enough PERLs to send.") + cli.logger.Error().Uint64("your_balance", balance).Uint64("amount_to_send", amount).Msg("You do not have enough PERLs to send.") return } - _, codeAvailable := wavelet.ReadAccountContractCode( - snapshot, payload.Recipient, - ) + _, codeAvailable := wavelet.ReadAccountContractCode(snapshot, payload.Recipient) if codeAvailable { // Set gas limit by default to the balance the user has. payload.GasLimit = balance - amount - sys.TransactionFeeAmount payload.FuncName = []byte("on_money_received") } - tx, err := cli.sendTransaction(wavelet.NewTransaction( - cli.keys, sys.TagTransfer, payload.Marshal(), - )) - + tx, err := cli.sendTransaction(wavelet.NewTransaction(cli.keys, sys.TagTransfer, payload.Marshal())) if err != nil { return } - cli.logger.Info(). - Msgf("Success! Your payment transaction ID: %x", tx.ID) + cli.logger.Info().Msgf("Success! Your payment transaction ID: %x", tx.ID) } -func (cli *CLI) call(ctx *cli.Context) { - var cmd = ctx.Args() - +func (cli *CLI) call(cmd []string) { if len(cmd) < 4 { - cli.logger.Error(). - Msg("Invalid usage: call [function parameters]") + fmt.Println("call [function parameters]") return } recipient, err := hex.DecodeString(cmd[0]) if err != nil { - cli.logger.Error().Err(err). - Msg("The smart contract address you specified is invalid.") + cli.logger.Error().Err(err).Msg("The smart contract address you specified is invalid.") return } if len(recipient) != wavelet.SizeAccountID { - cli.logger.Error().Int("length", len(recipient)). - Msg("You have specified an invalid account ID to find.") + cli.logger.Error().Int("length", len(recipient)).Msg("You have specified an invalid account ID to find.") return } @@ -181,30 +340,24 @@ func (cli *CLI) call(ctx *cli.Context) { _, codeAvailable := wavelet.ReadAccountContractCode(snapshot, payload.Recipient) if !codeAvailable { - cli.logger.Error(). - Msg("The smart contract address you specified does not belong to a smart contract.") + cli.logger.Error().Msg("The smart contract address you specified does not belong to a smart contract.") return } amount, err := strconv.ParseUint(cmd[1], 10, 64) if err != nil { - cli.logger.Error().Err(err). - Msg("Failed to convert payment amount to a uint64.") + cli.logger.Error().Err(err).Msg("Failed to convert payment amount to a uint64.") return } gasLimit, err := strconv.ParseUint(cmd[2], 10, 64) if err != nil { - cli.logger.Error().Err(err). - Msg("Failed to convert gas limit to a uint64.") + cli.logger.Error().Err(err).Msg("Failed to convert gas limit to a uint64.") return } if balance < amount+gasLimit { - cli.logger.Error(). - Uint64("your_balance", balance). - Uint64("cost", amount+gasLimit). - Msg("You do not have enough PERLs to pay for the costs to invoke the smart contract function you wanted.") + cli.logger.Error().Uint64("your_balance", balance).Uint64("cost", amount+gasLimit).Msg("You do not have enough PERLs to pay for the costs to invoke the smart contract function you wanted.") return } @@ -231,8 +384,7 @@ func (cli *CLI) call(ctx *cli.Context) { var val uint64 _, err := fmt.Sscanf(arg[1:], "%d", &val) if err != nil { - cli.logger.Error().Err(err). - Msgf("Got an error parsing integer: %+v", arg[1:]) + cli.logger.Error().Err(err).Msgf("Got an error parsing integer: %+v", arg[1:]) } switch arg[0] { @@ -252,39 +404,30 @@ func (cli *CLI) call(ctx *cli.Context) { buf, err := hex.DecodeString(arg[1:]) if err != nil { - cli.logger.Error().Err(err). - Msgf("Cannot decode hex: %s", arg[1:]) + cli.logger.Error().Err(err).Msgf("Cannot decode hex: %s", arg[1:]) return } params.Write(buf) default: - cli.logger.Error(). - Msgf("Invalid argument specified: %s", arg) + cli.logger.Error().Msgf("Invalid argument specified: %s", arg) return } } payload.FuncParams = params.Bytes() - tx, err := cli.sendTransaction(wavelet.NewTransaction( - cli.keys, sys.TagTransfer, payload.Marshal(), - )) - + tx, err := cli.sendTransaction(wavelet.NewTransaction(cli.keys, sys.TagTransfer, payload.Marshal())) if err != nil { return } - cli.logger.Info(). - Msgf("Success! Your smart contract invocation transaction ID: %x", tx.ID) + cli.logger.Info().Msgf("Success! Your smart contract invocation transaction ID: %x", tx.ID) } -func (cli *CLI) find(ctx *cli.Context) { - var cmd = ctx.Args() - - if len(cmd) < 1 { - cli.logger.Error(). - Msg("Invalid usage: find ") +func (cli *CLI) find(cmd []string) { + if len(cmd) != 1 { + fmt.Println("find ") return } @@ -294,14 +437,12 @@ func (cli *CLI) find(ctx *cli.Context) { buf, err := hex.DecodeString(address) if err != nil { - cli.logger.Error().Err(err). - Msg("Cannot decode address") + cli.logger.Error().Err(err).Msg("Cannot decode address") return } if len(buf) != wavelet.SizeTransactionID && len(buf) != wavelet.SizeAccountID { - cli.logger.Error().Int("length", len(buf)). - Msg("You have specified an invalid transaction/account ID to find.") + cli.logger.Error().Int("length", len(buf)).Msg("You have specified an invalid transaction/account ID to find.") return } @@ -357,12 +498,9 @@ func (cli *CLI) find(ctx *cli.Context) { } } -func (cli *CLI) spawn(ctx *cli.Context) { - var cmd = ctx.Args() - - if len(cmd) < 1 { - cli.logger.Error(). - Msg("Invalid usage: spawn ") +func (cli *CLI) spawn(cmd []string) { + if len(cmd) != 1 { + fmt.Println("spawn ") return } @@ -388,91 +526,15 @@ func (cli *CLI) spawn(ctx *cli.Context) { cli.logger.Info().Msgf("Success! Your smart contracts ID: %x", tx.ID) } -func (cli *CLI) depositGas(ctx *cli.Context) { - var cmd = ctx.Args() - - if len(cmd) < 2 { - cli.logger.Error(). - Msg("Invalid usage: deposit-gas ") - return - } - - // Get the recipient ID - recipient, err := hex.DecodeString(cmd[0]) - if err != nil { - cli.logger.Error().Err(err). - Msg("The recipient you specified is invalid.") - return - } - - // Check if the ID is actually invalid by length - if len(recipient) != wavelet.SizeAccountID { - cli.logger.Error().Int("length", len(recipient)). - Msg("You have specified an invalid account ID to find.") +func (cli *CLI) placeStake(cmd []string) { + if len(cmd) != 1 { + fmt.Println("place-stake ") return } - // Parse the gas amount amount, err := strconv.ParseUint(cmd[1], 10, 64) if err != nil { - cli.logger.Error().Err(err). - Msg("Failed to convert payment amount to an uint64.") - return - } - - // Make a new payload, copy the recipient over and assign the amount - var payload wavelet.Transfer - copy(payload.Recipient[:], recipient) - payload.GasDeposit = amount - - // Get snapshot - snapshot := cli.ledger.Snapshot() - - // Get balance and check if recipient is a smart contract - balance, _ := wavelet.ReadAccountBalance(snapshot, cli.keys.PublicKey()) - _, codeAvailable := wavelet.ReadAccountContractCode(snapshot, payload.Recipient) - - // Check balance - if balance < amount+sys.TransactionFeeAmount { - cli.logger.Error(). - Uint64("your_balance", balance). - Uint64("amount_to_send", amount). - Msg("You do not have enough PERLs to deposit into the smart contract.") - return - } - - // The recipient is not a smart contract - if !codeAvailable { - cli.logger.Error().Hex("recipient_id", recipient). - Msg("The recipient you specified is not a smart contract.") - return - } - - tx, err := cli.sendTransaction( - wavelet.NewTransaction(cli.keys, sys.TagTransfer, payload.Marshal()), - ) - - if err != nil { - return - } - - cli.logger.Info(). - Msgf("Success! Your gas deposit transaction ID: %x", tx.ID) -} - -func (cli *CLI) placeStake(ctx *cli.Context) { - var cmd = ctx.Args() - - if len(cmd) < 1 { - cli.logger.Error(). - Msg("Invalid usage: place-stake ") - return - } - - amount, err := strconv.ParseUint(cmd[0], 10, 64) - if err != nil { - cli.logger.Error().Err(err). - Msg("Failed to convert staking amount to a uint64.") + cli.logger.Error().Err(err).Msg("Failed to convert staking amount to a uint64.") return } @@ -481,10 +543,7 @@ func (cli *CLI) placeStake(ctx *cli.Context) { Amount: amount, } - tx, err := cli.sendTransaction(wavelet.NewTransaction( - cli.keys, sys.TagStake, payload.Marshal(), - )) - + tx, err := cli.sendTransaction(wavelet.NewTransaction(cli.keys, sys.TagStake, payload.Marshal())) if err != nil { return } @@ -493,58 +552,41 @@ func (cli *CLI) placeStake(ctx *cli.Context) { Msgf("Success! Your stake placement transaction ID: %x", tx.ID) } -func (cli *CLI) withdrawStake(ctx *cli.Context) { - var cmd = ctx.Args() - - if len(cmd) < 1 { - cli.logger.Error(). - Msg("Invalid usage: withdraw-stake ") +func (cli *CLI) withdrawStake(cmd []string) { + if len(cmd) != 1 { + fmt.Println("withdraw-stake ") return } - amount, err := strconv.ParseUint(cmd[0], 10, 64) + amount, err := strconv.ParseUint(cmd[1], 10, 64) if err != nil { - cli.logger.Error().Err(err). - Msg("Failed to convert withdraw amount to an uint64.") + cli.logger.Error().Err(err).Msg("Failed to convert staking amount to a uint64.") return } - var intBuf [8]byte - payload := bytes.NewBuffer(nil) - payload.WriteByte(sys.WithdrawStake) - binary.LittleEndian.PutUint64(intBuf[:8], uint64(amount)) - payload.Write(intBuf[:8]) - - tx, err := cli.sendTransaction(wavelet.NewTransaction( - cli.keys, sys.TagStake, payload.Bytes(), - )) + payload := wavelet.Stake{ + Opcode: sys.WithdrawStake, + Amount: amount, + } + tx, err := cli.sendTransaction(wavelet.NewTransaction(cli.keys, sys.TagStake, payload.Marshal())) if err != nil { return } - txID := hex.EncodeToString(tx.ID[:]) - - // Add the ID into the completion list - cli.addCompletion(txID) - cli.logger.Info(). - Msg("Success! Your stake withdrawal transaction ID: " + txID) + Msgf("Success! Your stake withdrawal transaction ID: %x", tx.ID) } -func (cli *CLI) withdrawReward(ctx *cli.Context) { - var cmd = ctx.Args() - - if len(cmd) < 1 { - cli.logger.Error(). - Msg("Invalid usage: withdraw-reward ") +func (cli *CLI) withdrawReward(cmd []string) { + if len(cmd) != 1 { + fmt.Println("withdraw-reward ") return } amount, err := strconv.ParseUint(cmd[0], 10, 64) if err != nil { - cli.logger.Error().Err(err). - Msg("Failed to convert withdraw amount to an uint64.") + cli.logger.Error().Err(err).Msg("Failed to convert withdraw amount to an uint64.") return } @@ -553,10 +595,7 @@ func (cli *CLI) withdrawReward(ctx *cli.Context) { Amount: amount, } - tx, err := cli.sendTransaction(wavelet.NewTransaction( - cli.keys, sys.TagStake, payload.Marshal(), - )) - + tx, err := cli.sendTransaction(wavelet.NewTransaction(cli.keys, sys.TagStake, payload.Marshal())) if err != nil { return } @@ -566,22 +605,15 @@ func (cli *CLI) withdrawReward(ctx *cli.Context) { } func (cli *CLI) sendTransaction(tx wavelet.Transaction) (wavelet.Transaction, error) { - tx = wavelet.AttachSenderToTransaction( - cli.keys, tx, cli.ledger.Graph().FindEligibleParents()..., - ) - - // Add the ID into the completion list - cli.addCompletion(hex.EncodeToString(tx.ID[:])) + tx = wavelet.AttachSenderToTransaction(cli.keys, tx, cli.ledger.Graph().FindEligibleParents()...) - if err := cli.ledger.AddTransaction(tx); err != nil { - if errors.Cause(err) != wavelet.ErrMissingParents { - cli.logger. - Err(err). - Hex("tx_id", tx.ID[:]). - Msg("Failed to create your transaction.") + if err := cli.ledger.AddTransaction(tx); err != nil && errors.Cause(err) != wavelet.ErrMissingParents { + cli.logger. + Err(err). + Hex("tx_id", tx.ID[:]). + Msg("Failed to create your transaction.") - return tx, err - } + return tx, err } return tx, nil diff --git a/common.go b/common.go index bd7b77c2..dc11976b 100644 --- a/common.go +++ b/common.go @@ -21,12 +21,11 @@ package wavelet import ( "crypto/md5" - "golang.org/x/crypto/blake2b" - - _ "github.com/perlin-network/wavelet/internal/snappy" ) +import _ "github.com/perlin-network/wavelet/internal/snappy" + const ( SizeTransactionID = blake2b.Size256 SizeTransactionSeed = blake2b.Size256 diff --git a/contract.go b/contract.go index 2dcf31a3..0e12def2 100644 --- a/contract.go +++ b/contract.go @@ -33,6 +33,8 @@ import ( "github.com/perlin-network/wavelet/sys" "github.com/pkg/errors" "golang.org/x/crypto/blake2b" + "reflect" + "unsafe" ) var ( @@ -215,14 +217,14 @@ func (e *ContractExecutor) ResolveGlobal(module, field string) int64 { func (e *ContractExecutor) Execute(snapshot *avl.Tree, id AccountID, round *Round, tx *Transaction, amount, gasLimit uint64, name string, params, code []byte) error { config := exec.VMConfig{ - DefaultMemoryPages: 4, - MaxMemoryPages: 32, + DefaultMemoryPages: sys.ContractDefaultMemoryPages, + MaxMemoryPages: sys.ContractMaxMemoryPages, - DefaultTableSize: PageSize, - MaxTableSize: PageSize, + DefaultTableSize: sys.ContractTableSize, + MaxTableSize: sys.ContractTableSize, - MaxValueSlots: 4096, - MaxCallStackDepth: 256, + MaxValueSlots: sys.ContractMaxValueSlots, + MaxCallStackDepth: sys.ContractMaxCallStackDepth, GasLimit: gasLimit, } @@ -231,10 +233,21 @@ func (e *ContractExecutor) Execute(snapshot *avl.Tree, id AccountID, round *Roun return errors.Wrap(err, "could not init vm") } + // We can safely initialize the VM first before checking this because the size of the global slice + // is proportional to the size of the contract's global section. + if len(vm.Globals) > sys.ContractMaxGlobals { + return errors.New("too many globals") + } + var firstRun bool if mem := LoadContractMemorySnapshot(snapshot, id); mem != nil { vm.Memory = mem + if globals, exists := LoadContractGlobals(snapshot, id); exists { + if len(globals) == len(vm.Globals) { + vm.Globals = globals + } + } } else { firstRun = true } @@ -290,6 +303,7 @@ func (e *ContractExecutor) Execute(snapshot *avl.Tree, id AccountID, round *Roun if vm.ExitError == nil && len(e.Error) == 0 { SaveContractMemorySnapshot(snapshot, id, vm.Memory) + SaveContractGlobals(snapshot, id, vm.Globals) } if vm.ExitError != nil && utils.UnifyError(vm.ExitError).Error() == "gas limit exceeded" { @@ -307,6 +321,35 @@ func (e *ContractExecutor) Execute(snapshot *avl.Tree, id AccountID, round *Roun } } +func LoadContractGlobals(snapshot *avl.Tree, id AccountID) ([]int64, bool) { + raw, exists := ReadAccountContractGlobals(snapshot, id) + if !exists { + return nil, false + } + + if len(raw)%8 != 0 { + return nil, false + } + + // We cannot use the unsafe method as in SaveContractGlobals due to possible alignment issues. + buf := make([]int64, 0, len(raw)/8) + for i := 0; i < len(raw); i += 8 { + buf = append(buf, int64(binary.LittleEndian.Uint64(raw[i:]))) + } + return buf, true +} + +func SaveContractGlobals(snapshot *avl.Tree, id AccountID, globals []int64) { + oldHeader := (*reflect.SliceHeader)(unsafe.Pointer(&globals)) + + header := reflect.SliceHeader{ + Data: oldHeader.Data, + Len: oldHeader.Len * 8, + Cap: oldHeader.Len * 8, // prevent appending in place + } + WriteAccountContractGlobals(snapshot, id, *(*[]byte)(unsafe.Pointer(&header))) +} + func LoadContractMemorySnapshot(snapshot *avl.Tree, id AccountID) []byte { numPages, exists := ReadAccountContractNumPages(snapshot, id) if !exists { diff --git a/db.go b/db.go index bc6f8b3d..8415d6af 100644 --- a/db.go +++ b/db.go @@ -50,6 +50,7 @@ var ( keyAccountContractNumPages = [...]byte{0x6} keyAccountContractPages = [...]byte{0x7} keyAccountContractGasBalance = [...]byte{0x8} + keyAccountContractGlobals = [...]byte{0x9} ) type RewardWithdrawalRequest struct { @@ -204,6 +205,14 @@ func WriteAccountContractNumPages(tree *avl.Tree, id TransactionID, numPages uin writeUnderAccounts(tree, id, keyAccountContractNumPages[:], buf[:]) } +func ReadAccountContractGlobals(tree *avl.Tree, id TransactionID) ([]byte, bool) { + return readUnderAccounts(tree, id, keyAccountContractGlobals[:]) +} + +func WriteAccountContractGlobals(tree *avl.Tree, id TransactionID, globals []byte) { + writeUnderAccounts(tree, id, keyAccountContractGlobals[:], globals) +} + func ReadAccountContractPage(tree *avl.Tree, id TransactionID, idx uint64) ([]byte, bool) { var idxBuf [8]byte binary.LittleEndian.PutUint64(idxBuf[:], idx) diff --git a/go.mod b/go.mod index 89ce4cc1..01454126 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,6 @@ require ( github.com/rs/zerolog v1.14.3 github.com/stretchr/testify v1.3.0 github.com/syndtr/goleveldb v1.0.0 - github.com/urfave/cli v1.20.0 github.com/valyala/bytebufferpool v1.0.0 github.com/valyala/fasthttp v1.3.0 github.com/valyala/fastjson v1.4.1 diff --git a/log/console.go b/log/console.go index 11e354ab..32e42100 100644 --- a/log/console.go +++ b/log/console.go @@ -236,7 +236,6 @@ func (w ConsoleWriter) writeFields(evt map[string]interface{}, buf *bytes.Buffer } } - // Insert the field key buf.WriteString(fn(field)) switch fValue := evt[field].(type) { diff --git a/sys/const.go b/sys/const.go index 7c757f53..2707f5c7 100644 --- a/sys/const.go +++ b/sys/const.go @@ -272,6 +272,13 @@ var ( `batch`: TagBatch, `stake`: TagStake, } + + ContractDefaultMemoryPages = 4 + ContractMaxMemoryPages = 32 + ContractTableSize = 4096 + ContractMaxValueSlots = 8192 + ContractMaxCallStackDepth = 256 + ContractMaxGlobals = 64 ) // String converts a given tag to a string.