├── .github ├── CODEOWNERS ├── logo.svg └── workflows │ ├── config │ └── .golangci.yml │ ├── dco.yml │ └── go.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.Apache2 ├── LICENSE.GPLv3.md ├── LICENSE.md ├── Makefile ├── README.md ├── VERSION ├── cmd └── dump │ ├── blockchain.go │ └── main.go ├── common ├── ir.go ├── netmap.go ├── nns.go ├── storage.go ├── transfer.go ├── update.go ├── version.go ├── vote.go └── witness.go ├── contracts ├── alphabet │ ├── config.yml │ ├── contract.go │ ├── doc.go │ └── migration_test.go ├── audit │ ├── config.yml │ ├── contract.go │ ├── doc.go │ └── migration_test.go ├── balance │ ├── config.yml │ ├── contract.go │ ├── doc.go │ └── migration_test.go ├── container │ ├── config.yml │ ├── contract.go │ ├── doc.go │ └── migration_test.go ├── neofs │ ├── config.yml │ ├── contract.go │ └── doc.go ├── neofsid │ ├── config.yml │ ├── contract.go │ ├── doc.go │ └── migration_test.go ├── netmap │ ├── config.yml │ ├── contract.go │ ├── doc.go │ └── migration_test.go ├── nns │ ├── config.yml │ ├── contract.go │ ├── migration_test.go │ ├── namestate.go │ ├── nns.yml │ ├── recordtype.go │ └── testdata │ │ ├── testnet-2281632-contracts.json │ │ └── testnet-2281632-storage.csv ├── processing │ ├── config.yml │ ├── contract.go │ └── doc.go ├── proxy │ ├── config.yml │ ├── contract.go │ └── doc.go └── reputation │ ├── config.yml │ ├── contract.go │ ├── doc.go │ └── migration_test.go ├── debian ├── changelog ├── control ├── copyright ├── neofs-contract.docs ├── postinst.ex ├── postrm.ex ├── preinst.ex ├── prerm.ex ├── rules └── source │ └── format ├── deploy ├── alphabet.go ├── contracts.go ├── deploy.go ├── deploy_test.go ├── funds.go ├── funds_test.go ├── netmap.go ├── nns.go ├── notary.go └── util.go ├── docs ├── labels.md └── release-instruction.md ├── go.mod ├── go.sum ├── rpc ├── alphabet │ └── rpcbinding.go ├── audit │ └── rpcbinding.go ├── balance │ └── rpcbinding.go ├── container │ └── rpcbinding.go ├── neofs │ └── rpcbinding.go ├── neofsid │ └── rpcbinding.go ├── netmap │ └── rpcbinding.go ├── nns │ ├── example_test.go │ ├── hashes.go │ ├── hashes_test.go │ ├── names.go │ ├── recordtype.go │ └── rpcbinding.go ├── processing │ └── rpcbinding.go ├── proxy │ └── rpcbinding.go └── reputation │ └── rpcbinding.go ├── testdata ├── mainnet-3309907-contracts.json ├── mainnet-3309907-storage.csv ├── testnet-1254789-contracts.json └── testnet-1254789-storage.csv └── tests ├── alphabet_test.go ├── balance_test.go ├── container_test.go ├── dump ├── common.go ├── creator.go ├── doc.go ├── reader.go └── util.go ├── helpers.go ├── migration ├── doc.go ├── storage.go └── util.go ├── neofs_test.go ├── neofsid_test.go ├── netmap_test.go ├── nns_test.go ├── processing_test.go ├── proxy_test.go ├── reputation_test.go ├── util.go └── version_test.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @carpawell @fyrchik @cthulhu-rider @roman-khimov 2 | -------------------------------------------------------------------------------- /.github/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 22 | 25 | 26 | 27 | 48 | 50 | 51 | 53 | image/svg+xml 54 | 56 | 57 | 58 | 59 | 60 | 64 | 67 | 71 | 72 | 75 | 78 | 81 | 85 | 86 | 89 | 93 | 94 | 97 | 101 | 102 | 105 | 109 | 110 | 111 | 112 | 115 | 119 | 120 | 123 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /.github/workflows/config/.golangci.yml: -------------------------------------------------------------------------------- 1 | issues: 2 | exclude-rules: 3 | - path: ./rpc 4 | linters: 5 | - unused # RPC bindings are allowed to contain some unused convertors. 6 | -------------------------------------------------------------------------------- /.github/workflows/dco.yml: -------------------------------------------------------------------------------- 1 | name: DCO check 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | dco: 10 | uses: nspcc-dev/.github/.github/workflows/dco.yml@master 11 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | lint: 9 | name: Lint 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-go@v5 15 | with: 16 | go-version-file: 'go.mod' 17 | - name: golangci-lint 18 | uses: golangci/golangci-lint-action@v4 19 | with: 20 | version: latest 21 | args: --config=.github/workflows/config/.golangci.yml 22 | 23 | tests: 24 | name: Tests 25 | runs-on: ${{ matrix.os }} 26 | strategy: 27 | matrix: 28 | go: [ '1.20', '1.21', '1.22' ] 29 | os: [ubuntu-latest, windows-2022, macos-14] 30 | exclude: 31 | # Only latest Go version for Windows and MacOS. 32 | - os: windows-2022 33 | go: '1.20' 34 | - os: windows-2022 35 | go: '1.21' 36 | - os: macos-14 37 | go: '1.20' 38 | - os: macos-14 39 | go: '1.21' 40 | fail-fast: false 41 | steps: 42 | - uses: actions/checkout@v4 43 | 44 | - name: Set up Go 45 | uses: actions/setup-go@v5 46 | with: 47 | go-version: '${{ matrix.go }}' 48 | 49 | - name: Test 50 | run: go test -v ./... 51 | 52 | build: 53 | name: Build contracts and RPC bindings 54 | runs-on: ubuntu-latest 55 | strategy: 56 | matrix: 57 | go_versions: [ '1.20', '1.21', '1.22' ] 58 | fail-fast: false 59 | steps: 60 | - uses: actions/checkout@v4 61 | 62 | - name: Set up Go 63 | uses: actions/setup-go@v5 64 | with: 65 | go-version: '${{ matrix.go_versions }}' 66 | 67 | - name: Clear built RPC bindings 68 | run: make clean 69 | 70 | - name: Compile contracts and build RPC bindings 71 | run: make build 72 | 73 | - name: Check that committed RPC bindings match generated ones 74 | run: | 75 | if [[ $(git diff --name-only | grep '^rpc/*' -c) != 0 ]]; then 76 | echo "Fresh version of RPC bindings should be committed for the following contracts:"; 77 | git diff --name-only; 78 | exit 1; 79 | fi 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.avm 2 | *.nef 3 | *~ 4 | bindings_config.yml 5 | config.json 6 | /vendor/ 7 | .idea 8 | /bin/ 9 | 10 | # debhelpers 11 | **/.debhelper 12 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This repository provides several different components that use different 2 | licenses. Contracts, tests and commands use GPLv3 (see [LICENSE.GPLv3.md](LICENSE.GPLv3.md)), 3 | while RPC bindings (95% of which are autogenerated code) and other 4 | code intended to be used as a library to integrate with these contracts 5 | is distributed under Apache 2.0 license (see [LICENSE.Apache2](LICENSE.Apache2)). 6 | 7 | Specifically, GPLv3 is used by code in these folders: 8 | * cmd 9 | * common 10 | * contracts 11 | * tests 12 | 13 | Apache 2.0 is used in: 14 | * rpc 15 | * deploy -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | SHELL=bash 4 | # GOBIN is used only to install neo-go and allows to override 5 | # the location of written binary. 6 | export GOBIN ?= $(shell pwd)/bin 7 | export CGO_ENABLED=0 8 | NEOGO ?= $(GOBIN)/cli 9 | VERSION ?= $(shell git describe --tags --dirty --match "v*" --always --abbrev=8 2>/dev/null || cat VERSION 2>/dev/null || echo "develop") 10 | NEOGOORIGMOD = github.com/epicchainlabs/epicchain-go@v0.104.0 11 | NEOGOMOD = $(shell go list -f '{{.Path}}' -m $(NEOGOORIGMOD)) 12 | NEOGOVER = $(shell go list -f '{{.Version}}' -m $(NEOGOORIGMOD) | tr -d v) 13 | 14 | # .deb package versioning 15 | OS_RELEASE = $(shell lsb_release -cs) 16 | PKG_VERSION ?= $(shell echo $(VERSION) | sed "s/^v//" | \ 17 | sed -E "s/(.*)-(g[a-fA-F0-9]{6,8})(.*)/\1\3~\2/" | \ 18 | sed "s/-/~/")-${OS_RELEASE} 19 | 20 | .PHONY: all build clean test neo-go 21 | .PHONY: alphabet mainnet morph nns sidechain 22 | .PHONY: debpackage debclean 23 | build: neo-go all 24 | all: sidechain mainnet 25 | sidechain: alphabet morph nns 26 | 27 | alphabet_sc = alphabet 28 | morph_sc = audit balance container neofsid netmap proxy reputation 29 | mainnet_sc = neofs processing 30 | nns_sc = nns 31 | 32 | all_sc = $(alphabet_sc) $(morph_sc) $(mainnet_sc) $(nns_sc) 33 | 34 | %/contract.nef %/bindings_config.yml %/config.json: $(NEOGO) %/contract.go %/config.yml 35 | $(NEOGO) contract compile -i $* -c $*/config.yml -m $*/config.json -o $*/contract.nef --bindings $*/bindings_config.yml 36 | 37 | rpc/%/rpcbinding.go: contracts/%/config.json contracts/%/bindings_config.yml 38 | mkdir -p rpc/$* 39 | $(NEOGO) contract generate-rpcwrapper -o rpc/$*/rpcbinding.go -m contracts/$*/config.json --config contracts/$*/bindings_config.yml 40 | 41 | alphabet: $(foreach sc,$(alphabet_sc),contracts/$(sc)/contract.nef contracts/$(sc)/config.json rpc/$(sc)/rpcbinding.go) 42 | morph: $(foreach sc,$(morph_sc),contracts/$(sc)/contract.nef contracts/$(sc)/config.json rpc/$(sc)/rpcbinding.go) 43 | mainnet: $(foreach sc,$(mainnet_sc),contracts/$(sc)/contract.nef contracts/$(sc)/config.json rpc/$(sc)/rpcbinding.go) 44 | nns: $(foreach sc,$(nns_sc),contracts/$(sc)/contract.nef contracts/$(sc)/config.json rpc/$(sc)/rpcbinding.go) 45 | 46 | neo-go: $(NEOGO) 47 | 48 | $(NEOGO): Makefile 49 | @go install -trimpath -v -ldflags "-X '$(NEOGOMOD)/pkg/config.Version=$(NEOGOVER)'" $(NEOGOMOD)/cli@v$(NEOGOVER) 50 | 51 | test: 52 | @go test ./... 53 | 54 | clean: 55 | rm -rf ./bin $(foreach sc,$(all_sc),contracts/$(sc)/contract.nef contracts/$(sc)/config.json contracts/$(sc)/bindings_config.yml) 56 | 57 | archive: neofs-contract-$(VERSION).tar.gz 58 | 59 | neofs-contract-$(VERSION).tar.gz: $(foreach sc,$(all_sc),contracts/$(sc)/contract.nef contracts/$(sc)/config.json) 60 | @tar --transform "s|^\(contracts\)/\([a-z]\+\)/\(contract.nef\)$$|\\1/\\2/\\2_\\3|" \ 61 | --transform "s|^contracts/|neofs-contract-$(VERSION)/|" \ 62 | -czf $@ \ 63 | $(shell find contracts -name '*.nef' -o -name 'config.json') 64 | 65 | # Package for Debian 66 | debpackage: 67 | dch --package neofs-contract \ 68 | --controlmaint \ 69 | --newversion $(PKG_VERSION) \ 70 | --distribution $(OS_RELEASE) \ 71 | "Please see CHANGELOG.md for code changes for $(VERSION)" 72 | dpkg-buildpackage --no-sign -b 73 | 74 | debclean: 75 | dh clean 76 | 77 | fmt: 78 | @gofmt -l -w -s $$(find . -type f -name '*.go'| grep -v "/vendor/") 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Overview 4 | 5 | EpicChain-Contract contains all EpicChain related contracts written for the [EpicChain-Go](https://github.com/epicchainlabs/epicchain-go) compiler. These contracts are deployed both on the mainchain and the sidechain. 6 | 7 | **Mainchain contracts:** 8 | 9 | - epicchain 10 | - processing 11 | 12 | **Sidechain contracts:** 13 | 14 | - alphabet 15 | - audit 16 | - balance 17 | - container 18 | - epicchainid 19 | - netmap 20 | - nns 21 | - proxy 22 | - reputation 23 | 24 | # Getting Started 25 | 26 | ## Prerequisites 27 | 28 | To compile smart contracts, you need: 29 | 30 | - [epicchain-go](https://github.com/epicchainlabs/epicchain-go) >= 0.104.0 31 | 32 | ## Compilation 33 | 34 | To build and compile smart contracts, run the `make all` command. Compiled contracts `*_contract.nef` and manifest `config.json` files are placed in the corresponding directories. Generated RPC binding files `rpcbinding.go` are placed in the corresponding `rpc` directories. 35 | 36 | ```bash 37 | $ make all 38 | /home/user/go/bin/cli contract compile -i alphabet -c alphabet/config.yml -m alphabet/config.json -o alphabet/alphabet_contract.nef --bindings alphabet/bindings_config.yml 39 | mkdir -p rpc/alphabet 40 | /home/user/go/bin/cli contract generate-rpcwrapper -o rpc/alphabet/rpcbinding.go -m alphabet/config.json --config alphabet/bindings_config.yml 41 | ... 42 | ``` 43 | 44 | You can specify the path to the `epicchain-go` binary with the `epicchainGO` environment variable: 45 | 46 | ```bash 47 | $ epicchainGO=/home/user/epicchain-go/bin/epicchain-go make all 48 | ``` 49 | 50 | Remove compiled files with the `make clean` command. 51 | 52 | ## Building Debian Package 53 | 54 | To build a Debian package containing compiled contracts, run the `make debpackage` command. The package will install compiled contracts `*_contract.nef` and manifest `config.json` with corresponding directories to `/var/lib/epicchain/contract` for further usage. It will download and build `epicchain-go` if needed. 55 | 56 | To clean package-related files, use `make debclean`. 57 | 58 | # Testing 59 | 60 | Smart contract tests reside in the `tests/` directory. To execute the test suite after applying changes, simply run `make test`. 61 | 62 | ```bash 63 | $ make test 64 | ok github.com/epicchainlabs/epicchain-contract/tests 0.462s 65 | ``` 66 | 67 | # License 68 | 69 | Contracts are licensed under the GPLv3 license, bindings and other integration code are provided under the Apache 2.0 license - see [LICENSE.md](LICENSE.md) for details. 70 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | v0.19.1 2 | -------------------------------------------------------------------------------- /cmd/dump/blockchain.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/epicchainlabs/epicchain-go/pkg/core/state" 9 | "github.com/epicchainlabs/epicchain-go/pkg/rpcclient" 10 | "github.com/epicchainlabs/epicchain-go/pkg/rpcclient/actor" 11 | "github.com/epicchainlabs/epicchain-go/pkg/util" 12 | "github.com/epicchainlabs/epicchain-go/pkg/wallet" 13 | "github.com/epicchainlabs/epicchain-contract/rpc/nns" 14 | ) 15 | 16 | // wrapper over rpcNeo providing NeoFS blockchain services needed for current command. 17 | type remoteBlockchain struct { 18 | rpc *rpcclient.Client 19 | actor *actor.Actor 20 | 21 | currentBlock uint32 22 | } 23 | 24 | // newRemoteBlockChain dials Neo RPC server and returns remoteBlockchain based 25 | // on the opened connection. Connection and all requests are done within 15 26 | // timeout. 27 | func newRemoteBlockChain(blockChainRPCEndpoint string) (*remoteBlockchain, error) { 28 | acc, err := wallet.NewAccount() 29 | if err != nil { 30 | return nil, fmt.Errorf("generate new Neo account: %w", err) 31 | } 32 | 33 | c, err := rpcclient.New(context.Background(), blockChainRPCEndpoint, rpcclient.Options{ 34 | DialTimeout: 15 * time.Second, 35 | RequestTimeout: 15 * time.Second, 36 | }) 37 | if err != nil { 38 | return nil, fmt.Errorf("RPC client dial: %w", err) 39 | } 40 | 41 | act, err := actor.NewSimple(c, acc) 42 | if err != nil { 43 | return nil, fmt.Errorf("init actor: %w", err) 44 | } 45 | 46 | nLatestBlock, err := act.GetBlockCount() 47 | if err != nil { 48 | return nil, fmt.Errorf("get number of the latest block: %w", err) 49 | } 50 | 51 | return &remoteBlockchain{ 52 | rpc: c, 53 | actor: act, 54 | currentBlock: nLatestBlock, 55 | }, nil 56 | } 57 | 58 | func (x *remoteBlockchain) close() { 59 | x.rpc.Close() 60 | } 61 | 62 | // getNeoFSContractByName requests state.Contract for the NeoFS contract 63 | // referenced by given name using provided NeoFS NNS contract. 64 | // 65 | // See also nns.Resolve. 66 | func (x *remoteBlockchain) getNeoFSContractByName(name string) (res state.Contract, err error) { 67 | nnsHash, err := nns.InferHash(x.rpc) 68 | if err != nil { 69 | return res, fmt.Errorf("inferring nns: %w", err) 70 | } 71 | r := nns.NewReader(x.actor, nnsHash) 72 | h, err := r.ResolveFSContract(name) 73 | if err != nil { 74 | return res, fmt.Errorf("resolving %s: %w", name, err) 75 | } 76 | 77 | contractState, err := x.rpc.GetContractStateByHash(h) 78 | if err != nil { 79 | return res, fmt.Errorf("get state of the requested contract by hash '%s': %w", h.StringLE(), err) 80 | } 81 | 82 | return *contractState, nil 83 | } 84 | 85 | // iterateContractStorage iterates over all storage items of the Neo smart 86 | // contract referenced by given address and passes them into f. 87 | // iterateContractStorage breaks on any f's error and returns it. 88 | func (x *remoteBlockchain) iterateContractStorage(contract util.Uint160, f func(key, value []byte) error) error { 89 | nLatestBlock, err := x.actor.GetBlockCount() 90 | if err != nil { 91 | return fmt.Errorf("get number of the latest block: %w", err) 92 | } 93 | 94 | stateRoot, err := x.rpc.GetStateRootByHeight(nLatestBlock - 1) 95 | if err != nil { 96 | return fmt.Errorf("get state root at penult block #%d: %w", nLatestBlock-1, err) 97 | } 98 | 99 | var start []byte 100 | 101 | for { 102 | res, err := x.rpc.FindStates(stateRoot.Root, contract, nil, start, nil) 103 | if err != nil { 104 | return fmt.Errorf("get historical storage items of the requested contract at state root '%s': %w", stateRoot.Root, err) 105 | } 106 | 107 | for i := range res.Results { 108 | err = f(res.Results[i].Key, res.Results[i].Value) 109 | if err != nil { 110 | return err 111 | } 112 | } 113 | 114 | if !res.Truncated { 115 | return nil 116 | } 117 | 118 | start = res.Results[len(res.Results)-1].Key 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /cmd/dump/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/epicchainlabs/epicchain-contract/tests/dump" 10 | ) 11 | 12 | func main() { 13 | neoRPCEndpoint := flag.String("rpc", "", "Network address of the Neo RPC server") 14 | chainLabel := flag.String("label", "", "Label of the blockchain environment (e.g. 'testnet')") 15 | 16 | flag.Parse() 17 | 18 | switch { 19 | case *neoRPCEndpoint == "": 20 | log.Fatal("missing Neo RPC endpoint") 21 | case *chainLabel == "": 22 | log.Fatal("missing blockchain label") 23 | } 24 | 25 | const rootDir = "testdata" 26 | 27 | err := os.MkdirAll(rootDir, 0700) 28 | if err != nil { 29 | log.Fatal(fmt.Errorf("create root dir: %v", err)) 30 | } 31 | 32 | err = _dump(*neoRPCEndpoint, rootDir, *chainLabel) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | log.Printf("NeoFS contracts are successfully dumped to '%s/'\n", rootDir) 38 | } 39 | 40 | func _dump(neoBlockchainRPCEndpoint, rootDir, label string) error { 41 | b, err := newRemoteBlockChain(neoBlockchainRPCEndpoint) 42 | if err != nil { 43 | return fmt.Errorf("init remote blockchain: %w", err) 44 | } 45 | 46 | defer b.close() 47 | 48 | d, err := dump.NewCreator(rootDir, dump.ID{ 49 | Label: label, 50 | Block: b.currentBlock, 51 | }) 52 | if err != nil { 53 | return fmt.Errorf("init local dumper: %w", err) 54 | } 55 | 56 | defer d.Close() 57 | 58 | err = overtakeContracts(b, d) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | err = d.Flush() 64 | if err != nil { 65 | return fmt.Errorf("flush dump: %w", err) 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func overtakeContracts(from *remoteBlockchain, to *dump.Creator) error { 72 | for _, name := range []string{ 73 | "alphabet0", 74 | "audit", 75 | "balance", 76 | "container", 77 | "neofsid", 78 | "netmap", 79 | "reputation", 80 | } { 81 | log.Printf("Processing contract '%s'...\n", name) 82 | 83 | ctr, err := from.getNeoFSContractByName(name) 84 | if err != nil { 85 | return fmt.Errorf("get '%s' contract state: %w", name, err) 86 | } 87 | 88 | s := to.AddContract(name, ctr) 89 | 90 | err = from.iterateContractStorage(ctr.Hash, s.Write) 91 | if err != nil { 92 | return fmt.Errorf("iterate '%s' contract storage: %w", name, err) 93 | } 94 | } 95 | 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /common/ir.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/epicchainlabs/epicchain-go/pkg/interop" 5 | "github.com/epicchainlabs/epicchain-go/pkg/interop/contract" 6 | "github.com/epicchainlabs/epicchain-go/pkg/interop/native/ledger" 7 | "github.com/epicchainlabs/epicchain-go/pkg/interop/native/neo" 8 | "github.com/epicchainlabs/epicchain-go/pkg/interop/native/roles" 9 | "github.com/epicchainlabs/epicchain-go/pkg/interop/runtime" 10 | ) 11 | 12 | type IRNode struct { 13 | PublicKey interop.PublicKey 14 | } 15 | 16 | const irListMethod = "innerRingList" 17 | 18 | // InnerRingInvoker returns the public key of the inner ring node that has invoked the contract. 19 | // Work around for environments without notary support. 20 | func InnerRingInvoker(ir []interop.PublicKey) interop.PublicKey { 21 | for i := 0; i < len(ir); i++ { 22 | node := ir[i] 23 | if runtime.CheckWitness(node) { 24 | return node 25 | } 26 | } 27 | 28 | return nil 29 | } 30 | 31 | // InnerRingNodes return a list of inner ring nodes from state validator role 32 | // in the sidechain. 33 | func InnerRingNodes() []interop.PublicKey { 34 | blockHeight := ledger.CurrentIndex() 35 | return roles.GetDesignatedByRole(roles.NeoFSAlphabet, uint32(blockHeight+1)) 36 | } 37 | 38 | // InnerRingNodesFromNetmap gets a list of inner ring nodes through 39 | // calling "innerRingList" method of smart contract. 40 | // Work around for environments without notary support. 41 | func InnerRingNodesFromNetmap(sc interop.Hash160) []interop.PublicKey { 42 | nodes := contract.Call(sc, irListMethod, contract.ReadOnly).([]IRNode) 43 | pubs := []interop.PublicKey{} 44 | for i := range nodes { 45 | pubs = append(pubs, nodes[i].PublicKey) 46 | } 47 | return pubs 48 | } 49 | 50 | // AlphabetNodes returns a list of alphabet nodes from committee in the sidechain. 51 | func AlphabetNodes() []interop.PublicKey { 52 | return neo.GetCommittee() 53 | } 54 | 55 | // AlphabetAddress returns multi address of alphabet public keys. 56 | func AlphabetAddress() []byte { 57 | alphabet := neo.GetCommittee() 58 | return Multiaddress(alphabet, false) 59 | } 60 | 61 | // CommitteeAddress returns multi address of committee. 62 | func CommitteeAddress() []byte { 63 | committee := neo.GetCommittee() 64 | return Multiaddress(committee, true) 65 | } 66 | 67 | // Multiaddress returns default multisignature account address for N keys. 68 | // If committee set to true, it is `M = N/2+1` committee account. 69 | func Multiaddress(n []interop.PublicKey, committee bool) []byte { 70 | threshold := len(n)*2/3 + 1 71 | if committee { 72 | threshold = len(n)/2 + 1 73 | } 74 | 75 | return contract.CreateMultisigAccount(threshold, n) 76 | } 77 | 78 | // ContainsAlphabetWitness checks whether carrier transaction contains either 79 | // (2/3N + 1) or (N/2 + 1) valid multi-signature of the NeoFS Alphabet. 80 | func ContainsAlphabetWitness() bool { 81 | alphabet := neo.GetCommittee() 82 | if runtime.CheckWitness(Multiaddress(alphabet, false)) { 83 | return true 84 | } 85 | return runtime.CheckWitness(Multiaddress(alphabet, true)) 86 | } 87 | -------------------------------------------------------------------------------- /common/netmap.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/epicchainlabs/epicchain-go/pkg/interop/contract" 5 | "github.com/epicchainlabs/epicchain-go/pkg/interop/runtime" 6 | ) 7 | 8 | // SubscribeForNewEpoch registers calling contract as a NewEpoch 9 | // callback requester. Netmap contract's address is taken from the 10 | // NNS contract, therefore, it must be presented and filled with 11 | // netmap information for a correct SubscribeForNewEpoch call; otherwise 12 | // a successive call is not guaranteed. 13 | // Caller must have `NewEpoch` method with a single numeric argument. 14 | func SubscribeForNewEpoch() { 15 | netmapContract := ResolveFSContract("netmap") 16 | contract.Call(netmapContract, "subscribeForNewEpoch", contract.All, runtime.GetExecutingScriptHash()) 17 | } 18 | -------------------------------------------------------------------------------- /common/nns.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/epicchainlabs/epicchain-go/pkg/interop" 5 | "github.com/epicchainlabs/epicchain-go/pkg/interop/contract" 6 | "github.com/epicchainlabs/epicchain-go/pkg/interop/lib/address" 7 | "github.com/epicchainlabs/epicchain-go/pkg/interop/native/management" 8 | "github.com/epicchainlabs/epicchain-go/pkg/interop/native/std" 9 | ) 10 | 11 | // NNSID is the ID of the NNS contract in NeoFS networks. It's always deployed 12 | // first. 13 | const NNSID = 1 14 | 15 | // ContractTLD is the default domain used by NeoFS contracts. 16 | const ContractTLD = "neofs" 17 | 18 | // InferNNSHash returns NNS contract hash by [NNSID] or panics if 19 | // it can't be resolved. 20 | func InferNNSHash() interop.Hash160 { 21 | var nns = management.GetContractByID(NNSID) 22 | if nns == nil { 23 | panic("no NNS contract") 24 | } 25 | return nns.Hash 26 | } 27 | 28 | // ResolveFSContract returns contract hash by name as registered in NNS or 29 | // panics if it can't be resolved. It's similar to [ResolveFSContractWithNNS], 30 | // but retrieves NNS hash automatically (see [InferNNSHash]). 31 | func ResolveFSContract(name string) interop.Hash160 { 32 | return ResolveFSContractWithNNS(InferNNSHash(), name) 33 | } 34 | 35 | // ResolveFSContractWithNNS uses given NNS contract and returns target contract 36 | // hash by name as registered in NNS (assuming NeoFS-specific NNS setup, see 37 | // [NNSID]) or panics if it can't be resolved. Contract name should be 38 | // lowercased and should not include [ContractTLD]. Example values: "netmap", 39 | // "container", etc. 40 | func ResolveFSContractWithNNS(nns interop.Hash160, contractName string) interop.Hash160 { 41 | resResolve := contract.Call(nns, "resolve", contract.ReadOnly, contractName+"."+ContractTLD, 16 /*TXT*/) 42 | records := resResolve.([]string) 43 | if len(records) == 0 { 44 | panic("did not find a record of the " + contractName + " contract in the NNS") 45 | } 46 | if len(records[0]) == 2*interop.Hash160Len { 47 | var h = make([]byte, interop.Hash160Len) 48 | for i := 0; i < interop.Hash160Len; i++ { 49 | ii := (interop.Hash160Len - i - 1) * 2 50 | h[i] = byte(std.Atoi(records[0][ii:ii+2], 16)) 51 | } 52 | return h 53 | } 54 | return address.ToHash160(records[0]) 55 | } 56 | -------------------------------------------------------------------------------- /common/storage.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/epicchainlabs/epicchain-go/pkg/interop/native/std" 5 | "github.com/epicchainlabs/epicchain-go/pkg/interop/storage" 6 | ) 7 | 8 | func GetList(ctx storage.Context, key any) [][]byte { 9 | data := storage.Get(ctx, key) 10 | if data != nil { 11 | return std.Deserialize(data.([]byte)).([][]byte) 12 | } 13 | 14 | return [][]byte{} 15 | } 16 | 17 | // SetSerialized serializes data and puts it into contract storage. 18 | func SetSerialized(ctx storage.Context, key any, value any) { 19 | data := std.Serialize(value) 20 | storage.Put(ctx, key, data) 21 | } 22 | -------------------------------------------------------------------------------- /common/transfer.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/epicchainlabs/epicchain-go/pkg/interop/runtime" 5 | "github.com/epicchainlabs/epicchain-go/pkg/interop/util" 6 | ) 7 | 8 | var ( 9 | mintPrefix = []byte{0x01} 10 | burnPrefix = []byte{0x02} 11 | lockPrefix = []byte{0x03} 12 | unlockPrefix = []byte{0x04} 13 | containerFeePrefix = []byte{0x10} 14 | ) 15 | 16 | func WalletToScriptHash(wallet []byte) []byte { 17 | // V2 format 18 | return wallet[1 : len(wallet)-4] 19 | } 20 | 21 | func MintTransferDetails(txDetails []byte) []byte { 22 | return append(mintPrefix, txDetails...) 23 | } 24 | 25 | func BurnTransferDetails(txDetails []byte) []byte { 26 | return append(burnPrefix, txDetails...) 27 | } 28 | 29 | func LockTransferDetails(txDetails []byte) []byte { 30 | return append(lockPrefix, txDetails...) 31 | } 32 | 33 | func UnlockTransferDetails(epoch int) []byte { 34 | var buf any = epoch 35 | return append(unlockPrefix, buf.([]byte)...) 36 | } 37 | 38 | func ContainerFeeTransferDetails(cid []byte) []byte { 39 | return append(containerFeePrefix, cid...) 40 | } 41 | 42 | // AbortWithMessage calls `runtime.Log` with the passed message 43 | // and calls `ABORT` opcode. 44 | func AbortWithMessage(msg string) { 45 | runtime.Log(msg) 46 | util.Abort() 47 | } 48 | -------------------------------------------------------------------------------- /common/update.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/epicchainlabs/epicchain-go/pkg/interop/runtime" 5 | ) 6 | 7 | // LegacyOwnerKey is storage key used to store contract owner. 8 | const LegacyOwnerKey = "contractOwner" 9 | 10 | // HasUpdateAccess returns true if contract can be updated. 11 | func HasUpdateAccess() bool { 12 | return runtime.CheckWitness(CommitteeAddress()) 13 | } 14 | -------------------------------------------------------------------------------- /common/version.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "github.com/epicchainlabs/epicchain-go/pkg/interop/native/std" 4 | 5 | const ( 6 | major = 0 7 | minor = 19 8 | patch = 1 9 | 10 | // Versions from which an update should be performed. 11 | // These should be used in a group (so prevMinor can be equal to minor if there are 12 | // any migration routines. 13 | prevMajor = 0 14 | prevMinor = 15 15 | prevPatch = 4 16 | 17 | Version = major*1_000_000 + minor*1_000 + patch 18 | 19 | PrevVersion = prevMajor*1_000_000 + prevMinor*1_000 + prevPatch 20 | 21 | // ErrVersionMismatch is thrown by CheckVersion in case of error. 22 | ErrVersionMismatch = "previous version mismatch" 23 | 24 | // ErrAlreadyUpdated is thrown by CheckVersion if current version equals to version contract 25 | // is being updated from. 26 | ErrAlreadyUpdated = "contract is already of the latest version" 27 | ) 28 | 29 | // CheckVersion checks that contract can be updated from given original version 30 | // to the current one correctly. Original version should not be less than 31 | // PrevVersion to prevent updates from no longer supported old versions 32 | // (otherwise CheckVersion throws ErrVersionMismatch fault exception) and should 33 | // be less than the current one to prevent rollbacks (ErrAlreadyUpdated in this 34 | // case). 35 | func CheckVersion(from int) { 36 | if from < PrevVersion { 37 | panic(ErrVersionMismatch + ": expected >=" + std.Itoa(PrevVersion, 10)) 38 | } 39 | if from >= Version { 40 | panic(ErrAlreadyUpdated + ": " + std.Itoa(Version, 10)) 41 | } 42 | } 43 | 44 | // AppendVersion appends current contract version to the list of deploy arguments. 45 | func AppendVersion(data any) []any { 46 | if data == nil { 47 | return []any{Version} 48 | } 49 | return append(data.([]any), Version) 50 | } 51 | -------------------------------------------------------------------------------- /common/vote.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/epicchainlabs/epicchain-go/pkg/interop" 5 | "github.com/epicchainlabs/epicchain-go/pkg/interop/native/crypto" 6 | "github.com/epicchainlabs/epicchain-go/pkg/interop/native/ledger" 7 | "github.com/epicchainlabs/epicchain-go/pkg/interop/native/std" 8 | "github.com/epicchainlabs/epicchain-go/pkg/interop/storage" 9 | "github.com/epicchainlabs/epicchain-go/pkg/interop/util" 10 | ) 11 | 12 | type Ballot struct { 13 | // ID of the voting decision. 14 | ID []byte 15 | 16 | // Public keys of the already voted inner ring nodes. 17 | Voters []interop.PublicKey 18 | 19 | // Height of block with the last vote. 20 | Height int 21 | } 22 | 23 | const voteKey = "ballots" 24 | 25 | const blockDiff = 20 // change base on performance evaluation 26 | 27 | func InitVote(ctx storage.Context) { 28 | SetSerialized(ctx, voteKey, []Ballot{}) 29 | } 30 | 31 | // Vote adds ballot for the decision with a specific 'id' and returns the amount 32 | // of unique voters for that decision. 33 | func Vote(ctx storage.Context, id, from []byte) int { 34 | var ( 35 | newCandidates []Ballot 36 | candidates = getBallots(ctx) 37 | found = -1 38 | blockHeight = ledger.CurrentIndex() 39 | ) 40 | 41 | for i := 0; i < len(candidates); i++ { 42 | cnd := candidates[i] 43 | 44 | if blockHeight-cnd.Height > blockDiff { 45 | continue 46 | } 47 | 48 | if BytesEqual(cnd.ID, id) { 49 | voters := cnd.Voters 50 | 51 | for j := range voters { 52 | if BytesEqual(voters[j], from) { 53 | return len(voters) 54 | } 55 | } 56 | 57 | voters = append(voters, from) 58 | cnd = Ballot{ID: id, Voters: voters, Height: blockHeight} 59 | found = len(voters) 60 | } 61 | 62 | newCandidates = append(newCandidates, cnd) 63 | } 64 | 65 | if found < 0 { 66 | voters := []interop.PublicKey{from} 67 | newCandidates = append(newCandidates, Ballot{ 68 | ID: id, 69 | Voters: voters, 70 | Height: blockHeight}) 71 | found = 1 72 | } 73 | 74 | SetSerialized(ctx, voteKey, newCandidates) 75 | 76 | return found 77 | } 78 | 79 | // RemoveVotes clears ballots of the decision that has been accepted by 80 | // inner ring nodes. 81 | func RemoveVotes(ctx storage.Context, id []byte) { 82 | var ( 83 | candidates = getBallots(ctx) 84 | index int 85 | ) 86 | 87 | for i := 0; i < len(candidates); i++ { 88 | cnd := candidates[i] 89 | if BytesEqual(cnd.ID, id) { 90 | index = i 91 | break 92 | } 93 | } 94 | 95 | util.Remove(candidates, index) 96 | SetSerialized(ctx, voteKey, candidates) 97 | } 98 | 99 | // TryPurgeVotes removes storage item by 'ballots' key if it doesn't contain any 100 | // in-progress vote. Otherwise, TryPurgeVotes returns false. 101 | func TryPurgeVotes(ctx storage.Context) bool { 102 | var ( 103 | candidates = getBallots(ctx) 104 | blockHeight = ledger.CurrentIndex() 105 | ) 106 | for i := 0; i < len(candidates); i++ { 107 | cnd := candidates[i] 108 | 109 | if blockHeight-cnd.Height <= blockDiff { 110 | return false 111 | } 112 | } 113 | 114 | storage.Delete(ctx, voteKey) 115 | 116 | return true 117 | } 118 | 119 | // getBallots returns a deserialized slice of vote ballots. 120 | func getBallots(ctx storage.Context) []Ballot { 121 | data := storage.Get(ctx, voteKey) 122 | if data != nil { 123 | return std.Deserialize(data.([]byte)).([]Ballot) 124 | } 125 | 126 | return []Ballot{} 127 | } 128 | 129 | // BytesEqual compares two slices of bytes by wrapping them into strings, 130 | // which is necessary with new util.Equals interop behaviour, see neo-go#1176. 131 | func BytesEqual(a []byte, b []byte) bool { 132 | return util.Equals(string(a), string(b)) 133 | } 134 | 135 | // InvokeID returns hashed value of prefix and args concatenation. Iy is used to 136 | // identify different ballots. 137 | func InvokeID(args []any, prefix []byte) []byte { 138 | for i := range args { 139 | arg := args[i].([]byte) 140 | prefix = append(prefix, arg...) 141 | } 142 | 143 | return crypto.Sha256(prefix) 144 | } 145 | 146 | /* 147 | Check if the invocation is made from known container or audit contracts. 148 | This is necessary because calls from these contracts require to do transfer 149 | without signature collection (1 invoke transfer). 150 | 151 | IR1, IR2, IR3, IR4 -(4 invokes)-> [ Container Contract ] -(1 invoke)-> [ Balance Contract ] 152 | 153 | We can do 1 invoke transfer if: 154 | - invokation has happened from inner ring node, 155 | - it is indirect invocation from another smart-contract. 156 | 157 | However, there is a possible attack, when a malicious inner ring node creates 158 | a malicious smart-contract in the morph chain to do indirect call. 159 | 160 | MaliciousIR -(1 invoke)-> [ Malicious Contract ] -(1 invoke)-> [ Balance Contract ] 161 | 162 | To prevent that, we have to allow 1 invoke transfer from authorised well-known 163 | smart-contracts, that will be set up at `Init` method. 164 | */ 165 | 166 | func FromKnownContract(ctx storage.Context, caller interop.Hash160, key string) bool { 167 | addr := storage.Get(ctx, key).(interop.Hash160) 168 | return caller.Equals(addr) 169 | } 170 | -------------------------------------------------------------------------------- /common/witness.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "github.com/epicchainlabs/epicchain-go/pkg/interop/runtime" 4 | 5 | var ( 6 | // ErrAlphabetWitnessFailed appears when the method must be 7 | // called by the Alphabet but was not. 8 | ErrAlphabetWitnessFailed = "alphabet witness check failed" 9 | // ErrOwnerWitnessFailed appears when the method must be called 10 | // by an owner of some assets but was not. 11 | ErrOwnerWitnessFailed = "owner witness check failed" 12 | // ErrWitnessFailed appears when the method must be called 13 | // using certain public key but was not. 14 | ErrWitnessFailed = "witness check failed" 15 | ) 16 | 17 | // CheckAlphabetWitness checks witness of the passed caller. 18 | // It panics with ErrAlphabetWitnessFailed message on fail. 19 | func CheckAlphabetWitness(caller []byte) { 20 | checkWitnessWithPanic(caller, ErrAlphabetWitnessFailed) 21 | } 22 | 23 | // CheckOwnerWitness checks witness of the passed caller. 24 | // It panics with ErrOwnerWitnessFailed message on fail. 25 | func CheckOwnerWitness(caller []byte) { 26 | checkWitnessWithPanic(caller, ErrOwnerWitnessFailed) 27 | } 28 | 29 | // CheckWitness checks witness of the passed caller. 30 | // It panics with ErrWitnessFailed message on fail. 31 | func CheckWitness(caller []byte) { 32 | checkWitnessWithPanic(caller, ErrWitnessFailed) 33 | } 34 | 35 | func checkWitnessWithPanic(caller []byte, panicMsg string) { 36 | if !runtime.CheckWitness(caller) { 37 | panic(panicMsg) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /contracts/alphabet/config.yml: -------------------------------------------------------------------------------- 1 | name: "NeoFS Alphabet" 2 | safemethods: ["gas", "neo", "name", "version", "verify"] 3 | permissions: 4 | - methods: ["update", "transfer", "vote"] 5 | -------------------------------------------------------------------------------- /contracts/alphabet/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package alphabet contains implementation of Alphabet contract deployed in NeoFS 3 | sidechain. 4 | 5 | Alphabet contract is designed to support GAS production and vote for new 6 | validators in the sidechain. NEO token is required to produce GAS and vote for 7 | a new committee. It can be distributed among alphabet nodes of the Inner Ring. 8 | However, some of them may be malicious, and some NEO can be lost. It will destabilize 9 | the economic of the sidechain. To avoid it, all 100,000,000 NEO are 10 | distributed among all alphabet contracts. 11 | 12 | To identify alphabet contracts, they are named with letters of the Glagolitic alphabet. 13 | Names are set at contract deploy. Alphabet nodes of the Inner Ring communicate with 14 | one of the alphabetical contracts to emit GAS. To vote for a new list of side 15 | chain committee, alphabet nodes of the Inner Ring create multisignature transactions 16 | for each alphabet contract. 17 | 18 | # Contract notifications 19 | 20 | Alphabet contract does not produce notifications to process. 21 | */ 22 | package alphabet 23 | 24 | /* 25 | Contract storage model. 26 | 27 | # Summary 28 | Key-value storage format: 29 | - 'netmapScriptHash' -> interop.Hash160 30 | Netmap contract reference 31 | - 'proxyScriptHash' -> interop.Hash160 32 | Proxy contract reference 33 | - 'name' -> string 34 | name (Glagolitic letter) of the contract 35 | - 'index' -> int 36 | member index in the Alphabet list 37 | - 'threshold' -> int 38 | currently unused value 39 | 40 | # Setting 41 | To handle some events, the contract refers to other contracts. 42 | 43 | # Membership 44 | Contracts are named and positioned in the Alphabet list of the NeoFS Sidechain. 45 | */ 46 | -------------------------------------------------------------------------------- /contracts/alphabet/migration_test.go: -------------------------------------------------------------------------------- 1 | package alphabet_test 2 | 3 | import ( 4 | "math/rand" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/epicchainlabs/epicchain-contract/tests/dump" 9 | "github.com/epicchainlabs/epicchain-contract/tests/migration" 10 | "github.com/epicchainlabs/epicchain-go/pkg/interop" 11 | "github.com/epicchainlabs/epicchain-go/pkg/util" 12 | "github.com/epicchainlabs/epicchain-go/pkg/vm/stackitem" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | const name = "alphabet" 17 | 18 | func TestMigration(t *testing.T) { 19 | err := dump.IterateDumps("../testdata", func(id dump.ID, r *dump.Reader) { 20 | t.Run(id.String()+"/"+name, func(t *testing.T) { 21 | testMigrationFromDump(t, r) 22 | }) 23 | }) 24 | require.NoError(t, err) 25 | } 26 | 27 | func replaceArgI(vs []any, i int, v any) []any { 28 | res := make([]any, len(vs)) 29 | copy(res, vs) 30 | res[i] = v 31 | return res 32 | } 33 | 34 | func randUint160() (u util.Uint160) { 35 | rand.Read(u[:]) //nolint:staticcheck // SA1019: rand.Read has been deprecated since Go 1.20 36 | return 37 | } 38 | 39 | var notaryDisabledKey = []byte("notary") 40 | 41 | func testMigrationFromDump(t *testing.T, d *dump.Reader) { 42 | // init test contract shell 43 | c := migration.NewContract(t, d, "alphabet0", migration.ContractOptions{ 44 | SourceCodeDir: filepath.Join("..", name), 45 | }) 46 | 47 | migration.SkipUnsupportedVersions(t, c) 48 | 49 | // gather values which can't be fetched via contract API 50 | v := c.GetStorageItem(notaryDisabledKey) 51 | notaryDisabled := len(v) == 1 && v[0] == 1 52 | 53 | readPendingVotes := func() bool { 54 | if v := c.GetStorageItem([]byte("ballots")); v != nil { 55 | item, err := stackitem.Deserialize(v) 56 | require.NoError(t, err) 57 | arr, ok := item.Value().([]stackitem.Item) 58 | if ok { 59 | return len(arr) > 0 60 | } else { 61 | require.Equal(t, stackitem.Null{}, item) 62 | } 63 | } 64 | return false 65 | } 66 | 67 | prevPendingVote := readPendingVotes() 68 | 69 | // read previous values using contract API 70 | readName := func() string { 71 | b, err := c.Call(t, "name").TryBytes() 72 | require.NoError(t, err) 73 | return string(b) 74 | } 75 | 76 | prevName := readName() 77 | 78 | // try to update the contract 79 | proxyContract := randUint160() 80 | updPrm := []any{ 81 | false, // non-notary mode 82 | randUint160(), // unused 83 | []byte{}, // Proxy contract (custom) 84 | "", // unused 85 | 0, // unused 86 | 0, // unused 87 | } 88 | 89 | if notaryDisabled { 90 | c.CheckUpdateFail(t, "address of the Proxy contract is missing or invalid", 91 | replaceArgI(updPrm, 2, make([]byte, interop.Hash160Len+1))...) 92 | c.CheckUpdateFail(t, "token not found", updPrm...) 93 | 94 | c.RegisterContractInNNS(t, "proxy", proxyContract) 95 | 96 | if prevPendingVote { 97 | c.CheckUpdateFail(t, "pending vote detected", updPrm...) 98 | return 99 | } 100 | } 101 | 102 | c.CheckUpdateSuccess(t, updPrm...) 103 | 104 | // check that contract was updates as expected 105 | newName := readName() 106 | newPendingVote := readPendingVotes() 107 | 108 | require.Nil(t, c.GetStorageItem(notaryDisabledKey), "notary flag should be removed") 109 | require.Nil(t, c.GetStorageItem([]byte("innerring")), "Inner Ring nodes should be removed") 110 | require.Equal(t, prevName, newName, "name should remain") 111 | require.False(t, newPendingVote, "there should be no more pending votes") 112 | 113 | if notaryDisabled { 114 | require.Equal(t, proxyContract[:], c.GetStorageItem([]byte("proxyScriptHash")), "name should remain") 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /contracts/audit/config.yml: -------------------------------------------------------------------------------- 1 | name: "NeoFS Audit" 2 | safemethods: ["get", "list", "listByEpoch", "listByCID", "listByNode", "version"] 3 | permissions: 4 | - methods: ["update"] 5 | -------------------------------------------------------------------------------- /contracts/audit/contract.go: -------------------------------------------------------------------------------- 1 | package audit 2 | 3 | import ( 4 | "github.com/epicchainlabs/epicchain-go/pkg/interop" 5 | "github.com/epicchainlabs/epicchain-go/pkg/interop/contract" 6 | "github.com/epicchainlabs/epicchain-go/pkg/interop/iterator" 7 | "github.com/epicchainlabs/epicchain-go/pkg/interop/native/crypto" 8 | "github.com/epicchainlabs/epicchain-go/pkg/interop/native/management" 9 | "github.com/epicchainlabs/epicchain-go/pkg/interop/runtime" 10 | "github.com/epicchainlabs/epicchain-go/pkg/interop/storage" 11 | "github.com/epicchainlabs/epicchain-contract/common" 12 | ) 13 | 14 | type ( 15 | AuditHeader struct { 16 | Epoch int 17 | CID []byte 18 | From interop.PublicKey 19 | } 20 | ) 21 | 22 | // Audit key is a combination of the epoch, the container ID and the public key of the node that 23 | // has executed the audit. Together, it shouldn't be more than 64 bytes. We can't shrink 24 | // epoch and container ID since we iterate over these values. But we can shrink 25 | // public key by using first bytes of the hashed value. 26 | 27 | // V2 format 28 | const maxKeySize = 24 // 24 + 32 (container ID length) + 8 (epoch length) = 64 29 | 30 | func (a AuditHeader) ID() []byte { 31 | var buf any = a.Epoch 32 | 33 | hashedKey := crypto.Sha256(a.From) 34 | shortedKey := hashedKey[:maxKeySize] 35 | 36 | return append(buf.([]byte), append(a.CID, shortedKey...)...) 37 | } 38 | 39 | // nolint:deadcode,unused 40 | func _deploy(data any, isUpdate bool) { 41 | ctx := storage.GetContext() 42 | if isUpdate { 43 | args := data.([]any) 44 | version := args[len(args)-1].(int) 45 | 46 | common.CheckVersion(version) 47 | 48 | // switch to notary mode if version of the current contract deployment is 49 | // earlier than v0.17.0 (initial version when non-notary mode was taken out of 50 | // use) 51 | // TODO: avoid number magic, add function for version comparison to common package 52 | if version < 17_000 { 53 | switchToNotary(ctx) 54 | } 55 | 56 | return 57 | } 58 | 59 | runtime.Log("audit contract initialized") 60 | } 61 | 62 | // re-initializes contract from non-notary to notary mode. Does nothing if 63 | // action has already been done. The function is called on contract update with 64 | // storage.Context from _deploy. 65 | // 66 | // switchToNotary removes values stored by 'netmapScriptHash' and 'notary' keys. 67 | // 68 | // nolint:unused 69 | func switchToNotary(ctx storage.Context) { 70 | const notaryDisabledKey = "notary" // non-notary legacy 71 | 72 | notaryVal := storage.Get(ctx, notaryDisabledKey) 73 | if notaryVal == nil { 74 | runtime.Log("contract is already notarized") 75 | return 76 | } 77 | 78 | storage.Delete(ctx, notaryDisabledKey) 79 | storage.Delete(ctx, "netmapScriptHash") 80 | 81 | if notaryVal.(bool) { 82 | runtime.Log("contract successfully notarized") 83 | } 84 | } 85 | 86 | // Update method updates contract source code and manifest. It can be invoked 87 | // only by committee. 88 | func Update(script []byte, manifest []byte, data any) { 89 | if !common.HasUpdateAccess() { 90 | panic("only committee can update contract") 91 | } 92 | 93 | contract.Call(interop.Hash160(management.Hash), "update", 94 | contract.All, script, manifest, common.AppendVersion(data)) 95 | runtime.Log("audit contract updated") 96 | } 97 | 98 | // Put method stores a stable marshalled `DataAuditResult` structure. It can be 99 | // invoked only by Inner Ring nodes. 100 | // 101 | // Inner Ring nodes perform audit of containers and produce `DataAuditResult` 102 | // structures. They are stored in audit contract and used for settlements 103 | // in later epochs. 104 | func Put(rawAuditResult []byte) { 105 | ctx := storage.GetContext() 106 | innerRing := common.InnerRingNodes() 107 | hdr := newAuditHeader(rawAuditResult) 108 | presented := false 109 | 110 | for i := range innerRing { 111 | ir := innerRing[i] 112 | if common.BytesEqual(ir, hdr.From) { 113 | presented = true 114 | 115 | break 116 | } 117 | } 118 | 119 | if !runtime.CheckWitness(hdr.From) || !presented { 120 | panic("put access denied") 121 | } 122 | 123 | storage.Put(ctx, hdr.ID(), rawAuditResult) 124 | 125 | runtime.Log("audit: result has been saved") 126 | } 127 | 128 | // Get method returns a stable marshaled DataAuditResult structure. 129 | // 130 | // The ID of the DataAuditResult can be obtained from listing methods. 131 | func Get(id []byte) []byte { 132 | ctx := storage.GetReadOnlyContext() 133 | return storage.Get(ctx, id).([]byte) 134 | } 135 | 136 | // List method returns a list of all available DataAuditResult IDs from 137 | // the contract storage. 138 | func List() [][]byte { 139 | ctx := storage.GetReadOnlyContext() 140 | it := storage.Find(ctx, []byte{}, storage.KeysOnly) 141 | 142 | return list(it) 143 | } 144 | 145 | // ListByEpoch method returns a list of DataAuditResult IDs generated during 146 | // the specified epoch. 147 | func ListByEpoch(epoch int) [][]byte { 148 | ctx := storage.GetReadOnlyContext() 149 | var buf any = epoch 150 | it := storage.Find(ctx, buf.([]byte), storage.KeysOnly) 151 | 152 | return list(it) 153 | } 154 | 155 | // ListByCID method returns a list of DataAuditResult IDs generated during 156 | // the specified epoch for the specified container. 157 | func ListByCID(epoch int, cid []byte) [][]byte { 158 | ctx := storage.GetReadOnlyContext() 159 | 160 | var buf any = epoch 161 | 162 | prefix := append(buf.([]byte), cid...) 163 | it := storage.Find(ctx, prefix, storage.KeysOnly) 164 | 165 | return list(it) 166 | } 167 | 168 | // ListByNode method returns a list of DataAuditResult IDs generated in 169 | // the specified epoch for the specified container by the specified Inner Ring node. 170 | func ListByNode(epoch int, cid []byte, key interop.PublicKey) [][]byte { 171 | ctx := storage.GetReadOnlyContext() 172 | hdr := AuditHeader{ 173 | Epoch: epoch, 174 | CID: cid, 175 | From: key, 176 | } 177 | 178 | it := storage.Find(ctx, hdr.ID(), storage.KeysOnly) 179 | 180 | return list(it) 181 | } 182 | 183 | func list(it iterator.Iterator) [][]byte { 184 | var result [][]byte 185 | 186 | for iterator.Next(it) { 187 | key := iterator.Value(it).([]byte) // iterator MUST BE `storage.KeysOnly` 188 | result = append(result, key) 189 | } 190 | 191 | return result 192 | } 193 | 194 | // Version returns the version of the contract. 195 | func Version() int { 196 | return common.Version 197 | } 198 | 199 | // readNext reads the length from the first byte, and then reads data (max 127 bytes). 200 | func readNext(input []byte) ([]byte, int) { 201 | var buf any = input[0] 202 | ln := buf.(int) 203 | 204 | return input[1 : 1+ln], 1 + ln 205 | } 206 | 207 | func newAuditHeader(input []byte) AuditHeader { 208 | // V2 format 209 | offset := int(input[1]) 210 | offset = 2 + offset + 1 // version prefix + version len + epoch prefix 211 | 212 | var buf any = input[offset : offset+8] // [ 8 integer bytes ] 213 | epoch := buf.(int) 214 | 215 | offset = offset + 8 216 | 217 | // cid is a nested structure with raw bytes 218 | // [ cid struct prefix (wireType + len = 2 bytes), cid value wireType (1 byte), ... ] 219 | cid, cidOffset := readNext(input[offset+2+1:]) 220 | 221 | // key is a raw byte 222 | // [ public key wireType (1 byte), ... ] 223 | key, _ := readNext(input[offset+2+1+cidOffset+1:]) 224 | 225 | return AuditHeader{ 226 | epoch, 227 | cid, 228 | key, 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /contracts/audit/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package audit contains implementation of Audit contract deployed in NeoFS 3 | sidechain. 4 | 5 | Inner Ring nodes perform audit of the registered containers during every epoch. 6 | If a container contains StorageGroup objects, an Inner Ring node initializes 7 | a series of audit checks. Based on the results of these checks, the Inner Ring 8 | node creates a DataAuditResult structure for the container. The content of this 9 | structure makes it possible to determine which storage nodes have been examined and 10 | see the status of these checks. Regarding this information, the container owner is 11 | charged for data storage. 12 | 13 | Audit contract is used as a reliable and verifiable storage for all 14 | DataAuditResult structures. At the end of data audit routine, Inner Ring 15 | nodes send a stable marshaled version of the DataAuditResult structure to the 16 | contract. When Alphabet nodes of the Inner Ring perform settlement operations, 17 | they make a list and get these AuditResultStructures from the audit contract. 18 | 19 | # Contract notifications 20 | 21 | Audit contract does not produce notifications to process. 22 | */ 23 | package audit 24 | 25 | /* 26 | Contract storage model. 27 | 28 | # Summary 29 | Key-value storage format: 30 | - -> []byte 31 | Data audit results encoded into NeoFS API binary protocol format. Results are 32 | identified by triplet concatenation: 33 | 1. little-endian unsigned integer NeoFS epoch when audit was performed 34 | 2. 32-byte identifier of the NeoFS container under audit 35 | 3. 24-byte prefix of SHA-256 hash of the auditor's (Inner Ring) public key 36 | 37 | # Audit history 38 | Contracts stores results of the NeoFS data audits performed by the Inner Ring. 39 | */ 40 | -------------------------------------------------------------------------------- /contracts/audit/migration_test.go: -------------------------------------------------------------------------------- 1 | package audit_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/epicchainlabs/epicchain-go/pkg/vm/stackitem" 7 | "github.com/epicchainlabs/epicchain-contract/tests/dump" 8 | "github.com/epicchainlabs/epicchain-contract/tests/migration" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | const name = "audit" 13 | 14 | func TestMigration(t *testing.T) { 15 | err := dump.IterateDumps("../testdata", func(id dump.ID, r *dump.Reader) { 16 | t.Run(id.String()+"/"+name, func(t *testing.T) { 17 | testMigrationFromDump(t, r) 18 | }) 19 | }) 20 | require.NoError(t, err) 21 | } 22 | 23 | func testMigrationFromDump(t *testing.T, d *dump.Reader) { 24 | // init test contract shell 25 | c := migration.NewContract(t, d, name, migration.ContractOptions{}) 26 | 27 | migration.SkipUnsupportedVersions(t, c) 28 | 29 | // read previous values using contract API 30 | readAllAuditResults := func() []stackitem.Item { 31 | r := c.Call(t, "list") 32 | items, ok := r.Value().([]stackitem.Item) 33 | if !ok { 34 | require.Equal(t, stackitem.Null{}, r) 35 | } 36 | 37 | var results []stackitem.Item 38 | 39 | for i := range items { 40 | bID, err := items[i].TryBytes() 41 | require.NoError(t, err) 42 | 43 | results = append(results, c.Call(t, "get", bID)) 44 | } 45 | 46 | return results 47 | } 48 | 49 | prevAuditResults := readAllAuditResults() 50 | 51 | // try to update the contract 52 | c.CheckUpdateSuccess(t) 53 | 54 | // check that contract was updates as expected 55 | newAuditResults := readAllAuditResults() 56 | 57 | require.Nil(t, c.GetStorageItem([]byte("notary")), "notary flag should be removed") 58 | require.Nil(t, c.GetStorageItem([]byte("netmapScriptHash")), "Netmap contract address should be removed") 59 | require.ElementsMatch(t, prevAuditResults, newAuditResults, "audit results should remain") 60 | } 61 | -------------------------------------------------------------------------------- /contracts/balance/config.yml: -------------------------------------------------------------------------------- 1 | name: "NeoFS Balance" 2 | supportedstandards: ["NEP-17"] 3 | safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "version"] 4 | permissions: 5 | - methods: ["update", "subscribeForNewEpoch"] 6 | events: 7 | - name: Lock 8 | parameters: 9 | - name: txID 10 | type: ByteArray 11 | - name: from 12 | type: Hash160 13 | - name: to 14 | type: Hash160 15 | - name: amount 16 | type: Integer 17 | - name: until 18 | type: Integer 19 | - name: Transfer 20 | parameters: 21 | - name: from 22 | type: Hash160 23 | - name: to 24 | type: Hash160 25 | - name: amount 26 | type: Integer 27 | - name: TransferX 28 | parameters: 29 | - name: from 30 | type: Hash160 31 | - name: to 32 | type: Hash160 33 | - name: amount 34 | type: Integer 35 | - name: details 36 | type: ByteArray 37 | -------------------------------------------------------------------------------- /contracts/balance/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package balance contains implementation of Balance contract deployed in NeoFS 3 | sidechain. 4 | 5 | Balance contract stores all NeoFS account balances. It is a NEP-17 compatible 6 | contract, so it can be tracked and controlled by N3 compatible network 7 | monitors and wallet software. 8 | 9 | This contract is used to store all micro transactions in the sidechain, such as 10 | data audit settlements or container fee payments. It is inefficient to make such 11 | small payment transactions in the mainchain. To process small transfers, balance 12 | contract has higher (12) decimal precision than native GAS contract. 13 | 14 | NeoFS balances are synchronized with mainchain operations. Deposit produces 15 | minting of NEOFS tokens in Balance contract. Withdraw locks some NEOFS tokens 16 | in a special lock account. When NeoFS contract transfers GAS assets back to the 17 | user, the lock account is destroyed with burn operation. 18 | 19 | # Contract notifications 20 | 21 | Transfer notification. This is a NEP-17 standard notification. 22 | 23 | Transfer: 24 | - name: from 25 | type: Hash160 26 | - name: to 27 | type: Hash160 28 | - name: amount 29 | type: Integer 30 | 31 | TransferX notification. This is an enhanced transfer notification with details. 32 | 33 | TransferX: 34 | - name: from 35 | type: Hash160 36 | - name: to 37 | type: Hash160 38 | - name: amount 39 | type: Integer 40 | - name: details 41 | type: ByteArray 42 | 43 | Lock notification. This notification is produced when a lock account is 44 | created. It contains information about the mainchain transaction that has produced 45 | the asset lock, the address of the lock account and the NeoFS epoch number until which the 46 | lock account is valid. Alphabet nodes of the Inner Ring catch notification and initialize 47 | Cheque method invocation of NeoFS contract. 48 | 49 | Lock: 50 | - name: txID 51 | type: ByteArray 52 | - name: from 53 | type: Hash160 54 | - name: to 55 | type: Hash160 56 | - name: amount 57 | type: Integer 58 | - name: until 59 | type: Integer 60 | */ 61 | package balance 62 | 63 | /* 64 | Contract storage model. 65 | 66 | # Summary 67 | Key-value storage format: 68 | - 'MainnetGAS' -> int 69 | total amount of Mainchain GAS deployed in the NeoFS network in Fixed12 70 | - interop.Hash160 -> std.Serialize(Account) 71 | balance sheet of all NeoFS users (here Account is a structure defined in current package) 72 | 73 | # Accounting 74 | Contract stores information about all NeoFS accounts. 75 | */ 76 | -------------------------------------------------------------------------------- /contracts/balance/migration_test.go: -------------------------------------------------------------------------------- 1 | package balance_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/epicchainlabs/epicchain-go/pkg/vm/stackitem" 7 | "github.com/epicchainlabs/epicchain-contract/tests/dump" 8 | "github.com/epicchainlabs/epicchain-contract/tests/migration" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | const name = "balance" 13 | 14 | func TestMigration(t *testing.T) { 15 | err := dump.IterateDumps("../testdata", func(id dump.ID, r *dump.Reader) { 16 | t.Run(id.String()+"/"+name, func(t *testing.T) { 17 | testMigrationFromDump(t, r) 18 | }) 19 | }) 20 | require.NoError(t, err) 21 | } 22 | 23 | var notaryDisabledKey = []byte("notary") 24 | 25 | func testMigrationFromDump(t *testing.T, d *dump.Reader) { 26 | // init test contract shell 27 | c := migration.NewContract(t, d, name, migration.ContractOptions{}) 28 | 29 | migration.SkipUnsupportedVersions(t, c) 30 | 31 | // gather values which can't be fetched via contract API 32 | v := c.GetStorageItem(notaryDisabledKey) 33 | notaryDisabled := len(v) == 1 && v[0] == 1 34 | 35 | readPendingVotes := func() bool { 36 | if v := c.GetStorageItem([]byte("ballots")); v != nil { 37 | item, err := stackitem.Deserialize(v) 38 | require.NoError(t, err) 39 | arr, ok := item.Value().([]stackitem.Item) 40 | if ok { 41 | return len(arr) > 0 42 | } else { 43 | require.Equal(t, stackitem.Null{}, item) 44 | } 45 | } 46 | return false 47 | } 48 | 49 | prevPendingVotes := readPendingVotes() 50 | 51 | // read previous values using contract API 52 | readTotalSupply := func() int64 { 53 | n, err := c.Call(t, "totalSupply").TryInteger() 54 | require.NoError(t, err) 55 | return n.Int64() 56 | } 57 | 58 | prevTotalSupply := readTotalSupply() 59 | 60 | // try to update the contract 61 | if notaryDisabled && prevPendingVotes { 62 | c.CheckUpdateFail(t, "pending vote detected") 63 | return 64 | } 65 | 66 | c.CheckUpdateSuccess(t) 67 | 68 | // check that contract was updates as expected 69 | newTotalSupply := readTotalSupply() 70 | newPendingVotes := readPendingVotes() 71 | 72 | require.False(t, newPendingVotes, "there should be no more pending votes") 73 | require.Nil(t, c.GetStorageItem(notaryDisabledKey), "notary flag should be removed") 74 | require.Nil(t, c.GetStorageItem([]byte("containerScriptHash")), "Container contract address should be removed") 75 | require.Nil(t, c.GetStorageItem([]byte("netmapScriptHash")), "Netmap contract address should be removed") 76 | 77 | require.Equal(t, prevTotalSupply, newTotalSupply) 78 | } 79 | -------------------------------------------------------------------------------- /contracts/container/config.yml: -------------------------------------------------------------------------------- 1 | name: "NeoFS Container" 2 | safemethods: ["alias", "count", "containersOf", "get", "owner", "list", "eACL", "getContainerSize", "listContainerSizes", "iterateContainerSizes", "iterateAllContainerSizes", "version"] 3 | permissions: 4 | - methods: ["update", "addKey", "transferX", 5 | "register", "registerTLD", "addRecord", "deleteRecords", "subscribeForNewEpoch"] 6 | events: 7 | - name: PutSuccess 8 | parameters: 9 | - name: containerID 10 | type: Hash256 11 | - name: publicKey 12 | type: PublicKey 13 | - name: DeleteSuccess 14 | parameters: 15 | - name: containerID 16 | type: ByteArray 17 | - name: SetEACLSuccess 18 | parameters: 19 | - name: containerID 20 | type: ByteArray 21 | - name: publicKey 22 | type: PublicKey 23 | - name: StartEstimation 24 | parameters: 25 | - name: epoch 26 | type: Integer 27 | - name: StopEstimation 28 | parameters: 29 | - name: epoch 30 | type: Integer 31 | -------------------------------------------------------------------------------- /contracts/container/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package container contains implementation of Container contract deployed in NeoFS 3 | sidechain. 4 | 5 | Container contract stores and manages containers, extended ACLs and container 6 | size estimations. Contract does not perform sanity or signature checks of 7 | containers or extended ACLs, it is done by Alphabet nodes of the Inner Ring. 8 | Alphabet nodes approve it by invoking the same Put or SetEACL methods with 9 | the same arguments. 10 | 11 | # Contract notifications 12 | 13 | containerPut notification. This notification is produced when a user wants to 14 | create a new container. Alphabet nodes of the Inner Ring catch the notification and 15 | validate container data, signature and token if present. 16 | 17 | containerPut: 18 | - name: container 19 | type: ByteArray 20 | - name: signature 21 | type: Signature 22 | - name: publicKey 23 | type: PublicKey 24 | - name: token 25 | type: ByteArray 26 | 27 | containerDelete notification. This notification is produced when a container owner 28 | wants to delete a container. Alphabet nodes of the Inner Ring catch the notification 29 | and validate container ownership, signature and token if present. 30 | 31 | containerDelete: 32 | - name: containerID 33 | type: ByteArray 34 | - name: signature 35 | type: Signature 36 | - name: token 37 | type: ByteArray 38 | 39 | setEACL notification. This notification is produced when a container owner wants 40 | to update an extended ACL of a container. Alphabet nodes of the Inner Ring catch 41 | the notification and validate container ownership, signature and token if 42 | present. 43 | 44 | setEACL: 45 | - name: eACL 46 | type: ByteArray 47 | - name: signature 48 | type: Signature 49 | - name: publicKey 50 | type: PublicKey 51 | - name: token 52 | type: ByteArray 53 | 54 | StartEstimation notification. This notification is produced when Storage nodes 55 | should exchange estimation values of container sizes among other Storage nodes. 56 | 57 | StartEstimation: 58 | - name: epoch 59 | type: Integer 60 | 61 | StopEstimation notification. This notification is produced when Storage nodes 62 | should calculate average container size based on received estimations and store 63 | it in Container contract. 64 | 65 | StopEstimation: 66 | - name: epoch 67 | type: Integer 68 | */ 69 | package container 70 | 71 | /* 72 | Contract storage model. 73 | 74 | # Summary 75 | Current conventions: 76 | : 32-byte container identifier (SHA-256 hashes of container data) 77 | : 25-byte NEO3 account of owner of the particular container 78 | : little-endian unsigned integer NeoFS epoch 79 | 80 | Key-value storage format: 81 | - 'netmapScriptHash' -> interop.Hash160 82 | Netmap contract reference 83 | - 'balanceScriptHash' -> interop.Hash160 84 | Balance contract reference 85 | - 'identityScriptHash' -> interop.Hash160 86 | NeoFSID contract reference 87 | - 'nnsScriptHash' -> interop.Hash160 88 | NNS contract reference 89 | - 'nnsRoot' -> interop.Hash160 90 | NNS root domain zone for containers 91 | - 'x' -> []byte 92 | container descriptors encoded into NeoFS API binary protocol format 93 | - 'o' -> 94 | user-by-user containers 95 | - 'nnsHasAlias' -> string 96 | domains registered for containers in the NNS 97 | - 'cnr' + [10]byte -> std.Serialize(estimation) 98 | estimation of the container size sent by the storage node. Key suffix is first 99 | 10 bytes of RIPEMD-160 hash of the storage node's public key 100 | (interop.PublicKey). Here estimation is a type. 101 | - 'est' + [20]byte -> [] 102 | list of NeoFS epochs when particular storage node sent estimations. Suffix is 103 | RIPEMD-160 hash of the storage node's public key (interop.PublicKey). 104 | 105 | # Setting 106 | To handle some events, the contract refers to other contracts. 107 | 108 | # Containers 109 | Contract stores information about all containers (incl. extended ACL tables) 110 | presented in the NeoFS network for which the contract is deployed. For 111 | performance optimization, container are additionally indexed by their owners. 112 | 113 | # NNS 114 | Contract tracks container-related domains registered in the NNS. By default 115 | "container" TLD is used (unless overridden on deploy). 116 | 117 | # Size estimations 118 | Contract stores containers' size estimations came from NeoFS storage nodes. 119 | */ 120 | -------------------------------------------------------------------------------- /contracts/container/migration_test.go: -------------------------------------------------------------------------------- 1 | package container_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/epicchainlabs/epicchain-contract/tests/dump" 8 | "github.com/epicchainlabs/epicchain-contract/tests/migration" 9 | "github.com/epicchainlabs/epicchain-go/pkg/vm/stackitem" 10 | "github.com/mr-tron/base58" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | const name = "container" 15 | 16 | func TestMigration(t *testing.T) { 17 | err := dump.IterateDumps("../testdata", func(id dump.ID, r *dump.Reader) { 18 | t.Run(id.String()+"/"+name, func(t *testing.T) { 19 | testMigrationFromDump(t, r) 20 | }) 21 | }) 22 | require.NoError(t, err) 23 | } 24 | 25 | var notaryDisabledKey = []byte("notary") 26 | 27 | func testMigrationFromDump(t *testing.T, d *dump.Reader) { 28 | // gather values which can't be fetched via contract API 29 | var owners [][]byte 30 | 31 | c := migration.NewContract(t, d, "container", migration.ContractOptions{ 32 | StorageDumpHandler: func(key, value []byte) { 33 | const ownerLen = 25 34 | if len(key) == ownerLen+32 { // + cid 35 | for i := range owners { 36 | if bytes.Equal(owners[i], key[:ownerLen]) { 37 | return 38 | } 39 | } 40 | owners = append(owners, key[:ownerLen]) 41 | } 42 | }, 43 | }) 44 | 45 | migration.SkipUnsupportedVersions(t, c) 46 | 47 | v := c.GetStorageItem(notaryDisabledKey) 48 | notaryDisabled := len(v) == 1 && v[0] == 1 49 | 50 | readPendingVotes := func() bool { 51 | if v := c.GetStorageItem([]byte("ballots")); v != nil { 52 | item, err := stackitem.Deserialize(v) 53 | require.NoError(t, err) 54 | arr, ok := item.Value().([]stackitem.Item) 55 | if ok { 56 | return len(arr) > 0 57 | } else { 58 | require.Equal(t, stackitem.Null{}, item) 59 | } 60 | } 61 | return false 62 | } 63 | 64 | prevPendingVote := readPendingVotes() 65 | 66 | // read previous values using contract API 67 | readAllContainers := func() []stackitem.Item { 68 | containers, ok := c.Call(t, "list", []byte{}).Value().([]stackitem.Item) 69 | require.True(t, ok) 70 | return containers 71 | } 72 | 73 | readContainerCount := func() uint64 { 74 | nContainers, err := c.Call(t, "count").TryInteger() 75 | require.NoError(t, err) 76 | return nContainers.Uint64() 77 | } 78 | 79 | readOwnersToContainers := func() map[string][]stackitem.Item { 80 | m := make(map[string][]stackitem.Item, len(owners)) 81 | for i := range owners { 82 | m[string(owners[i])] = c.Call(t, "list", owners[i]).Value().([]stackitem.Item) 83 | } 84 | return m 85 | } 86 | 87 | prevContainers := readAllContainers() 88 | prevContainerCount := readContainerCount() 89 | prevOwnersToContainers := readOwnersToContainers() 90 | 91 | // try to update the contract 92 | if notaryDisabled && prevPendingVote { 93 | c.CheckUpdateFail(t, "pending vote detected") 94 | return 95 | } 96 | 97 | c.CheckUpdateSuccess(t) 98 | 99 | // check that contract was updates as expected 100 | newPendingVote := readPendingVotes() 101 | newContainers := readAllContainers() 102 | newContainerCount := readContainerCount() 103 | newOwnersToContainers := readOwnersToContainers() 104 | 105 | require.Nil(t, c.GetStorageItem(notaryDisabledKey), "notary flag should be removed") 106 | require.Equal(t, prevContainerCount, newContainerCount, "number of containers should remain") 107 | require.ElementsMatch(t, prevContainers, newContainers, "container list should remain") 108 | require.False(t, newPendingVote, "there should be no more pending votes") 109 | 110 | require.Equal(t, len(prevOwnersToContainers), len(newOwnersToContainers)) 111 | for k, vPrev := range prevOwnersToContainers { 112 | vNew, ok := newOwnersToContainers[k] 113 | require.True(t, ok) 114 | require.ElementsMatch(t, vPrev, vNew, "containers of '%s' owner should remain", base58.Encode([]byte(k))) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /contracts/neofs/config.yml: -------------------------------------------------------------------------------- 1 | name: "NeoFS" 2 | safemethods: ["alphabetList", "alphabetAddress", "innerRingCandidates", "config", "listConfig", "version"] 3 | permissions: 4 | - methods: ["update", "transfer"] 5 | events: 6 | - name: Deposit 7 | parameters: 8 | - name: from 9 | type: Hash160 10 | - name: amount 11 | type: Integer 12 | - name: receiver 13 | type: Hash160 14 | - name: txHash 15 | type: Hash256 16 | - name: Withdraw 17 | parameters: 18 | - name: user 19 | type: Hash160 20 | - name: amount 21 | type: Integer 22 | - name: txHash 23 | type: Hash256 24 | - name: Cheque 25 | parameters: 26 | - name: id 27 | type: ByteArray 28 | - name: user 29 | type: Hash160 30 | - name: amount 31 | type: Integer 32 | - name: lockAccount 33 | type: ByteArray 34 | - name: Bind 35 | parameters: 36 | - name: user 37 | type: ByteArray 38 | - name: keys 39 | type: Array 40 | extendedtype: 41 | base: Array 42 | value: 43 | base: PublicKey 44 | - name: Unbind 45 | parameters: 46 | - name: user 47 | type: ByteArray 48 | - name: keys 49 | type: Array 50 | extendedtype: 51 | base: Array 52 | value: 53 | base: PublicKey 54 | - name: AlphabetUpdate 55 | parameters: 56 | - name: id 57 | type: ByteArray 58 | - name: alphabet 59 | type: Array 60 | extendedtype: 61 | base: Array 62 | value: 63 | base: PublicKey 64 | - name: SetConfig 65 | parameters: 66 | - name: id 67 | type: ByteArray 68 | - name: key 69 | type: ByteArray 70 | - name: value 71 | type: ByteArray 72 | -------------------------------------------------------------------------------- /contracts/neofs/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package neofs contains implementation of NeoFS contract deployed in NeoFS mainchain. 3 | 4 | NeoFS contract is an entry point to NeoFS users. This contract stores all NeoFS 5 | related GAS, registers new Inner Ring candidates and produces notifications 6 | to control the sidechain. 7 | 8 | While mainchain committee controls the list of Alphabet nodes in native 9 | RoleManagement contract, NeoFS can't change more than 1\3 keys at a time. 10 | NeoFS contract contains the actual list of Alphabet nodes in the sidechain. 11 | 12 | Network configuration is also stored in NeoFS contract. All changes in 13 | configuration are mirrored in the sidechain with notifications. 14 | 15 | # Contract notifications 16 | 17 | Deposit notification. This notification is produced when user transfers native 18 | GAS to the NeoFS contract address. The same amount of NEOFS token will be 19 | minted in Balance contract in the sidechain. 20 | 21 | Deposit: 22 | - name: from 23 | type: Hash160 24 | - name: amount 25 | type: Integer 26 | - name: receiver 27 | type: Hash160 28 | - name: txHash 29 | type: Hash256 30 | 31 | Withdraw notification. This notification is produced when a user wants to 32 | withdraw GAS from the internal NeoFS balance and has paid fee for that. 33 | 34 | Withdraw: 35 | - name: user 36 | type: Hash160 37 | - name: amount 38 | type: Integer 39 | - name: txHash 40 | type: Hash256 41 | 42 | Cheque notification. This notification is produced when NeoFS contract 43 | has successfully transferred assets back to the user after withdraw. 44 | 45 | Cheque: 46 | - name: id 47 | type: ByteArray 48 | - name: user 49 | type: Hash160 50 | - name: amount 51 | type: Integer 52 | - name: lockAccount 53 | type: ByteArray 54 | 55 | Bind notification. This notification is produced when a user wants to bind 56 | public keys with the user account (OwnerID). Keys argument is an array of ByteArray. 57 | 58 | Bind: 59 | - name: user 60 | type: ByteArray 61 | - name: keys 62 | type: Array 63 | 64 | Unbind notification. This notification is produced when a user wants to unbind 65 | public keys with the user account (OwnerID). Keys argument is an array of ByteArray. 66 | 67 | Unbind: 68 | - name: user 69 | type: ByteArray 70 | - name: keys 71 | type: Array 72 | 73 | AlphabetUpdate notification. This notification is produced when Alphabet nodes 74 | have updated their lists in the contract. Alphabet argument is an array of ByteArray. It 75 | contains public keys of new alphabet nodes. 76 | 77 | AlphabetUpdate: 78 | - name: id 79 | type: ByteArray 80 | - name: alphabet 81 | type: Array 82 | 83 | SetConfig notification. This notification is produced when Alphabet nodes update 84 | NeoFS network configuration value. 85 | 86 | SetConfig 87 | - name: id 88 | type: ByteArray 89 | - name: key 90 | type: ByteArray 91 | - name: value 92 | type: ByteArray 93 | */ 94 | package neofs 95 | 96 | /* 97 | Contract storage model. 98 | 99 | # Summary 100 | Key-value storage format: 101 | - 'notary' -> bool 102 | is notary mode disabled 103 | - 'ballots' -> std.Serialize([]Ballot) 104 | collected ballots for pending voting if notary disabled (here Ballot is a 105 | structure defined in common package) 106 | - 'processingScriptHash' -> interop.Hash160 107 | Processing contract reference 108 | - 'candidates' + interop.PublicKey -> 1 109 | each participant who is considered for entry into the Inner Ring 110 | - 'alphabet' -> []interop.PublicKey 111 | list of the NeoFS Alphabet members 112 | 113 | # Setting 114 | Contract can be deployed in notary and notary-disabled mode. 115 | 116 | To handle some events, the contract refers to other contracts. 117 | 118 | # Network configuration 119 | Contract storage configuration of the NeoFS network within which the contract is 120 | deployed. 121 | 122 | # Inner Ring Contract accumulates candidates for the Inner Ring. It also holds 123 | current NeoFS Alphabet. 124 | 125 | # Voting 126 | Contract collects voting data in notary-disabled installation. 127 | */ 128 | -------------------------------------------------------------------------------- /contracts/neofsid/config.yml: -------------------------------------------------------------------------------- 1 | name: "NeoFS ID" 2 | safemethods: ["key", "version"] 3 | permissions: 4 | - methods: ["update"] 5 | -------------------------------------------------------------------------------- /contracts/neofsid/contract.go: -------------------------------------------------------------------------------- 1 | package neofsid 2 | 3 | import ( 4 | "github.com/epicchainlabs/epicchain-go/pkg/interop" 5 | "github.com/epicchainlabs/epicchain-go/pkg/interop/contract" 6 | "github.com/epicchainlabs/epicchain-go/pkg/interop/iterator" 7 | "github.com/epicchainlabs/epicchain-go/pkg/interop/native/management" 8 | "github.com/epicchainlabs/epicchain-go/pkg/interop/runtime" 9 | "github.com/epicchainlabs/epicchain-go/pkg/interop/storage" 10 | "github.com/epicchainlabs/epicchain-contract/common" 11 | ) 12 | 13 | type ( 14 | UserInfo struct { 15 | Keys [][]byte 16 | } 17 | ) 18 | 19 | const ( 20 | ownerSize = 1 + interop.Hash160Len + 4 21 | ) 22 | 23 | const ( 24 | ownerKeysPrefix = 'o' 25 | ) 26 | 27 | // nolint:deadcode,unused 28 | func _deploy(data any, isUpdate bool) { 29 | ctx := storage.GetContext() 30 | 31 | if isUpdate { 32 | args := data.([]any) 33 | version := args[len(args)-1].(int) 34 | 35 | common.CheckVersion(version) 36 | 37 | // switch to notary mode if version of the current contract deployment is 38 | // earlier than v0.17.0 (initial version when non-notary mode was taken out of 39 | // use) 40 | // TODO: avoid number magic, add function for version comparison to common package 41 | if version < 17_000 { 42 | switchToNotary(ctx) 43 | } 44 | 45 | // netmap is not used for quite some time and deleted in 0.19.0. 46 | if version < 19_000 { 47 | storage.Delete(ctx, "netmapScriptHash") 48 | } 49 | return 50 | } 51 | 52 | runtime.Log("neofsid contract initialized") 53 | } 54 | 55 | // re-initializes contract from non-notary to notary mode. Does nothing if 56 | // action has already been done. The function is called on contract update with 57 | // storage.Context from _deploy. 58 | // 59 | // If contract stores non-empty value by 'ballots' key, switchToNotary panics. 60 | // Otherwise, existing value is removed. 61 | // 62 | // switchToNotary removes values stored by 'containerScriptHash' and 'notary' 63 | // keys. 64 | // 65 | // nolint:unused 66 | func switchToNotary(ctx storage.Context) { 67 | const notaryDisabledKey = "notary" // non-notary legacy 68 | 69 | notaryVal := storage.Get(ctx, notaryDisabledKey) 70 | if notaryVal == nil { 71 | runtime.Log("contract is already notarized") 72 | return 73 | } else if notaryVal.(bool) && !common.TryPurgeVotes(ctx) { 74 | panic("pending vote detected") 75 | } 76 | 77 | storage.Delete(ctx, notaryDisabledKey) 78 | storage.Delete(ctx, "containerScriptHash") 79 | 80 | if notaryVal.(bool) { 81 | runtime.Log("contract successfully notarized") 82 | } 83 | } 84 | 85 | // Update method updates contract source code and manifest. It can be invoked 86 | // only by committee. 87 | func Update(script []byte, manifest []byte, data any) { 88 | if !common.HasUpdateAccess() { 89 | panic("only committee can update contract") 90 | } 91 | 92 | contract.Call(interop.Hash160(management.Hash), "update", 93 | contract.All, script, manifest, common.AppendVersion(data)) 94 | runtime.Log("neofsid contract updated") 95 | } 96 | 97 | // AddKey binds a list of the provided public keys to the OwnerID. It can be invoked only by 98 | // Alphabet nodes. 99 | // 100 | // This method panics if the OwnerID is not an ownerSize byte or the public key is not 33 byte long. 101 | // If the key is already bound, the method ignores it. 102 | func AddKey(owner []byte, keys []interop.PublicKey) { 103 | // V2 format 104 | if len(owner) != ownerSize { 105 | panic("incorrect owner") 106 | } 107 | 108 | for i := range keys { 109 | if len(keys[i]) != interop.PublicKeyCompressedLen { 110 | panic("incorrect public key") 111 | } 112 | } 113 | 114 | ctx := storage.GetContext() 115 | 116 | multiaddr := common.AlphabetAddress() 117 | common.CheckAlphabetWitness(multiaddr) 118 | 119 | ownerKey := append([]byte{ownerKeysPrefix}, owner...) 120 | for i := range keys { 121 | stKey := append(ownerKey, keys[i]...) 122 | storage.Put(ctx, stKey, []byte{1}) 123 | } 124 | 125 | runtime.Log("key bound to the owner") 126 | } 127 | 128 | // RemoveKey unbinds the provided public keys from the OwnerID. It can be invoked only by 129 | // Alphabet nodes. 130 | // 131 | // This method panics if the OwnerID is not an ownerSize byte or the public key is not 33 byte long. 132 | // If the key is already unbound, the method ignores it. 133 | func RemoveKey(owner []byte, keys []interop.PublicKey) { 134 | // V2 format 135 | if len(owner) != ownerSize { 136 | panic("incorrect owner") 137 | } 138 | 139 | for i := range keys { 140 | if len(keys[i]) != interop.PublicKeyCompressedLen { 141 | panic("incorrect public key") 142 | } 143 | } 144 | 145 | ctx := storage.GetContext() 146 | 147 | multiaddr := common.AlphabetAddress() 148 | if !runtime.CheckWitness(multiaddr) { 149 | panic("invocation from non inner ring node") 150 | } 151 | 152 | ownerKey := append([]byte{ownerKeysPrefix}, owner...) 153 | for i := range keys { 154 | stKey := append(ownerKey, keys[i]...) 155 | storage.Delete(ctx, stKey) 156 | } 157 | } 158 | 159 | // Key method returns a list of 33-byte public keys bound with the OwnerID. 160 | // 161 | // This method panics if the owner is not ownerSize byte long. 162 | func Key(owner []byte) [][]byte { 163 | // V2 format 164 | if len(owner) != ownerSize { 165 | panic("incorrect owner") 166 | } 167 | 168 | ctx := storage.GetReadOnlyContext() 169 | 170 | ownerKey := append([]byte{ownerKeysPrefix}, owner...) 171 | info := getUserInfo(ctx, ownerKey) 172 | 173 | return info.Keys 174 | } 175 | 176 | // Version returns the version of the contract. 177 | func Version() int { 178 | return common.Version 179 | } 180 | 181 | func getUserInfo(ctx storage.Context, key any) UserInfo { 182 | it := storage.Find(ctx, key, storage.KeysOnly|storage.RemovePrefix) 183 | pubs := [][]byte{} 184 | for iterator.Next(it) { 185 | pub := iterator.Value(it).([]byte) 186 | pubs = append(pubs, pub) 187 | } 188 | 189 | return UserInfo{Keys: pubs} 190 | } 191 | -------------------------------------------------------------------------------- /contracts/neofsid/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package neofsid contains implementation of NeoFSID contract deployed in NeoFS 3 | sidechain. 4 | 5 | NeoFSID contract is used to store connection between an OwnerID and its public keys. 6 | OwnerID is a 25-byte N3 wallet address that can be produced from a public key. 7 | It is one-way conversion. In simple cases, NeoFS verifies ownership by checking 8 | signature and relation between a public key and an OwnerID. 9 | 10 | In more complex cases, a user can use public keys unrelated to the OwnerID to maintain 11 | secure access to the data. NeoFSID contract stores relation between an OwnerID and 12 | arbitrary public keys. Data owner can bind a public key with its account or unbind it 13 | by invoking Bind or Unbind methods of NeoFS contract in the mainchain. After that, 14 | Alphabet nodes produce multisigned AddKey and RemoveKey invocations of NeoFSID 15 | contract. 16 | 17 | # Contract notifications 18 | 19 | NeoFSID contract does not produce notifications to process. 20 | */ 21 | package neofsid 22 | 23 | /* 24 | Contract storage model. 25 | 26 | # Summary 27 | Key-value storage format: 28 | - 'netmapScriptHash' -> interop.Hash160 29 | Netmap contract reference (currently unused) 30 | - 'o' + ID + interop.PublicKey -> 1 31 | each key of the NeoFS user identified by 25-byte NEO3 account 32 | 33 | # Keychains 34 | Contract collects all keys of the NeoFS users except ones that may be directly 35 | resolved into user ID. 36 | */ 37 | -------------------------------------------------------------------------------- /contracts/neofsid/migration_test.go: -------------------------------------------------------------------------------- 1 | package neofsid_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/epicchainlabs/epicchain-contract/tests/dump" 8 | "github.com/epicchainlabs/epicchain-contract/tests/migration" 9 | "github.com/epicchainlabs/epicchain-go/pkg/interop" 10 | "github.com/epicchainlabs/epicchain-go/pkg/vm/stackitem" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | const name = "neofsid" 15 | 16 | func TestMigration(t *testing.T) { 17 | err := dump.IterateDumps("../testdata", func(id dump.ID, r *dump.Reader) { 18 | t.Run(id.String()+"/"+name, func(t *testing.T) { 19 | testMigrationFromDump(t, r) 20 | }) 21 | }) 22 | require.NoError(t, err) 23 | } 24 | 25 | var notaryDisabledKey = []byte("notary") 26 | 27 | func testMigrationFromDump(t *testing.T, d *dump.Reader) { 28 | // gather values which can't be fetched via contract API 29 | var owners [][]byte 30 | 31 | c := migration.NewContract(t, d, "neofsid", migration.ContractOptions{ 32 | StorageDumpHandler: func(key, value []byte) { 33 | const ownerLen = 25 34 | if bytes.HasPrefix(key, []byte{'o'}) && len(key[1:]) == ownerLen+interop.PublicKeyCompressedLen { 35 | owners = append(owners, key[1:1+ownerLen]) 36 | } 37 | }, 38 | }) 39 | 40 | migration.SkipUnsupportedVersions(t, c) 41 | 42 | v := c.GetStorageItem(notaryDisabledKey) 43 | notaryDisabled := len(v) == 1 && v[0] == 1 44 | 45 | readPendingVotes := func() bool { 46 | if v := c.GetStorageItem([]byte("ballots")); v != nil { 47 | item, err := stackitem.Deserialize(v) 48 | require.NoError(t, err) 49 | arr, ok := item.Value().([]stackitem.Item) 50 | if ok { 51 | return len(arr) > 0 52 | } else { 53 | require.Equal(t, stackitem.Null{}, item) 54 | } 55 | } 56 | return false 57 | } 58 | 59 | prevPendingVote := readPendingVotes() 60 | 61 | // read previous values using contract API 62 | readOwnersToKeys := func() map[string][]stackitem.Item { 63 | m := make(map[string][]stackitem.Item, len(owners)) 64 | for i := range owners { 65 | m[string(owners[i])] = c.Call(t, "key", owners[i]).Value().([]stackitem.Item) 66 | } 67 | return m 68 | } 69 | 70 | prevOwnersToKeys := readOwnersToKeys() 71 | 72 | // try to update the contract 73 | if notaryDisabled && prevPendingVote { 74 | c.CheckUpdateFail(t, "pending vote detected") 75 | return 76 | } 77 | 78 | c.CheckUpdateSuccess(t) 79 | 80 | // check that contract was updates as expected 81 | newPendingVotes := readPendingVotes() 82 | newOwnersToKeys := readOwnersToKeys() 83 | 84 | require.Nil(t, c.GetStorageItem(notaryDisabledKey), "notary flag should be removed") 85 | require.Nil(t, c.GetStorageItem([]byte("containerScriptHash")), "Container contract address should be removed") 86 | require.Nil(t, c.GetStorageItem([]byte("netmapScriptHash")), "Netmap contract address should be removed") 87 | require.False(t, newPendingVotes, "there should be no more pending votes") 88 | 89 | require.Equal(t, len(prevOwnersToKeys), len(newOwnersToKeys)) 90 | for k, vPrev := range prevOwnersToKeys { 91 | vNew, ok := newOwnersToKeys[k] 92 | require.True(t, ok) 93 | require.ElementsMatch(t, vPrev, vNew) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /contracts/netmap/config.yml: -------------------------------------------------------------------------------- 1 | name: "NeoFS Netmap" 2 | safemethods: ["innerRingList", "epoch", "netmap", "netmapCandidates", "snapshot", "snapshotByEpoch", "config", "listConfig", "version"] 3 | permissions: 4 | - methods: ["update", "newEpoch"] 5 | events: 6 | - name: AddPeerSuccess 7 | parameters: 8 | - name: publicKey 9 | type: PublicKey 10 | - name: UpdateStateSuccess 11 | parameters: 12 | - name: publicKey 13 | type: PublicKey 14 | - name: state 15 | type: Integer 16 | - name: NewEpoch 17 | parameters: 18 | - name: epoch 19 | type: Integer 20 | - name: NewEpochSubscription 21 | parameters: 22 | - name: contract 23 | type: Hash160 24 | -------------------------------------------------------------------------------- /contracts/netmap/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package netmap contains implementation of the Netmap contract for NeoFS systems. 3 | 4 | Netmap contract stores and manages NeoFS network map, Storage node candidates 5 | and epoch number counter. In notary disabled environment, contract also stores 6 | a list of Inner Ring node keys. 7 | 8 | # Contract notifications 9 | 10 | AddPeer notification. This notification is produced when a Storage node sends 11 | a bootstrap request by invoking AddPeer method. 12 | 13 | AddPeer 14 | - name: nodeInfo 15 | type: ByteArray 16 | 17 | UpdateState notification. This notification is produced when a Storage node wants 18 | to change its state (go offline) by invoking UpdateState method. Supported 19 | states: (2) -- offline. 20 | 21 | UpdateState 22 | - name: state 23 | type: Integer 24 | - name: publicKey 25 | type: PublicKey 26 | 27 | NewEpoch notification. This notification is produced when a new epoch is applied 28 | in the network by invoking NewEpoch method. 29 | 30 | NewEpoch 31 | - name: epoch 32 | type: Integer 33 | */ 34 | package netmap 35 | 36 | /* 37 | Contract storage model. 38 | 39 | # Summary 40 | Key-value storage format: 41 | - 'snapshotEpoch' -> int 42 | current epoch 43 | - 'snapshotBlock' -> int 44 | block which "ticked" the current epoch 45 | - 'snapshotCount' -> int 46 | number of stored network maps including current one 47 | - 'snapshot_' -> std.Serialize([]Node) 48 | network map by snapshot ID (where Node is a type) 49 | - 'snapshotCurrent' -> int 50 | ID of the snapshot representing current network map 51 | - 'candidate' -> std.Serialize(Node) 52 | information about the particular network map candidate (where Node is a type) 53 | - 'containerScriptHash' -> 20-byte script hash 54 | Container contract reference 55 | - 'balanceScriptHash' -> 20-byte script hash 56 | Balance contract reference 57 | - 'config' -> []byte 58 | value of the particular NeoFS network parameter 59 | 60 | # Setting 61 | To handle some events, the contract refers to other contracts. 62 | 63 | # Epoch 64 | Contract stores the current (last) NeoFS timestamp for the network within which 65 | the contract is deployed. 66 | 67 | # Network maps 68 | Contract records set of network parties representing the network map. Current 69 | network map is updated on each epoch tick. Contract also holds limited number of 70 | previous network maps (SNAPSHOT_LIMIT). Timestamped network maps are called 71 | snapshots. Snapshots are identified by the numerical ring [0:SNAPSHOT_LIMIT). 72 | 73 | # Network map candidates 74 | Contract stores information about the network parties which were requested to be 75 | added to the network map. 76 | 77 | # Network configuration 78 | Contract stores NeoFS network configuration declared in the NeoFS API protocol. 79 | */ 80 | -------------------------------------------------------------------------------- /contracts/netmap/migration_test.go: -------------------------------------------------------------------------------- 1 | package netmap_test 2 | 3 | import ( 4 | "bytes" 5 | "math/big" 6 | "testing" 7 | 8 | "github.com/epicchainlabs/epicchain-contract/rpc/netmap" 9 | "github.com/epicchainlabs/epicchain-contract/tests/dump" 10 | "github.com/epicchainlabs/epicchain-contract/tests/migration" 11 | "github.com/epicchainlabs/epicchain-go/pkg/crypto/keys" 12 | "github.com/epicchainlabs/epicchain-go/pkg/io" 13 | "github.com/epicchainlabs/epicchain-go/pkg/util" 14 | "github.com/epicchainlabs/epicchain-go/pkg/vm/stackitem" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | const name = "netmap" 19 | 20 | func TestMigration(t *testing.T) { 21 | err := dump.IterateDumps("../testdata", func(id dump.ID, r *dump.Reader) { 22 | t.Run(id.String()+"/"+name, func(t *testing.T) { 23 | testMigrationFromDump(t, r) 24 | }) 25 | }) 26 | require.NoError(t, err) 27 | } 28 | 29 | var ( 30 | notaryDisabledKey = []byte("notary") 31 | 32 | newEpochSubsNewPrefix = []byte("e") 33 | containerHashOldKey = []byte("containerScriptHash") 34 | balanceHashOldKey = []byte("balanceScriptHash") 35 | ) 36 | 37 | func testMigrationFromDump(t *testing.T, d *dump.Reader) { 38 | var containerHash util.Uint160 39 | var balanceHash util.Uint160 40 | var err error 41 | 42 | // init test contract shell 43 | c := migration.NewContract(t, d, "netmap", migration.ContractOptions{ 44 | StorageDumpHandler: func(key, value []byte) { 45 | if bytes.Equal(containerHashOldKey, key) { 46 | containerHash, err = util.Uint160DecodeBytesLE(value) 47 | require.NoError(t, err) 48 | 49 | return 50 | } 51 | 52 | if bytes.Equal(balanceHashOldKey, key) { 53 | balanceHash, err = util.Uint160DecodeBytesLE(value) 54 | require.NoError(t, err) 55 | 56 | return 57 | } 58 | }, 59 | }) 60 | 61 | require.NotZerof(t, balanceHash, "missing storage item %q with Balance contract address", balanceHashOldKey) 62 | require.NotZerof(t, containerHash, "missing storage item %q with Container contract address", containerHashOldKey) 63 | 64 | updPrm := []any{ 65 | false, 66 | util.Uint160{}, // Balance contract 67 | util.Uint160{}, // Container contract 68 | []any{}, // Key list, unused 69 | []any{}, // Config 70 | } 71 | 72 | migration.SkipUnsupportedVersions(t, c, updPrm...) 73 | 74 | // gather values which can't be fetched via contract API 75 | vSnapshotCount := c.GetStorageItem([]byte("snapshotCount")) 76 | require.NotNil(t, vSnapshotCount) 77 | snapshotCount := io.NewBinReaderFromBuf(vSnapshotCount).ReadVarUint() 78 | 79 | v := c.GetStorageItem(notaryDisabledKey) 80 | notaryDisabled := len(v) == 1 && v[0] == 1 81 | 82 | readPendingVotes := func() bool { 83 | if v := c.GetStorageItem([]byte("ballots")); v != nil { 84 | item, err := stackitem.Deserialize(v) 85 | require.NoError(t, err) 86 | arr, ok := item.Value().([]stackitem.Item) 87 | if ok { 88 | return len(arr) > 0 89 | } else { 90 | require.Equal(t, stackitem.Null{}, item) 91 | } 92 | } 93 | return false 94 | } 95 | 96 | prevPendingVote := readPendingVotes() 97 | 98 | // read previous values using contract API 99 | readUint64 := func(method string) uint64 { 100 | n, err := c.Call(t, method).TryInteger() 101 | require.NoError(t, err) 102 | return n.Uint64() 103 | } 104 | 105 | parseNetmapNodes := func(version uint64, items []stackitem.Item) []netmap.NetmapNode { 106 | res := make([]netmap.NetmapNode, len(items)) 107 | var err error 108 | for i := range items { 109 | arr := items[i].Value().([]stackitem.Item) 110 | res[i].BLOB, err = arr[0].TryBytes() 111 | require.NoError(t, err) 112 | 113 | if version <= 15_004 { 114 | res[i].State = big.NewInt(1) 115 | } else { 116 | n, err := arr[1].TryInteger() 117 | require.NoError(t, err) 118 | res[i].State = n 119 | } 120 | } 121 | return res 122 | } 123 | 124 | readDiffToSnapshots := func(version uint64) map[int][]netmap.NetmapNode { 125 | m := make(map[int][]netmap.NetmapNode) 126 | for i := 0; uint64(i) < snapshotCount; i++ { 127 | m[i] = parseNetmapNodes(version, c.Call(t, "snapshot", int64(i)).Value().([]stackitem.Item)) 128 | } 129 | return m 130 | } 131 | readVersion := func() uint64 { return readUint64("version") } 132 | readCurrentEpoch := func() uint64 { return readUint64("epoch") } 133 | readCurrentEpochBlock := func() uint64 { return readUint64("lastEpochBlock") } 134 | readCurrentNetmap := func(version uint64) []netmap.NetmapNode { 135 | return parseNetmapNodes(version, c.Call(t, "netmap").Value().([]stackitem.Item)) 136 | } 137 | readNetmapCandidates := func(version uint64) []netmap.NetmapNode { 138 | items := c.Call(t, "netmapCandidates").Value().([]stackitem.Item) 139 | res := make([]netmap.NetmapNode, len(items)) 140 | var err error 141 | for i := range items { 142 | arr := items[i].Value().([]stackitem.Item) 143 | if version <= 15_004 { 144 | res[i].BLOB, err = arr[0].Value().([]stackitem.Item)[0].TryBytes() 145 | require.NoError(t, err) 146 | } else { 147 | res[i].BLOB, err = arr[0].TryBytes() 148 | require.NoError(t, err) 149 | } 150 | 151 | n, err := arr[1].TryInteger() 152 | require.NoError(t, err) 153 | res[i].State = n 154 | } 155 | return res 156 | } 157 | readConfigs := func() []stackitem.Item { 158 | return c.Call(t, "listConfig").Value().([]stackitem.Item) 159 | } 160 | 161 | prevVersion := readVersion() 162 | prevDiffToSnapshots := readDiffToSnapshots(prevVersion) 163 | prevCurrentEpoch := readCurrentEpoch() 164 | prevCurrentEpochBlock := readCurrentEpochBlock() 165 | prevCurrentNetmap := readCurrentNetmap(prevVersion) 166 | prevNetmapCandidates := readNetmapCandidates(prevVersion) 167 | prevConfigs := readConfigs() 168 | 169 | // pre-set Inner Ring 170 | ir := make(keys.PublicKeys, 2) 171 | for i := range ir { 172 | k, err := keys.NewPrivateKey() 173 | require.NoError(t, err) 174 | ir[i] = k.PublicKey() 175 | } 176 | 177 | c.SetInnerRing(t, ir) 178 | 179 | // try to update the contract 180 | if notaryDisabled && prevPendingVote { 181 | c.CheckUpdateFail(t, "pending vote detected", updPrm...) 182 | return 183 | } 184 | 185 | c.CheckUpdateSuccess(t, updPrm...) 186 | 187 | checkNewEpochSubscribers(t, c, balanceHash, containerHash) 188 | 189 | // check that contract was updates as expected 190 | newPendingVotes := readPendingVotes() 191 | newVersion := readVersion() 192 | newDiffToSnapshots := readDiffToSnapshots(newVersion) 193 | newCurrentEpoch := readCurrentEpoch() 194 | newCurrentEpochBlock := readCurrentEpochBlock() 195 | newCurrentNetmap := readCurrentNetmap(newVersion) 196 | newNetmapCandidates := readNetmapCandidates(newVersion) 197 | newConfigs := readConfigs() 198 | 199 | require.False(t, newPendingVotes, "notary flag should be removed") 200 | require.Nil(t, c.GetStorageItem(notaryDisabledKey), "notary flag should be removed") 201 | require.Nil(t, c.GetStorageItem([]byte("innerring")), "Inner Ring nodes should be removed") 202 | require.Equal(t, prevCurrentEpoch, newCurrentEpoch, "current epoch should remain") 203 | require.Equal(t, prevCurrentEpochBlock, newCurrentEpochBlock, "current epoch block should remain") 204 | require.ElementsMatch(t, prevConfigs, newConfigs, "config should remain") 205 | require.ElementsMatch(t, prevCurrentNetmap, newCurrentNetmap, "current netmap should remain") 206 | require.ElementsMatch(t, prevNetmapCandidates, newNetmapCandidates, "netmap candidates should remain") 207 | require.ElementsMatch(t, ir, c.InnerRing(t)) 208 | 209 | require.Equal(t, len(prevDiffToSnapshots), len(newDiffToSnapshots)) 210 | for k, vPrev := range prevDiffToSnapshots { 211 | vNew, ok := newDiffToSnapshots[k] 212 | require.True(t, ok) 213 | require.ElementsMatch(t, vPrev, vNew, "%d-th past netmap snapshot should remain", k) 214 | } 215 | } 216 | 217 | func checkNewEpochSubscribers(t *testing.T, contract *migration.Contract, balanceWant, containerWant util.Uint160) { 218 | require.Nil(t, contract.GetStorageItem(balanceHashOldKey)) 219 | require.Nil(t, contract.GetStorageItem(containerHashOldKey)) 220 | 221 | // contracts are migrated in alphabetical order at least for now 222 | 223 | var balanceMigrated bool 224 | var containerMigrated bool 225 | 226 | contract.SeekStorage(append(newEpochSubsNewPrefix, 0), func(k, v []byte) bool { 227 | balanceGot, err := util.Uint160DecodeBytesLE(k) 228 | require.NoError(t, err) 229 | require.Equal(t, balanceWant, balanceGot) 230 | 231 | balanceMigrated = true 232 | 233 | return true 234 | }) 235 | 236 | contract.SeekStorage(append(newEpochSubsNewPrefix, 1), func(k, v []byte) bool { 237 | containerGot, err := util.Uint160DecodeBytesLE(k) 238 | require.NoError(t, err) 239 | require.Equal(t, containerWant, containerGot) 240 | 241 | containerMigrated = true 242 | 243 | return true 244 | }) 245 | 246 | require.True(t, balanceMigrated, "balance contact hash migration") 247 | require.True(t, containerMigrated, "container contact hash migration") 248 | } 249 | -------------------------------------------------------------------------------- /contracts/nns/config.yml: -------------------------------------------------------------------------------- 1 | name: "NameService" 2 | supportedstandards: ["NEP-11"] 3 | safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf", 4 | "tokens", "properties", "roots", "getPrice", "isAvailable", "getRecords", 5 | "resolve", "version", "getAllRecords"] 6 | events: 7 | - name: Transfer 8 | parameters: 9 | - name: from 10 | type: Hash160 11 | - name: to 12 | type: Hash160 13 | - name: amount 14 | type: Integer 15 | - name: tokenId 16 | type: ByteArray 17 | permissions: 18 | - hash: fffdc93764dbaddd97c48f252a53ea4643faa3fd 19 | methods: ["update"] 20 | - methods: ["onNEP11Payment"] 21 | -------------------------------------------------------------------------------- /contracts/nns/migration_test.go: -------------------------------------------------------------------------------- 1 | package nns_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/epicchainlabs/epicchain-contract/tests/dump" 8 | "github.com/epicchainlabs/epicchain-contract/tests/migration" 9 | "github.com/epicchainlabs/epicchain-go/pkg/vm/stackitem" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | const name = "nns" 14 | 15 | func TestMigration(t *testing.T) { 16 | err := dump.IterateDumps("testdata", func(id dump.ID, r *dump.Reader) { 17 | t.Run(id.String()+"/"+name, func(t *testing.T) { 18 | testMigrationFromDump(t, r) 19 | }) 20 | }) 21 | require.NoError(t, err) 22 | } 23 | 24 | func testMigrationFromDump(t *testing.T, d *dump.Reader) { 25 | // gather values which can't be fetched via contract API 26 | var ( 27 | tlds [][]byte 28 | roots [][]byte 29 | owners [][]byte 30 | balances []int64 31 | ) 32 | 33 | c := migration.NewContract(t, d, name, migration.ContractOptions{ 34 | StorageDumpHandler: func(key, value []byte) { 35 | if key[0] == 0x21 { // prefixName 36 | rec, err := stackitem.Deserialize(value) 37 | require.NoError(t, err) 38 | itms := rec.Value().([]stackitem.Item) 39 | require.Equal(t, 4, len(itms)) 40 | name, err := itms[1].TryBytes() 41 | require.NoError(t, err) 42 | if !bytes.Contains(name, []byte(".")) { 43 | tlds = append(tlds, name) 44 | } 45 | } 46 | if key[0] == 0x20 { // prefixRoot 47 | roots = append(roots, key[1:]) 48 | } 49 | }, 50 | }) 51 | require.EqualValues(t, roots, tlds) 52 | 53 | migration.SkipUnsupportedVersions(t, c) 54 | 55 | for _, tld := range tlds { 56 | owner, err := c.Call(t, "ownerOf", tld).TryBytes() 57 | require.NoError(t, err) 58 | owners = append(owners, owner) 59 | 60 | bal, err := c.Call(t, "balanceOf", owner).TryInteger() 61 | require.NoError(t, err) 62 | require.NotEqual(t, 0, bal.Int64()) 63 | balances = append(balances, bal.Int64()) 64 | } 65 | 66 | c.CheckUpdateSuccess(t) 67 | 68 | for i, tld := range tlds { 69 | // There is no owner after the upgrade. 70 | _, err := c.TestInvoke(t, "ownerOf", tld) 71 | require.ErrorContains(t, err, "token not found") 72 | 73 | bal, err := c.Call(t, "balanceOf", owners[i]).TryInteger() 74 | require.NoError(t, err) 75 | require.Greater(t, balances[i], bal.Int64()) 76 | 77 | // We've got alphabet signer, so renew should work even though 78 | // there is no owner. 79 | _ = c.InvokeAndCheck(t, func(t testing.TB, stack []stackitem.Item) { 80 | require.Equal(t, 1, len(stack)) 81 | _, err := stack[0].TryInteger() 82 | require.NoError(t, err) 83 | }, "renew", tld) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /contracts/nns/namestate.go: -------------------------------------------------------------------------------- 1 | package nns 2 | 3 | import ( 4 | "github.com/epicchainlabs/epicchain-go/pkg/interop" 5 | "github.com/epicchainlabs/epicchain-go/pkg/interop/runtime" 6 | ) 7 | 8 | // NameState represents domain name state. 9 | type NameState struct { 10 | // Domain name owner. Nil if owned by the committee. 11 | Owner interop.Hash160 12 | Name string 13 | Expiration int64 14 | Admin interop.Hash160 15 | } 16 | 17 | // ensureNotExpired panics if domain name is expired. 18 | func (n NameState) ensureNotExpired() { 19 | if int64(runtime.GetTime()) >= n.Expiration { 20 | panic("name has expired") 21 | } 22 | } 23 | 24 | // checkAdmin panics if script container is not signed by the domain name admin. 25 | func (n NameState) checkAdmin() { 26 | if len(n.Owner) == 0 { 27 | checkCommittee() 28 | return 29 | } 30 | if runtime.CheckWitness(n.Owner) { 31 | return 32 | } 33 | if n.Admin == nil || !runtime.CheckWitness(n.Admin) { 34 | panic("not witnessed by admin") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /contracts/nns/nns.yml: -------------------------------------------------------------------------------- 1 | name: "NameService" 2 | supportedstandards: ["NEP-11"] 3 | safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf", 4 | "tokens", "properties", "roots", "getPrice", "isAvailable", "getRecord", 5 | "resolve", "getAllRecords"] 6 | events: 7 | - name: Transfer 8 | parameters: 9 | - name: from 10 | type: Hash160 11 | - name: to 12 | type: Hash160 13 | - name: amount 14 | type: Integer 15 | - name: tokenId 16 | type: ByteArray 17 | permissions: 18 | - hash: fffdc93764dbaddd97c48f252a53ea4643faa3fd 19 | methods: ["update"] 20 | - methods: ["onNEP11Payment"] 21 | -------------------------------------------------------------------------------- /contracts/nns/recordtype.go: -------------------------------------------------------------------------------- 1 | package nns 2 | 3 | // RecordType is domain name service record types. 4 | type RecordType byte 5 | 6 | // WARNING! 7 | // Always update RPC binding code when changing these. 8 | 9 | // Record types are defined in [RFC 1035](https://tools.ietf.org/html/rfc1035) 10 | const ( 11 | // A represents address record type. 12 | A RecordType = 1 13 | // CNAME represents canonical name record type. 14 | CNAME RecordType = 5 15 | // SOA represents start of authority record type. 16 | SOA RecordType = 6 17 | // TXT represents text record type. 18 | TXT RecordType = 16 19 | ) 20 | 21 | // Record types are defined in [RFC 3596](https://tools.ietf.org/html/rfc3596) 22 | const ( 23 | // AAAA represents IPv6 address record type. 24 | AAAA RecordType = 28 25 | ) 26 | -------------------------------------------------------------------------------- /contracts/processing/config.yml: -------------------------------------------------------------------------------- 1 | name: "NeoFS Multi Signature Processing" 2 | safemethods: ["verify", "version"] 3 | permissions: 4 | - methods: ["update"] 5 | -------------------------------------------------------------------------------- /contracts/processing/contract.go: -------------------------------------------------------------------------------- 1 | package processing 2 | 3 | import ( 4 | "github.com/epicchainlabs/epicchain-go/pkg/interop" 5 | "github.com/epicchainlabs/epicchain-go/pkg/interop/contract" 6 | "github.com/epicchainlabs/epicchain-go/pkg/interop/native/gas" 7 | "github.com/epicchainlabs/epicchain-go/pkg/interop/native/ledger" 8 | "github.com/epicchainlabs/epicchain-go/pkg/interop/native/management" 9 | "github.com/epicchainlabs/epicchain-go/pkg/interop/native/roles" 10 | "github.com/epicchainlabs/epicchain-go/pkg/interop/runtime" 11 | "github.com/epicchainlabs/epicchain-go/pkg/interop/storage" 12 | "github.com/epicchainlabs/epicchain-contract/common" 13 | ) 14 | 15 | const ( 16 | neofsContractKey = "neofsScriptHash" 17 | 18 | multiaddrMethod = "alphabetAddress" 19 | ) 20 | 21 | // OnNEP17Payment is a callback for NEP-17 compatible native GAS contract. 22 | func OnNEP17Payment(from interop.Hash160, amount int, data any) { 23 | caller := runtime.GetCallingScriptHash() 24 | if !caller.Equals(gas.Hash) { 25 | common.AbortWithMessage("processing contract accepts GAS only") 26 | } 27 | } 28 | 29 | // nolint:deadcode,unused 30 | func _deploy(data any, isUpdate bool) { 31 | if isUpdate { 32 | args := data.([]any) 33 | common.CheckVersion(args[len(args)-1].(int)) 34 | return 35 | } 36 | 37 | args := data.(struct { 38 | addrNeoFS interop.Hash160 39 | }) 40 | 41 | ctx := storage.GetContext() 42 | 43 | if len(args.addrNeoFS) != interop.Hash160Len { 44 | panic("incorrect length of contract script hash") 45 | } 46 | 47 | storage.Put(ctx, neofsContractKey, args.addrNeoFS) 48 | 49 | runtime.Log("processing contract initialized") 50 | } 51 | 52 | // Update method updates contract source code and manifest. It can be invoked 53 | // only by the sidechain committee. 54 | func Update(script []byte, manifest []byte, data any) { 55 | blockHeight := ledger.CurrentIndex() 56 | alphabetKeys := roles.GetDesignatedByRole(roles.NeoFSAlphabet, uint32(blockHeight+1)) 57 | alphabetCommittee := common.Multiaddress(alphabetKeys, true) 58 | 59 | if !runtime.CheckWitness(alphabetCommittee) { 60 | panic("only side chain committee can update contract") 61 | } 62 | 63 | contract.Call(interop.Hash160(management.Hash), "update", 64 | contract.All, script, manifest, common.AppendVersion(data)) 65 | runtime.Log("processing contract updated") 66 | } 67 | 68 | // Verify method returns true if transaction contains valid multisignature of 69 | // Alphabet nodes of the Inner Ring. 70 | func Verify() bool { 71 | ctx := storage.GetContext() 72 | neofsContractAddr := storage.Get(ctx, neofsContractKey).(interop.Hash160) 73 | multiaddr := contract.Call(neofsContractAddr, multiaddrMethod, contract.ReadOnly).(interop.Hash160) 74 | 75 | return runtime.CheckWitness(multiaddr) 76 | } 77 | 78 | // Version returns the version of the contract. 79 | func Version() int { 80 | return common.Version 81 | } 82 | -------------------------------------------------------------------------------- /contracts/processing/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package processing contains implementation of Processing contract deployed in 3 | NeoFS mainchain. 4 | 5 | Processing contract pays for all multisignature transaction executions when notary 6 | service is enabled in the mainchain. Notary service prepares multisigned transactions, 7 | however they should contain sidechain GAS to be executed. It is inconvenient to 8 | ask Alphabet nodes to pay for these transactions: nodes can change over time, 9 | some nodes will spend sidechain GAS faster. It leads to economic instability. 10 | 11 | Processing contract exists to solve this issue. At the Withdraw invocation of 12 | NeoFS contract, a user pays fee directly to this contract. This fee is used to 13 | pay for Cheque invocation of NeoFS contract that returns mainchain GAS back 14 | to the user. The address of the Processing contract is used as the first signer in 15 | the multisignature transaction. Therefore, NeoVM executes Verify method of the 16 | contract and if invocation is verified, Processing contract pays for the 17 | execution. 18 | 19 | # Contract notifications 20 | 21 | Processing contract does not produce notifications to process. 22 | */ 23 | package processing 24 | 25 | /* 26 | Contract storage model. 27 | 28 | # Summary 29 | Key-value storage format: 30 | - 'neofsScriptHash' -> interop.Hash160 31 | NeoFS contract reference 32 | 33 | # Setting 34 | To handle some events, the contract refers to other contracts. 35 | */ 36 | -------------------------------------------------------------------------------- /contracts/proxy/config.yml: -------------------------------------------------------------------------------- 1 | name: "NeoFS Notary Proxy" 2 | safemethods: ["verify", "version"] 3 | permissions: 4 | - methods: ["update"] 5 | -------------------------------------------------------------------------------- /contracts/proxy/contract.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "github.com/epicchainlabs/epicchain-go/pkg/interop" 5 | "github.com/epicchainlabs/epicchain-go/pkg/interop/contract" 6 | "github.com/epicchainlabs/epicchain-go/pkg/interop/native/gas" 7 | "github.com/epicchainlabs/epicchain-go/pkg/interop/native/management" 8 | "github.com/epicchainlabs/epicchain-go/pkg/interop/runtime" 9 | "github.com/epicchainlabs/epicchain-contract/common" 10 | ) 11 | 12 | // OnNEP17Payment is a callback for NEP-17 compatible native GAS contract. 13 | func OnNEP17Payment(from interop.Hash160, amount int, data any) { 14 | caller := runtime.GetCallingScriptHash() 15 | if !caller.Equals(gas.Hash) { 16 | common.AbortWithMessage("proxy contract accepts GAS only") 17 | } 18 | } 19 | 20 | // nolint:deadcode,unused 21 | func _deploy(data any, isUpdate bool) { 22 | if isUpdate { 23 | args := data.([]any) 24 | common.CheckVersion(args[len(args)-1].(int)) 25 | return 26 | } 27 | 28 | runtime.Log("proxy contract initialized") 29 | } 30 | 31 | // Update method updates contract source code and manifest. It can be invoked 32 | // only by committee. 33 | func Update(script []byte, manifest []byte, data any) { 34 | if !common.HasUpdateAccess() { 35 | panic("only committee can update contract") 36 | } 37 | 38 | contract.Call(interop.Hash160(management.Hash), "update", 39 | contract.All, script, manifest, common.AppendVersion(data)) 40 | runtime.Log("proxy contract updated") 41 | } 42 | 43 | // Verify checks whether carrier transaction contains either (2/3N + 1) or 44 | // (N/2 + 1) valid multi-signature of the NeoFS Alphabet. 45 | func Verify() bool { 46 | return common.ContainsAlphabetWitness() 47 | } 48 | 49 | // Version returns the version of the contract. 50 | func Version() int { 51 | return common.Version 52 | } 53 | -------------------------------------------------------------------------------- /contracts/proxy/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package proxy contains implementation of Proxy contract deployed in NeoFS 3 | sidechain. 4 | 5 | Proxy contract pays for all multisignature transaction executions when notary 6 | service is enabled in the sidechain. Notary service prepares multisigned transactions, 7 | however they should contain sidechain GAS to be executed. It is inconvenient to 8 | ask Alphabet nodes to pay for these transactions: nodes can change over time, 9 | some nodes will spend sidechain GAS faster. It leads to economic instability. 10 | 11 | Proxy contract exists to solve this issue. While Alphabet contracts hold all 12 | sidechain NEO, proxy contract holds most of the sidechain GAS. Alphabet 13 | contracts emit half of the available GAS to the proxy contract. The address of the 14 | Proxy contract is used as the first signer in a multisignature transaction. 15 | Therefore, NeoVM executes Verify method of the contract; and if invocation is 16 | verified, Proxy contract pays for the execution. 17 | 18 | # Contract notifications 19 | 20 | Proxy contract does not produce notifications to process. 21 | */ 22 | package proxy 23 | 24 | /* 25 | Contract storage model. 26 | 27 | At the moment, no data is stored in the contract. 28 | */ 29 | -------------------------------------------------------------------------------- /contracts/reputation/config.yml: -------------------------------------------------------------------------------- 1 | name: "NeoFS Reputation" 2 | safemethods: ["get", "getByID", "listByEpoch"] 3 | permissions: 4 | - methods: ["update"] 5 | events: 6 | -------------------------------------------------------------------------------- /contracts/reputation/contract.go: -------------------------------------------------------------------------------- 1 | package reputation 2 | 3 | import ( 4 | "github.com/epicchainlabs/epicchain-go/pkg/interop" 5 | "github.com/epicchainlabs/epicchain-go/pkg/interop/contract" 6 | "github.com/epicchainlabs/epicchain-go/pkg/interop/convert" 7 | "github.com/epicchainlabs/epicchain-go/pkg/interop/iterator" 8 | "github.com/epicchainlabs/epicchain-go/pkg/interop/native/management" 9 | "github.com/epicchainlabs/epicchain-go/pkg/interop/runtime" 10 | "github.com/epicchainlabs/epicchain-go/pkg/interop/storage" 11 | "github.com/epicchainlabs/epicchain-contract/common" 12 | ) 13 | 14 | const ( 15 | reputationValuePrefix = 'r' 16 | reputationCountPrefix = 'c' 17 | ) 18 | 19 | // nolint:deadcode,unused 20 | func _deploy(data any, isUpdate bool) { 21 | ctx := storage.GetContext() 22 | 23 | if isUpdate { 24 | args := data.([]any) 25 | version := args[len(args)-1].(int) 26 | 27 | common.CheckVersion(version) 28 | 29 | // switch to notary mode if version of the current contract deployment is 30 | // earlier than v0.17.0 (initial version when non-notary mode was taken out of 31 | // use) 32 | // TODO: avoid number magic, add function for version comparison to common package 33 | if version < 17_000 { 34 | switchToNotary(ctx) 35 | } 36 | 37 | return 38 | } 39 | 40 | runtime.Log("reputation contract initialized") 41 | } 42 | 43 | // re-initializes contract from non-notary to notary mode. Does nothing if 44 | // action has already been done. The function is called on contract update with 45 | // storage.Context from _deploy. 46 | // 47 | // If contract stores non-empty value by 'ballots' key, switchToNotary panics. 48 | // Otherwise, existing value is removed. 49 | // 50 | // switchToNotary removes value stored by 'notary' key. 51 | // 52 | // nolint:unused 53 | func switchToNotary(ctx storage.Context) { 54 | const notaryDisabledKey = "notary" // non-notary legacy 55 | 56 | notaryVal := storage.Get(ctx, notaryDisabledKey) 57 | if notaryVal == nil { 58 | runtime.Log("contract is already notarized") 59 | return 60 | } else if notaryVal.(bool) && !common.TryPurgeVotes(ctx) { 61 | panic("pending vote detected") 62 | } 63 | 64 | storage.Delete(ctx, notaryDisabledKey) 65 | 66 | if notaryVal.(bool) { 67 | runtime.Log("contract successfully notarized") 68 | } 69 | } 70 | 71 | // Update method updates contract source code and manifest. It can be invoked 72 | // only by committee. 73 | func Update(script []byte, manifest []byte, data any) { 74 | if !common.HasUpdateAccess() { 75 | panic("only committee can update contract") 76 | } 77 | 78 | contract.Call(interop.Hash160(management.Hash), "update", 79 | contract.All, script, manifest, common.AppendVersion(data)) 80 | runtime.Log("reputation contract updated") 81 | } 82 | 83 | // Put method saves global trust data in contract storage. It can be invoked only by 84 | // storage nodes with Alphabet assistance (multisignature witness). 85 | // 86 | // Epoch is the epoch number when GlobalTrust structure was generated. 87 | // PeerID contains public key of the storage node that is the subject of the GlobalTrust. 88 | // Value contains a stable marshaled structure of GlobalTrust. 89 | func Put(epoch int, peerID []byte, value []byte) { 90 | ctx := storage.GetContext() 91 | 92 | multiaddr := common.AlphabetAddress() 93 | common.CheckAlphabetWitness(multiaddr) 94 | 95 | id := storageID(epoch, peerID) 96 | 97 | key := getReputationKey(reputationCountPrefix, id) 98 | rawCnt := storage.Get(ctx, key) 99 | cnt := 0 100 | if rawCnt != nil { 101 | cnt = rawCnt.(int) 102 | } 103 | cnt++ 104 | storage.Put(ctx, key, cnt) 105 | 106 | key[0] = reputationValuePrefix 107 | key = append(key, convert.ToBytes(cnt)...) 108 | storage.Put(ctx, key, value) 109 | } 110 | 111 | // Get method returns a list of all stable marshaled GlobalTrust structures 112 | // known for the given peer during the specified epoch. 113 | func Get(epoch int, peerID []byte) [][]byte { 114 | id := storageID(epoch, peerID) 115 | return GetByID(id) 116 | } 117 | 118 | // GetByID method returns a list of all stable marshaled GlobalTrust with 119 | // the specified id. Use ListByEpoch method to obtain the id. 120 | func GetByID(id []byte) [][]byte { 121 | ctx := storage.GetReadOnlyContext() 122 | 123 | var data [][]byte 124 | 125 | it := storage.Find(ctx, getReputationKey(reputationValuePrefix, id), storage.ValuesOnly) 126 | for iterator.Next(it) { 127 | data = append(data, iterator.Value(it).([]byte)) 128 | } 129 | return data 130 | } 131 | 132 | func getReputationKey(prefix byte, id []byte) []byte { 133 | return append([]byte{prefix}, id...) 134 | } 135 | 136 | // ListByEpoch returns a list of IDs that may be used to get reputation data 137 | // with GetByID method. 138 | func ListByEpoch(epoch int) [][]byte { 139 | ctx := storage.GetReadOnlyContext() 140 | key := getReputationKey(reputationCountPrefix, convert.ToBytes(epoch)) 141 | it := storage.Find(ctx, key, storage.KeysOnly) 142 | 143 | var result [][]byte 144 | 145 | for iterator.Next(it) { 146 | key := iterator.Value(it).([]byte) // iterator MUST BE `storage.KeysOnly` 147 | result = append(result, key[1:]) 148 | } 149 | 150 | return result 151 | } 152 | 153 | // Version returns the version of the contract. 154 | func Version() int { 155 | return common.Version 156 | } 157 | 158 | func storageID(epoch int, peerID []byte) []byte { 159 | var buf any = epoch 160 | 161 | return append(buf.([]byte), peerID...) 162 | } 163 | -------------------------------------------------------------------------------- /contracts/reputation/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package reputation contains implementation of Reputation contract deployed in NeoFS 3 | sidechain. 4 | 5 | Storage nodes collect reputation data while communicating with other nodes. 6 | This data is exchanged and the end result (global trust values) is stored in 7 | the contract as opaque data. 8 | 9 | # Contract notifications 10 | 11 | Reputation contract does not produce notifications to process. 12 | */ 13 | package reputation 14 | 15 | /* 16 | Contract storage model. 17 | 18 | Current conventions: 19 | : binary unique identifier of the NeoFS Reputation system participant 20 | : little-endian unsigned integer NeoFS epoch 21 | 22 | # Summary 23 | Key-value storage format: 24 | - 'c' -> int 25 | Number of values got from calculated by fixed peer at fixed NeoFS epoch 26 | - 'r' + count -> []byte 27 | binary-encoded global trust values submitted calculated at fixed epoch by 28 | particular peer. All such values are counted starting from 0. 29 | 30 | # Trust 31 | Contract stores trust values collected within NeoFS Reputation system lifetime. 32 | */ 33 | -------------------------------------------------------------------------------- /contracts/reputation/migration_test.go: -------------------------------------------------------------------------------- 1 | package reputation_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/epicchainlabs/epicchain-contract/tests/dump" 8 | "github.com/epicchainlabs/epicchain-contract/tests/migration" 9 | "github.com/epicchainlabs/epicchain-go/pkg/io" 10 | "github.com/epicchainlabs/epicchain-go/pkg/vm/stackitem" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | const name = "reputation" 15 | 16 | func TestMigration(t *testing.T) { 17 | err := dump.IterateDumps("../testdata", func(id dump.ID, r *dump.Reader) { 18 | t.Run(id.String()+"/"+name, func(t *testing.T) { 19 | testMigrationFromDump(t, r) 20 | }) 21 | }) 22 | require.NoError(t, err) 23 | } 24 | 25 | var notaryDisabledKey = []byte("notary") 26 | 27 | func testMigrationFromDump(t *testing.T, d *dump.Reader) { 28 | // gather values which can't be fetched via contract API 29 | var epochs []uint64 30 | 31 | c := migration.NewContract(t, d, "reputation", migration.ContractOptions{ 32 | StorageDumpHandler: func(key, value []byte) { 33 | if bytes.HasPrefix(key, []byte{'c'}) { 34 | epoch := io.NewBinReaderFromBuf(key[1:]).ReadVarUint() 35 | for i := range epochs { 36 | if epochs[i] == epoch { 37 | return 38 | } 39 | } 40 | epochs = append(epochs, epoch) 41 | } 42 | }, 43 | }) 44 | 45 | migration.SkipUnsupportedVersions(t, c) 46 | 47 | v := c.GetStorageItem(notaryDisabledKey) 48 | notaryDisabled := len(v) == 1 && v[0] == 1 49 | 50 | readPendingVotes := func() bool { 51 | if v := c.GetStorageItem([]byte("ballots")); v != nil { 52 | item, err := stackitem.Deserialize(v) 53 | require.NoError(t, err) 54 | arr, ok := item.Value().([]stackitem.Item) 55 | if ok { 56 | return len(arr) > 0 57 | } else { 58 | require.Equal(t, stackitem.Null{}, item) 59 | } 60 | } 61 | return false 62 | } 63 | 64 | prevPendingVotes := readPendingVotes() 65 | 66 | // read previous values using contract API 67 | readEpochsToTrustValues := func() map[uint64][]stackitem.Item { 68 | m := make(map[uint64][]stackitem.Item, len(epochs)) 69 | for i := range epochs { 70 | r := c.Call(t, "listByEpoch", int64(epochs[i])) 71 | items, ok := r.Value().([]stackitem.Item) 72 | if !ok { 73 | require.Equal(t, stackitem.Null{}, r) 74 | } 75 | 76 | var results []stackitem.Item 77 | for j := range items { 78 | bID, err := items[j].TryBytes() 79 | require.NoError(t, err) 80 | 81 | r := c.Call(t, "getByID", bID) 82 | res, ok := r.Value().([]stackitem.Item) 83 | if !ok { 84 | require.Equal(t, stackitem.Null{}, r) 85 | } 86 | 87 | results = append(results, res...) 88 | } 89 | 90 | m[epochs[i]] = results 91 | } 92 | return m 93 | } 94 | 95 | prevEpochsToTrustValues := readEpochsToTrustValues() 96 | 97 | // try to update the contract 98 | if notaryDisabled && prevPendingVotes { 99 | c.CheckUpdateFail(t, "pending vote detected") 100 | return 101 | } 102 | 103 | c.CheckUpdateSuccess(t) 104 | 105 | // check that contract was updates as expected 106 | newEpochsToTrustValues := readEpochsToTrustValues() 107 | newPendingVotes := readPendingVotes() 108 | 109 | require.False(t, newPendingVotes, "there should be no more pending votes") 110 | require.Nil(t, c.GetStorageItem(notaryDisabledKey), "notary flag should be removed") 111 | 112 | require.Equal(t, len(prevEpochsToTrustValues), len(newEpochsToTrustValues)) 113 | for k, vPrev := range prevEpochsToTrustValues { 114 | vNew, ok := newEpochsToTrustValues[k] 115 | require.True(t, ok) 116 | require.ElementsMatch(t, vPrev, vNew) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | neofs-contract (0.0.0) stable; urgency=medium 2 | 3 | * Initial release 4 | 5 | -- NeoSPCC Wed, 24 Aug 2022 18:29:49 +0300 6 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: neofs-contract 2 | Section: misc 3 | Priority: optional 4 | Maintainer: NeoSPCC 5 | Build-Depends: debhelper-compat (= 13), git, devscripts, neo-go 6 | Standards-Version: 4.5.1 7 | Homepage: https://fs.neo.org/ 8 | Vcs-Git: https://github.com/epicchainlabs/epicchain-contract.git 9 | Vcs-Browser: https://github.com/epicchainlabs/epicchain-contract 10 | 11 | Package: neofs-contract 12 | Architecture: all 13 | Depends: ${misc:Depends} 14 | Description: NeoFS-Contract contains all NeoFS related contracts. 15 | Contracts are written for neo-go compiler. 16 | These contracts are deployed both in the mainchain and the sidechain. 17 | . 18 | Mainchain contracts: 19 | . 20 | - neofs 21 | - processing 22 | . 23 | Sidechain contracts: 24 | . 25 | - alphabet 26 | - audit 27 | - balance 28 | - container 29 | - neofsid 30 | - netmap 31 | - nns 32 | - proxy 33 | - reputation 34 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: neofs-contract 3 | Upstream-Contact: tech@nspcc.ru 4 | Source: https://github.com/epicchainlabs/epicchain-contract 5 | 6 | Files: * 7 | Copyright: 2018-2022 NeoSPCC (@nspcc-dev) 8 | 9 | License: GPL-3 10 | This program is free software: you can redistribute it and/or modify it 11 | under the terms of the GNU General Public License as published 12 | by the Free Software Foundation; either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 18 | General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program or at /usr/share/common-licenses/GPL-3. 22 | If not, see . 23 | -------------------------------------------------------------------------------- /debian/neofs-contract.docs: -------------------------------------------------------------------------------- 1 | README* 2 | -------------------------------------------------------------------------------- /debian/postinst.ex: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # postinst script for neofs-contract 3 | # 4 | # see: dh_installdeb(1) 5 | 6 | set -e 7 | 8 | # summary of how this script can be called: 9 | # * `configure' 10 | # * `abort-upgrade' 11 | # * `abort-remove' `in-favour' 12 | # 13 | # * `abort-remove' 14 | # * `abort-deconfigure' `in-favour' 15 | # `removing' 16 | # 17 | # for details, see https://www.debian.org/doc/debian-policy/ or 18 | # the debian-policy package 19 | 20 | 21 | case "$1" in 22 | configure) 23 | ;; 24 | 25 | abort-upgrade|abort-remove|abort-deconfigure) 26 | ;; 27 | 28 | *) 29 | echo "postinst called with unknown argument \`$1'" >&2 30 | exit 1 31 | ;; 32 | esac 33 | 34 | # dh_installdeb will replace this with shell code automatically 35 | # generated by other debhelper scripts. 36 | 37 | #DEBHELPER# 38 | 39 | exit 0 40 | -------------------------------------------------------------------------------- /debian/postrm.ex: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # postrm script for neofs-contract 3 | # 4 | # see: dh_installdeb(1) 5 | 6 | set -e 7 | 8 | # summary of how this script can be called: 9 | # * `remove' 10 | # * `purge' 11 | # * `upgrade' 12 | # * `failed-upgrade' 13 | # * `abort-install' 14 | # * `abort-install' 15 | # * `abort-upgrade' 16 | # * `disappear' 17 | # 18 | # for details, see https://www.debian.org/doc/debian-policy/ or 19 | # the debian-policy package 20 | 21 | 22 | case "$1" in 23 | purge|remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) 24 | ;; 25 | 26 | *) 27 | echo "postrm called with unknown argument \`$1'" >&2 28 | exit 1 29 | ;; 30 | esac 31 | 32 | # dh_installdeb will replace this with shell code automatically 33 | # generated by other debhelper scripts. 34 | 35 | #DEBHELPER# 36 | 37 | exit 0 38 | -------------------------------------------------------------------------------- /debian/preinst.ex: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # preinst script for neofs-contract 3 | # 4 | # see: dh_installdeb(1) 5 | 6 | set -e 7 | 8 | # summary of how this script can be called: 9 | # * `install' 10 | # * `install' 11 | # * `upgrade' 12 | # * `abort-upgrade' 13 | # for details, see https://www.debian.org/doc/debian-policy/ or 14 | # the debian-policy package 15 | 16 | 17 | case "$1" in 18 | install|upgrade) 19 | ;; 20 | 21 | abort-upgrade) 22 | ;; 23 | 24 | *) 25 | echo "preinst called with unknown argument \`$1'" >&2 26 | exit 1 27 | ;; 28 | esac 29 | 30 | # dh_installdeb will replace this with shell code automatically 31 | # generated by other debhelper scripts. 32 | 33 | #DEBHELPER# 34 | 35 | exit 0 36 | -------------------------------------------------------------------------------- /debian/prerm.ex: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # prerm script for neofs-contract 3 | # 4 | # see: dh_installdeb(1) 5 | 6 | set -e 7 | 8 | # summary of how this script can be called: 9 | # * `remove' 10 | # * `upgrade' 11 | # * `failed-upgrade' 12 | # * `remove' `in-favour' 13 | # * `deconfigure' `in-favour' 14 | # `removing' 15 | # 16 | # for details, see https://www.debian.org/doc/debian-policy/ or 17 | # the debian-policy package 18 | 19 | 20 | case "$1" in 21 | remove|upgrade|deconfigure) 22 | ;; 23 | 24 | failed-upgrade) 25 | ;; 26 | 27 | *) 28 | echo "prerm called with unknown argument \`$1'" >&2 29 | exit 1 30 | ;; 31 | esac 32 | 33 | # dh_installdeb will replace this with shell code automatically 34 | # generated by other debhelper scripts. 35 | 36 | #DEBHELPER# 37 | 38 | exit 0 39 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | SERVICE = neofs-contract 4 | export NEOGO ?= $(shell command -v neo-go) 5 | 6 | %: 7 | dh $@ 8 | 9 | override_dh_auto_build: 10 | 11 | make all 12 | 13 | override_dh_auto_install: 14 | install -D -m 0750 -d debian/$(SERVICE)/var/lib/neofs/contract 15 | find . -maxdepth 2 \( -name '*.nef' -o -name 'config.json' \) -exec cp --parents \{\} debian/$(SERVICE)/var/lib/neofs/contract \; 16 | 17 | override_dh_installchangelogs: 18 | dh_installchangelogs -k CHANGELOG.md 19 | 20 | 21 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /deploy/deploy_test.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/epicchainlabs/epicchain-go/pkg/core/transaction" 8 | "github.com/epicchainlabs/epicchain-go/pkg/neorpc/result" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestNeoFSRuntimeTransactionModifier(t *testing.T) { 13 | t.Run("invalid invocation result state", func(t *testing.T) { 14 | var res result.Invoke 15 | res.State = "FAULT" // any non-HALT 16 | 17 | err := neoFSRuntimeTransactionModifier(func() uint32 { return 0 })(&res, new(transaction.Transaction)) 18 | require.Error(t, err) 19 | }) 20 | 21 | var validRes result.Invoke 22 | validRes.State = "HALT" 23 | 24 | for _, tc := range []struct { 25 | curHeight uint32 26 | expectedNonce uint32 27 | expectedVUB uint32 28 | }{ 29 | {curHeight: 0, expectedNonce: 0, expectedVUB: 100}, 30 | {curHeight: 1, expectedNonce: 0, expectedVUB: 100}, 31 | {curHeight: 99, expectedNonce: 0, expectedVUB: 100}, 32 | {curHeight: 100, expectedNonce: 100, expectedVUB: 200}, 33 | {curHeight: 199, expectedNonce: 100, expectedVUB: 200}, 34 | {curHeight: 200, expectedNonce: 200, expectedVUB: 300}, 35 | {curHeight: math.MaxUint32 - 50, expectedNonce: 100 * (math.MaxUint32 / 100), expectedVUB: math.MaxUint32}, 36 | } { 37 | m := neoFSRuntimeTransactionModifier(func() uint32 { return tc.curHeight }) 38 | 39 | var tx transaction.Transaction 40 | 41 | err := m(&validRes, &tx) 42 | require.NoError(t, err, tc) 43 | require.EqualValues(t, tc.expectedNonce, tx.Nonce, tc) 44 | require.EqualValues(t, tc.expectedVUB, tx.ValidUntilBlock, tc) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /deploy/funds_test.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestDivideFundsEvenly(t *testing.T) { 11 | t.Run("zero", func(t *testing.T) { 12 | var vals []uint64 13 | 14 | divideFundsEvenly(0, 5, func(ind int, amount uint64) { 15 | vals = append(vals, amount) 16 | }) 17 | require.Empty(t, vals) 18 | }) 19 | 20 | t.Run("less than N", func(t *testing.T) { 21 | var vals []uint64 22 | 23 | divideFundsEvenly(4, 5, func(ind int, amount uint64) { 24 | vals = append(vals, amount) 25 | }) 26 | require.Len(t, vals, 4) 27 | for i := range vals { 28 | require.EqualValues(t, 1, vals[i]) 29 | } 30 | }) 31 | 32 | t.Run("multiple", func(t *testing.T) { 33 | var vals []uint64 34 | 35 | divideFundsEvenly(15, 3, func(ind int, amount uint64) { 36 | vals = append(vals, amount) 37 | }) 38 | require.Len(t, vals, 3) 39 | for i := range vals { 40 | require.EqualValues(t, 5, vals[i]) 41 | } 42 | }) 43 | 44 | t.Run("with remainder", func(t *testing.T) { 45 | var vals []uint64 46 | 47 | divideFundsEvenly(16, 3, func(ind int, amount uint64) { 48 | vals = append(vals, amount) 49 | }) 50 | require.Len(t, vals, 3) 51 | require.EqualValues(t, 6, vals[0]) 52 | require.EqualValues(t, 5, vals[1]) 53 | require.EqualValues(t, 5, vals[2]) 54 | }) 55 | } 56 | 57 | func BenchmarkDivideFundsEvenly(b *testing.B) { 58 | for _, tc := range []struct { 59 | n int 60 | a uint64 61 | }{ 62 | {n: 7, a: 705}, 63 | {n: 100, a: 100_000_000}, 64 | } { 65 | b.Run(fmt.Sprintf("N=%d,amount=%d", tc.n, tc.a), func(b *testing.B) { 66 | b.ReportAllocs() 67 | b.ResetTimer() 68 | 69 | for i := 0; i < b.N; i++ { 70 | divideFundsEvenly(tc.a, tc.n, func(ind int, amount uint64) {}) 71 | } 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /deploy/netmap.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | const ( 4 | MaxObjectSizeConfig = "MaxObjectSize" 5 | BasicIncomeRateConfig = "BasicIncomeRate" 6 | AuditFeeConfig = "AuditFee" 7 | EpochDurationConfig = "EpochDuration" 8 | ContainerFeeConfig = "ContainerFee" 9 | ContainerAliasFeeConfig = "ContainerAliasFee" 10 | EigenTrustIterationsConfig = "EigenTrustIterations" 11 | EigenTrustAlphaConfig = "EigenTrustAlpha" 12 | InnerRingCandidateFeeConfig = "InnerRingCandidateFee" 13 | WithdrawFeeConfig = "WithdrawFee" 14 | HomomorphicHashingDisabledKey = "HomomorphicHashingDisabled" 15 | MaintenanceModeAllowedConfig = "MaintenanceModeAllowed" 16 | ) 17 | 18 | // RawNetworkParameter is a NeoFS network parameter which is transmitted but 19 | // not interpreted by the NeoFS API protocol. 20 | type RawNetworkParameter struct { 21 | // Name of the parameter. 22 | Name string 23 | 24 | // Raw parameter value. 25 | Value []byte 26 | } 27 | 28 | // NetworkConfiguration represents NeoFS network configuration stored 29 | // in the NeoFS Sidechain. 30 | type NetworkConfiguration struct { 31 | MaxObjectSize uint64 32 | StoragePrice uint64 33 | AuditFee uint64 34 | EpochDuration uint64 35 | ContainerFee uint64 36 | ContainerAliasFee uint64 37 | EigenTrustIterations uint64 38 | EigenTrustAlpha float64 39 | IRCandidateFee uint64 40 | WithdrawalFee uint64 41 | HomomorphicHashingDisabled bool 42 | MaintenanceModeAllowed bool 43 | Raw []RawNetworkParameter 44 | } 45 | -------------------------------------------------------------------------------- /deploy/util.go: -------------------------------------------------------------------------------- 1 | package deploy 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "sync/atomic" 9 | "time" 10 | 11 | "github.com/epicchainlabs/epicchain-go/pkg/core/state" 12 | "github.com/epicchainlabs/epicchain-go/pkg/encoding/address" 13 | "github.com/epicchainlabs/epicchain-go/pkg/neorpc" 14 | "github.com/epicchainlabs/epicchain-go/pkg/rpcclient/invoker" 15 | "github.com/epicchainlabs/epicchain-go/pkg/util" 16 | "github.com/epicchainlabs/epicchain-contract/common" 17 | "go.uber.org/zap" 18 | ) 19 | 20 | func isErrContractAlreadyUpdated(err error) bool { 21 | return strings.Contains(err.Error(), common.ErrAlreadyUpdated) 22 | } 23 | 24 | func isErrTLDNotFound(err error) bool { 25 | return strings.Contains(err.Error(), "TLD not found") 26 | } 27 | 28 | // blockchainMonitor is a thin utility around Blockchain providing state 29 | // monitoring. 30 | type blockchainMonitor struct { 31 | logger *zap.Logger 32 | 33 | blockchain Blockchain 34 | 35 | blockInterval time.Duration 36 | 37 | height atomic.Uint32 38 | 39 | chConnLost chan struct{} 40 | chExit chan struct{} 41 | } 42 | 43 | // newBlockchainMonitor constructs and runs monitor for the given Blockchain. 44 | func newBlockchainMonitor(l *zap.Logger, b Blockchain, chNewBlock chan<- struct{}) (*blockchainMonitor, error) { 45 | ver, err := b.GetVersion() 46 | if err != nil { 47 | return nil, fmt.Errorf("request Neo protocol configuration: %w", err) 48 | } 49 | 50 | initialBlock, err := b.GetBlockCount() 51 | if err != nil { 52 | return nil, fmt.Errorf("get current blockchain height: %w", err) 53 | } 54 | 55 | blockCh, err := b.SubscribeToNewBlocks() 56 | if err != nil { 57 | return nil, fmt.Errorf("subscribe to new blocks of the chain: %w", err) 58 | } 59 | 60 | res := &blockchainMonitor{ 61 | logger: l, 62 | blockchain: b, 63 | blockInterval: time.Duration(ver.Protocol.MillisecondsPerBlock) * time.Millisecond, 64 | chConnLost: make(chan struct{}), 65 | chExit: make(chan struct{}), 66 | } 67 | 68 | res.height.Store(initialBlock) 69 | 70 | go func() { 71 | l.Info("listening to new blocks...") 72 | for { 73 | b, ok := <-blockCh 74 | if !ok { 75 | close(chNewBlock) 76 | close(res.chConnLost) 77 | close(res.chExit) 78 | l.Info("new blocks channel is closed, listening stopped") 79 | return 80 | } 81 | 82 | res.height.Store(b.Index) 83 | 84 | select { 85 | case chNewBlock <- struct{}{}: 86 | case <-res.chExit: 87 | l.Info("monitoring new blocks channel is closed, listening stopped") 88 | return 89 | default: 90 | } 91 | 92 | l.Info("new block arrived", zap.Uint32("height", b.Index)) 93 | } 94 | }() 95 | 96 | return res, nil 97 | } 98 | 99 | // currentHeight returns current blockchain height. 100 | func (x *blockchainMonitor) currentHeight() uint32 { 101 | return x.height.Load() 102 | } 103 | 104 | // currentHeight returns current blockchain height. 105 | func (x *blockchainMonitor) stop() { 106 | x.chExit <- struct{}{} 107 | close(x.chExit) 108 | } 109 | 110 | // waitForNextBlock blocks until blockchainMonitor encounters new block on the 111 | // chain, underlying connection with the [Blockchain] is lost or provided 112 | // context is done (returns context error). 113 | func (x *blockchainMonitor) waitForNextBlock(ctx context.Context) error { 114 | initialBlock := x.currentHeight() 115 | 116 | ticker := time.NewTicker(x.blockInterval) 117 | defer ticker.Stop() 118 | 119 | for { 120 | select { 121 | case <-ctx.Done(): 122 | return ctx.Err() 123 | case <-x.chConnLost: 124 | return errors.New("connection to the blockchain is lost") 125 | case <-ticker.C: 126 | if x.height.Load() > initialBlock { 127 | return nil 128 | } 129 | } 130 | } 131 | } 132 | 133 | // readNNSOnChainState reads state of the NeoFS NNS contract in the given 134 | // Blockchain. Returns both nil if contract is missing. 135 | func readNNSOnChainState(b Blockchain) (*state.Contract, error) { 136 | // NNS must always have ID=1 in the NeoFS Sidechain 137 | const nnsContractID = 1 138 | res, err := b.GetContractStateByID(nnsContractID) 139 | if err != nil { 140 | if errors.Is(err, neorpc.ErrUnknownContract) { 141 | return nil, nil 142 | } 143 | return nil, fmt.Errorf("read contract state by ID=%d: %w", nnsContractID, err) 144 | } 145 | return res, nil 146 | } 147 | 148 | type transactionGroupWaiter interface { 149 | WaitAny(ctx context.Context, vub uint32, hashes ...util.Uint256) (*state.AppExecResult, error) 150 | } 151 | 152 | type transactionGroupMonitor struct { 153 | waiter transactionGroupWaiter 154 | pending atomic.Bool 155 | } 156 | 157 | func newTransactionGroupMonitor(w transactionGroupWaiter) *transactionGroupMonitor { 158 | return &transactionGroupMonitor{ 159 | waiter: w, 160 | } 161 | } 162 | 163 | func (x *transactionGroupMonitor) reset() { 164 | x.pending.Store(false) 165 | } 166 | 167 | func (x *transactionGroupMonitor) isPending() bool { 168 | return x.pending.Load() 169 | } 170 | 171 | func (x *transactionGroupMonitor) trackPendingTransactionsAsync(ctx context.Context, vub uint32, txs ...util.Uint256) { 172 | if len(txs) == 0 { 173 | panic("missing transactions") 174 | } 175 | 176 | x.pending.Store(true) 177 | 178 | waitCtx, cancel := context.WithCancel(ctx) 179 | 180 | go func() { 181 | _, _ = x.waiter.WaitAny(waitCtx, vub, txs...) 182 | x.reset() 183 | cancel() 184 | }() 185 | } 186 | 187 | var errInvalidContractDomainRecord = errors.New("invalid contract domain record") 188 | 189 | // readContractOnChainStateByDomainName reads address state of contract deployed 190 | // in the given Blockchain and recorded in the NNS with the specified domain 191 | // name. Returns errMissingDomain if domain doesn't exist. Returns 192 | // errMissingDomainRecord if domain has no records. Returns 193 | // errInvalidContractDomainRecord if domain record has invalid/unsupported 194 | // format. Returns [neorpc.ErrUnknownContract] if contract is recorded in the NNS but 195 | // missing in the Blockchain. 196 | func readContractOnChainStateByDomainName(b Blockchain, nnsContract util.Uint160, domainName string) (*state.Contract, error) { 197 | rec, err := lookupNNSDomainRecord(invoker.New(b, nil), nnsContract, domainName) 198 | if err != nil { 199 | return nil, err 200 | } 201 | 202 | // historically two formats may occur 203 | addr, err := util.Uint160DecodeStringLE(rec) 204 | if err != nil { 205 | addr, err = address.StringToUint160(rec) 206 | if err != nil { 207 | return nil, fmt.Errorf("%w: domain record '%s' neither NEO address nor little-endian hex-encoded script hash", errInvalidContractDomainRecord, rec) 208 | } 209 | } 210 | 211 | res, err := b.GetContractStateByHash(addr) 212 | if err != nil { 213 | return nil, fmt.Errorf("get contract by address=%s: %w", addr, err) 214 | } 215 | 216 | return res, nil 217 | } 218 | -------------------------------------------------------------------------------- /docs/labels.md: -------------------------------------------------------------------------------- 1 | # Project-specific labels 2 | 3 | ## Component 4 | 5 | We naturally have a set of contracts here, so each contract has a label of its 6 | own (the same color is used for each except `neofs` since it's not an FS chain 7 | contract, they rarely mix in a single issue): 8 | 9 | - alphabet 10 | - audit 11 | - balance 12 | - container 13 | - neofs 14 | - neofsid 15 | - netmap 16 | - nns 17 | - proxy 18 | - reputation 19 | - subnet 20 | 21 | Should not be used for new issues since the contract is gone, but kept for 22 | old ones. 23 | -------------------------------------------------------------------------------- /docs/release-instruction.md: -------------------------------------------------------------------------------- 1 | # Release instructions 2 | 3 | This document outlines the neofs-contract release process. It can be used as a 4 | todo list for a new release. 5 | 6 | ## Check the state 7 | 8 | These should run successfully: 9 | * build 10 | * unit-tests 11 | * lint 12 | 13 | ## Update CHANGELOG 14 | 15 | Add an entry to the CHANGELOG.md following the style established there. 16 | 17 | ## Update versions 18 | 19 | Ensure VERSION and common/version.go files contain the proper target version, 20 | update if needed. 21 | 22 | Create a PR with CHANGELOG/version changes, review/merge it. 23 | 24 | ## Create a GitHub release and a tag 25 | 26 | Use "Draft a new release" button in the "Releases" section. Create a new 27 | `vX.Y.Z` tag for it following the semantic versioning standard. Put change log 28 | for this release into the description. Do not attach any binaries at this step. 29 | Set the "Set as the latest release" checkbox if this is the latest stable 30 | release or "Set as a pre-release" if this is an unstable pre-release. 31 | Press the "Publish release" button. 32 | 33 | ## Add neofs-contract tarball 34 | 35 | Fetch the new tag from the GitHub, do `make clean && make archive` locally. 36 | It should produce neofs-contract-vX.Y.Z.tar.gz tarball. 37 | 38 | ## Close GitHub milestone 39 | 40 | Close corresponding X.Y.Z GitHub milestone. 41 | 42 | ## Deployment 43 | 44 | Update NeoFS node with the new contracts. 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/epicchainlabs/epicchain-contract 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/google/uuid v1.6.0 7 | github.com/mr-tron/base58 v1.2.0 8 | github.com/epicchainlabs/epicchain-go v0.106.0 9 | github.com/epicchainlabs/epicchain-go/pkg/interop v0.0.0-20240521124852-5cbfe215a4e9 10 | github.com/stretchr/testify v1.9.0 11 | go.uber.org/zap v1.27.0 12 | ) 13 | 14 | require ( 15 | github.com/beorn7/perks v1.0.1 // indirect 16 | github.com/bits-and-blooms/bitset v1.8.0 // indirect 17 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 18 | github.com/consensys/bavard v0.1.13 // indirect 19 | github.com/consensys/gnark-crypto v0.12.2-0.20231013160410-1f65e75b6dfb // indirect 20 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 21 | github.com/davecgh/go-spew v1.1.1 // indirect 22 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 23 | github.com/golang/snappy v0.0.1 // indirect 24 | github.com/gorilla/websocket v1.5.1 // indirect 25 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 26 | github.com/holiman/uint256 v1.2.4 // indirect 27 | github.com/mmcloughlin/addchain v0.4.0 // indirect 28 | github.com/nspcc-dev/go-ordered-json v0.0.0-20240301084351-0246b013f8b2 // indirect 29 | github.com/nspcc-dev/rfc6979 v0.2.1 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | github.com/prometheus/client_golang v1.19.0 // indirect 32 | github.com/prometheus/client_model v0.5.0 // indirect 33 | github.com/prometheus/common v0.48.0 // indirect 34 | github.com/prometheus/procfs v0.12.0 // indirect 35 | github.com/rogpeppe/go-internal v1.11.0 // indirect 36 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 37 | github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954 // indirect 38 | github.com/twmb/murmur3 v1.1.8 // indirect 39 | github.com/urfave/cli v1.22.5 // indirect 40 | go.etcd.io/bbolt v1.3.9 // indirect 41 | go.uber.org/multierr v1.10.0 // indirect 42 | golang.org/x/crypto v0.21.0 // indirect 43 | golang.org/x/mod v0.16.0 // indirect 44 | golang.org/x/net v0.23.0 // indirect 45 | golang.org/x/sys v0.18.0 // indirect 46 | golang.org/x/term v0.18.0 // indirect 47 | golang.org/x/text v0.14.0 // indirect 48 | golang.org/x/tools v0.19.0 // indirect 49 | google.golang.org/protobuf v1.33.0 // indirect 50 | gopkg.in/yaml.v3 v3.0.1 // indirect 51 | rsc.io/tmplfunc v0.0.3 // indirect 52 | ) 53 | -------------------------------------------------------------------------------- /rpc/audit/rpcbinding.go: -------------------------------------------------------------------------------- 1 | // Package audit contains RPC wrappers for NeoFS Audit contract. 2 | // 3 | // Code generated by neo-go contract generate-rpcwrapper --manifest --out [--hash ] [--config ]; DO NOT EDIT. 4 | package audit 5 | 6 | import ( 7 | "crypto/elliptic" 8 | "errors" 9 | "fmt" 10 | "github.com/epicchainlabs/epicchain-go/pkg/core/transaction" 11 | "github.com/epicchainlabs/epicchain-go/pkg/crypto/keys" 12 | "github.com/epicchainlabs/epicchain-go/pkg/neorpc/result" 13 | "github.com/epicchainlabs/epicchain-go/pkg/rpcclient/unwrap" 14 | "github.com/epicchainlabs/epicchain-go/pkg/util" 15 | "github.com/epicchainlabs/epicchain-go/pkg/vm/stackitem" 16 | "math/big" 17 | ) 18 | 19 | // AuditAuditHeader is a contract-specific audit.AuditHeader type used by its methods. 20 | type AuditAuditHeader struct { 21 | Epoch *big.Int 22 | CID []byte 23 | From *keys.PublicKey 24 | } 25 | 26 | // Invoker is used by ContractReader to call various safe methods. 27 | type Invoker interface { 28 | Call(contract util.Uint160, operation string, params ...any) (*result.Invoke, error) 29 | } 30 | 31 | // Actor is used by Contract to call state-changing methods. 32 | type Actor interface { 33 | Invoker 34 | 35 | MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error) 36 | MakeRun(script []byte) (*transaction.Transaction, error) 37 | MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...any) (*transaction.Transaction, error) 38 | MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) 39 | SendCall(contract util.Uint160, method string, params ...any) (util.Uint256, uint32, error) 40 | SendRun(script []byte) (util.Uint256, uint32, error) 41 | } 42 | 43 | // ContractReader implements safe contract methods. 44 | type ContractReader struct { 45 | invoker Invoker 46 | hash util.Uint160 47 | } 48 | 49 | // Contract implements all contract methods. 50 | type Contract struct { 51 | ContractReader 52 | actor Actor 53 | hash util.Uint160 54 | } 55 | 56 | // NewReader creates an instance of ContractReader using provided contract hash and the given Invoker. 57 | func NewReader(invoker Invoker, hash util.Uint160) *ContractReader { 58 | return &ContractReader{invoker, hash} 59 | } 60 | 61 | // New creates an instance of Contract using provided contract hash and the given Actor. 62 | func New(actor Actor, hash util.Uint160) *Contract { 63 | return &Contract{ContractReader{actor, hash}, actor, hash} 64 | } 65 | 66 | // Get invokes `get` method of contract. 67 | func (c *ContractReader) Get(id []byte) ([]byte, error) { 68 | return unwrap.Bytes(c.invoker.Call(c.hash, "get", id)) 69 | } 70 | 71 | // List invokes `list` method of contract. 72 | func (c *ContractReader) List() ([][]byte, error) { 73 | return unwrap.ArrayOfBytes(c.invoker.Call(c.hash, "list")) 74 | } 75 | 76 | // ListByCID invokes `listByCID` method of contract. 77 | func (c *ContractReader) ListByCID(epoch *big.Int, cid []byte) ([][]byte, error) { 78 | return unwrap.ArrayOfBytes(c.invoker.Call(c.hash, "listByCID", epoch, cid)) 79 | } 80 | 81 | // ListByEpoch invokes `listByEpoch` method of contract. 82 | func (c *ContractReader) ListByEpoch(epoch *big.Int) ([][]byte, error) { 83 | return unwrap.ArrayOfBytes(c.invoker.Call(c.hash, "listByEpoch", epoch)) 84 | } 85 | 86 | // ListByNode invokes `listByNode` method of contract. 87 | func (c *ContractReader) ListByNode(epoch *big.Int, cid []byte, key *keys.PublicKey) ([][]byte, error) { 88 | return unwrap.ArrayOfBytes(c.invoker.Call(c.hash, "listByNode", epoch, cid, key)) 89 | } 90 | 91 | // Version invokes `version` method of contract. 92 | func (c *ContractReader) Version() (*big.Int, error) { 93 | return unwrap.BigInt(c.invoker.Call(c.hash, "version")) 94 | } 95 | 96 | // Put creates a transaction invoking `put` method of the contract. 97 | // This transaction is signed and immediately sent to the network. 98 | // The values returned are its hash, ValidUntilBlock value and error if any. 99 | func (c *Contract) Put(rawAuditResult []byte) (util.Uint256, uint32, error) { 100 | return c.actor.SendCall(c.hash, "put", rawAuditResult) 101 | } 102 | 103 | // PutTransaction creates a transaction invoking `put` method of the contract. 104 | // This transaction is signed, but not sent to the network, instead it's 105 | // returned to the caller. 106 | func (c *Contract) PutTransaction(rawAuditResult []byte) (*transaction.Transaction, error) { 107 | return c.actor.MakeCall(c.hash, "put", rawAuditResult) 108 | } 109 | 110 | // PutUnsigned creates a transaction invoking `put` method of the contract. 111 | // This transaction is not signed, it's simply returned to the caller. 112 | // Any fields of it that do not affect fees can be changed (ValidUntilBlock, 113 | // Nonce), fee values (NetworkFee, SystemFee) can be increased as well. 114 | func (c *Contract) PutUnsigned(rawAuditResult []byte) (*transaction.Transaction, error) { 115 | return c.actor.MakeUnsignedCall(c.hash, "put", nil, rawAuditResult) 116 | } 117 | 118 | // Update creates a transaction invoking `update` method of the contract. 119 | // This transaction is signed and immediately sent to the network. 120 | // The values returned are its hash, ValidUntilBlock value and error if any. 121 | func (c *Contract) Update(script []byte, manifest []byte, data any) (util.Uint256, uint32, error) { 122 | return c.actor.SendCall(c.hash, "update", script, manifest, data) 123 | } 124 | 125 | // UpdateTransaction creates a transaction invoking `update` method of the contract. 126 | // This transaction is signed, but not sent to the network, instead it's 127 | // returned to the caller. 128 | func (c *Contract) UpdateTransaction(script []byte, manifest []byte, data any) (*transaction.Transaction, error) { 129 | return c.actor.MakeCall(c.hash, "update", script, manifest, data) 130 | } 131 | 132 | // UpdateUnsigned creates a transaction invoking `update` method of the contract. 133 | // This transaction is not signed, it's simply returned to the caller. 134 | // Any fields of it that do not affect fees can be changed (ValidUntilBlock, 135 | // Nonce), fee values (NetworkFee, SystemFee) can be increased as well. 136 | func (c *Contract) UpdateUnsigned(script []byte, manifest []byte, data any) (*transaction.Transaction, error) { 137 | return c.actor.MakeUnsignedCall(c.hash, "update", nil, script, manifest, data) 138 | } 139 | 140 | // itemToAuditAuditHeader converts stack item into *AuditAuditHeader. 141 | func itemToAuditAuditHeader(item stackitem.Item, err error) (*AuditAuditHeader, error) { 142 | if err != nil { 143 | return nil, err 144 | } 145 | var res = new(AuditAuditHeader) 146 | err = res.FromStackItem(item) 147 | return res, err 148 | } 149 | 150 | // FromStackItem retrieves fields of AuditAuditHeader from the given 151 | // [stackitem.Item] or returns an error if it's not possible to do to so. 152 | func (res *AuditAuditHeader) FromStackItem(item stackitem.Item) error { 153 | arr, ok := item.Value().([]stackitem.Item) 154 | if !ok { 155 | return errors.New("not an array") 156 | } 157 | if len(arr) != 3 { 158 | return errors.New("wrong number of structure elements") 159 | } 160 | 161 | var ( 162 | index = -1 163 | err error 164 | ) 165 | index++ 166 | res.Epoch, err = arr[index].TryInteger() 167 | if err != nil { 168 | return fmt.Errorf("field Epoch: %w", err) 169 | } 170 | 171 | index++ 172 | res.CID, err = arr[index].TryBytes() 173 | if err != nil { 174 | return fmt.Errorf("field CID: %w", err) 175 | } 176 | 177 | index++ 178 | res.From, err = func(item stackitem.Item) (*keys.PublicKey, error) { 179 | b, err := item.TryBytes() 180 | if err != nil { 181 | return nil, err 182 | } 183 | k, err := keys.NewPublicKeyFromBytes(b, elliptic.P256()) 184 | if err != nil { 185 | return nil, err 186 | } 187 | return k, nil 188 | }(arr[index]) 189 | if err != nil { 190 | return fmt.Errorf("field From: %w", err) 191 | } 192 | 193 | return nil 194 | } 195 | -------------------------------------------------------------------------------- /rpc/nns/example_test.go: -------------------------------------------------------------------------------- 1 | package nns_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/epicchainlabs/epicchain-go/pkg/rpcclient" 9 | "github.com/epicchainlabs/epicchain-go/pkg/rpcclient/invoker" 10 | "github.com/epicchainlabs/epicchain-contract/rpc/nns" 11 | ) 12 | 13 | // Resolve addresses of NeoFS smart contracts deployed in a particular 14 | // NeoFS sidechain by their NNS domain names. 15 | func ExampleContractReader_ResolveFSContract() { 16 | const sidechainRPCEndpoint = "https://rpc1.morph.fs.neo.org:40341" 17 | 18 | c, err := rpcclient.New(context.Background(), sidechainRPCEndpoint, rpcclient.Options{}) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | 23 | err = c.Init() 24 | if err != nil { 25 | log.Fatal(err) 26 | } 27 | 28 | nnsAddress, err := nns.InferHash(c) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | nnsContract := nns.NewReader(invoker.New(c, nil), nnsAddress) 34 | 35 | for _, name := range []string{ 36 | nns.NameAudit, 37 | nns.NameBalance, 38 | nns.NameContainer, 39 | nns.NameNeoFSID, 40 | nns.NameNetmap, 41 | nns.NameProxy, 42 | nns.NameReputation, 43 | } { 44 | addr, err := nnsContract.ResolveFSContract(name) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | 49 | fmt.Printf("%s: %s\n", name, addr) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /rpc/nns/hashes.go: -------------------------------------------------------------------------------- 1 | package nns 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/epicchainlabs/epicchain-go/pkg/core/state" 7 | "github.com/epicchainlabs/epicchain-go/pkg/encoding/address" 8 | "github.com/epicchainlabs/epicchain-go/pkg/util" 9 | ) 10 | 11 | // ID is the default NNS contract ID in all NeoFS networks. NeoFS networks 12 | // always deploy NNS first and can't work without it, therefore it always gets 13 | // an ID of 1. 14 | const ID = 1 15 | 16 | // ContractTLD is the default TLD for NeoFS contracts. It's a convention that 17 | // is not likely to be used by any non-NeoFS networks, but for NeoFS ones it 18 | // allows to find contract hashes more easily. 19 | const ContractTLD = "neofs" 20 | 21 | // ContractStateGetter is the interface required for contract state resolution 22 | // using a known contract ID. 23 | type ContractStateGetter interface { 24 | GetContractStateByID(int32) (*state.Contract, error) 25 | } 26 | 27 | // InferHash simplifies resolving NNS contract hash in existing NeoFS networks. 28 | // It assumes that NNS follows [ID] assignment assumptions which likely won't 29 | // be the case for any non-NeoFS network. 30 | func InferHash(sg ContractStateGetter) (util.Uint160, error) { 31 | c, err := sg.GetContractStateByID(ID) 32 | if err != nil { 33 | return util.Uint160{}, err 34 | } 35 | 36 | return c.Hash, nil 37 | } 38 | 39 | // NewInferredReader creates an instance of [ContractReader] using hash obtained via 40 | // [InferHash]. 41 | func NewInferredReader(sg ContractStateGetter, invoker Invoker) (*ContractReader, error) { 42 | h, err := InferHash(sg) 43 | if err != nil { 44 | return nil, err 45 | } 46 | return NewReader(invoker, h), nil 47 | } 48 | 49 | // NewInferred creates an instance of [Contract] using hash obtained via [InferHash]. 50 | func NewInferred(sg ContractStateGetter, actor Actor) (*Contract, error) { 51 | h, err := InferHash(sg) 52 | if err != nil { 53 | return nil, err 54 | } 55 | return New(actor, h), nil 56 | } 57 | 58 | // AddressFromRecord extracts [util.Uint160] hash from the string using one of 59 | // the following formats: 60 | // - hex-encoded LE (reversed) string 61 | // - Neo address ("Nxxxx") 62 | // 63 | // NeoFS used both for contract hashes stored in NNS at various stages of its 64 | // development. 65 | // 66 | // See also: [AddressFromRecords]. 67 | func AddressFromRecord(s string) (util.Uint160, error) { 68 | h, err := util.Uint160DecodeStringLE(s) 69 | if err == nil { 70 | return h, nil 71 | } 72 | 73 | h, err = address.StringToUint160(s) 74 | if err == nil { 75 | return h, nil 76 | } 77 | return util.Uint160{}, errors.New("no valid address found") 78 | } 79 | 80 | // AddressFromRecords extracts [util.Uint160] hash from the set of given 81 | // strings using [AddressFromRecord]. Returns the first result that can be 82 | // interpreted as address. 83 | func AddressFromRecords(strs []string) (util.Uint160, error) { 84 | for i := range strs { 85 | h, err := AddressFromRecord(strs[i]) 86 | if err == nil { 87 | return h, nil 88 | } 89 | } 90 | return util.Uint160{}, errors.New("no valid addresses are found") 91 | } 92 | 93 | // ResolveFSContract is a convenience method that doesn't exist in the NNS 94 | // contract itself (it doesn't care which data is stored there). It assumes 95 | // that contracts follow the [ContractTLD] convention, gets simple contract 96 | // names (like "container" or "netmap") and extracts the hash for the 97 | // respective NNS record using [AddressFromRecords]. 98 | func (c *ContractReader) ResolveFSContract(name string) (util.Uint160, error) { 99 | strs, err := c.Resolve(name+"."+ContractTLD, TXT) 100 | if err != nil { 101 | return util.Uint160{}, err 102 | } 103 | return AddressFromRecords(strs) 104 | } 105 | -------------------------------------------------------------------------------- /rpc/nns/hashes_test.go: -------------------------------------------------------------------------------- 1 | package nns 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/epicchainlabs/epicchain-go/pkg/core/state" 8 | "github.com/epicchainlabs/epicchain-go/pkg/encoding/address" 9 | "github.com/epicchainlabs/epicchain-go/pkg/neorpc/result" 10 | "github.com/epicchainlabs/epicchain-go/pkg/util" 11 | "github.com/epicchainlabs/epicchain-go/pkg/vm/stackitem" 12 | "github.com/google/uuid" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | type stateGetter struct { 17 | f func(int32) (*state.Contract, error) 18 | } 19 | 20 | func (s stateGetter) GetContractStateByID(id int32) (*state.Contract, error) { 21 | return s.f(id) 22 | } 23 | 24 | func TestInferHash(t *testing.T) { 25 | var sg stateGetter 26 | sg.f = func(int32) (*state.Contract, error) { 27 | return nil, errors.New("bad") 28 | } 29 | _, err := InferHash(sg) 30 | require.Error(t, err) 31 | sg.f = func(int32) (*state.Contract, error) { 32 | return &state.Contract{ 33 | ContractBase: state.ContractBase{ 34 | Hash: util.Uint160{0x01, 0x02, 0x03}, 35 | }, 36 | }, nil 37 | } 38 | h, err := InferHash(sg) 39 | require.NoError(t, err) 40 | require.Equal(t, util.Uint160{0x01, 0x02, 0x03}, h) 41 | } 42 | 43 | type testInv struct { 44 | err error 45 | res *result.Invoke 46 | } 47 | 48 | func (t *testInv) Call(contract util.Uint160, operation string, params ...any) (*result.Invoke, error) { 49 | return t.res, t.err 50 | } 51 | 52 | func (t *testInv) CallAndExpandIterator(contract util.Uint160, operation string, i int, params ...any) (*result.Invoke, error) { 53 | return t.res, t.err 54 | } 55 | func (t *testInv) TraverseIterator(uuid.UUID, *result.Iterator, int) ([]stackitem.Item, error) { 56 | return nil, nil 57 | } 58 | func (t *testInv) TerminateSession(uuid.UUID) error { 59 | return nil 60 | } 61 | 62 | func TestBaseErrors(t *testing.T) { 63 | ti := new(testInv) 64 | r := NewReader(ti, util.Uint160{1, 2, 3}) 65 | 66 | ti.err = errors.New("bad") 67 | _, err := r.ResolveFSContract("blah") 68 | require.Error(t, err) 69 | 70 | ti.err = nil 71 | ti.res = &result.Invoke{ 72 | State: "HALT", 73 | Stack: []stackitem.Item{ 74 | stackitem.Make([]stackitem.Item{}), 75 | }, 76 | } 77 | _, err = r.ResolveFSContract("blah") 78 | require.Error(t, err) 79 | 80 | ti.res = &result.Invoke{ 81 | State: "HALT", 82 | Stack: []stackitem.Item{ 83 | stackitem.Make([]stackitem.Item{ 84 | stackitem.Make(100500), 85 | }), 86 | }, 87 | } 88 | _, err = r.ResolveFSContract("blah") 89 | require.Error(t, err) 90 | 91 | h := util.Uint160{1, 2, 3, 4, 5} 92 | ti.res = &result.Invoke{ 93 | State: "HALT", 94 | Stack: []stackitem.Item{ 95 | stackitem.Make([]stackitem.Item{ 96 | stackitem.Make(h.StringLE()), 97 | }), 98 | }, 99 | } 100 | res, err := r.ResolveFSContract("blah") 101 | require.NoError(t, err) 102 | require.Equal(t, h, res) 103 | 104 | ti.res = &result.Invoke{ 105 | State: "HALT", 106 | Stack: []stackitem.Item{ 107 | stackitem.Make([]stackitem.Item{ 108 | stackitem.Make(address.Uint160ToString(h)), 109 | }), 110 | }, 111 | } 112 | res, err = r.ResolveFSContract("blah") 113 | require.NoError(t, err) 114 | require.Equal(t, h, res) 115 | } 116 | -------------------------------------------------------------------------------- /rpc/nns/names.go: -------------------------------------------------------------------------------- 1 | package nns 2 | 3 | // A set of standard contract names deployed into NeoFS sidechain. 4 | const ( 5 | // NameAlphabetPrefix differs from other names in this list, because 6 | // in reality there will be multiple alphabets contract deployed to 7 | // a network named alphabet0, alphabet1, alphabet2, etc. 8 | NameAlphabetPrefix = "alphabet" 9 | NameAudit = "audit" 10 | NameBalance = "balance" 11 | NameContainer = "container" 12 | NameNeoFSID = "neofsid" 13 | NameNetmap = "netmap" 14 | NameProxy = "proxy" 15 | NameReputation = "reputation" 16 | ) 17 | -------------------------------------------------------------------------------- /rpc/nns/recordtype.go: -------------------------------------------------------------------------------- 1 | package nns 2 | 3 | import "math/big" 4 | 5 | // Record types are defined in [RFC 1035](https://tools.ietf.org/html/rfc1035) 6 | // These variables are provided to be used with autogenerated NNS wrapper that 7 | // accepts *big.Int for record type parameters, values MUST NOT be changed. 8 | var ( 9 | // A represents address record type. 10 | A = big.NewInt(1) 11 | // CNAME represents canonical name record type. 12 | CNAME = big.NewInt(5) 13 | // SOA represents start of authority record type. 14 | SOA = big.NewInt(6) 15 | // TXT represents text record type. 16 | TXT = big.NewInt(16) 17 | ) 18 | 19 | // Record types are defined in [RFC 3596](https://tools.ietf.org/html/rfc3596) 20 | // These variables are provided to be used with autogenerated NNS wrapper that 21 | // accepts *big.Int for record type parameters, values MUST NOT be changed. 22 | var ( 23 | // AAAA represents IPv6 address record type. 24 | AAAA = big.NewInt(28) 25 | ) 26 | -------------------------------------------------------------------------------- /rpc/processing/rpcbinding.go: -------------------------------------------------------------------------------- 1 | // Package processing contains RPC wrappers for NeoFS Multi Signature Processing contract. 2 | // 3 | // Code generated by neo-go contract generate-rpcwrapper --manifest --out [--hash ] [--config ]; DO NOT EDIT. 4 | package processing 5 | 6 | import ( 7 | "github.com/epicchainlabs/epicchain-go/pkg/core/transaction" 8 | "github.com/epicchainlabs/epicchain-go/pkg/neorpc/result" 9 | "github.com/epicchainlabs/epicchain-go/pkg/rpcclient/unwrap" 10 | "github.com/epicchainlabs/epicchain-go/pkg/util" 11 | "math/big" 12 | ) 13 | 14 | // Invoker is used by ContractReader to call various safe methods. 15 | type Invoker interface { 16 | Call(contract util.Uint160, operation string, params ...any) (*result.Invoke, error) 17 | } 18 | 19 | // Actor is used by Contract to call state-changing methods. 20 | type Actor interface { 21 | Invoker 22 | 23 | MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error) 24 | MakeRun(script []byte) (*transaction.Transaction, error) 25 | MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...any) (*transaction.Transaction, error) 26 | MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) 27 | SendCall(contract util.Uint160, method string, params ...any) (util.Uint256, uint32, error) 28 | SendRun(script []byte) (util.Uint256, uint32, error) 29 | } 30 | 31 | // ContractReader implements safe contract methods. 32 | type ContractReader struct { 33 | invoker Invoker 34 | hash util.Uint160 35 | } 36 | 37 | // Contract implements all contract methods. 38 | type Contract struct { 39 | ContractReader 40 | actor Actor 41 | hash util.Uint160 42 | } 43 | 44 | // NewReader creates an instance of ContractReader using provided contract hash and the given Invoker. 45 | func NewReader(invoker Invoker, hash util.Uint160) *ContractReader { 46 | return &ContractReader{invoker, hash} 47 | } 48 | 49 | // New creates an instance of Contract using provided contract hash and the given Actor. 50 | func New(actor Actor, hash util.Uint160) *Contract { 51 | return &Contract{ContractReader{actor, hash}, actor, hash} 52 | } 53 | 54 | // Verify invokes `verify` method of contract. 55 | func (c *ContractReader) Verify() (bool, error) { 56 | return unwrap.Bool(c.invoker.Call(c.hash, "verify")) 57 | } 58 | 59 | // Version invokes `version` method of contract. 60 | func (c *ContractReader) Version() (*big.Int, error) { 61 | return unwrap.BigInt(c.invoker.Call(c.hash, "version")) 62 | } 63 | 64 | // Update creates a transaction invoking `update` method of the contract. 65 | // This transaction is signed and immediately sent to the network. 66 | // The values returned are its hash, ValidUntilBlock value and error if any. 67 | func (c *Contract) Update(script []byte, manifest []byte, data any) (util.Uint256, uint32, error) { 68 | return c.actor.SendCall(c.hash, "update", script, manifest, data) 69 | } 70 | 71 | // UpdateTransaction creates a transaction invoking `update` method of the contract. 72 | // This transaction is signed, but not sent to the network, instead it's 73 | // returned to the caller. 74 | func (c *Contract) UpdateTransaction(script []byte, manifest []byte, data any) (*transaction.Transaction, error) { 75 | return c.actor.MakeCall(c.hash, "update", script, manifest, data) 76 | } 77 | 78 | // UpdateUnsigned creates a transaction invoking `update` method of the contract. 79 | // This transaction is not signed, it's simply returned to the caller. 80 | // Any fields of it that do not affect fees can be changed (ValidUntilBlock, 81 | // Nonce), fee values (NetworkFee, SystemFee) can be increased as well. 82 | func (c *Contract) UpdateUnsigned(script []byte, manifest []byte, data any) (*transaction.Transaction, error) { 83 | return c.actor.MakeUnsignedCall(c.hash, "update", nil, script, manifest, data) 84 | } 85 | -------------------------------------------------------------------------------- /rpc/proxy/rpcbinding.go: -------------------------------------------------------------------------------- 1 | // Package proxy contains RPC wrappers for NeoFS Notary Proxy contract. 2 | // 3 | // Code generated by neo-go contract generate-rpcwrapper --manifest --out [--hash ] [--config ]; DO NOT EDIT. 4 | package proxy 5 | 6 | import ( 7 | "github.com/epicchainlabs/epicchain-go/pkg/core/transaction" 8 | "github.com/epicchainlabs/epicchain-go/pkg/neorpc/result" 9 | "github.com/epicchainlabs/epicchain-go/pkg/rpcclient/unwrap" 10 | "github.com/epicchainlabs/epicchain-go/pkg/util" 11 | "math/big" 12 | ) 13 | 14 | // Invoker is used by ContractReader to call various safe methods. 15 | type Invoker interface { 16 | Call(contract util.Uint160, operation string, params ...any) (*result.Invoke, error) 17 | } 18 | 19 | // Actor is used by Contract to call state-changing methods. 20 | type Actor interface { 21 | Invoker 22 | 23 | MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error) 24 | MakeRun(script []byte) (*transaction.Transaction, error) 25 | MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...any) (*transaction.Transaction, error) 26 | MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) 27 | SendCall(contract util.Uint160, method string, params ...any) (util.Uint256, uint32, error) 28 | SendRun(script []byte) (util.Uint256, uint32, error) 29 | } 30 | 31 | // ContractReader implements safe contract methods. 32 | type ContractReader struct { 33 | invoker Invoker 34 | hash util.Uint160 35 | } 36 | 37 | // Contract implements all contract methods. 38 | type Contract struct { 39 | ContractReader 40 | actor Actor 41 | hash util.Uint160 42 | } 43 | 44 | // NewReader creates an instance of ContractReader using provided contract hash and the given Invoker. 45 | func NewReader(invoker Invoker, hash util.Uint160) *ContractReader { 46 | return &ContractReader{invoker, hash} 47 | } 48 | 49 | // New creates an instance of Contract using provided contract hash and the given Actor. 50 | func New(actor Actor, hash util.Uint160) *Contract { 51 | return &Contract{ContractReader{actor, hash}, actor, hash} 52 | } 53 | 54 | // Verify invokes `verify` method of contract. 55 | func (c *ContractReader) Verify() (bool, error) { 56 | return unwrap.Bool(c.invoker.Call(c.hash, "verify")) 57 | } 58 | 59 | // Version invokes `version` method of contract. 60 | func (c *ContractReader) Version() (*big.Int, error) { 61 | return unwrap.BigInt(c.invoker.Call(c.hash, "version")) 62 | } 63 | 64 | // Update creates a transaction invoking `update` method of the contract. 65 | // This transaction is signed and immediately sent to the network. 66 | // The values returned are its hash, ValidUntilBlock value and error if any. 67 | func (c *Contract) Update(script []byte, manifest []byte, data any) (util.Uint256, uint32, error) { 68 | return c.actor.SendCall(c.hash, "update", script, manifest, data) 69 | } 70 | 71 | // UpdateTransaction creates a transaction invoking `update` method of the contract. 72 | // This transaction is signed, but not sent to the network, instead it's 73 | // returned to the caller. 74 | func (c *Contract) UpdateTransaction(script []byte, manifest []byte, data any) (*transaction.Transaction, error) { 75 | return c.actor.MakeCall(c.hash, "update", script, manifest, data) 76 | } 77 | 78 | // UpdateUnsigned creates a transaction invoking `update` method of the contract. 79 | // This transaction is not signed, it's simply returned to the caller. 80 | // Any fields of it that do not affect fees can be changed (ValidUntilBlock, 81 | // Nonce), fee values (NetworkFee, SystemFee) can be increased as well. 82 | func (c *Contract) UpdateUnsigned(script []byte, manifest []byte, data any) (*transaction.Transaction, error) { 83 | return c.actor.MakeUnsignedCall(c.hash, "update", nil, script, manifest, data) 84 | } 85 | -------------------------------------------------------------------------------- /rpc/reputation/rpcbinding.go: -------------------------------------------------------------------------------- 1 | // Package reputation contains RPC wrappers for NeoFS Reputation contract. 2 | // 3 | // Code generated by neo-go contract generate-rpcwrapper --manifest --out [--hash ] [--config ]; DO NOT EDIT. 4 | package reputation 5 | 6 | import ( 7 | "crypto/elliptic" 8 | "errors" 9 | "fmt" 10 | "github.com/epicchainlabs/epicchain-go/pkg/core/transaction" 11 | "github.com/epicchainlabs/epicchain-go/pkg/crypto/keys" 12 | "github.com/epicchainlabs/epicchain-go/pkg/neorpc/result" 13 | "github.com/epicchainlabs/epicchain-go/pkg/rpcclient/unwrap" 14 | "github.com/epicchainlabs/epicchain-go/pkg/util" 15 | "github.com/epicchainlabs/epicchain-go/pkg/vm/stackitem" 16 | "math/big" 17 | ) 18 | 19 | // CommonBallot is a contract-specific common.Ballot type used by its methods. 20 | type CommonBallot struct { 21 | ID []byte 22 | Voters keys.PublicKeys 23 | Height *big.Int 24 | } 25 | 26 | // Invoker is used by ContractReader to call various safe methods. 27 | type Invoker interface { 28 | Call(contract util.Uint160, operation string, params ...any) (*result.Invoke, error) 29 | } 30 | 31 | // Actor is used by Contract to call state-changing methods. 32 | type Actor interface { 33 | Invoker 34 | 35 | MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error) 36 | MakeRun(script []byte) (*transaction.Transaction, error) 37 | MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...any) (*transaction.Transaction, error) 38 | MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) 39 | SendCall(contract util.Uint160, method string, params ...any) (util.Uint256, uint32, error) 40 | SendRun(script []byte) (util.Uint256, uint32, error) 41 | } 42 | 43 | // ContractReader implements safe contract methods. 44 | type ContractReader struct { 45 | invoker Invoker 46 | hash util.Uint160 47 | } 48 | 49 | // Contract implements all contract methods. 50 | type Contract struct { 51 | ContractReader 52 | actor Actor 53 | hash util.Uint160 54 | } 55 | 56 | // NewReader creates an instance of ContractReader using provided contract hash and the given Invoker. 57 | func NewReader(invoker Invoker, hash util.Uint160) *ContractReader { 58 | return &ContractReader{invoker, hash} 59 | } 60 | 61 | // New creates an instance of Contract using provided contract hash and the given Actor. 62 | func New(actor Actor, hash util.Uint160) *Contract { 63 | return &Contract{ContractReader{actor, hash}, actor, hash} 64 | } 65 | 66 | // Get invokes `get` method of contract. 67 | func (c *ContractReader) Get(epoch *big.Int, peerID []byte) ([][]byte, error) { 68 | return unwrap.ArrayOfBytes(c.invoker.Call(c.hash, "get", epoch, peerID)) 69 | } 70 | 71 | // GetByID invokes `getByID` method of contract. 72 | func (c *ContractReader) GetByID(id []byte) ([][]byte, error) { 73 | return unwrap.ArrayOfBytes(c.invoker.Call(c.hash, "getByID", id)) 74 | } 75 | 76 | // ListByEpoch invokes `listByEpoch` method of contract. 77 | func (c *ContractReader) ListByEpoch(epoch *big.Int) ([][]byte, error) { 78 | return unwrap.ArrayOfBytes(c.invoker.Call(c.hash, "listByEpoch", epoch)) 79 | } 80 | 81 | // Put creates a transaction invoking `put` method of the contract. 82 | // This transaction is signed and immediately sent to the network. 83 | // The values returned are its hash, ValidUntilBlock value and error if any. 84 | func (c *Contract) Put(epoch *big.Int, peerID []byte, value []byte) (util.Uint256, uint32, error) { 85 | return c.actor.SendCall(c.hash, "put", epoch, peerID, value) 86 | } 87 | 88 | // PutTransaction creates a transaction invoking `put` method of the contract. 89 | // This transaction is signed, but not sent to the network, instead it's 90 | // returned to the caller. 91 | func (c *Contract) PutTransaction(epoch *big.Int, peerID []byte, value []byte) (*transaction.Transaction, error) { 92 | return c.actor.MakeCall(c.hash, "put", epoch, peerID, value) 93 | } 94 | 95 | // PutUnsigned creates a transaction invoking `put` method of the contract. 96 | // This transaction is not signed, it's simply returned to the caller. 97 | // Any fields of it that do not affect fees can be changed (ValidUntilBlock, 98 | // Nonce), fee values (NetworkFee, SystemFee) can be increased as well. 99 | func (c *Contract) PutUnsigned(epoch *big.Int, peerID []byte, value []byte) (*transaction.Transaction, error) { 100 | return c.actor.MakeUnsignedCall(c.hash, "put", nil, epoch, peerID, value) 101 | } 102 | 103 | // Update creates a transaction invoking `update` method of the contract. 104 | // This transaction is signed and immediately sent to the network. 105 | // The values returned are its hash, ValidUntilBlock value and error if any. 106 | func (c *Contract) Update(script []byte, manifest []byte, data any) (util.Uint256, uint32, error) { 107 | return c.actor.SendCall(c.hash, "update", script, manifest, data) 108 | } 109 | 110 | // UpdateTransaction creates a transaction invoking `update` method of the contract. 111 | // This transaction is signed, but not sent to the network, instead it's 112 | // returned to the caller. 113 | func (c *Contract) UpdateTransaction(script []byte, manifest []byte, data any) (*transaction.Transaction, error) { 114 | return c.actor.MakeCall(c.hash, "update", script, manifest, data) 115 | } 116 | 117 | // UpdateUnsigned creates a transaction invoking `update` method of the contract. 118 | // This transaction is not signed, it's simply returned to the caller. 119 | // Any fields of it that do not affect fees can be changed (ValidUntilBlock, 120 | // Nonce), fee values (NetworkFee, SystemFee) can be increased as well. 121 | func (c *Contract) UpdateUnsigned(script []byte, manifest []byte, data any) (*transaction.Transaction, error) { 122 | return c.actor.MakeUnsignedCall(c.hash, "update", nil, script, manifest, data) 123 | } 124 | 125 | // Version creates a transaction invoking `version` method of the contract. 126 | // This transaction is signed and immediately sent to the network. 127 | // The values returned are its hash, ValidUntilBlock value and error if any. 128 | func (c *Contract) Version() (util.Uint256, uint32, error) { 129 | return c.actor.SendCall(c.hash, "version") 130 | } 131 | 132 | // VersionTransaction creates a transaction invoking `version` method of the contract. 133 | // This transaction is signed, but not sent to the network, instead it's 134 | // returned to the caller. 135 | func (c *Contract) VersionTransaction() (*transaction.Transaction, error) { 136 | return c.actor.MakeCall(c.hash, "version") 137 | } 138 | 139 | // VersionUnsigned creates a transaction invoking `version` method of the contract. 140 | // This transaction is not signed, it's simply returned to the caller. 141 | // Any fields of it that do not affect fees can be changed (ValidUntilBlock, 142 | // Nonce), fee values (NetworkFee, SystemFee) can be increased as well. 143 | func (c *Contract) VersionUnsigned() (*transaction.Transaction, error) { 144 | return c.actor.MakeUnsignedCall(c.hash, "version", nil) 145 | } 146 | 147 | // itemToCommonBallot converts stack item into *CommonBallot. 148 | func itemToCommonBallot(item stackitem.Item, err error) (*CommonBallot, error) { 149 | if err != nil { 150 | return nil, err 151 | } 152 | var res = new(CommonBallot) 153 | err = res.FromStackItem(item) 154 | return res, err 155 | } 156 | 157 | // FromStackItem retrieves fields of CommonBallot from the given 158 | // [stackitem.Item] or returns an error if it's not possible to do to so. 159 | func (res *CommonBallot) FromStackItem(item stackitem.Item) error { 160 | arr, ok := item.Value().([]stackitem.Item) 161 | if !ok { 162 | return errors.New("not an array") 163 | } 164 | if len(arr) != 3 { 165 | return errors.New("wrong number of structure elements") 166 | } 167 | 168 | var ( 169 | index = -1 170 | err error 171 | ) 172 | index++ 173 | res.ID, err = arr[index].TryBytes() 174 | if err != nil { 175 | return fmt.Errorf("field ID: %w", err) 176 | } 177 | 178 | index++ 179 | res.Voters, err = func(item stackitem.Item) (keys.PublicKeys, error) { 180 | arr, ok := item.Value().([]stackitem.Item) 181 | if !ok { 182 | return nil, errors.New("not an array") 183 | } 184 | res := make(keys.PublicKeys, len(arr)) 185 | for i := range res { 186 | res[i], err = func(item stackitem.Item) (*keys.PublicKey, error) { 187 | b, err := item.TryBytes() 188 | if err != nil { 189 | return nil, err 190 | } 191 | k, err := keys.NewPublicKeyFromBytes(b, elliptic.P256()) 192 | if err != nil { 193 | return nil, err 194 | } 195 | return k, nil 196 | }(arr[i]) 197 | if err != nil { 198 | return nil, fmt.Errorf("item %d: %w", i, err) 199 | } 200 | } 201 | return res, nil 202 | }(arr[index]) 203 | if err != nil { 204 | return fmt.Errorf("field Voters: %w", err) 205 | } 206 | 207 | index++ 208 | res.Height, err = arr[index].TryInteger() 209 | if err != nil { 210 | return fmt.Errorf("field Height: %w", err) 211 | } 212 | 213 | return nil 214 | } 215 | -------------------------------------------------------------------------------- /tests/alphabet_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "path" 5 | "testing" 6 | 7 | "github.com/epicchainlabs/epicchain-contract/common" 8 | "github.com/epicchainlabs/epicchain-contract/contracts/container" 9 | "github.com/epicchainlabs/epicchain-go/pkg/core/native/nativenames" 10 | "github.com/epicchainlabs/epicchain-go/pkg/neotest" 11 | "github.com/epicchainlabs/epicchain-go/pkg/util" 12 | "github.com/epicchainlabs/epicchain-go/pkg/vm" 13 | "github.com/epicchainlabs/epicchain-go/pkg/vm/stackitem" 14 | "github.com/epicchainlabs/epicchain-go/pkg/wallet" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | const alphabetPath = "../contracts/alphabet" 19 | 20 | func deployAlphabetContract(t *testing.T, e *neotest.Executor, addrNetmap, addrProxy *util.Uint160, name string, index, total int64) util.Uint160 { 21 | c := neotest.CompileFile(t, e.CommitteeHash, alphabetPath, path.Join(alphabetPath, "config.yml")) 22 | 23 | args := make([]any, 6) 24 | args[0] = false 25 | args[1] = addrNetmap 26 | args[2] = addrProxy 27 | args[3] = name 28 | args[4] = index 29 | args[5] = total 30 | 31 | e.DeployContract(t, c, args) 32 | return c.Hash 33 | } 34 | 35 | func newAlphabetInvoker(t *testing.T, autohashes bool) (*neotest.Executor, *neotest.ContractInvoker) { 36 | e := newExecutor(t) 37 | 38 | ctrNetmap := neotest.CompileFile(t, e.CommitteeHash, netmapPath, path.Join(netmapPath, "config.yml")) 39 | ctrBalance := neotest.CompileFile(t, e.CommitteeHash, balancePath, path.Join(balancePath, "config.yml")) 40 | ctrContainer := neotest.CompileFile(t, e.CommitteeHash, containerPath, path.Join(containerPath, "config.yml")) 41 | ctrProxy := neotest.CompileFile(t, e.CommitteeHash, proxyPath, path.Join(proxyPath, "config.yml")) 42 | 43 | nnsHash := deployDefaultNNS(t, e) 44 | deployNetmapContract(t, e, container.RegistrationFeeKey, int64(containerFee), 45 | container.AliasFeeKey, int64(containerAliasFee)) 46 | deployBalanceContract(t, e, ctrNetmap.Hash, ctrContainer.Hash) 47 | deployContainerContract(t, e, &ctrNetmap.Hash, &ctrBalance.Hash, &nnsHash) 48 | deployProxyContract(t, e) 49 | 50 | var addrNetmap, addrProxy *util.Uint160 51 | if !autohashes { 52 | addrNetmap, addrProxy = &ctrNetmap.Hash, &ctrProxy.Hash 53 | } 54 | hash := deployAlphabetContract(t, e, addrNetmap, addrProxy, "Az", 0, 1) 55 | 56 | alphabet := getAlphabetAcc(t, e) 57 | 58 | setAlphabetRole(t, e, alphabet.PrivateKey().PublicKey().Bytes()) 59 | 60 | return e, e.CommitteeInvoker(hash) 61 | } 62 | 63 | func TestEmit(t *testing.T) { 64 | for autohashes, name := range map[bool]string{ 65 | false: "standard deploy", 66 | true: "deploy with no hashes", 67 | } { 68 | t.Run(name, func(t *testing.T) { 69 | _, c := newAlphabetInvoker(t, autohashes) 70 | 71 | const method = "emit" 72 | 73 | alphabet := getAlphabetAcc(t, c.Executor) 74 | 75 | cCommittee := c.WithSigners(neotest.NewSingleSigner(alphabet)) 76 | cCommittee.InvokeFail(t, "no gas to emit", method) 77 | 78 | transferNeoToContract(t, c) 79 | 80 | cCommittee.Invoke(t, stackitem.Null{}, method) 81 | 82 | notAlphabet := c.NewAccount(t) 83 | cNotAlphabet := c.WithSigners(notAlphabet) 84 | 85 | cNotAlphabet.InvokeFail(t, "invalid invoker", method) 86 | }) 87 | } 88 | } 89 | 90 | func TestVote(t *testing.T) { 91 | for autohashes, name := range map[bool]string{ 92 | false: "standard deploy", 93 | true: "deploy with no hashes", 94 | } { 95 | t.Run(name, func(t *testing.T) { 96 | e, c := newAlphabetInvoker(t, autohashes) 97 | 98 | const method = "vote" 99 | 100 | newAlphabet := c.NewAccount(t) 101 | newAlphabetPub, ok := vm.ParseSignatureContract(newAlphabet.Script()) 102 | require.True(t, ok) 103 | cNewAlphabet := c.WithSigners(newAlphabet) 104 | 105 | cNewAlphabet.InvokeFail(t, common.ErrAlphabetWitnessFailed, method, int64(0), []any{newAlphabetPub}) 106 | c.InvokeFail(t, "invalid epoch", method, int64(1), []any{newAlphabetPub}) 107 | 108 | setAlphabetRole(t, e, newAlphabetPub) 109 | transferNeoToContract(t, c) 110 | 111 | neoSH := e.NativeHash(t, nativenames.Neo) 112 | neoInvoker := c.CommitteeInvoker(neoSH) 113 | 114 | gasSH := e.NativeHash(t, nativenames.Gas) 115 | gasInvoker := e.CommitteeInvoker(gasSH) 116 | 117 | res, err := gasInvoker.TestInvoke(t, "balanceOf", gasInvoker.Committee.ScriptHash()) 118 | require.NoError(t, err) 119 | 120 | // transfer some GAS to the new alphabet node 121 | gasInvoker.Invoke(t, stackitem.NewBool(true), "transfer", gasInvoker.Committee.ScriptHash(), newAlphabet.ScriptHash(), res.Top().BigInt().Int64()/2, nil) 122 | 123 | newInvoker := neoInvoker.WithSigners(newAlphabet) 124 | 125 | newInvoker.Invoke(t, stackitem.NewBool(true), "registerCandidate", newAlphabetPub) 126 | c.Invoke(t, stackitem.Null{}, method, int64(0), []any{newAlphabetPub}) 127 | 128 | // wait one block util 129 | // a new committee is accepted 130 | c.AddNewBlock(t) 131 | 132 | cNewAlphabet.Invoke(t, stackitem.Null{}, "emit") 133 | c.InvokeFail(t, "invalid invoker", "emit") 134 | }) 135 | } 136 | } 137 | 138 | func transferNeoToContract(t *testing.T, invoker *neotest.ContractInvoker) { 139 | neoSH, err := invoker.Chain.GetNativeContractScriptHash(nativenames.Neo) 140 | require.NoError(t, err) 141 | 142 | neoInvoker := invoker.CommitteeInvoker(neoSH) 143 | 144 | res, err := neoInvoker.TestInvoke(t, "balanceOf", neoInvoker.Committee.ScriptHash()) 145 | require.NoError(t, err) 146 | 147 | // transfer all NEO to alphabet contract 148 | neoInvoker.Invoke(t, stackitem.NewBool(true), "transfer", neoInvoker.Committee.ScriptHash(), invoker.Hash, res.Top().BigInt().Int64(), nil) 149 | } 150 | 151 | func getAlphabetAcc(t *testing.T, e *neotest.Executor) *wallet.Account { 152 | multi, ok := e.Committee.(neotest.MultiSigner) 153 | require.True(t, ok) 154 | 155 | return multi.Single(0).Account() 156 | } 157 | 158 | func TestAlphabetVerify(t *testing.T) { 159 | _, contract := newAlphabetInvoker(t, false) 160 | testVerify(t, contract) 161 | } 162 | -------------------------------------------------------------------------------- /tests/balance_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "path" 5 | "testing" 6 | 7 | "github.com/epicchainlabs/epicchain-go/pkg/neotest" 8 | "github.com/epicchainlabs/epicchain-go/pkg/util" 9 | "github.com/epicchainlabs/epicchain-go/pkg/vm/stackitem" 10 | ) 11 | 12 | const balancePath = "../contracts/balance" 13 | 14 | func deployBalanceContract(t *testing.T, e *neotest.Executor, addrNetmap, addrContainer util.Uint160) util.Uint160 { 15 | c := neotest.CompileFile(t, e.CommitteeHash, balancePath, path.Join(balancePath, "config.yml")) 16 | 17 | args := make([]any, 3) 18 | args[0] = false 19 | args[1] = addrNetmap 20 | args[2] = addrContainer 21 | 22 | e.DeployContract(t, c, args) 23 | regContractNNS(t, e, "balance", c.Hash) 24 | return c.Hash 25 | } 26 | 27 | func balanceMint(t *testing.T, c *neotest.ContractInvoker, acc neotest.Signer, amount int64, details []byte) { 28 | c.Invoke(t, stackitem.Null{}, "mint", acc.ScriptHash(), amount, details) 29 | } 30 | -------------------------------------------------------------------------------- /tests/dump/common.go: -------------------------------------------------------------------------------- 1 | package dump 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "strconv" 10 | "strings" 11 | 12 | "github.com/epicchainlabs/epicchain-go/pkg/core/state" 13 | ) 14 | 15 | // ID is a unique identifier of the dump prepared according to the model 16 | // described in the current package. 17 | type ID struct { 18 | // Label of the dump source (e.g. testnet, mainnet). 19 | Label string 20 | // Blockchain height at which the state was pulled. 21 | Block uint32 22 | } 23 | 24 | // String returns hyphen-separated ID fields. 25 | func (x ID) String() string { 26 | return x.Label + sep + strconv.FormatUint(uint64(x.Block), 10) 27 | } 28 | 29 | // decodes ID fields from the hyphen-separated string. 30 | func (x *ID) decodeString(s string) error { 31 | ss := strings.Split(s, sep) 32 | if len(ss) < 2 { 33 | return fmt.Errorf("expected '%s'-separated string with at least 2 items", sep) 34 | } 35 | 36 | n, err := strconv.ParseUint(ss[1], 10, 32) 37 | if err != nil { 38 | return fmt.Errorf("decode block number from '%s': %w", ss[1], err) 39 | } 40 | 41 | x.Label = ss[0] 42 | x.Block = uint32(n) 43 | 44 | return nil 45 | } 46 | 47 | // global encoding of binary values. 48 | var _encoding = base64.StdEncoding 49 | 50 | // dumpContractState is a JSON-encoded information about the dumped contract. 51 | type dumpContractState struct { 52 | Name string `json:"name"` 53 | State state.Contract `json:"state"` 54 | } 55 | 56 | // dumpStreams groups data streams for contracts' states and storages. 57 | type dumpStreams struct { 58 | contracts, storageItems io.ReadWriteCloser 59 | } 60 | 61 | // close closes all streams. 62 | func (x *dumpStreams) close() { 63 | _ = x.storageItems.Close() 64 | _ = x.contracts.Close() 65 | } 66 | 67 | const ( 68 | // word separator used in dump file naming 69 | sep = "-" 70 | // suffix of file with contracts' states 71 | statesFileSuffix = "contracts.json" 72 | ) 73 | 74 | // initDumpStreams opens data streams for the dump files located in the 75 | // specified directory. If read flag is set, streams are read-only. Otherwise, 76 | // files must not exist, and streams are write only. 77 | func initDumpStreams(d *dumpStreams, dir string, id ID, read bool) error { 78 | var err error 79 | 80 | pathStorage := filepath.Join(dir, strings.Join([]string{id.String(), "storage.csv"}, sep)) 81 | if !read { 82 | if err = checkFileNotExists(pathStorage); err != nil { 83 | return err 84 | } 85 | } 86 | 87 | pathContracts := filepath.Join(dir, strings.Join([]string{id.String(), statesFileSuffix}, sep)) 88 | if !read { 89 | if err = checkFileNotExists(pathContracts); err != nil { 90 | return err 91 | } 92 | } 93 | 94 | var flag int 95 | var perm os.FileMode 96 | 97 | if read { 98 | flag = os.O_RDONLY 99 | } else { 100 | flag = os.O_CREATE | os.O_WRONLY 101 | perm = 0600 102 | } 103 | 104 | d.storageItems, err = os.OpenFile(pathStorage, flag, perm) 105 | if err != nil { 106 | return fmt.Errorf("open file with storage items: %w", err) 107 | } 108 | 109 | d.contracts, err = os.OpenFile(pathContracts, flag, perm) 110 | if err != nil { 111 | return fmt.Errorf("open file with contract states: %w", err) 112 | } 113 | 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /tests/dump/creator.go: -------------------------------------------------------------------------------- 1 | package dump 2 | 3 | import ( 4 | "encoding/csv" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/epicchainlabs/epicchain-go/pkg/core/state" 9 | ) 10 | 11 | // Creator dumps states of the Neo smart contracts. Output file format: 12 | // 13 | // '