diff --git a/cmd/neofs-adm/internal/modules/morph/container.go b/cmd/neofs-adm/internal/modules/morph/container.go new file mode 100644 index 00000000..ea42b383 --- /dev/null +++ b/cmd/neofs-adm/internal/modules/morph/container.go @@ -0,0 +1,202 @@ +package morph + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var errInvalidContainerResponse = errors.New("invalid response from container contract") + +func dumpContainers(cmd *cobra.Command, _ []string) error { + filename, err := cmd.Flags().GetString(containerDumpFlag) + if err != nil { + return fmt.Errorf("invalid filename: %w", err) + } + + c, err := getN3Client(viper.GetViper()) + if err != nil { + return fmt.Errorf("can't create N3 client: %w", err) + } + + nnsCs, err := c.GetContractStateByID(1) + if err != nil { + return fmt.Errorf("can't get NNS contract state: %w", err) + } + + ch, err := nnsResolveHash(c, nnsCs.Hash, containerContract+".neofs") + if err != nil { + return fmt.Errorf("can't fetch container contract hash: %w", err) + } + + res, err := c.InvokeFunction(ch, "list", + []smartcontract.Parameter{{Type: smartcontract.StringType, Value: ""}}, nil) + if err != nil { + return fmt.Errorf("%w: %v", errInvalidContainerResponse, err) + } + + var cids [][]byte + arr, ok := res.Stack[0].Value().([]stackitem.Item) + if !ok { + return fmt.Errorf("%w: not a struct", errInvalidContainerResponse) + } + for _, item := range arr { + id, err := item.TryBytes() + if err != nil { + return fmt.Errorf("%w: %v", errInvalidContainerResponse, err) + } + cids = append(cids, id) + } + + var containers []*Container + bw := io.NewBufBinWriter() + for _, id := range cids { + bw.Reset() + emit.AppCall(bw.BinWriter, ch, "get", callflag.All, id) + emit.AppCall(bw.BinWriter, ch, "eACL", callflag.All, id) + res, err := c.InvokeScript(bw.Bytes(), nil) + if err != nil { + return fmt.Errorf("can't get container info: %w", err) + } + if len(res.Stack) != 2 { + return fmt.Errorf("%w: expected 2 items on stack", errInvalidContainerResponse) + } + + cnt := new(Container) + err = cnt.FromStackItem(res.Stack[0]) + if err != nil { + return fmt.Errorf("%w: %v", errInvalidContainerResponse, err) + } + + ea := new(EACL) + err = ea.FromStackItem(res.Stack[1]) + if err != nil { + return fmt.Errorf("%w: %v", errInvalidContainerResponse, err) + } + if len(ea.Value) != 0 { + cnt.EACL = ea + } + + containers = append(containers, cnt) + } + + out, err := json.Marshal(containers) + if err != nil { + return err + } + return ioutil.WriteFile(filename, out, 0o660) +} + +// Container represents container struct in contract storage. +type Container struct { + Value []byte `json:"value"` + Signature []byte `json:"signature"` + PublicKey []byte `json:"public_key"` + Token []byte `json:"token"` + EACL *EACL `json:"eacl"` +} + +// EACL represents extended ACL struct in contract storage. +type EACL struct { + Value []byte `json:"value"` + Signature []byte `json:"signature"` + PublicKey []byte `json:"public_key"` + Token []byte `json:"token"` +} + +// ToStackItem implements stackitem.Convertible. +func (c *Container) ToStackItem() (stackitem.Item, error) { + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewByteArray(c.Value), + stackitem.NewByteArray(c.Signature), + stackitem.NewByteArray(c.PublicKey), + stackitem.NewByteArray(c.Token), + }), nil +} + +// FromStackItem implements stackitem.Convertible. +func (c *Container) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok || len(arr) != 4 { + return errors.New("invalid stack item type") + } + + value, err := arr[0].TryBytes() + if err != nil { + return errors.New("invalid container value") + } + + sig, err := arr[1].TryBytes() + if err != nil { + return errors.New("invalid container signature") + } + + pub, err := arr[2].TryBytes() + if err != nil { + return errors.New("invalid container public key") + } + + tok, err := arr[3].TryBytes() + if err != nil { + return errors.New("invalid container token") + } + + c.Value = value + c.Signature = sig + c.PublicKey = pub + c.Token = tok + return nil +} + +// ToStackItem implements stackitem.Convertible. +func (c *EACL) ToStackItem() (stackitem.Item, error) { + return stackitem.NewStruct([]stackitem.Item{ + stackitem.NewByteArray(c.Value), + stackitem.NewByteArray(c.Signature), + stackitem.NewByteArray(c.PublicKey), + stackitem.NewByteArray(c.Token), + }), nil +} + +// FromStackItem implements stackitem.Convertible. +func (c *EACL) FromStackItem(item stackitem.Item) error { + arr, ok := item.Value().([]stackitem.Item) + if !ok || len(arr) != 4 { + return errors.New("invalid stack item type") + } + + value, err := arr[0].TryBytes() + if err != nil { + return errors.New("invalid eACL value") + } + + sig, err := arr[1].TryBytes() + if err != nil { + return errors.New("invalid eACL signature") + } + + pub, err := arr[2].TryBytes() + if err != nil { + return errors.New("invalid eACL public key") + } + + tok, err := arr[3].TryBytes() + if err != nil { + return errors.New("invalid eACL token") + } + + c.Value = value + c.Signature = sig + c.PublicKey = pub + c.Token = tok + return nil +} diff --git a/cmd/neofs-adm/internal/modules/morph/root.go b/cmd/neofs-adm/internal/modules/morph/root.go index 42e5b3fa..ba04035b 100644 --- a/cmd/neofs-adm/internal/modules/morph/root.go +++ b/cmd/neofs-adm/internal/modules/morph/root.go @@ -27,6 +27,7 @@ const ( candidateFeeCLIFlag = "candidate-fee" withdrawFeeInitFlag = "network.fee.withdraw" withdrawFeeCLIFlag = "withdraw-fee" + containerDumpFlag = "dump" ) var ( @@ -111,6 +112,15 @@ var ( }, RunE: updateContracts, } + + dumpContainersCmd = &cobra.Command{ + Use: "dump-containers", + Short: "Dump NeoFS containers to file.", + PreRun: func(cmd *cobra.Command, _ []string) { + _ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag)) + }, + RunE: dumpContainers, + } ) func init() { @@ -145,4 +155,8 @@ func init() { updateContractsCmd.Flags().String(alphabetWalletsFlag, "", "path to alphabet wallets dir") updateContractsCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint") updateContractsCmd.Flags().String(contractsInitFlag, "", "path to archive with compiled NeoFS contracts (default fetched from latest github release)") + + RootCmd.AddCommand(dumpContainersCmd) + dumpContainersCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint") + dumpContainersCmd.Flags().String(containerDumpFlag, "", "file where to save dumped containers") }