package frostfsid import ( "errors" "fmt" "math/big" "sort" frostfsidclient "git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client" frostfsidrpclient "git.frostfs.info/TrueCloudLab/frostfs-contract/rpcclient/frostfsid" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants" "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper" commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common" "github.com/google/uuid" "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/io" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" "github.com/nspcc-dev/neo-go/pkg/rpcclient/management" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/util" "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" ) const iteratorBatchSize = 1 const ( namespaceFlag = "namespace" subjectNameFlag = "subject-name" subjectKeyFlag = "subject-key" subjectAddressFlag = "subject-address" includeNamesFlag = "include-names" groupNameFlag = "group-name" groupIDFlag = "group-id" rootNamespacePlaceholder = "" ) var ( Cmd = &cobra.Command{ Use: "frostfsid", Short: "Section for frostfsid interactions commands", } frostfsidCreateNamespaceCmd = &cobra.Command{ Use: "create-namespace", Short: "Create new namespace in frostfsid contract", PreRun: func(cmd *cobra.Command, _ []string) { _ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag)) _ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag)) }, Run: frostfsidCreateNamespace, } frostfsidListNamespacesCmd = &cobra.Command{ Use: "list-namespaces", Short: "List all namespaces in frostfsid", PreRun: func(cmd *cobra.Command, _ []string) { _ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag)) }, Run: frostfsidListNamespaces, } frostfsidCreateSubjectCmd = &cobra.Command{ Use: "create-subject", Short: "Create subject in frostfsid contract", PreRun: func(cmd *cobra.Command, _ []string) { _ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag)) _ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag)) }, Run: frostfsidCreateSubject, } frostfsidDeleteSubjectCmd = &cobra.Command{ Use: "delete-subject", Short: "Delete subject from frostfsid contract", PreRun: func(cmd *cobra.Command, _ []string) { _ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag)) _ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag)) }, Run: frostfsidDeleteSubject, } frostfsidListSubjectsCmd = &cobra.Command{ Use: "list-subjects", Short: "List subjects in namespace", PreRun: func(cmd *cobra.Command, _ []string) { _ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag)) }, Run: frostfsidListSubjects, } frostfsidCreateGroupCmd = &cobra.Command{ Use: "create-group", Short: "Create group in frostfsid contract", PreRun: func(cmd *cobra.Command, _ []string) { _ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag)) _ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag)) }, Run: frostfsidCreateGroup, } frostfsidDeleteGroupCmd = &cobra.Command{ Use: "delete-group", Short: "Delete group from frostfsid contract", PreRun: func(cmd *cobra.Command, _ []string) { _ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag)) _ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag)) }, Run: frostfsidDeleteGroup, } frostfsidListGroupsCmd = &cobra.Command{ Use: "list-groups", Short: "List groups in namespace", PreRun: func(cmd *cobra.Command, _ []string) { _ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag)) }, Run: frostfsidListGroups, } frostfsidAddSubjectToGroupCmd = &cobra.Command{ Use: "add-subject-to-group", Short: "Add subject to group", PreRun: func(cmd *cobra.Command, _ []string) { _ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag)) _ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag)) }, Run: frostfsidAddSubjectToGroup, } frostfsidRemoveSubjectFromGroupCmd = &cobra.Command{ Use: "remove-subject-from-group", Short: "Remove subject from group", PreRun: func(cmd *cobra.Command, _ []string) { _ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag)) _ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag)) }, Run: frostfsidRemoveSubjectFromGroup, } frostfsidListGroupSubjectsCmd = &cobra.Command{ Use: "list-group-subjects", Short: "List subjects in group", PreRun: func(cmd *cobra.Command, _ []string) { _ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag)) }, Run: frostfsidListGroupSubjects, } ) func initFrostfsIDCreateNamespaceCmd() { Cmd.AddCommand(frostfsidCreateNamespaceCmd) frostfsidCreateNamespaceCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc) frostfsidCreateNamespaceCmd.Flags().String(namespaceFlag, "", "Namespace name to create") frostfsidCreateNamespaceCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc) _ = frostfsidCreateNamespaceCmd.MarkFlagRequired(namespaceFlag) } func initFrostfsIDListNamespacesCmd() { Cmd.AddCommand(frostfsidListNamespacesCmd) frostfsidListNamespacesCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc) } func initFrostfsIDCreateSubjectCmd() { Cmd.AddCommand(frostfsidCreateSubjectCmd) frostfsidCreateSubjectCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc) frostfsidCreateSubjectCmd.Flags().String(namespaceFlag, "", "Namespace where create subject") frostfsidCreateSubjectCmd.Flags().String(subjectNameFlag, "", "Subject name, must be unique in namespace") frostfsidCreateSubjectCmd.Flags().String(subjectKeyFlag, "", "Subject hex-encoded public key") frostfsidCreateSubjectCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc) } func initFrostfsIDDeleteSubjectCmd() { Cmd.AddCommand(frostfsidDeleteSubjectCmd) frostfsidDeleteSubjectCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc) frostfsidDeleteSubjectCmd.Flags().String(subjectAddressFlag, "", "Subject address") frostfsidDeleteSubjectCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc) } func initFrostfsIDListSubjectsCmd() { Cmd.AddCommand(frostfsidListSubjectsCmd) frostfsidListSubjectsCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc) frostfsidListSubjectsCmd.Flags().String(namespaceFlag, "", "Namespace to list subjects") frostfsidListSubjectsCmd.Flags().Bool(includeNamesFlag, false, "Whether include subject name (require additional requests)") } func initFrostfsIDCreateGroupCmd() { Cmd.AddCommand(frostfsidCreateGroupCmd) frostfsidCreateGroupCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc) frostfsidCreateGroupCmd.Flags().String(namespaceFlag, "", "Namespace where create group") frostfsidCreateGroupCmd.Flags().String(groupNameFlag, "", "Group name, must be unique in namespace") frostfsidCreateGroupCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc) _ = frostfsidCreateGroupCmd.MarkFlagRequired(groupNameFlag) } func initFrostfsIDDeleteGroupCmd() { Cmd.AddCommand(frostfsidDeleteGroupCmd) frostfsidDeleteGroupCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc) frostfsidDeleteGroupCmd.Flags().String(namespaceFlag, "", "Namespace to delete group") frostfsidDeleteGroupCmd.Flags().Int64(groupIDFlag, 0, "Group id") frostfsidDeleteGroupCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc) } func initFrostfsIDListGroupsCmd() { Cmd.AddCommand(frostfsidListGroupsCmd) frostfsidListGroupsCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc) frostfsidListGroupsCmd.Flags().String(namespaceFlag, "", "Namespace to list groups") } func initFrostfsIDAddSubjectToGroupCmd() { Cmd.AddCommand(frostfsidAddSubjectToGroupCmd) frostfsidAddSubjectToGroupCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc) frostfsidAddSubjectToGroupCmd.Flags().String(subjectAddressFlag, "", "Subject address") frostfsidAddSubjectToGroupCmd.Flags().Int64(groupIDFlag, 0, "Group id") frostfsidAddSubjectToGroupCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc) } func initFrostfsIDRemoveSubjectFromGroupCmd() { Cmd.AddCommand(frostfsidRemoveSubjectFromGroupCmd) frostfsidRemoveSubjectFromGroupCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc) frostfsidRemoveSubjectFromGroupCmd.Flags().String(subjectAddressFlag, "", "Subject address") frostfsidRemoveSubjectFromGroupCmd.Flags().Int64(groupIDFlag, 0, "Group id") frostfsidRemoveSubjectFromGroupCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc) } func initFrostfsIDListGroupSubjectsCmd() { Cmd.AddCommand(frostfsidListGroupSubjectsCmd) frostfsidListGroupSubjectsCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc) frostfsidListGroupSubjectsCmd.Flags().String(namespaceFlag, "", "Namespace name") frostfsidListGroupSubjectsCmd.Flags().Int64(groupIDFlag, 0, "Group id") frostfsidListGroupSubjectsCmd.Flags().Bool(includeNamesFlag, false, "Whether include subject name (require additional requests)") } func frostfsidCreateNamespace(cmd *cobra.Command, _ []string) { ns := getFrostfsIDNamespace(cmd) ffsid, err := newFrostfsIDClient(cmd) commonCmd.ExitOnErr(cmd, "init contract client: %w", err) ffsid.addCall(ffsid.roCli.CreateNamespaceCall(ns)) err = ffsid.sendWait() commonCmd.ExitOnErr(cmd, "create namespace error: %w", err) } func frostfsidListNamespaces(cmd *cobra.Command, _ []string) { inv, _, hash := initInvoker(cmd) reader := frostfsidrpclient.NewReader(inv, hash) sessionID, it, err := reader.ListNamespaces() commonCmd.ExitOnErr(cmd, "can't get namespace: %w", err) items, err := readIterator(inv, &it, iteratorBatchSize, sessionID) commonCmd.ExitOnErr(cmd, "can't read iterator: %w", err) namespaces, err := frostfsidclient.ParseNamespaces(items) commonCmd.ExitOnErr(cmd, "can't parse namespace: %w", err) sort.Slice(namespaces, func(i, j int) bool { return namespaces[i].Name < namespaces[j].Name }) for _, namespace := range namespaces { if namespace.Name == "" { namespace.Name = rootNamespacePlaceholder } cmd.Printf("%s\n", namespace.Name) } } func frostfsidCreateSubject(cmd *cobra.Command, _ []string) { ns := getFrostfsIDNamespace(cmd) subjName := getFrostfsIDSubjectName(cmd) subjKey := getFrostfsIDSubjectKey(cmd) ffsid, err := newFrostfsIDClient(cmd) commonCmd.ExitOnErr(cmd, "init contract client: %w", err) ffsid.addCall(ffsid.roCli.CreateSubjectCall(ns, subjKey)) if subjName != "" { ffsid.addCall(ffsid.roCli.SetSubjectNameCall(subjKey.GetScriptHash(), subjName)) } err = ffsid.sendWait() commonCmd.ExitOnErr(cmd, "create subject: %w", err) } func frostfsidDeleteSubject(cmd *cobra.Command, _ []string) { subjectAddress := getFrostfsIDSubjectAddress(cmd) ffsid, err := newFrostfsIDClient(cmd) commonCmd.ExitOnErr(cmd, "init contract client: %w", err) ffsid.addCall(ffsid.roCli.DeleteSubjectCall(subjectAddress)) err = ffsid.sendWait() commonCmd.ExitOnErr(cmd, "delete subject error: %w", err) } func frostfsidListSubjects(cmd *cobra.Command, _ []string) { includeNames, _ := cmd.Flags().GetBool(includeNamesFlag) ns := getFrostfsIDNamespace(cmd) inv, _, hash := initInvoker(cmd) reader := frostfsidrpclient.NewReader(inv, hash) sessionID, it, err := reader.ListNamespaceSubjects(ns) commonCmd.ExitOnErr(cmd, "can't get namespace: %w", err) subAddresses, err := frostfsidclient.UnwrapArrayOfUint160(readIterator(inv, &it, iteratorBatchSize, sessionID)) commonCmd.ExitOnErr(cmd, "can't unwrap: %w", err) sort.Slice(subAddresses, func(i, j int) bool { return subAddresses[i].Less(subAddresses[j]) }) for _, addr := range subAddresses { if !includeNames { cmd.Println(address.Uint160ToString(addr)) continue } sessionID, it, err := reader.ListSubjects() commonCmd.ExitOnErr(cmd, "can't get subject: %w", err) items, err := readIterator(inv, &it, iteratorBatchSize, sessionID) commonCmd.ExitOnErr(cmd, "can't read iterator: %w", err) subj, err := frostfsidclient.ParseSubject(items) commonCmd.ExitOnErr(cmd, "can't parse subject: %w", err) cmd.Printf("%s (%s)\n", address.Uint160ToString(addr), subj.Name) } } func frostfsidCreateGroup(cmd *cobra.Command, _ []string) { ns := getFrostfsIDNamespace(cmd) groupName := getFrostfsIDGroupName(cmd) ffsid, err := newFrostfsIDClient(cmd) commonCmd.ExitOnErr(cmd, "init contract client: %w", err) ffsid.addCall(ffsid.roCli.CreateGroupCall(ns, groupName)) groupID, err := ffsid.roCli.ParseGroupID(ffsid.sendWaitRes()) commonCmd.ExitOnErr(cmd, "create group: %w", err) cmd.Printf("group '%s' created with id: %d\n", groupName, groupID) } func frostfsidDeleteGroup(cmd *cobra.Command, _ []string) { ns := getFrostfsIDNamespace(cmd) groupID := getFrostfsIDGroupID(cmd) ffsid, err := newFrostfsIDClient(cmd) commonCmd.ExitOnErr(cmd, "init contract client: %w", err) ffsid.addCall(ffsid.roCli.DeleteGroupCall(ns, groupID)) err = ffsid.sendWait() commonCmd.ExitOnErr(cmd, "delete group error: %w", err) } func frostfsidListGroups(cmd *cobra.Command, _ []string) { inv, _, hash := initInvoker(cmd) ns := getFrostfsIDNamespace(cmd) reader := frostfsidrpclient.NewReader(inv, hash) sessionID, it, err := reader.ListGroups(ns) commonCmd.ExitOnErr(cmd, "can't get namespace: %w", err) items, err := readIterator(inv, &it, iteratorBatchSize, sessionID) commonCmd.ExitOnErr(cmd, "can't list groups: %w", err) groups, err := frostfsidclient.ParseGroups(items) commonCmd.ExitOnErr(cmd, "can't parse groups: %w", err) sort.Slice(groups, func(i, j int) bool { return groups[i].Name < groups[j].Name }) for _, group := range groups { cmd.Printf("%s (%d)\n", group.Name, group.ID) } } func frostfsidAddSubjectToGroup(cmd *cobra.Command, _ []string) { subjectAddress := getFrostfsIDSubjectAddress(cmd) groupID := getFrostfsIDGroupID(cmd) ffsid, err := newFrostfsIDClient(cmd) commonCmd.ExitOnErr(cmd, "init contract client: %w", err) ffsid.addCall(ffsid.roCli.AddSubjectToGroupCall(subjectAddress, groupID)) err = ffsid.sendWait() commonCmd.ExitOnErr(cmd, "add subject to group error: %w", err) } func frostfsidRemoveSubjectFromGroup(cmd *cobra.Command, _ []string) { subjectAddress := getFrostfsIDSubjectAddress(cmd) groupID := getFrostfsIDGroupID(cmd) ffsid, err := newFrostfsIDClient(cmd) commonCmd.ExitOnErr(cmd, "init contract client: %w", err) ffsid.addCall(ffsid.roCli.RemoveSubjectFromGroupCall(subjectAddress, groupID)) err = ffsid.sendWait() commonCmd.ExitOnErr(cmd, "remove subject from group error: %w", err) } func frostfsidListGroupSubjects(cmd *cobra.Command, _ []string) { ns := getFrostfsIDNamespace(cmd) groupID := getFrostfsIDGroupID(cmd) includeNames, _ := cmd.Flags().GetBool(includeNamesFlag) inv, cs, hash := initInvoker(cmd) _, err := helper.NNSResolveHash(inv, cs.Hash, helper.DomainOf(constants.FrostfsIDContract)) commonCmd.ExitOnErr(cmd, "can't get netmap contract hash: %w", err) reader := frostfsidrpclient.NewReader(inv, hash) sessionID, it, err := reader.ListGroupSubjects(ns, big.NewInt(groupID)) commonCmd.ExitOnErr(cmd, "can't list groups: %w", err) items, err := readIterator(inv, &it, iteratorBatchSize, sessionID) commonCmd.ExitOnErr(cmd, "can't read iterator: %w", err) subjects, err := frostfsidclient.UnwrapArrayOfUint160(items, err) commonCmd.ExitOnErr(cmd, "can't unwrap: %w", err) sort.Slice(subjects, func(i, j int) bool { return subjects[i].Less(subjects[j]) }) for _, subjAddr := range subjects { if !includeNames { cmd.Println(address.Uint160ToString(subjAddr)) continue } items, err := reader.GetSubject(subjAddr) commonCmd.ExitOnErr(cmd, "can't get subject: %w", err) subj, err := frostfsidclient.ParseSubject(items) commonCmd.ExitOnErr(cmd, "can't parse subject: %w", err) cmd.Printf("%s (%s)\n", address.Uint160ToString(subjAddr), subj.Name) } } type frostfsidClient struct { bw *io.BufBinWriter contractHash util.Uint160 roCli *frostfsidclient.Client // client can be used only for waiting tx, parsing and forming method params wCtx *helper.InitializeContext } func newFrostfsIDClient(cmd *cobra.Command) (*frostfsidClient, error) { wCtx, err := helper.NewInitializeContext(cmd, viper.GetViper()) if err != nil { return nil, fmt.Errorf("can't initialize context: %w", err) } r := management.NewReader(wCtx.ReadOnlyInvoker) cs, err := helper.GetContractByID(r, 1) if err != nil { return nil, fmt.Errorf("can't get NNS contract info: %w", err) } ffsidHash, err := helper.NNSResolveHash(wCtx.ReadOnlyInvoker, cs.Hash, helper.DomainOf(constants.FrostfsIDContract)) if err != nil { return nil, fmt.Errorf("can't get proxy contract hash: %w", err) } return &frostfsidClient{ bw: io.NewBufBinWriter(), contractHash: ffsidHash, roCli: frostfsidclient.NewSimple(wCtx.CommitteeAct, ffsidHash), wCtx: wCtx, }, nil } func (f *frostfsidClient) addCall(method string, args []any) { emit.AppCall(f.bw.BinWriter, f.contractHash, method, callflag.All, args...) } func (f *frostfsidClient) sendWait() error { if err := f.wCtx.SendConsensusTx(f.bw.Bytes()); err != nil { return err } f.bw.Reset() return f.wCtx.AwaitTx() } func (f *frostfsidClient) sendWaitRes() (*state.AppExecResult, error) { if err := f.wCtx.SendConsensusTx(f.bw.Bytes()); err != nil { return nil, err } f.bw.Reset() if len(f.wCtx.SentTxs) == 0 { return nil, errors.New("no transactions to wait") } f.wCtx.Command.Println("Waiting for transactions to persist...") return f.roCli.Wait(f.wCtx.SentTxs[0].Hash, f.wCtx.SentTxs[0].Vub, nil) } func readIterator(inv *invoker.Invoker, iter *result.Iterator, batchSize int, sessionID uuid.UUID) ([]stackitem.Item, error) { var shouldStop bool res := make([]stackitem.Item, 0) for !shouldStop { items, err := inv.TraverseIterator(sessionID, iter, batchSize) if err != nil { return nil, err } res = append(res, items...) shouldStop = len(items) < batchSize } return res, nil } func initInvoker(cmd *cobra.Command) (*invoker.Invoker, *state.Contract, util.Uint160) { c, err := helper.GetN3Client(viper.GetViper()) commonCmd.ExitOnErr(cmd, "can't create N3 client: %w", err) inv := invoker.New(c, nil) r := management.NewReader(inv) cs, err := r.GetContractByID(1) commonCmd.ExitOnErr(cmd, "can't get NNS contract info: %w", err) nmHash, err := helper.NNSResolveHash(inv, cs.Hash, helper.DomainOf(constants.FrostfsIDContract)) commonCmd.ExitOnErr(cmd, "can't get netmap contract hash: %w", err) return inv, cs, nmHash }