├── .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 |
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 | // '