From 74ab62eb58a1324abe678d34a7acaab7ea3301e0 Mon Sep 17 00:00:00 2001 From: OpacicAleksa Date: Wed, 31 Aug 2022 08:41:15 +0200 Subject: [PATCH] Permission smart contract deployment (#692) * Add contract deployment whitelist CLI commands * Validate smart contract deployment in txPool * Remove whitelist from genesis if nill * code clean up * Fix linter errors * Add command for listing all whitelists * Add unit tests * Typo, replace comma with dot * Fix typo in addAddress flag * Convert flags to snake-case * Alocate arrays to specific size * Fix comment for adding addresses * Make deployment result omitempty * change show command description * Fix typo in whitelist command description * Change permission smart contract error * Make global test var local * Extract errors * Make deployment whitelist key const * Remove genesis config dependecy from show command * Fix lint * extract unmarshalling raw addresses * Switch commands to kebab-case * Refactor unmarshall raw address * Fix lint * Remove test * make addressExists exportable * make canDeployContract helper method * Rename methods * Rename attribute * Reserve space for whitelist * Rename config param * extract config helper to seperate file * Change methods names * Optimize list insertion and deletion * Tidy whotelist object in command * Fix variable names * Tidy txpool * Rename typo * Extract init deploymentWhitelist * Remove unused methods * Remove canDeploy method --- chain/params.go | 8 + command/root/root.go | 2 + command/whitelist/deployment/deployment.go | 66 +++++++++ command/whitelist/deployment/params.go | 162 +++++++++++++++++++++ command/whitelist/deployment/result.go | 32 ++++ command/whitelist/show/params.go | 72 +++++++++ command/whitelist/show/result.go | 20 +++ command/whitelist/show/show.go | 41 ++++++ command/whitelist/whitelist.go | 25 ++++ helper/config/config.go | 25 ++++ server/server.go | 16 +- txpool/txpool.go | 56 ++++++- txpool/txpool_test.go | 76 +++++++++- types/transaction.go | 1 + 14 files changed, 590 insertions(+), 12 deletions(-) create mode 100644 command/whitelist/deployment/deployment.go create mode 100644 command/whitelist/deployment/params.go create mode 100644 command/whitelist/deployment/result.go create mode 100644 command/whitelist/show/params.go create mode 100644 command/whitelist/show/result.go create mode 100644 command/whitelist/show/show.go create mode 100644 command/whitelist/whitelist.go create mode 100644 helper/config/config.go diff --git a/chain/params.go b/chain/params.go index 52efb9c677..57502bf728 100644 --- a/chain/params.go +++ b/chain/params.go @@ -2,6 +2,8 @@ package chain import ( "math/big" + + "github.com/0xPolygon/polygon-edge/types" ) // Params are all the set of params for the chain @@ -9,6 +11,7 @@ type Params struct { Forks *Forks `json:"forks"` ChainID int `json:"chainID"` Engine map[string]interface{} `json:"engine"` + Whitelists *Whitelists `json:"whitelists,omitempty"` BlockGasTarget uint64 `json:"blockGasTarget"` } @@ -21,6 +24,11 @@ func (p *Params) GetEngine() string { return "" } +// Whitelists specifies supported whitelists +type Whitelists struct { + Deployment []types.Address `json:"deployment,omitempty"` +} + // Forks specifies when each fork is activated type Forks struct { Homestead *Fork `json:"homestead,omitempty"` diff --git a/command/root/root.go b/command/root/root.go index ea5962b22d..ffb3e28c25 100644 --- a/command/root/root.go +++ b/command/root/root.go @@ -17,6 +17,7 @@ import ( "github.com/0xPolygon/polygon-edge/command/status" "github.com/0xPolygon/polygon-edge/command/txpool" "github.com/0xPolygon/polygon-edge/command/version" + "github.com/0xPolygon/polygon-edge/command/whitelist" "github.com/spf13/cobra" ) @@ -51,6 +52,7 @@ func (rc *RootCommand) registerSubCommands() { backup.GetCommand(), genesis.GetCommand(), server.GetCommand(), + whitelist.GetCommand(), license.GetCommand(), ) } diff --git a/command/whitelist/deployment/deployment.go b/command/whitelist/deployment/deployment.go new file mode 100644 index 0000000000..0a350481c2 --- /dev/null +++ b/command/whitelist/deployment/deployment.go @@ -0,0 +1,66 @@ +package deployment + +import ( + "fmt" + + "github.com/0xPolygon/polygon-edge/command" + "github.com/spf13/cobra" +) + +func GetCommand() *cobra.Command { + deploymentCmd := &cobra.Command{ + Use: "deployment", + Short: "Top level command for updating smart contract deployment whitelist. Only accepts subcommands", + PreRunE: runPreRun, + Run: runCommand, + } + + setFlags(deploymentCmd) + + return deploymentCmd +} + +func setFlags(cmd *cobra.Command) { + cmd.Flags().StringVar( + ¶ms.genesisPath, + chainFlag, + fmt.Sprintf("./%s", command.DefaultGenesisFileName), + "the genesis file to update", + ) + cmd.Flags().StringArrayVar( + ¶ms.addAddressRaw, + addAddressFlag, + []string{}, + "adds a new address to the contract deployment whitelist", + ) + + cmd.Flags().StringArrayVar( + ¶ms.removeAddressRaw, + removeAddressFlag, + []string{}, + "removes a new address from the contract deployment whitelist", + ) +} + +func runPreRun(_ *cobra.Command, _ []string) error { + return params.initRawParams() +} + +func runCommand(cmd *cobra.Command, _ []string) { + outputter := command.InitializeOutputter(cmd) + defer outputter.WriteOutput() + + if err := params.updateGenesisConfig(); err != nil { + outputter.SetError(err) + + return + } + + if err := params.overrideGenesisConfig(); err != nil { + outputter.SetError(err) + + return + } + + outputter.SetCommandResult(params.getResult()) +} diff --git a/command/whitelist/deployment/params.go b/command/whitelist/deployment/params.go new file mode 100644 index 0000000000..6d7f4619b4 --- /dev/null +++ b/command/whitelist/deployment/params.go @@ -0,0 +1,162 @@ +package deployment + +import ( + "fmt" + "os" + + "github.com/0xPolygon/polygon-edge/chain" + "github.com/0xPolygon/polygon-edge/command" + "github.com/0xPolygon/polygon-edge/command/helper" + "github.com/0xPolygon/polygon-edge/helper/config" + "github.com/0xPolygon/polygon-edge/types" +) + +const ( + chainFlag = "chain" + addAddressFlag = "add" + removeAddressFlag = "remove" +) + +var ( + params = &deploymentParams{} +) + +type deploymentParams struct { + // raw addresses, entered by CLI commands + addAddressRaw []string + removeAddressRaw []string + + // addresses, converted from raw addresses + addAddresses []types.Address + removeAddresses []types.Address + + // genesis file + genesisPath string + genesisConfig *chain.Chain + + // deployment whitelist from genesis configuration + whitelist []types.Address +} + +func (p *deploymentParams) initRawParams() error { + // convert raw addresses to appropriate format + if err := p.initRawAddresses(); err != nil { + return err + } + + // init genesis configuration + if err := p.initChain(); err != nil { + return err + } + + return nil +} + +func (p *deploymentParams) initRawAddresses() error { + // convert addresses to be added from string to type.Address + p.addAddresses = unmarshallRawAddresses(p.addAddressRaw) + + // convert addresses to be removed from string to type.Address + p.removeAddresses = unmarshallRawAddresses(p.removeAddressRaw) + + return nil +} + +func (p *deploymentParams) initChain() error { + // import genesis configuration + cc, err := chain.Import(p.genesisPath) + if err != nil { + return fmt.Errorf( + "failed to load chain config from %s: %w", + p.genesisPath, + err, + ) + } + + // set genesis configuration + p.genesisConfig = cc + + return nil +} + +func (p *deploymentParams) updateGenesisConfig() error { + // Fetch contract deployment whitelist from genesis config + deploymentWhitelist, err := config.GetDeploymentWhitelist(p.genesisConfig) + if err != nil { + return err + } + + doesExist := map[types.Address]bool{} + + for _, a := range deploymentWhitelist { + doesExist[a] = true + } + + for _, a := range p.addAddresses { + doesExist[a] = true + } + + for _, a := range p.removeAddresses { + doesExist[a] = false + } + + newDeploymentWhitelist := make([]types.Address, 0) + + for addr, exists := range doesExist { + if exists { + newDeploymentWhitelist = append(newDeploymentWhitelist, addr) + } + } + + // Set whitelist in genesis configuration + whitelistConfig := config.GetWhitelist(p.genesisConfig) + + if whitelistConfig == nil { + whitelistConfig = &chain.Whitelists{} + } + + whitelistConfig.Deployment = newDeploymentWhitelist + p.genesisConfig.Params.Whitelists = whitelistConfig + + // Save whitelist for result + p.whitelist = newDeploymentWhitelist + + return nil +} + +func (p *deploymentParams) overrideGenesisConfig() error { + // Remove the current genesis configuration from the disk + if err := os.Remove(p.genesisPath); err != nil { + return err + } + + // Save the new genesis configuration + if err := helper.WriteGenesisConfigToDisk( + p.genesisConfig, + p.genesisPath, + ); err != nil { + return err + } + + return nil +} + +func (p *deploymentParams) getResult() command.CommandResult { + result := &DeploymentResult{ + AddAddresses: p.addAddresses, + RemoveAddresses: p.removeAddresses, + Whitelist: p.whitelist, + } + + return result +} + +func unmarshallRawAddresses(addresses []string) []types.Address { + marshalledAddresses := make([]types.Address, len(addresses)) + + for indx, address := range addresses { + marshalledAddresses[indx] = types.StringToAddress(address) + } + + return marshalledAddresses +} diff --git a/command/whitelist/deployment/result.go b/command/whitelist/deployment/result.go new file mode 100644 index 0000000000..17ce4dbba3 --- /dev/null +++ b/command/whitelist/deployment/result.go @@ -0,0 +1,32 @@ +package deployment + +import ( + "bytes" + "fmt" + + "github.com/0xPolygon/polygon-edge/types" +) + +type DeploymentResult struct { + AddAddresses []types.Address `json:"addAddress,omitempty"` + RemoveAddresses []types.Address `json:"removeAddress,omitempty"` + Whitelist []types.Address `json:"whitelist"` +} + +func (r *DeploymentResult) GetOutput() string { + var buffer bytes.Buffer + + buffer.WriteString("\n[CONTRACT DEPLOYMENT WHITELIST]\n\n") + + if len(r.AddAddresses) != 0 { + buffer.WriteString(fmt.Sprintf("Added addresses: %s,\n", r.AddAddresses)) + } + + if len(r.RemoveAddresses) != 0 { + buffer.WriteString(fmt.Sprintf("Removed addresses: %s,\n", r.RemoveAddresses)) + } + + buffer.WriteString(fmt.Sprintf("Contract deployment whitelist : %s,\n", r.Whitelist)) + + return buffer.String() +} diff --git a/command/whitelist/show/params.go b/command/whitelist/show/params.go new file mode 100644 index 0000000000..acc55ab6c7 --- /dev/null +++ b/command/whitelist/show/params.go @@ -0,0 +1,72 @@ +package show + +import ( + "fmt" + + "github.com/0xPolygon/polygon-edge/chain" + "github.com/0xPolygon/polygon-edge/command" + "github.com/0xPolygon/polygon-edge/helper/config" + "github.com/0xPolygon/polygon-edge/types" +) + +const ( + chainFlag = "chain" +) + +var ( + params = &showParams{} +) + +type showParams struct { + // genesis file path + genesisPath string + + // deployment whitelist + whitelists Whitelists +} + +type Whitelists struct { + deployment []types.Address +} + +func (p *showParams) initRawParams() error { + // init genesis configuration + if err := p.initWhitelists(); err != nil { + return err + } + + return nil +} + +func (p *showParams) initWhitelists() error { + // import genesis configuration + genesisConfig, err := chain.Import(p.genesisPath) + if err != nil { + return fmt.Errorf( + "failed to load chain config from %s: %w", + p.genesisPath, + err, + ) + } + + // fetch whitelists + deploymentWhitelist, err := config.GetDeploymentWhitelist(genesisConfig) + if err != nil { + return err + } + + // set whitelists + p.whitelists = Whitelists{ + deployment: deploymentWhitelist, + } + + return nil +} + +func (p *showParams) getResult() command.CommandResult { + result := &ShowResult{ + Whitelists: p.whitelists, + } + + return result +} diff --git a/command/whitelist/show/result.go b/command/whitelist/show/result.go new file mode 100644 index 0000000000..fbbba91c1e --- /dev/null +++ b/command/whitelist/show/result.go @@ -0,0 +1,20 @@ +package show + +import ( + "bytes" + "fmt" +) + +type ShowResult struct { + Whitelists Whitelists +} + +func (r *ShowResult) GetOutput() string { + var buffer bytes.Buffer + + buffer.WriteString("\n[WHITELISTS]\n\n") + + buffer.WriteString(fmt.Sprintf("Contract deployment whitelist : %s,\n", r.Whitelists.deployment)) + + return buffer.String() +} diff --git a/command/whitelist/show/show.go b/command/whitelist/show/show.go new file mode 100644 index 0000000000..43113aa46c --- /dev/null +++ b/command/whitelist/show/show.go @@ -0,0 +1,41 @@ +package show + +import ( + "fmt" + + "github.com/0xPolygon/polygon-edge/command" + "github.com/spf13/cobra" +) + +func GetCommand() *cobra.Command { + showCmd := &cobra.Command{ + Use: "show", + Short: "Displays whitelist information", + PreRunE: runPreRun, + Run: runCommand, + } + + setFlags(showCmd) + + return showCmd +} + +func setFlags(cmd *cobra.Command) { + cmd.Flags().StringVar( + ¶ms.genesisPath, + chainFlag, + fmt.Sprintf("./%s", command.DefaultGenesisFileName), + "the genesis file to update", + ) +} + +func runPreRun(_ *cobra.Command, _ []string) error { + return params.initRawParams() +} + +func runCommand(cmd *cobra.Command, _ []string) { + outputter := command.InitializeOutputter(cmd) + defer outputter.WriteOutput() + + outputter.SetCommandResult(params.getResult()) +} diff --git a/command/whitelist/whitelist.go b/command/whitelist/whitelist.go new file mode 100644 index 0000000000..42376252f7 --- /dev/null +++ b/command/whitelist/whitelist.go @@ -0,0 +1,25 @@ +package whitelist + +import ( + "github.com/0xPolygon/polygon-edge/command/whitelist/deployment" + "github.com/0xPolygon/polygon-edge/command/whitelist/show" + "github.com/spf13/cobra" +) + +func GetCommand() *cobra.Command { + whitelistCmd := &cobra.Command{ + Use: "whitelist", + Short: "Top level command for modifying the Polygon Edge whitelists within the config. Only accepts subcommands.", + } + + registerSubcommands(whitelistCmd) + + return whitelistCmd +} + +func registerSubcommands(baseCmd *cobra.Command) { + baseCmd.AddCommand( + deployment.GetCommand(), + show.GetCommand(), + ) +} diff --git a/helper/config/config.go b/helper/config/config.go new file mode 100644 index 0000000000..ce75a0981a --- /dev/null +++ b/helper/config/config.go @@ -0,0 +1,25 @@ +package config + +import ( + "github.com/0xPolygon/polygon-edge/chain" + "github.com/0xPolygon/polygon-edge/types" +) + +// GetWhitelist fetches whitelist object from the config +func GetWhitelist(config *chain.Chain) *chain.Whitelists { + return config.Params.Whitelists +} + +// GetDeploymentWhitelist fetches deployment whitelist from the genesis config +// if doesn't exist returns empty list +func GetDeploymentWhitelist(genesisConfig *chain.Chain) ([]types.Address, error) { + // Fetch whitelist config if exists, if not init + whitelistConfig := GetWhitelist(genesisConfig) + + // Extract deployment whitelist if exists, if not init + if whitelistConfig == nil { + return make([]types.Address, 0), nil + } + + return whitelistConfig.Deployment, nil +} diff --git a/server/server.go b/server/server.go index 60b4ddc70d..57bebfb771 100644 --- a/server/server.go +++ b/server/server.go @@ -17,6 +17,7 @@ import ( "github.com/0xPolygon/polygon-edge/consensus" "github.com/0xPolygon/polygon-edge/crypto" "github.com/0xPolygon/polygon-edge/helper/common" + configHelper "github.com/0xPolygon/polygon-edge/helper/config" "github.com/0xPolygon/polygon-edge/helper/keccak" "github.com/0xPolygon/polygon-edge/helper/progress" "github.com/0xPolygon/polygon-edge/jsonrpc" @@ -200,6 +201,12 @@ func NewServer(config *Config) (*Server, error) { state: m.state, Blockchain: m.blockchain, } + + deploymentWhitelist, err := configHelper.GetDeploymentWhitelist(config.Chain) + if err != nil { + return nil, err + } + // start transaction pool m.txpool, err = txpool.NewTxPool( logger, @@ -209,10 +216,11 @@ func NewServer(config *Config) (*Server, error) { m.network, m.serverMetrics.txpool, &txpool.Config{ - Sealing: m.config.Seal, - MaxSlots: m.config.MaxSlots, - PriceLimit: m.config.PriceLimit, - MaxAccountEnqueued: m.config.MaxAccountEnqueued, + Sealing: m.config.Seal, + MaxSlots: m.config.MaxSlots, + PriceLimit: m.config.PriceLimit, + MaxAccountEnqueued: m.config.MaxAccountEnqueued, + DeploymentWhitelist: deploymentWhitelist, }, ) if err != nil { diff --git a/txpool/txpool.go b/txpool/txpool.go index ef6efc0e6a..5fb6d7d589 100644 --- a/txpool/txpool.go +++ b/txpool/txpool.go @@ -47,6 +47,7 @@ var ( ErrOversizedData = errors.New("oversized data") ErrMaxEnqueuedLimitReached = errors.New("maximum number of enqueued transactions reached") ErrRejectFutureTx = errors.New("rejected future tx due to low slots") + ErrSmartContractRestricted = errors.New("smart contract deployment restricted") ) // indicates origin of a transaction @@ -84,10 +85,11 @@ type signer interface { } type Config struct { - PriceLimit uint64 - MaxSlots uint64 - MaxAccountEnqueued uint64 - Sealing bool + PriceLimit uint64 + MaxSlots uint64 + MaxAccountEnqueued uint64 + Sealing bool + DeploymentWhitelist []types.Address } /* All requests are passed to the main loop @@ -171,10 +173,48 @@ type TxPool struct { // Event manager for txpool events eventManager *eventManager + // deploymentWhitelist map + deploymentWhitelist deploymentWhitelist + // indicates which txpool operator commands should be implemented proto.UnimplementedTxnPoolOperatorServer } +// deploymentWhitelist map which contains all addresses which can deploy contracts +// if empty anyone can +type deploymentWhitelist struct { + // Contract deployment whitelist + addresses map[string]bool +} + +// add an address to deploymentWhitelist map +func (w *deploymentWhitelist) add(addr types.Address) { + w.addresses[addr.String()] = true +} + +// allowed checks if address can deploy smart contract +func (w *deploymentWhitelist) allowed(addr types.Address) bool { + if len(w.addresses) == 0 { + return true + } + + _, ok := w.addresses[addr.String()] + + return ok +} + +func newDeploymentWhitelist(deploymentWhitelistRaw []types.Address) deploymentWhitelist { + deploymentWhitelist := deploymentWhitelist{ + addresses: map[string]bool{}, + } + + for _, addr := range deploymentWhitelistRaw { + deploymentWhitelist.add(addr) + } + + return deploymentWhitelist +} + // NewTxPool returns a new pool for processing incoming transactions. func NewTxPool( logger hclog.Logger, @@ -221,6 +261,9 @@ func NewTxPool( pool.topic = topic } + // initialize deployment whitelist + pool.deploymentWhitelist = newDeploymentWhitelist(config.DeploymentWhitelist) + if grpcServer != nil { proto.RegisterTxnPoolOperatorServer(grpcServer, pool) } @@ -562,6 +605,11 @@ func (p *TxPool) validateTx(tx *types.Transaction) error { tx.From = from } + // Check if transaction can deploy smart contract + if tx.IsContractCreation() && !p.deploymentWhitelist.allowed(tx.From) { + return ErrSmartContractRestricted + } + // Reject underpriced transactions if tx.IsUnderpriced(p.priceLimit) { return ErrUnderpriced diff --git a/txpool/txpool_test.go b/txpool/txpool_test.go index 26bc99f476..a3a8dbcbf7 100644 --- a/txpool/txpool_test.go +++ b/txpool/txpool_test.go @@ -89,10 +89,11 @@ func newTestPoolWithSlots(maxSlots uint64, mockStore ...store) (*TxPool, error) nil, nilMetrics, &Config{ - PriceLimit: defaultPriceLimit, - MaxSlots: maxSlots, - Sealing: false, - MaxAccountEnqueued: defaultMaxAccountEnqueued, + PriceLimit: defaultPriceLimit, + MaxSlots: maxSlots, + MaxAccountEnqueued: defaultMaxAccountEnqueued, + Sealing: false, + DeploymentWhitelist: []types.Address{}, }, ) } @@ -1404,6 +1405,73 @@ func TestDemote(t *testing.T) { }) } +// TestPermissionSmartContractDeployment tests sending deployment tx with deployment whitelist +func TestPermissionSmartContractDeployment(t *testing.T) { + t.Parallel() + + signer := crypto.NewEIP155Signer(uint64(100)) + + poolSigner := crypto.NewEIP155Signer(100) + + // Generate a private key and address + defaultKey, defaultAddr := tests.GenerateKeyAndAddr(t) + + setupPool := func() *TxPool { + pool, err := newTestPool() + if err != nil { + t.Fatalf("cannot create txpool - err: %v\n", err) + } + + pool.SetSigner(signer) + + return pool + } + + signTx := func(transaction *types.Transaction) *types.Transaction { + signedTx, signErr := poolSigner.SignTx(transaction, defaultKey) + if signErr != nil { + t.Fatalf("Unable to sign transaction, %v", signErr) + } + + return signedTx + } + + t.Run("contract deployment whitelist empty, anyone can deploy", func(t *testing.T) { + t.Parallel() + pool := setupPool() + + tx := newTx(defaultAddr, 0, 1) + tx.To = nil + + assert.NoError(t, pool.validateTx(signTx(tx))) + }) + t.Run("Addresses inside whitelist can deploy smart contract", func(t *testing.T) { + t.Parallel() + pool := setupPool() + pool.deploymentWhitelist.add(addr1) + pool.deploymentWhitelist.add(defaultAddr) + + tx := newTx(defaultAddr, 0, 1) + tx.To = nil + + assert.NoError(t, pool.validateTx(signTx(tx))) + }) + t.Run("Addresses outside whitelist can not deploy smart contract", func(t *testing.T) { + t.Parallel() + pool := setupPool() + pool.deploymentWhitelist.add(addr1) + pool.deploymentWhitelist.add(addr2) + + tx := newTx(defaultAddr, 0, 1) + tx.To = nil + + assert.ErrorIs(t, + pool.validateTx(signTx(tx)), + ErrSmartContractRestricted, + ) + }) +} + /* "Integrated" tests */ // The following tests ensure that the pool's inner event loop diff --git a/types/transaction.go b/types/transaction.go index ced118c9b7..010b249e87 100644 --- a/types/transaction.go +++ b/types/transaction.go @@ -24,6 +24,7 @@ type Transaction struct { size atomic.Value } +// IsContractCreation checks if tx is contract creation func (t *Transaction) IsContractCreation() bool { return t.To == nil }