├── .github
├── CODEOWNERS
├── logo.svg
└── workflows
│ ├── dco.yml
│ └── go.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE.md
├── Makefile
├── README.md
├── VERSION
├── alphabet
├── alphabet_contract.go
├── config.yml
└── doc.go
├── audit
├── audit_contract.go
├── config.yml
└── doc.go
├── balance
├── balance_contract.go
├── config.yml
└── doc.go
├── common
├── ir.go
├── storage.go
├── transfer.go
├── update.go
├── version.go
├── vote.go
└── witness.go
├── container
├── config.yml
├── container_contract.go
└── doc.go
├── debian
├── changelog
├── control
├── copyright
├── neofs-contract.docs
├── postinst.ex
├── postrm.ex
├── preinst.ex
├── prerm.ex
├── rules
└── source
│ └── format
├── frostfs
├── config.yml
├── doc.go
└── frostfs_contract.go
├── frostfsid
├── config.yml
├── doc.go
└── frostfsid_contract.go
├── go.mod
├── go.sum
├── netmap
├── config.yml
├── doc.go
└── netmap_contract.go
├── nns
├── config.yml
├── namestate.go
├── nns.yml
├── nns_contract.go
└── recordtype.go
├── processing
├── config.yml
├── doc.go
└── processing_contract.go
├── proxy
├── config.yml
├── doc.go
└── proxy_contract.go
├── reputation
├── config.yml
├── doc.go
└── reputation_contract.go
├── subnet
├── config.yml
├── doc.go
└── subnet_contract.go
└── tests
├── alphabet_test.go
├── balance_test.go
├── container_test.go
├── frostfs_test.go
├── frostfsid_test.go
├── helpers.go
├── netmap_test.go
├── nns_test.go
├── processing_test.go
├── proxy_test.go
├── reputation_test.go
├── subnet_test.go
├── util.go
└── version_test.go
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @carpawell @fyrchik @cthulhu-rider
2 |
--------------------------------------------------------------------------------
/.github/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
71 |
--------------------------------------------------------------------------------
/.github/workflows/dco.yml:
--------------------------------------------------------------------------------
1 | name: DCO check
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | commits_check_job:
10 | runs-on: ubuntu-latest
11 | name: Commits Check
12 | steps:
13 | - name: Get PR Commits
14 | id: 'get-pr-commits'
15 | uses: tim-actions/get-pr-commits@master
16 | with:
17 | token: ${{ secrets.GITHUB_TOKEN }}
18 | - name: DCO Check
19 | uses: tim-actions/dco@master
20 | with:
21 | commits: ${{ steps.get-pr-commits.outputs.commits }}
22 |
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 |
3 | on:
4 | pull_request:
5 | branches: [ master ]
6 |
7 | jobs:
8 |
9 | tests:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 |
14 | - name: Set up Go
15 | uses: actions/setup-go@v2
16 | with:
17 | go-version: 1.17
18 |
19 | - name: Test
20 | run: go test -v ./...
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.avm
2 | *.nef
3 | *.manifest.json
4 | config.json
5 | /vendor/
6 | .idea
7 | /bin/
8 |
9 | # debhelpers
10 | **/.debhelper
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 | Changelog for FrostFS Contract
3 |
4 | ## [Unreleased]
5 |
6 | ### Added
7 | ### Changed
8 | ### Updated
9 | - `neo-go` to `v0.99.4`
10 |
11 | ### Fixed
12 | ### Updating from v0.16.0
13 |
14 | ## [0.16.0] - 2022-10-17 - Anmado (안마도, 鞍馬島)
15 |
16 | ### Added
17 | - Support `MAINTENANCE` state of storage nodes (#269)
18 |
19 | ### Changed
20 | - `netmap.Snapshot` and all similar methods return (#269)
21 |
22 | ### Updated
23 | - NNS contract now sets domain expiration based on `register` arguments (#262)
24 |
25 | ### Fixed
26 | - NNS `renew` now can only be done by the domain owner
27 |
28 | ### Updating from v0.15.5
29 | Update deployed `Netmap` contract using `Update` method: storage of the contract
30 | has been incompatibly changed.
31 |
32 | ## [0.15.5] - 2022-08-23
33 |
34 | ### Updated
35 | - Update neo-go to v0.99.2 (#261)
36 | - Makefile now takes only `v*` tags into account (#255)
37 |
38 | ## [0.15.4] - 2022-07-27
39 | Only a version bump to update manifest.
40 |
41 | ## [0.15.3] - 2022-07-22
42 |
43 | ### Added
44 | - Allow to build archive from source (#250)
45 |
46 | ### Changed
47 | - Update neo-go to the latest version
48 | - Use proper type for integer constants (#248)
49 |
50 | ## [0.15.2] - 2022-06-07
51 |
52 | ### Added
53 | - `container.Count` method (#242)
54 |
55 | ### Changed
56 | - Update neo-go to v0.99.0 (#246)
57 |
58 | ## [0.15.1] - 2022-04-13
59 |
60 | ### Fixed
61 | - Max domain name fragement length (#238)
62 |
63 | ### Added
64 | - `netmap.UpdateSnapshotCount` method (#232)
65 | - Notifications of successful container and storage node operations (#236)
66 |
67 | ### Changed
68 | - Update neo-go to v0.98.2 (#234)
69 |
70 | ## [0.15.0] - 2022-03-23 - Heuksando (흑산도, 黑山島)
71 |
72 | ### Fixed
73 | - Split `UpdateState` method to allow Alphabet nodes remove storage nodes from
74 | network map based on consensus decision in notary-enabled environment (#225)
75 |
76 | ### Changed
77 | - Increase from 2 to 10 stored network maps in netmap contract (#224)
78 | - Use public keys instead of `IRNode` structures in neofs and netmap contracts
79 | (#222)
80 |
81 | ## [0.14.2] - 2022-02-07
82 |
83 | ### Fixed
84 | - Remove duplicate records in NNS contract (#196)
85 |
86 | ### Changed
87 | - Evict container estimations on every put (#215)
88 | - Update neo-go to v0.98.1
89 |
90 | ## [0.14.1] - 2022-01-24
91 |
92 | ### Fixed
93 | - Remove migration routine for reputation contract update (#220)
94 | - Remove version check for subnet contract update (#220)
95 |
96 | ### Added
97 | - Append version to `Update` arguments for subnet contract (#220)
98 |
99 | ## [0.14.0] - 2022-01-14 - Geojedo (거제도, 巨濟島)
100 |
101 | ### Fixed
102 | - Sync `Update` method signature in NNS contract (#197)
103 | - Use current block index in all `GetDisgnatedByRole` invocations (#209)
104 |
105 | ### Added
106 | - Version check during contract update (#204)
107 |
108 | ### Changed
109 | - Use `storage.RemovePrefix` in subnet contract (#199)
110 |
111 | ### Removed
112 | - Netmap contract hash usage in proxy contract (#205)
113 | - Legacy contract owner records from contract storage (#202)
114 |
115 | ## [0.13.2] - 2021-12-14
116 |
117 | ### Fixed
118 | - Reputation contract migration (#201)
119 |
120 | ## [0.13.1] - 2021-12-08
121 |
122 | ### Fixed
123 | - Specify container contract as owner of all container related domain zones
124 | (#194)
125 |
126 | ## [0.13.0] - 2021-12-07 - Sinjido (신지도, 薪智島)
127 |
128 | Support of subnetwork contract from NeoFS API v2.11.0.
129 |
130 | ### Fixed
131 | - Records with duplicate values are not allowed in NNS anymore (#165)
132 | - Allow multiple `reputation.Put`, `container.PutCotnainerSize`,
133 | `neofsid.AddKey`, `neofsid.RemoveKey`, `neofs.InnerRingCandidateAdd`,
134 | `neofs.InnerRingCandidateRemove` invocations in one block (#101)
135 | - `netmap.UpdateState` checks both node and alphabet signatures in notary
136 | enabled environment (#154)
137 |
138 | ### Added
139 | - Version method in NNS contract (#158)
140 | - Subnet contract (#122)
141 | - `netmap.Register` method for notary enabled environment (#154)
142 |
143 | ### Changed
144 | - Container contract throws panic if required container is missing (#142)
145 | - Container contract does not throw panic if deleting container is already
146 | removed (#142)
147 | - NNS stores root as regular TLD (#139)
148 | - Use testing framework from neo-go (#161)
149 | - Allow hyphen in domain names in NNS (#180)
150 | - Panic messages do not heave method name prefix anymore (#179)
151 | - `OnNEP17Payment` method calls `Abort` instead of panic (#179)
152 | - Allow arbitrary-level domains in NNS (#172)
153 | - Refactor (#169)
154 |
155 | ## [0.12.2] - 2021-11-26
156 |
157 | ### Fixed
158 | - Domain owner check in container contract (#156)
159 | - Missing NNS related keys in container contract (#181)
160 |
161 | ### Added
162 | - Update functions now provide contract version in data (#164)
163 |
164 | ## [0.12.1] - 2021-10-19
165 |
166 | ### Fixed
167 | - Sanity checks for notary enabled environment in container contract (#149)
168 |
169 | ### Added
170 | - NeoFS global configuration parameter `ContainerAliasFee`. This parameter
171 | used as additional fee for container registration with nice name alias (#148).
172 |
173 | ### Changed
174 | - `netmap.AddPeer` method can update `NodeInfo` structures (#146)
175 | - `netmap.Update` allows to redefine any key-value pair of global config (#151)
176 |
177 | ## [0.12.0] - 2021-10-15 - Udo (우도, 牛島)
178 |
179 | NNS update with native container names in container contract.
180 |
181 | ### Fixed
182 | - Safe methods list in reputation contract manifest (#144)
183 |
184 | ### Added
185 | - SOA record type support in NNS (#125)
186 | - Test framework for N3 contracts written in go (#137)
187 | - Unit tests for container and NNS contracts (#135, #137)
188 | - `PutNamed` method in container contract that registers domain in NNS (#135)
189 |
190 | ### Changed
191 | - NNS contract supports multiple records of the same type (#125)
192 |
193 | ## [0.11.0] - 2021-09-22 - Mungapdo (문갑도, 文甲島)
194 |
195 | Contract owners are removed, now side chain committee is in charge of contract
196 | update routine.
197 |
198 | ### Fixed
199 | - Container contract does not throw PICKITEM panic when trying access
200 | non-existent container, instead panics with user-friendly message (#121)
201 |
202 | ### Changed
203 | - NNS contract has been updated to the latest version from Neo upstream (#123)
204 | - Container contract does not throw panic on deleting non-existent container
205 | (#121)
206 | - Migrate methods renamed to Update (#128)
207 | - Contracts now throw panic if update routine fails (#130)
208 | - Side chain committee is able to update contracts (#107)
209 |
210 | ### Removed
211 | - Contract owner arguments at deploy stage (#107)
212 |
213 | ## [0.10.1] - 2021-07-29
214 |
215 | ### Changed
216 | - `Version` method returns encoded semver value (#98)
217 |
218 | ### Removed
219 | - `InitConfig` methods from neofs and netmap contracts. Network configuration
220 | now provided as contract deploy parameter. (#115)
221 |
222 | ## [0.10.0] - 2021-07-23 - Wando (완도, 莞島)
223 |
224 | ### Fixed
225 | - Alphabet contract does not emit GAS to proxy contract and does not check
226 | proxy contract script hash length at deploy stage if notary disabled (#106)
227 |
228 | ### Added
229 | - Netmap contract stores block height when last epoch was applied and provides
230 | `LastEpochBlock` method to get it (#110)
231 | - NNS contract (#108)
232 | - Enhanced documentation for autodoc tools (#105)
233 |
234 | ### Changed
235 | - Update neo-go to v0.96.0
236 |
237 | ### Removed
238 | - v0.9.1 to v0.9.2 migration code (#104)
239 |
240 | ## [0.9.2] - 2021-07-01
241 |
242 | ### Fixed
243 | - Execution of multiple `container.Put`, `container.Delete`,
244 | `container.PutContainerSize` and `netmap.AddPeer` invocations now possible
245 | in the single block (#100, #102).
246 |
247 | ### Added
248 | - Target NeoFS API version in README.md.
249 |
250 | ### Changed
251 | - Notary enabled images for neofs-dev-env do not contain predefined network
252 | map anymore.
253 |
254 | ## [0.9.1] - 2021-06-24
255 |
256 | ### Fixed
257 | - Notification parameter types in container, neofs and netmap manifests (#94).
258 | - Method permissions in manifests (#96).
259 |
260 | ### Added
261 | - Balance check before notification at `container.Put` method.
262 |
263 | ### Removed
264 | - v0.8.0 to v0.9.0 migration code.
265 |
266 | ## [0.9.0] - 2021-06-03 - Seongmodo (석모도, 席毛島)
267 |
268 | Session token support in container contract.
269 |
270 | ### Fixed
271 | - `_deploy` methods process `isUpdate` argument now.
272 |
273 | ### Added
274 | - Changelog file.
275 | - `netmap.NetmapCandidates` method.
276 |
277 | ### Changed
278 | - Container contract now stores public key, signature and session token of
279 | new containers and extended ACL tables.
280 | - Most of the contract methods that invoked by inner ring do not return bool
281 | value anymore. Such methods throw panic instead.
282 | - Migrate methods now accept data.
283 |
284 | ### Removed
285 | - Container and extended ACL signature checks in container contract.
286 |
287 | ## [0.8.0] - 2021-05-19 - Dolsando (돌산도, 突山島)
288 |
289 | N3 Testnet RC2 compatible contracts.
290 |
291 | ### Changed
292 |
293 | - Contract initialization moved to `_deploy` method.
294 |
295 | ### Removed
296 |
297 | - `Deposit` method from `NeoFS` contract uses direct GAS transfer to contract address for deposit.
298 | - Unused transfer description variables in `Balance` contract and total function in `Alphabet` contract.
299 |
300 | ### Updated
301 |
302 | - NEO Go to N3 RC2 compatible v0.95.0.
303 |
304 | ## [0.7.0] - 2021-05-04 - Daecheongdo (대청도, 大靑島)
305 |
306 | Combine notary and non-notary work flows in smart contracts.
307 |
308 | ### Fixed
309 |
310 | - Integers are not used as search prefixes anymore.
311 |
312 | ### Added
313 |
314 | - Notary and non-notary work flows in all contracts. Notary can be disabled at contract initialization.
315 | - `Processing` contract in main chain to pay for `NeoFS` contract invocations from alphabet when notary enabled.
316 | - Fee payments at `neofs.Withdraw` invocation.
317 |
318 | ### Changed
319 |
320 | - `Reputation` contract stores new global reputation structures.
321 | - All `balance.transferX` invocations are provided with encoded transfer details.
322 |
323 | ### Removed
324 |
325 | - Cheque storage in `NeoFS` contract to decrease invocation costs.
326 |
327 | ## [0.6.0] - 2021-03-26 - Yeongheungdo (영흥도, 靈興島)
328 |
329 | Governance update.
330 |
331 | ### Fixed
332 |
333 | - Threshold (N) calculation.
334 |
335 | ### Changed
336 |
337 | - Inner ring keys are accessed from `NeoFSAlphabet` role in side chain.
338 | - Alphabet keys are accessed from committee in side chain.
339 | - `NeoFS` contract now manages alphabet keys and do not update candidate list automatically.
340 | - `NeoFS` contract can be initiated with any non zero amount of alphabet keys now.
341 | - `neofs.InnerRingList` renamed to `neofs.AlphabetList`.
342 | - `neofs.InnerRingUpdate` renamed to `neofs.AlphabetUpdate` and it produces `AlphabetUpdate` event.
343 | - `Netmap` contract does not manage inner ring keys now.
344 |
345 | ### Removed
346 |
347 | - `neofs.IsInnerRing`, `netmap.InnerRingLost`, `netmap.Multiaddress`,
348 | `netmap.Committee`, `netmap.UpdateInnerRing` methods.
349 |
350 | ## [0.5.1] - 2021-03-22
351 |
352 | ### Fixed
353 |
354 | - Methods with notifications are no longer considered to be safe.
355 |
356 | ## [0.5.0] - 2021-03-22 - Jebudo (제부도, 濟扶島)
357 |
358 | ### Fixed
359 |
360 | - Various typos.
361 |
362 | ### Added
363 |
364 | - Proxy contract.
365 | - `Multiaddress` and `Committee` methods in `Netmap` contract.
366 | - List of safe methods in contract configs.
367 |
368 | ### Changed
369 |
370 | - Smart contracts use read-only storage context where it is possible.
371 | - `Netmap` contract triggers clean-up methods on new epoch.
372 | - Contracts use `interop.Hash160` and other type aliases instead of `[]byte`.
373 |
374 | ### Removed
375 |
376 | - Multi signatures in side chain now collected with native notary contract,
377 | thus side chain contracts do not use ballots anymore.
378 |
379 | ### Updated
380 |
381 | - NEO Go to testnet compatible v0.94.0.
382 |
383 | ## [0.4.0] - 2021-02-15 - Seonyudo (선유도, 仙遊島)
384 |
385 | ### Fixed
386 |
387 | - Old ballots are now removed before processing new ballot.
388 |
389 | ### Added
390 |
391 | - Methods in container contract to store container size estimations.
392 | - `common` package that contains shared code for all contracts.
393 | - Contracts migration methods.
394 |
395 | ### Updated
396 |
397 | - NEO Go to preview5 compatible v0.93.0.
398 | - Contract manifests.
399 |
400 | ## [0.3.1] - 2020-12-29
401 |
402 | Preview4-testnet version of NeoFS contracts.
403 |
404 | ## 0.3.0 - 2020-12-29
405 |
406 | Preview4 compatible contracts.
407 |
408 | [Unreleased]: https://github.com/nspcc-dev/neofs-contract/compare/v0.16.0...master
409 | [0.16.0]: https://github.com/nspcc-dev/neofs-contract/compare/v0.15.5...v0.16.0
410 | [0.15.5]: https://github.com/nspcc-dev/neofs-contract/compare/v0.15.4...v0.15.5
411 | [0.15.4]: https://github.com/nspcc-dev/neofs-contract/compare/v0.15.3...v0.15.4
412 | [0.15.3]: https://github.com/nspcc-dev/neofs-contract/compare/v0.15.2...v0.15.3
413 | [0.15.2]: https://github.com/nspcc-dev/neofs-contract/compare/v0.15.1...v0.15.2
414 | [0.15.1]: https://github.com/nspcc-dev/neofs-contract/compare/v0.15.0...v0.15.1
415 | [0.15.0]: https://github.com/nspcc-dev/neofs-contract/compare/v0.14.2...v0.15.0
416 | [0.14.2]: https://github.com/nspcc-dev/neofs-contract/compare/v0.14.1...v0.14.2
417 | [0.14.1]: https://github.com/nspcc-dev/neofs-contract/compare/v0.14.0...v0.14.1
418 | [0.14.0]: https://github.com/nspcc-dev/neofs-contract/compare/v0.13.2...v0.14.0
419 | [0.13.2]: https://github.com/nspcc-dev/neofs-contract/compare/v0.13.1...v0.13.2
420 | [0.13.1]: https://github.com/nspcc-dev/neofs-contract/compare/v0.13.0...v0.13.1
421 | [0.13.0]: https://github.com/nspcc-dev/neofs-contract/compare/v0.12.2...v0.13.0
422 | [0.12.2]: https://github.com/nspcc-dev/neofs-contract/compare/v0.12.1...v0.12.2
423 | [0.12.1]: https://github.com/nspcc-dev/neofs-contract/compare/v0.12.0...v0.12.1
424 | [0.12.0]: https://github.com/nspcc-dev/neofs-contract/compare/v0.11.0...v0.12.0
425 | [0.11.0]: https://github.com/nspcc-dev/neofs-contract/compare/v0.10.1...v0.11.0
426 | [0.10.1]: https://github.com/nspcc-dev/neofs-contract/compare/v0.10.0...v0.10.1
427 | [0.10.0]: https://github.com/nspcc-dev/neofs-contract/compare/v0.9.2...v0.10.0
428 | [0.9.2]: https://github.com/nspcc-dev/neofs-contract/compare/v0.9.1...v0.9.2
429 | [0.9.1]: https://github.com/nspcc-dev/neofs-contract/compare/v0.9.0...v0.9.1
430 | [0.9.0]: https://github.com/nspcc-dev/neofs-contract/compare/v0.8.0...v0.9.0
431 | [0.8.0]: https://github.com/nspcc-dev/neofs-contract/compare/v0.7.0...v0.8.0
432 | [0.7.0]: https://github.com/nspcc-dev/neofs-contract/compare/v0.6.0...v0.7.0
433 | [0.6.0]: https://github.com/nspcc-dev/neofs-contract/compare/v0.5.1...v0.6.0
434 | [0.5.1]: https://github.com/nspcc-dev/neofs-contract/compare/v0.5.0...v0.5.1
435 | [0.5.0]: https://github.com/nspcc-dev/neofs-contract/compare/v0.4.0...v0.5.0
436 | [0.4.0]: https://github.com/nspcc-dev/neofs-contract/compare/v0.3.1...v0.4.0
437 | [0.3.1]: https://github.com/nspcc-dev/neofs-contract/compare/v0.3.0...v0.3.1
438 |
--------------------------------------------------------------------------------
/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 | NEOGO ?= $(GOBIN)/cli
8 | VERSION ?= $(shell git describe --tags --dirty --match "v*" --always --abbrev=8 2>/dev/null || cat VERSION 2>/dev/null || echo "develop")
9 |
10 |
11 | # .deb package versioning
12 | OS_RELEASE = $(shell lsb_release -cs)
13 | PKG_VERSION ?= $(shell echo $(VERSION) | sed "s/^v//" | \
14 | sed -E "s/(.*)-(g[a-fA-F0-9]{6,8})(.*)/\1\3~\2/" | \
15 | sed "s/-/~/")-${OS_RELEASE}
16 |
17 | .PHONY: all build clean test neo-go
18 | .PHONY: alphabet mainnet morph nns sidechain
19 | .PHONY: debpackage debclean
20 | build: neo-go all
21 | all: sidechain mainnet
22 | sidechain: alphabet morph nns
23 |
24 | alphabet_sc = alphabet
25 | morph_sc = audit balance container frostfsid netmap proxy reputation subnet
26 | mainnet_sc = frostfs processing
27 | nns_sc = nns
28 |
29 | define sc_template
30 | $(2)$(1)/$(1)_contract.nef: $(2)$(1)/$(1)_contract.go
31 | $(NEOGO) contract compile -i $(2)$(1) -c $(if $(2),$(2),$(1)/)config.yml -m $(2)$(1)/config.json -o $(2)$(1)/$(1)_contract.nef
32 |
33 | $(if $(2),$(2)$(1)/$(1)_contract.go: alphabet/alphabet.go alphabet/alphabet.tpl
34 | go run alphabet/alphabet.go
35 | )
36 | endef
37 |
38 | $(foreach sc,$(alphabet_sc),$(eval $(call sc_template,$(sc))))
39 | $(foreach sc,$(morph_sc),$(eval $(call sc_template,$(sc))))
40 | $(foreach sc,$(mainnet_sc),$(eval $(call sc_template,$(sc))))
41 | $(foreach sc,$(nns_sc),$(eval $(call sc_template,$(sc))))
42 |
43 | alphabet: $(foreach sc,$(alphabet_sc),$(sc)/$(sc)_contract.nef)
44 | morph: $(foreach sc,$(morph_sc),$(sc)/$(sc)_contract.nef)
45 | mainnet: $(foreach sc,$(mainnet_sc),$(sc)/$(sc)_contract.nef)
46 | nns: $(foreach sc,$(nns_sc),$(sc)/$(sc)_contract.nef)
47 |
48 | neo-go:
49 | @go list -f '{{.Path}}/...@{{.Version}}' -m github.com/nspcc-dev/neo-go \
50 | | xargs go install -v
51 |
52 | test:
53 | @go test ./tests/...
54 |
55 | clean:
56 | find . -name '*.nef' -exec rm -rf {} \;
57 | find . -name 'config.json' -exec rm -rf {} \;
58 | rm -rf ./bin/
59 |
60 | mr_proper: clean
61 | for sc in $(alphabet_sc); do\
62 | rm -rf alphabet/$$sc; \
63 | done
64 |
65 | archive: build
66 | @tar --transform "s|^./|frostfs-contract-$(VERSION)/|" \
67 | -czf frostfs-contract-$(VERSION).tar.gz \
68 | $(shell find . -name '*.nef' -o -name 'config.json')
69 |
70 | # Package for Debian
71 | debpackage:
72 | dch --package frostfs-contract \
73 | --controlmaint \
74 | --newversion $(PKG_VERSION) \
75 | --distribution $(OS_RELEASE) \
76 | "Please see CHANGELOG.md for code changes for $(VERSION)"
77 | dpkg-buildpackage --no-sign -b
78 |
79 | debclean:
80 | dh clean
81 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | FrostFS related smart contracts.
6 |
7 |
8 | ---
9 |
10 | # Overview
11 |
12 | FrostFS-Contract contains all FrostFS related contracts written for
13 | [neo-go](https://github.com/nspcc-dev/neo-go) compiler. These contracts
14 | are deployed both in the mainchain and the sidechain.
15 |
16 | Mainchain contracts:
17 |
18 | - frostfs
19 | - processing
20 |
21 | Sidechain contracts:
22 |
23 | - alphabet
24 | - audit
25 | - balance
26 | - container
27 | - frostfsid
28 | - netmap
29 | - nns
30 | - proxy
31 | - reputation
32 | - subnet
33 |
34 | # Getting started
35 |
36 | ## Prerequisites
37 |
38 | To compile smart contracts you need:
39 |
40 | - [neo-go](https://github.com/nspcc-dev/neo-go) >= 0.99.2
41 |
42 | ## Compilation
43 |
44 | To build and compile smart contract, run `make all` command. Compiled contracts
45 | `*_contract.nef` and manifest `config.json` files are placed in the
46 | corresponding directories.
47 |
48 | ```
49 | $ make all
50 | /home/user/go/bin/cli contract compile -i alphabet -c alphabet/config.yml -m alphabet/config.json -o alphabet/alphabet_contract.nef
51 | /home/user/go/bin/cli contract compile -i audit -c audit/config.yml -m audit/config.json -o audit/audit_contract.nef
52 | /home/user/go/bin/cli contract compile -i balance -c balance/config.yml -m balance/config.json -o balance/balance_contract.nef
53 | /home/user/go/bin/cli contract compile -i container -c container/config.yml -m container/config.json -o container/container_contract.nef
54 | /home/user/go/bin/cli contract compile -i frostfsid -c frostfsid/config.yml -m frostfsid/config.json -o frostfsid/frostfsid_contract.nef
55 | /home/user/go/bin/cli contract compile -i netmap -c netmap/config.yml -m netmap/config.json -o netmap/netmap_contract.nef
56 | /home/user/go/bin/cli contract compile -i proxy -c proxy/config.yml -m proxy/config.json -o proxy/proxy_contract.nef
57 | /home/user/go/bin/cli contract compile -i reputation -c reputation/config.yml -m reputation/config.json -o reputation/reputation_contract.nef
58 | /home/user/go/bin/cli contract compile -i subnet -c subnet/config.yml -m subnet/config.json -o subnet/subnet_contract.nef
59 | /home/user/go/bin/cli contract compile -i nns -c nns/config.yml -m nns/config.json -o nns/nns_contract.nef
60 | /home/user/go/bin/cli contract compile -i frostfs -c frostfs/config.yml -m frostfs/config.json -o frostfs/frostfs_contract.nef
61 | /home/user/go/bin/cli contract compile -i processing -c processing/config.yml -m processing/config.json -o processing/processing_contract.nef
62 | ```
63 |
64 | You can specify path to the `neo-go` binary with `NEOGO` environment variable:
65 |
66 | ```
67 | $ NEOGO=/home/user/neo-go/bin/neo-go make all
68 | ```
69 |
70 | Remove compiled files with `make clean` or `make mr_proper` command.
71 |
72 | ## Building Debian package
73 |
74 | To build Debian package containing compiled contracts, run `make debpackage`
75 | command. Package will install compiled contracts `*_contract.nef` and manifest
76 | `config.json` with corresponding directories to `/var/lib/neofs/contract` for
77 | further usage.
78 | It will download and build neo-go, if needed.
79 |
80 | To clean package-related files, use `make debclean`.
81 |
82 | # Testing
83 | Smartcontract tests reside in `tests/` directory. To execute test suite
84 | after applying changes, simply run `make test`.
85 | ```
86 | $ make test
87 | ok github.com/TrueCloudLab/frostfs-contract/tests 0.462s
88 | ```
89 |
90 | # License
91 |
92 | This project is licensed under the GPLv3 License - see the
93 | [LICENSE.md](LICENSE.md) file for details
94 |
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | v0.16.1
2 |
--------------------------------------------------------------------------------
/alphabet/alphabet_contract.go:
--------------------------------------------------------------------------------
1 | package alphabet
2 |
3 | import (
4 | "github.com/TrueCloudLab/frostfs-contract/common"
5 | "github.com/nspcc-dev/neo-go/pkg/interop"
6 | "github.com/nspcc-dev/neo-go/pkg/interop/contract"
7 | "github.com/nspcc-dev/neo-go/pkg/interop/native/crypto"
8 | "github.com/nspcc-dev/neo-go/pkg/interop/native/gas"
9 | "github.com/nspcc-dev/neo-go/pkg/interop/native/management"
10 | "github.com/nspcc-dev/neo-go/pkg/interop/native/neo"
11 | "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
12 | "github.com/nspcc-dev/neo-go/pkg/interop/storage"
13 | )
14 |
15 | const (
16 | netmapKey = "netmapScriptHash"
17 | proxyKey = "proxyScriptHash"
18 |
19 | indexKey = "index"
20 | totalKey = "threshold"
21 | nameKey = "name"
22 |
23 | notaryDisabledKey = "notary"
24 | )
25 |
26 | // OnNEP17Payment is a callback for NEP-17 compatible native GAS and NEO
27 | // contracts.
28 | func OnNEP17Payment(from interop.Hash160, amount int, data interface{}) {
29 | caller := runtime.GetCallingScriptHash()
30 | if !common.BytesEqual(caller, []byte(gas.Hash)) && !common.BytesEqual(caller, []byte(neo.Hash)) {
31 | common.AbortWithMessage("alphabet contract accepts GAS and NEO only")
32 | }
33 | }
34 |
35 | func _deploy(data interface{}, isUpdate bool) {
36 | ctx := storage.GetContext()
37 | if isUpdate {
38 | args := data.([]interface{})
39 | common.CheckVersion(args[len(args)-1].(int))
40 | return
41 | }
42 |
43 | args := data.(struct {
44 | notaryDisabled bool
45 | addrNetmap interop.Hash160
46 | addrProxy interop.Hash160
47 | name string
48 | index int
49 | total int
50 | })
51 |
52 | if len(args.addrNetmap) != interop.Hash160Len || !args.notaryDisabled && len(args.addrProxy) != interop.Hash160Len {
53 | panic("incorrect length of contract script hash")
54 | }
55 |
56 | storage.Put(ctx, netmapKey, args.addrNetmap)
57 | storage.Put(ctx, proxyKey, args.addrProxy)
58 | storage.Put(ctx, nameKey, args.name)
59 | storage.Put(ctx, indexKey, args.index)
60 | storage.Put(ctx, totalKey, args.total)
61 |
62 | // initialize the way to collect signatures
63 | storage.Put(ctx, notaryDisabledKey, args.notaryDisabled)
64 | if args.notaryDisabled {
65 | common.InitVote(ctx)
66 | runtime.Log(args.name + " notary disabled")
67 | }
68 |
69 | runtime.Log(args.name + " contract initialized")
70 | }
71 |
72 | // Update method updates contract source code and manifest. It can be invoked
73 | // only by committee.
74 | func Update(script []byte, manifest []byte, data interface{}) {
75 | if !common.HasUpdateAccess() {
76 | panic("only committee can update contract")
77 | }
78 |
79 | contract.Call(interop.Hash160(management.Hash), "update",
80 | contract.All, script, manifest, common.AppendVersion(data))
81 | runtime.Log("alphabet contract updated")
82 | }
83 |
84 | // GAS returns the amount of the sidechain GAS stored in the contract account.
85 | func Gas() int {
86 | return gas.BalanceOf(runtime.GetExecutingScriptHash())
87 | }
88 |
89 | // NEO returns the amount of sidechain NEO stored in the contract account.
90 | func Neo() int {
91 | return neo.BalanceOf(runtime.GetExecutingScriptHash())
92 | }
93 |
94 | func currentEpoch(ctx storage.Context) int {
95 | netmapContractAddr := storage.Get(ctx, netmapKey).(interop.Hash160)
96 | return contract.Call(netmapContractAddr, "epoch", contract.ReadOnly).(int)
97 | }
98 |
99 | func name(ctx storage.Context) string {
100 | return storage.Get(ctx, nameKey).(string)
101 | }
102 |
103 | func index(ctx storage.Context) int {
104 | return storage.Get(ctx, indexKey).(int)
105 | }
106 |
107 | func checkPermission(ir []interop.PublicKey) bool {
108 | ctx := storage.GetReadOnlyContext()
109 | index := index(ctx) // read from contract memory
110 |
111 | if len(ir) <= index {
112 | return false
113 | }
114 |
115 | node := ir[index]
116 | return runtime.CheckWitness(node)
117 | }
118 |
119 | // Emit method produces sidechain GAS and distributes it among Inner Ring nodes
120 | // and proxy contract. It can be invoked only by an Alphabet node of the Inner Ring.
121 | //
122 | // To produce GAS, an alphabet contract transfers all available NEO from the contract
123 | // account to itself. If notary is enabled, 50% of the GAS in the contract account
124 | // are transferred to proxy contract. 43.75% of the GAS are equally distributed
125 | // among all Inner Ring nodes. Remaining 6.25% of the GAS stay in the contract.
126 | //
127 | // If notary is disabled, 87.5% of the GAS are equally distributed among all
128 | // Inner Ring nodes. Remaining 12.5% of the GAS stay in the contract.
129 | func Emit() {
130 | ctx := storage.GetReadOnlyContext()
131 | notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
132 |
133 | alphabet := common.AlphabetNodes()
134 | if !checkPermission(alphabet) {
135 | panic("invalid invoker")
136 | }
137 |
138 | contractHash := runtime.GetExecutingScriptHash()
139 |
140 | if !neo.Transfer(contractHash, contractHash, neo.BalanceOf(contractHash), nil) {
141 | panic("failed to transfer funds, aborting")
142 | }
143 |
144 | gasBalance := gas.BalanceOf(contractHash)
145 |
146 | if !notaryDisabled {
147 | proxyAddr := storage.Get(ctx, proxyKey).(interop.Hash160)
148 |
149 | proxyGas := gasBalance / 2
150 | if proxyGas == 0 {
151 | panic("no gas to emit")
152 | }
153 |
154 | if !gas.Transfer(contractHash, proxyAddr, proxyGas, nil) {
155 | runtime.Log("could not transfer GAS to proxy contract")
156 | }
157 |
158 | gasBalance -= proxyGas
159 |
160 | runtime.Log("utility token has been emitted to proxy contract")
161 | }
162 |
163 | var innerRing []interop.PublicKey
164 |
165 | if notaryDisabled {
166 | netmapContract := storage.Get(ctx, netmapKey).(interop.Hash160)
167 | innerRing = common.InnerRingNodesFromNetmap(netmapContract)
168 | } else {
169 | innerRing = common.InnerRingNodes()
170 | }
171 |
172 | gasPerNode := gasBalance * 7 / 8 / len(innerRing)
173 |
174 | if gasPerNode != 0 {
175 | for _, node := range innerRing {
176 | address := contract.CreateStandardAccount(node)
177 | if !gas.Transfer(contractHash, address, gasPerNode, nil) {
178 | runtime.Log("could not transfer GAS to one of IR node")
179 | }
180 | }
181 |
182 | runtime.Log("utility token has been emitted to inner ring nodes")
183 | }
184 | }
185 |
186 | // Vote method votes for the sidechain committee. It requires multisignature from
187 | // Alphabet nodes of the Inner Ring.
188 | //
189 | // This method is used when governance changes the list of Alphabet nodes of the
190 | // Inner Ring. Alphabet nodes share keys with sidechain validators, therefore
191 | // it is required to change them as well. To do that, NEO holders (which are
192 | // alphabet contracts) should vote for a new committee.
193 | func Vote(epoch int, candidates []interop.PublicKey) {
194 | ctx := storage.GetContext()
195 | notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
196 | index := index(ctx)
197 | name := name(ctx)
198 |
199 | var ( // for invocation collection without notary
200 | alphabet []interop.PublicKey
201 | nodeKey []byte
202 | )
203 |
204 | if notaryDisabled {
205 | alphabet = common.AlphabetNodes()
206 | nodeKey = common.InnerRingInvoker(alphabet)
207 | if len(nodeKey) == 0 {
208 | panic("invalid invoker")
209 | }
210 | } else {
211 | multiaddr := common.AlphabetAddress()
212 | common.CheckAlphabetWitness(multiaddr)
213 | }
214 |
215 | curEpoch := currentEpoch(ctx)
216 | if epoch != curEpoch {
217 | panic("invalid epoch")
218 | }
219 |
220 | candidate := candidates[index%len(candidates)]
221 | address := runtime.GetExecutingScriptHash()
222 |
223 | if notaryDisabled {
224 | threshold := len(alphabet)*2/3 + 1
225 | id := voteID(epoch, candidates)
226 |
227 | n := common.Vote(ctx, id, nodeKey)
228 | if n < threshold {
229 | return
230 | }
231 |
232 | common.RemoveVotes(ctx, id)
233 | }
234 |
235 | ok := neo.Vote(address, candidate)
236 | if ok {
237 | runtime.Log(name + ": successfully voted for validator")
238 | } else {
239 | runtime.Log(name + ": vote has been failed")
240 | }
241 | }
242 |
243 | func voteID(epoch interface{}, args []interop.PublicKey) []byte {
244 | var (
245 | result []byte
246 | epochBytes = epoch.([]byte)
247 | )
248 |
249 | result = append(result, epochBytes...)
250 |
251 | for i := range args {
252 | result = append(result, args[i]...)
253 | }
254 |
255 | return crypto.Sha256(result)
256 | }
257 |
258 | // Name returns the Glagolitic name of the contract.
259 | func Name() string {
260 | ctx := storage.GetReadOnlyContext()
261 | return name(ctx)
262 | }
263 |
264 | // Version returns the version of the contract.
265 | func Version() int {
266 | return common.Version
267 | }
268 |
--------------------------------------------------------------------------------
/alphabet/config.yml:
--------------------------------------------------------------------------------
1 | name: "FrostFS Alphabet"
2 | safemethods: ["gas", "neo", "name", "version"]
3 | permissions:
4 | - methods: ["update", "transfer", "vote"]
5 |
--------------------------------------------------------------------------------
/alphabet/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Alphabet contract is a contract deployed in FrostFS sidechain.
3 |
4 | Alphabet contract is designed to support GAS production and vote for new
5 | validators in the sidechain. NEO token is required to produce GAS and vote for
6 | a new committee. It can be distributed among alphabet nodes of the Inner Ring.
7 | However, some of them may be malicious, and some NEO can be lost. It will destabilize
8 | the economic of the sidechain. To avoid it, all 100,000,000 NEO are
9 | distributed among all alphabet contracts.
10 |
11 | To identify alphabet contracts, they are named with letters of the Glagolitic alphabet.
12 | Names are set at contract deploy. Alphabet nodes of the Inner Ring communicate with
13 | one of the alphabetical contracts to emit GAS. To vote for a new list of side
14 | chain committee, alphabet nodes of the Inner Ring create multisignature transactions
15 | for each alphabet contract.
16 |
17 | # Contract notifications
18 |
19 | Alphabet contract does not produce notifications to process.
20 | */
21 | package alphabet
22 |
--------------------------------------------------------------------------------
/audit/audit_contract.go:
--------------------------------------------------------------------------------
1 | package audit
2 |
3 | import (
4 | "github.com/TrueCloudLab/frostfs-contract/common"
5 | "github.com/nspcc-dev/neo-go/pkg/interop"
6 | "github.com/nspcc-dev/neo-go/pkg/interop/contract"
7 | "github.com/nspcc-dev/neo-go/pkg/interop/iterator"
8 | "github.com/nspcc-dev/neo-go/pkg/interop/native/crypto"
9 | "github.com/nspcc-dev/neo-go/pkg/interop/native/management"
10 | "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
11 | "github.com/nspcc-dev/neo-go/pkg/interop/storage"
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 interface{} = 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 | const (
40 | netmapContractKey = "netmapScriptHash"
41 |
42 | notaryDisabledKey = "notary"
43 | )
44 |
45 | func _deploy(data interface{}, isUpdate bool) {
46 | ctx := storage.GetContext()
47 | if isUpdate {
48 | args := data.([]interface{})
49 | common.CheckVersion(args[len(args)-1].(int))
50 | return
51 | }
52 |
53 | args := data.(struct {
54 | notaryDisabled bool
55 | addrNetmap interop.Hash160
56 | })
57 |
58 | if len(args.addrNetmap) != interop.Hash160Len {
59 | panic("incorrect length of contract script hash")
60 | }
61 |
62 | storage.Put(ctx, netmapContractKey, args.addrNetmap)
63 |
64 | // initialize the way to collect signatures
65 | storage.Put(ctx, notaryDisabledKey, args.notaryDisabled)
66 | if args.notaryDisabled {
67 | runtime.Log("audit contract notary disabled")
68 | }
69 |
70 | runtime.Log("audit contract initialized")
71 | }
72 |
73 | // Update method updates contract source code and manifest. It can be invoked
74 | // only by committee.
75 | func Update(script []byte, manifest []byte, data interface{}) {
76 | if !common.HasUpdateAccess() {
77 | panic("only committee can update contract")
78 | }
79 |
80 | contract.Call(interop.Hash160(management.Hash), "update",
81 | contract.All, script, manifest, common.AppendVersion(data))
82 | runtime.Log("audit contract updated")
83 | }
84 |
85 | // Put method stores a stable marshalled `DataAuditResult` structure. It can be
86 | // invoked only by Inner Ring nodes.
87 | //
88 | // Inner Ring nodes perform audit of containers and produce `DataAuditResult`
89 | // structures. They are stored in audit contract and used for settlements
90 | // in later epochs.
91 | func Put(rawAuditResult []byte) {
92 | ctx := storage.GetContext()
93 | notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
94 |
95 | var innerRing []interop.PublicKey
96 |
97 | if notaryDisabled {
98 | netmapContract := storage.Get(ctx, netmapContractKey).(interop.Hash160)
99 | innerRing = common.InnerRingNodesFromNetmap(netmapContract)
100 | } else {
101 | innerRing = common.InnerRingNodes()
102 | }
103 |
104 | hdr := newAuditHeader(rawAuditResult)
105 | presented := false
106 |
107 | for i := range innerRing {
108 | ir := innerRing[i]
109 | if common.BytesEqual(ir, hdr.from) {
110 | presented = true
111 |
112 | break
113 | }
114 | }
115 |
116 | if !runtime.CheckWitness(hdr.from) || !presented {
117 | panic("put access denied")
118 | }
119 |
120 | storage.Put(ctx, hdr.ID(), rawAuditResult)
121 |
122 | runtime.Log("audit: result has been saved")
123 | }
124 |
125 | // Get method returns a stable marshaled DataAuditResult structure.
126 | //
127 | // The ID of the DataAuditResult can be obtained from listing methods.
128 | func Get(id []byte) []byte {
129 | ctx := storage.GetReadOnlyContext()
130 | return storage.Get(ctx, id).([]byte)
131 | }
132 |
133 | // List method returns a list of all available DataAuditResult IDs from
134 | // the contract storage.
135 | func List() [][]byte {
136 | ctx := storage.GetReadOnlyContext()
137 | it := storage.Find(ctx, []byte{}, storage.KeysOnly)
138 |
139 | return list(it)
140 | }
141 |
142 | // ListByEpoch method returns a list of DataAuditResult IDs generated during
143 | // the specified epoch.
144 | func ListByEpoch(epoch int) [][]byte {
145 | ctx := storage.GetReadOnlyContext()
146 | var buf interface{} = epoch
147 | it := storage.Find(ctx, buf.([]byte), storage.KeysOnly)
148 |
149 | return list(it)
150 | }
151 |
152 | // ListByCID method returns a list of DataAuditResult IDs generated during
153 | // the specified epoch for the specified container.
154 | func ListByCID(epoch int, cid []byte) [][]byte {
155 | ctx := storage.GetReadOnlyContext()
156 |
157 | var buf interface{} = epoch
158 |
159 | prefix := append(buf.([]byte), cid...)
160 | it := storage.Find(ctx, prefix, storage.KeysOnly)
161 |
162 | return list(it)
163 | }
164 |
165 | // ListByNode method returns a list of DataAuditResult IDs generated in
166 | // the specified epoch for the specified container by the specified Inner Ring node.
167 | func ListByNode(epoch int, cid []byte, key interop.PublicKey) [][]byte {
168 | ctx := storage.GetReadOnlyContext()
169 | hdr := auditHeader{
170 | epoch: epoch,
171 | cid: cid,
172 | from: key,
173 | }
174 |
175 | it := storage.Find(ctx, hdr.ID(), storage.KeysOnly)
176 |
177 | return list(it)
178 | }
179 |
180 | func list(it iterator.Iterator) [][]byte {
181 | var result [][]byte
182 |
183 | ignore := [][]byte{
184 | []byte(netmapContractKey),
185 | []byte(notaryDisabledKey),
186 | }
187 |
188 | loop:
189 | for iterator.Next(it) {
190 | key := iterator.Value(it).([]byte) // iterator MUST BE `storage.KeysOnly`
191 | for _, ignoreKey := range ignore {
192 | if common.BytesEqual(key, ignoreKey) {
193 | continue loop
194 | }
195 | }
196 |
197 | result = append(result, key)
198 | }
199 |
200 | return result
201 | }
202 |
203 | // Version returns the version of the contract.
204 | func Version() int {
205 | return common.Version
206 | }
207 |
208 | // readNext reads the length from the first byte, and then reads data (max 127 bytes).
209 | func readNext(input []byte) ([]byte, int) {
210 | var buf interface{} = input[0]
211 | ln := buf.(int)
212 |
213 | return input[1 : 1+ln], 1 + ln
214 | }
215 |
216 | func newAuditHeader(input []byte) auditHeader {
217 | // V2 format
218 | offset := int(input[1])
219 | offset = 2 + offset + 1 // version prefix + version len + epoch prefix
220 |
221 | var buf interface{} = input[offset : offset+8] // [ 8 integer bytes ]
222 | epoch := buf.(int)
223 |
224 | offset = offset + 8
225 |
226 | // cid is a nested structure with raw bytes
227 | // [ cid struct prefix (wireType + len = 2 bytes), cid value wireType (1 byte), ... ]
228 | cid, cidOffset := readNext(input[offset+2+1:])
229 |
230 | // key is a raw byte
231 | // [ public key wireType (1 byte), ... ]
232 | key, _ := readNext(input[offset+2+1+cidOffset+1:])
233 |
234 | return auditHeader{
235 | epoch,
236 | cid,
237 | key,
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/audit/config.yml:
--------------------------------------------------------------------------------
1 | name: "FrostFS Audit"
2 | safemethods: ["get", "list", "listByEpoch", "listByCID", "listByNode", "version"]
3 | permissions:
4 | - methods: ["update"]
5 |
--------------------------------------------------------------------------------
/audit/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Audit contract is a contract deployed in FrostFS sidechain.
3 |
4 | Inner Ring nodes perform audit of the registered containers during every epoch.
5 | If a container contains StorageGroup objects, an Inner Ring node initializes
6 | a series of audit checks. Based on the results of these checks, the Inner Ring
7 | node creates a DataAuditResult structure for the container. The content of this
8 | structure makes it possible to determine which storage nodes have been examined and
9 | see the status of these checks. Regarding this information, the container owner is
10 | charged for data storage.
11 |
12 | Audit contract is used as a reliable and verifiable storage for all
13 | DataAuditResult structures. At the end of data audit routine, Inner Ring
14 | nodes send a stable marshaled version of the DataAuditResult structure to the
15 | contract. When Alphabet nodes of the Inner Ring perform settlement operations,
16 | they make a list and get these AuditResultStructures from the audit contract.
17 |
18 | # Contract notifications
19 |
20 | Audit contract does not produce notifications to process.
21 | */
22 | package audit
23 |
--------------------------------------------------------------------------------
/balance/balance_contract.go:
--------------------------------------------------------------------------------
1 | package balance
2 |
3 | import (
4 | "github.com/TrueCloudLab/frostfs-contract/common"
5 | "github.com/nspcc-dev/neo-go/pkg/interop"
6 | "github.com/nspcc-dev/neo-go/pkg/interop/contract"
7 | "github.com/nspcc-dev/neo-go/pkg/interop/iterator"
8 | "github.com/nspcc-dev/neo-go/pkg/interop/native/management"
9 | "github.com/nspcc-dev/neo-go/pkg/interop/native/std"
10 | "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
11 | "github.com/nspcc-dev/neo-go/pkg/interop/storage"
12 | )
13 |
14 | type (
15 | // Token holds all token info.
16 | Token struct {
17 | // Ticker symbol
18 | Symbol string
19 | // Amount of decimals
20 | Decimals int
21 | // Storage key for circulation value
22 | CirculationKey string
23 | }
24 |
25 | // Account structure stores metadata of each FrostFS balance account.
26 | Account struct {
27 | // Active balance
28 | Balance int
29 | // Until valid for lock accounts
30 | Until int
31 | // Parent field used in lock accounts, used to return assets back if
32 | // account wasn't burnt.
33 | Parent []byte
34 | }
35 | )
36 |
37 | const (
38 | symbol = "FROSTFS"
39 | decimals = 12
40 | circulation = "MainnetGAS"
41 |
42 | netmapContractKey = "netmapScriptHash"
43 | containerContractKey = "containerScriptHash"
44 | notaryDisabledKey = "notary"
45 | )
46 |
47 | var token Token
48 |
49 | func createToken() Token {
50 | return Token{
51 | Symbol: symbol,
52 | Decimals: decimals,
53 | CirculationKey: circulation,
54 | }
55 | }
56 |
57 | func init() {
58 | token = createToken()
59 | }
60 |
61 | func _deploy(data interface{}, isUpdate bool) {
62 | ctx := storage.GetContext()
63 | if isUpdate {
64 | args := data.([]interface{})
65 | common.CheckVersion(args[len(args)-1].(int))
66 | return
67 | }
68 |
69 | args := data.(struct {
70 | notaryDisabled bool
71 | addrNetmap interop.Hash160
72 | addrContainer interop.Hash160
73 | })
74 |
75 | if len(args.addrNetmap) != interop.Hash160Len || len(args.addrContainer) != interop.Hash160Len {
76 | panic("incorrect length of contract script hash")
77 | }
78 |
79 | storage.Put(ctx, netmapContractKey, args.addrNetmap)
80 | storage.Put(ctx, containerContractKey, args.addrContainer)
81 |
82 | // initialize the way to collect signatures
83 | storage.Put(ctx, notaryDisabledKey, args.notaryDisabled)
84 | if args.notaryDisabled {
85 | common.InitVote(ctx)
86 | runtime.Log("balance contract notary disabled")
87 | }
88 |
89 | runtime.Log("balance contract initialized")
90 | }
91 |
92 | // Update method updates contract source code and manifest. It can be invoked
93 | // only by committee.
94 | func Update(script []byte, manifest []byte, data interface{}) {
95 | if !common.HasUpdateAccess() {
96 | panic("only committee can update contract")
97 | }
98 |
99 | contract.Call(interop.Hash160(management.Hash), "update",
100 | contract.All, script, manifest, common.AppendVersion(data))
101 | runtime.Log("balance contract updated")
102 | }
103 |
104 | // Symbol is a NEP-17 standard method that returns FROSTFS token symbol.
105 | func Symbol() string {
106 | return token.Symbol
107 | }
108 |
109 | // Decimals is a NEP-17 standard method that returns precision of FrostFS
110 | // balances.
111 | func Decimals() int {
112 | return token.Decimals
113 | }
114 |
115 | // TotalSupply is a NEP-17 standard method that returns total amount of main
116 | // chain GAS in FrostFS network.
117 | func TotalSupply() int {
118 | ctx := storage.GetReadOnlyContext()
119 | return token.getSupply(ctx)
120 | }
121 |
122 | // BalanceOf is a NEP-17 standard method that returns FrostFS balance of the specified
123 | // account.
124 | func BalanceOf(account interop.Hash160) int {
125 | ctx := storage.GetReadOnlyContext()
126 | return token.balanceOf(ctx, account)
127 | }
128 |
129 | // Transfer is a NEP-17 standard method that transfers FrostFS balance from one
130 | // account to another. It can be invoked only by the account owner.
131 | //
132 | // It produces Transfer and TransferX notifications. TransferX notification
133 | // will have empty details field.
134 | func Transfer(from, to interop.Hash160, amount int, data interface{}) bool {
135 | ctx := storage.GetContext()
136 | return token.transfer(ctx, from, to, amount, false, nil)
137 | }
138 |
139 | // TransferX is a method for FrostFS balance to be transferred from one account to
140 | // another. It can be invoked by the account owner or by Alphabet nodes.
141 | //
142 | // It produces Transfer and TransferX notifications.
143 | //
144 | // TransferX method expands Transfer method by having extra details argument.
145 | // TransferX method also allows to transfer assets by Alphabet nodes of the
146 | // Inner Ring with multisignature.
147 | func TransferX(from, to interop.Hash160, amount int, details []byte) {
148 | ctx := storage.GetContext()
149 | notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
150 |
151 | var ( // for invocation collection without notary
152 | alphabet []interop.PublicKey
153 | nodeKey []byte
154 | indirectCall bool
155 | )
156 |
157 | if notaryDisabled {
158 | alphabet = common.AlphabetNodes()
159 | nodeKey = common.InnerRingInvoker(alphabet)
160 | if len(nodeKey) == 0 {
161 | panic("this method must be invoked from inner ring")
162 | }
163 |
164 | indirectCall = common.FromKnownContract(
165 | ctx,
166 | runtime.GetCallingScriptHash(),
167 | containerContractKey,
168 | )
169 | } else {
170 | multiaddr := common.AlphabetAddress()
171 | common.CheckAlphabetWitness(multiaddr)
172 | }
173 |
174 | if notaryDisabled && !indirectCall {
175 | threshold := len(alphabet)*2/3 + 1
176 | id := common.InvokeID([]interface{}{from, to, amount}, []byte("transfer"))
177 |
178 | n := common.Vote(ctx, id, nodeKey)
179 | if n < threshold {
180 | return
181 | }
182 |
183 | common.RemoveVotes(ctx, id)
184 | }
185 |
186 | result := token.transfer(ctx, from, to, amount, true, details)
187 | if !result {
188 | panic("can't transfer assets")
189 | }
190 |
191 | runtime.Log("successfully transferred assets")
192 | }
193 |
194 | // Lock is a method that transfers assets from a user account to the lock account
195 | // related to the user. It can be invoked only by Alphabet nodes of the Inner Ring.
196 | //
197 | // It produces Lock, Transfer and TransferX notifications.
198 | //
199 | // Lock method is invoked by Alphabet nodes of the Inner Ring when they process
200 | // Withdraw notification from FrostFS contract. This should transfer assets
201 | // to a new lock account that won't be used for anything beside Unlock and Burn.
202 | func Lock(txDetails []byte, from, to interop.Hash160, amount, until int) {
203 | ctx := storage.GetContext()
204 | notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
205 |
206 | var ( // for invocation collection without notary
207 | alphabet []interop.PublicKey
208 | nodeKey []byte
209 | )
210 |
211 | if notaryDisabled {
212 | alphabet = common.AlphabetNodes()
213 | nodeKey = common.InnerRingInvoker(alphabet)
214 | if len(nodeKey) == 0 {
215 | panic("this method must be invoked from inner ring")
216 | }
217 | } else {
218 | multiaddr := common.AlphabetAddress()
219 | common.CheckAlphabetWitness(multiaddr)
220 | }
221 |
222 | details := common.LockTransferDetails(txDetails)
223 |
224 | lockAccount := Account{
225 | Balance: 0,
226 | Until: until,
227 | Parent: from,
228 | }
229 |
230 | if notaryDisabled {
231 | threshold := len(alphabet)*2/3 + 1
232 | id := common.InvokeID([]interface{}{txDetails}, []byte("lock"))
233 |
234 | n := common.Vote(ctx, id, nodeKey)
235 | if n < threshold {
236 | return
237 | }
238 |
239 | common.RemoveVotes(ctx, id)
240 | }
241 |
242 | common.SetSerialized(ctx, to, lockAccount)
243 |
244 | result := token.transfer(ctx, from, to, amount, true, details)
245 | if !result {
246 | // consider using `return false` to remove votes
247 | panic("can't lock funds")
248 | }
249 |
250 | runtime.Log("created lock account")
251 | runtime.Notify("Lock", txDetails, from, to, amount, until)
252 | }
253 |
254 | // NewEpoch is a method that checks timeout on lock accounts and returns assets
255 | // if lock is not available anymore. It can be invoked only by NewEpoch method
256 | // of Netmap contract.
257 | //
258 | // It produces Transfer and TransferX notifications.
259 | func NewEpoch(epochNum int) {
260 | ctx := storage.GetContext()
261 | notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
262 |
263 | if notaryDisabled {
264 | indirectCall := common.FromKnownContract(
265 | ctx,
266 | runtime.GetCallingScriptHash(),
267 | netmapContractKey,
268 | )
269 | if !indirectCall {
270 | panic("this method must be invoked from inner ring")
271 | }
272 | } else {
273 | multiaddr := common.AlphabetAddress()
274 | common.CheckAlphabetWitness(multiaddr)
275 | }
276 |
277 | it := storage.Find(ctx, []byte{}, storage.KeysOnly)
278 | for iterator.Next(it) {
279 | addr := iterator.Value(it).(interop.Hash160) // it MUST BE `storage.KeysOnly`
280 | if len(addr) != interop.Hash160Len {
281 | continue
282 | }
283 |
284 | acc := getAccount(ctx, addr)
285 | if acc.Until == 0 {
286 | continue
287 | }
288 |
289 | if epochNum >= acc.Until {
290 | details := common.UnlockTransferDetails(epochNum)
291 | // return assets back to the parent
292 | token.transfer(ctx, addr, acc.Parent, acc.Balance, true, details)
293 | }
294 | }
295 | }
296 |
297 | // Mint is a method that transfers assets to a user account from an empty account.
298 | // It can be invoked only by Alphabet nodes of the Inner Ring.
299 | //
300 | // It produces Mint, Transfer and TransferX notifications.
301 | //
302 | // Mint method is invoked by Alphabet nodes of the Inner Ring when they process
303 | // Deposit notification from FrostFS contract. Before that, Alphabet nodes should
304 | // synchronize precision of mainchain GAS contract and Balance contract.
305 | // Mint increases total supply of NEP-17 compatible FrostFS token.
306 | func Mint(to interop.Hash160, amount int, txDetails []byte) {
307 | ctx := storage.GetContext()
308 | notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
309 |
310 | var ( // for invocation collection without notary
311 | alphabet []interop.PublicKey
312 | nodeKey []byte
313 | )
314 |
315 | if notaryDisabled {
316 | alphabet = common.AlphabetNodes()
317 | nodeKey = common.InnerRingInvoker(alphabet)
318 | if len(nodeKey) == 0 {
319 | panic("this method must be invoked from inner ring")
320 | }
321 | } else {
322 | multiaddr := common.AlphabetAddress()
323 | common.CheckAlphabetWitness(multiaddr)
324 | }
325 |
326 | details := common.MintTransferDetails(txDetails)
327 |
328 | if notaryDisabled {
329 | threshold := len(alphabet)*2/3 + 1
330 | id := common.InvokeID([]interface{}{txDetails}, []byte("mint"))
331 |
332 | n := common.Vote(ctx, id, nodeKey)
333 | if n < threshold {
334 | return
335 | }
336 |
337 | common.RemoveVotes(ctx, id)
338 | }
339 |
340 | ok := token.transfer(ctx, nil, to, amount, true, details)
341 | if !ok {
342 | panic("can't transfer assets")
343 | }
344 |
345 | supply := token.getSupply(ctx)
346 | supply = supply + amount
347 | storage.Put(ctx, token.CirculationKey, supply)
348 | runtime.Log("assets were minted")
349 | runtime.Notify("Mint", to, amount)
350 | }
351 |
352 | // Burn is a method that transfers assets from a user account to an empty account.
353 | // It can be invoked only by Alphabet nodes of the Inner Ring.
354 | //
355 | // It produces Burn, Transfer and TransferX notifications.
356 | //
357 | // Burn method is invoked by Alphabet nodes of the Inner Ring when they process
358 | // Cheque notification from FrostFS contract. It means that locked assets have been
359 | // transferred to the user in the mainchain, therefore the lock account should be destroyed.
360 | // Before that, Alphabet nodes should synchronize precision of mainchain GAS
361 | // contract and Balance contract. Burn decreases total supply of NEP-17
362 | // compatible FrostFS token.
363 | func Burn(from interop.Hash160, amount int, txDetails []byte) {
364 | ctx := storage.GetContext()
365 | notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
366 |
367 | var ( // for invocation collection without notary
368 | alphabet []interop.PublicKey
369 | nodeKey []byte
370 | )
371 |
372 | if notaryDisabled {
373 | alphabet = common.AlphabetNodes()
374 | nodeKey = common.InnerRingInvoker(alphabet)
375 | if len(nodeKey) == 0 {
376 | panic("this method must be invoked from inner ring")
377 | }
378 | } else {
379 | multiaddr := common.AlphabetAddress()
380 | common.CheckAlphabetWitness(multiaddr)
381 | }
382 |
383 | details := common.BurnTransferDetails(txDetails)
384 |
385 | if notaryDisabled {
386 | threshold := len(alphabet)*2/3 + 1
387 | id := common.InvokeID([]interface{}{txDetails}, []byte("burn"))
388 |
389 | n := common.Vote(ctx, id, nodeKey)
390 | if n < threshold {
391 | return
392 | }
393 |
394 | common.RemoveVotes(ctx, id)
395 | }
396 |
397 | ok := token.transfer(ctx, from, nil, amount, true, details)
398 | if !ok {
399 | panic("can't transfer assets")
400 | }
401 |
402 | supply := token.getSupply(ctx)
403 | if supply < amount {
404 | panic("negative supply after burn")
405 | }
406 |
407 | supply = supply - amount
408 | storage.Put(ctx, token.CirculationKey, supply)
409 | runtime.Log("assets were burned")
410 | runtime.Notify("Burn", from, amount)
411 | }
412 |
413 | // Version returns the version of the contract.
414 | func Version() int {
415 | return common.Version
416 | }
417 |
418 | // getSupply gets the token totalSupply value from VM storage.
419 | func (t Token) getSupply(ctx storage.Context) int {
420 | supply := storage.Get(ctx, t.CirculationKey)
421 | if supply != nil {
422 | return supply.(int)
423 | }
424 |
425 | return 0
426 | }
427 |
428 | // BalanceOf gets the token balance of a specific address.
429 | func (t Token) balanceOf(ctx storage.Context, holder interop.Hash160) int {
430 | acc := getAccount(ctx, holder)
431 |
432 | return acc.Balance
433 | }
434 |
435 | func (t Token) transfer(ctx storage.Context, from, to interop.Hash160, amount int, innerRing bool, details []byte) bool {
436 | amountFrom, ok := t.canTransfer(ctx, from, to, amount, innerRing)
437 | if !ok {
438 | return false
439 | }
440 |
441 | if len(from) == 20 {
442 | if amountFrom.Balance == amount {
443 | storage.Delete(ctx, from)
444 | } else {
445 | amountFrom.Balance = amountFrom.Balance - amount // neo-go#953
446 | common.SetSerialized(ctx, from, amountFrom)
447 | }
448 | }
449 |
450 | if len(to) == 20 {
451 | amountTo := getAccount(ctx, to)
452 | amountTo.Balance = amountTo.Balance + amount // neo-go#953
453 | common.SetSerialized(ctx, to, amountTo)
454 | }
455 |
456 | runtime.Notify("Transfer", from, to, amount)
457 | runtime.Notify("TransferX", from, to, amount, details)
458 |
459 | return true
460 | }
461 |
462 | // canTransfer returns the amount it can transfer.
463 | func (t Token) canTransfer(ctx storage.Context, from, to interop.Hash160, amount int, innerRing bool) (Account, bool) {
464 | var (
465 | emptyAcc = Account{}
466 | )
467 |
468 | if !innerRing {
469 | if len(to) != interop.Hash160Len || !isUsableAddress(from) {
470 | runtime.Log("bad script hashes")
471 | return emptyAcc, false
472 | }
473 | } else if len(from) == 0 {
474 | return emptyAcc, true
475 | }
476 |
477 | amountFrom := getAccount(ctx, from)
478 | if amountFrom.Balance < amount {
479 | runtime.Log("not enough assets")
480 | return emptyAcc, false
481 | }
482 |
483 | // return amountFrom value back to transfer, reduces extra Get
484 | return amountFrom, true
485 | }
486 |
487 | // isUsableAddress checks if the sender is either a correct NEO address or SC address.
488 | func isUsableAddress(addr interop.Hash160) bool {
489 | if len(addr) == 20 {
490 | if runtime.CheckWitness(addr) {
491 | return true
492 | }
493 |
494 | // Check if a smart contract is calling script hash
495 | callingScriptHash := runtime.GetCallingScriptHash()
496 | if common.BytesEqual(callingScriptHash, addr) {
497 | return true
498 | }
499 | }
500 |
501 | return false
502 | }
503 |
504 | func getAccount(ctx storage.Context, key interface{}) Account {
505 | data := storage.Get(ctx, key)
506 | if data != nil {
507 | return std.Deserialize(data.([]byte)).(Account)
508 | }
509 |
510 | return Account{}
511 | }
512 |
--------------------------------------------------------------------------------
/balance/config.yml:
--------------------------------------------------------------------------------
1 | name: "FrostFS Balance"
2 | supportedstandards: ["NEP-17"]
3 | safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "version"]
4 | permissions:
5 | - methods: ["update"]
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 | - name: Mint
38 | parameters:
39 | - name: to
40 | type: Hash160
41 | - name: amount
42 | type: Integer
43 | - name: Burn
44 | parameters:
45 | - name: from
46 | type: Hash160
47 | - name: amount
48 | type: Integer
--------------------------------------------------------------------------------
/balance/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Balance contract is a contract deployed in FrostFS sidechain.
3 |
4 | Balance contract stores all FrostFS account balances. It is a NEP-17 compatible
5 | contract, so it can be tracked and controlled by N3 compatible network
6 | monitors and wallet software.
7 |
8 | This contract is used to store all micro transactions in the sidechain, such as
9 | data audit settlements or container fee payments. It is inefficient to make such
10 | small payment transactions in the mainchain. To process small transfers, balance
11 | contract has higher (12) decimal precision than native GAS contract.
12 |
13 | FrostFS balances are synchronized with mainchain operations. Deposit produces
14 | minting of FROSTFS tokens in Balance contract. Withdraw locks some FROSTFS tokens
15 | in a special lock account. When FrostFS contract transfers GAS assets back to the
16 | user, the lock account is destroyed with burn operation.
17 |
18 | # Contract notifications
19 |
20 | Transfer notification. This is a NEP-17 standard notification.
21 |
22 | Transfer:
23 | - name: from
24 | type: Hash160
25 | - name: to
26 | type: Hash160
27 | - name: amount
28 | type: Integer
29 |
30 | TransferX notification. This is an enhanced transfer notification with details.
31 |
32 | TransferX:
33 | - name: from
34 | type: Hash160
35 | - name: to
36 | type: Hash160
37 | - name: amount
38 | type: Integer
39 | - name: details
40 | type: ByteArray
41 |
42 | Lock notification. This notification is produced when a lock account is
43 | created. It contains information about the mainchain transaction that has produced
44 | the asset lock, the address of the lock account and the FrostFS epoch number until which the
45 | lock account is valid. Alphabet nodes of the Inner Ring catch notification and initialize
46 | Cheque method invocation of FrostFS contract.
47 |
48 | Lock:
49 | - name: txID
50 | type: ByteArray
51 | - name: from
52 | type: Hash160
53 | - name: to
54 | type: Hash160
55 | - name: amount
56 | type: Integer
57 | - name: until
58 | type: Integer
59 |
60 | Mint notification. This notification is produced when user balance is
61 | replenished from deposit in the mainchain.
62 |
63 | Mint:
64 | - name: to
65 | type: Hash160
66 | - name: amount
67 | type: Integer
68 |
69 | Burn notification. This notification is produced after user balance is reduced
70 | when FrostFS contract has transferred GAS assets back to the user.
71 |
72 | Burn:
73 | - name: from
74 | type: Hash160
75 | - name: amount
76 | type: Integer
77 | */
78 | package balance
79 |
--------------------------------------------------------------------------------
/common/ir.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "github.com/nspcc-dev/neo-go/pkg/interop"
5 | "github.com/nspcc-dev/neo-go/pkg/interop/contract"
6 | "github.com/nspcc-dev/neo-go/pkg/interop/native/ledger"
7 | "github.com/nspcc-dev/neo-go/pkg/interop/native/neo"
8 | "github.com/nspcc-dev/neo-go/pkg/interop/native/roles"
9 | "github.com/nspcc-dev/neo-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 |
--------------------------------------------------------------------------------
/common/storage.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "github.com/nspcc-dev/neo-go/pkg/interop/native/std"
5 | "github.com/nspcc-dev/neo-go/pkg/interop/storage"
6 | )
7 |
8 | func GetList(ctx storage.Context, key interface{}) [][]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 interface{}, value interface{}) {
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/nspcc-dev/neo-go/pkg/interop/runtime"
5 | "github.com/nspcc-dev/neo-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 interface{} = 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/nspcc-dev/neo-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/nspcc-dev/neo-go/pkg/interop/native/std"
4 |
5 | const (
6 | major = 0
7 | minor = 16
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 = 16
15 | prevPatch = 0
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 previous version is more than PrevVersion to ensure migrating contract data
30 | // was done successfully.
31 | func CheckVersion(from int) {
32 | if from < PrevVersion {
33 | panic(ErrVersionMismatch + ": expected >=" + std.Itoa(PrevVersion, 10))
34 | }
35 | if from == Version {
36 | panic(ErrAlreadyUpdated + ": " + std.Itoa(Version, 10))
37 | }
38 | }
39 |
40 | // AppendVersion appends current contract version to the list of deploy arguments.
41 | func AppendVersion(data interface{}) []interface{} {
42 | if data == nil {
43 | return []interface{}{Version}
44 | }
45 | return append(data.([]interface{}), Version)
46 | }
47 |
--------------------------------------------------------------------------------
/common/vote.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import (
4 | "github.com/nspcc-dev/neo-go/pkg/interop"
5 | "github.com/nspcc-dev/neo-go/pkg/interop/native/crypto"
6 | "github.com/nspcc-dev/neo-go/pkg/interop/native/ledger"
7 | "github.com/nspcc-dev/neo-go/pkg/interop/native/std"
8 | "github.com/nspcc-dev/neo-go/pkg/interop/storage"
9 | "github.com/nspcc-dev/neo-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 | // getBallots returns a deserialized slice of vote ballots.
100 | func getBallots(ctx storage.Context) []Ballot {
101 | data := storage.Get(ctx, voteKey)
102 | if data != nil {
103 | return std.Deserialize(data.([]byte)).([]Ballot)
104 | }
105 |
106 | return []Ballot{}
107 | }
108 |
109 | // BytesEqual compares two slices of bytes by wrapping them into strings,
110 | // which is necessary with new util.Equals interop behaviour, see neo-go#1176.
111 | func BytesEqual(a []byte, b []byte) bool {
112 | return util.Equals(string(a), string(b))
113 | }
114 |
115 | // InvokeID returns hashed value of prefix and args concatenation. Iy is used to
116 | // identify different ballots.
117 | func InvokeID(args []interface{}, prefix []byte) []byte {
118 | for i := range args {
119 | arg := args[i].([]byte)
120 | prefix = append(prefix, arg...)
121 | }
122 |
123 | return crypto.Sha256(prefix)
124 | }
125 |
126 | /*
127 | Check if the invocation is made from known container or audit contracts.
128 | This is necessary because calls from these contracts require to do transfer
129 | without signature collection (1 invoke transfer).
130 |
131 | IR1, IR2, IR3, IR4 -(4 invokes)-> [ Container Contract ] -(1 invoke)-> [ Balance Contract ]
132 |
133 | We can do 1 invoke transfer if:
134 | - invokation has happened from inner ring node,
135 | - it is indirect invocation from another smart-contract.
136 |
137 | However, there is a possible attack, when a malicious inner ring node creates
138 | a malicious smart-contract in the morph chain to do indirect call.
139 |
140 | MaliciousIR -(1 invoke)-> [ Malicious Contract ] -(1 invoke)-> [ Balance Contract ]
141 |
142 | To prevent that, we have to allow 1 invoke transfer from authorised well-known
143 | smart-contracts, that will be set up at `Init` method.
144 | */
145 |
146 | func FromKnownContract(ctx storage.Context, caller interop.Hash160, key string) bool {
147 | addr := storage.Get(ctx, key).(interop.Hash160)
148 | return BytesEqual(caller, addr)
149 | }
150 |
--------------------------------------------------------------------------------
/common/witness.go:
--------------------------------------------------------------------------------
1 | package common
2 |
3 | import "github.com/nspcc-dev/neo-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 |
--------------------------------------------------------------------------------
/container/config.yml:
--------------------------------------------------------------------------------
1 | name: "FrostFS Container"
2 | safemethods: ["count", "containersOf", "get", "owner", "list", "eACL", "getContainerSize", "listContainerSizes", "iterateContainerSizes", "version"]
3 | permissions:
4 | - methods: ["update", "addKey", "transferX",
5 | "register", "addRecord", "deleteRecords"]
6 | events:
7 | - name: containerPut
8 | parameters:
9 | - name: container
10 | type: ByteArray
11 | - name: signature
12 | type: Signature
13 | - name: publicKey
14 | type: PublicKey
15 | - name: token
16 | type: ByteArray
17 | - name: PutSuccess
18 | parameters:
19 | - name: containerID
20 | type: Hash256
21 | - name: publicKey
22 | type: PublicKey
23 | - name: containerDelete
24 | parameters:
25 | - name: containerID
26 | type: ByteArray
27 | - name: signature
28 | type: Signature
29 | - name: token
30 | type: ByteArray
31 | - name: DeleteSuccess
32 | parameters:
33 | - name: containerID
34 | type: ByteArray
35 | - name: setEACL
36 | parameters:
37 | - name: eACL
38 | type: ByteArray
39 | - name: signature
40 | type: Signature
41 | - name: publicKey
42 | type: PublicKey
43 | - name: token
44 | type: ByteArray
45 | - name: SetEACLSuccess
46 | parameters:
47 | - name: containerID
48 | type: ByteArray
49 | - name: publicKey
50 | type: PublicKey
51 | - name: StartEstimation
52 | parameters:
53 | - name: epoch
54 | type: Integer
55 | - name: StopEstimation
56 | parameters:
57 | - name: epoch
58 | type: Integer
59 |
--------------------------------------------------------------------------------
/container/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Container contract is a contract deployed in FrostFS sidechain.
3 |
4 | Container contract stores and manages containers, extended ACLs and container
5 | size estimations. Contract does not perform sanity or signature checks of
6 | containers or extended ACLs, it is done by Alphabet nodes of the Inner Ring.
7 | Alphabet nodes approve it by invoking the same Put or SetEACL methods with
8 | the same arguments.
9 |
10 | # Contract notifications
11 |
12 | containerPut notification. This notification is produced when a user wants to
13 | create a new container. Alphabet nodes of the Inner Ring catch the notification and
14 | validate container data, signature and token if present.
15 |
16 | containerPut:
17 | - name: container
18 | type: ByteArray
19 | - name: signature
20 | type: Signature
21 | - name: publicKey
22 | type: PublicKey
23 | - name: token
24 | type: ByteArray
25 |
26 | containerDelete notification. This notification is produced when a container owner
27 | wants to delete a container. Alphabet nodes of the Inner Ring catch the notification
28 | and validate container ownership, signature and token if present.
29 |
30 | containerDelete:
31 | - name: containerID
32 | type: ByteArray
33 | - name: signature
34 | type: Signature
35 | - name: token
36 | type: ByteArray
37 |
38 | setEACL notification. This notification is produced when a container owner wants
39 | to update an extended ACL of a container. Alphabet nodes of the Inner Ring catch
40 | the notification and validate container ownership, signature and token if
41 | present.
42 |
43 | setEACL:
44 | - name: eACL
45 | type: ByteArray
46 | - name: signature
47 | type: Signature
48 | - name: publicKey
49 | type: PublicKey
50 | - name: token
51 | type: ByteArray
52 |
53 | StartEstimation notification. This notification is produced when Storage nodes
54 | should exchange estimation values of container sizes among other Storage nodes.
55 |
56 | StartEstimation:
57 | - name: epoch
58 | type: Integer
59 |
60 | StopEstimation notification. This notification is produced when Storage nodes
61 | should calculate average container size based on received estimations and store
62 | it in Container contract.
63 |
64 | StopEstimation:
65 | - name: epoch
66 | type: Integer
67 | */
68 | package container
69 |
--------------------------------------------------------------------------------
/debian/changelog:
--------------------------------------------------------------------------------
1 | frostfs-contract (0.0.0) stable; urgency=medium
2 |
3 | * Initial release
4 |
5 | -- TrueCloudLab Wed, 24 Aug 2022 18:29:49 +0300
6 |
--------------------------------------------------------------------------------
/debian/control:
--------------------------------------------------------------------------------
1 | Source: frostfs-contract
2 | Section: misc
3 | Priority: optional
4 | Maintainer: FrostFS
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/TrueCloudLab/frostfs-contract.git
9 | Vcs-Browser: https://github.com/TrueCloudLab/frostfs-contract
10 |
11 | Package: frostfs-contract
12 | Architecture: all
13 | Depends: ${misc:Depends}
14 | Description: FrostFS-Contract contains all FrostFS 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 | - frostfs
21 | - processing
22 | .
23 | Sidechain contracts:
24 | .
25 | - alphabet
26 | - audit
27 | - balance
28 | - container
29 | - frostfsid
30 | - netmap
31 | - nns
32 | - proxy
33 | - reputation
34 | - subnet
35 |
--------------------------------------------------------------------------------
/debian/copyright:
--------------------------------------------------------------------------------
1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
2 | Upstream-Name: frostfs-contract
3 | Upstream-Contact: tech@frostfs.info
4 | Source: https://github.com/TrueCloudLab/frostfs-contract
5 |
6 | Files: *
7 | Copyright: 2022 TrueCloudLab (@TrueCloudLab)
8 | Copyright: 2018-2022 NeoSPCC (@nspcc-dev)
9 |
10 | License: GPL-3
11 | This program is free software: you can redistribute it and/or modify it
12 | under the terms of the GNU General Public License as published
13 | by the Free Software Foundation; either version 3 of the License, or
14 | (at your option) any later version.
15 |
16 | This program is distributed in the hope that it will be useful,
17 | but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 | General Public License for more details.
20 |
21 | You should have received a copy of the GNU General Public License
22 | along with this program or at /usr/share/common-licenses/GPL-3.
23 | If not, see .
24 |
--------------------------------------------------------------------------------
/debian/neofs-contract.docs:
--------------------------------------------------------------------------------
1 | README*
2 |
--------------------------------------------------------------------------------
/debian/postinst.ex:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # postinst script for frostfs-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 frostfs-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 frostfs-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 frostfs-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 = frostfs-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/frostfs/contract
15 | find . -maxdepth 2 \( -name '*.nef' -o -name 'config.json' \) -exec cp --parents \{\} debian/$(SERVICE)/var/lib/frostfs/contract \;
16 |
17 | override_dh_installchangelogs:
18 | dh_installchangelogs -k CHANGELOG.md
19 |
20 |
21 |
--------------------------------------------------------------------------------
/debian/source/format:
--------------------------------------------------------------------------------
1 | 3.0 (quilt)
2 |
--------------------------------------------------------------------------------
/frostfs/config.yml:
--------------------------------------------------------------------------------
1 | name: "FrostFS"
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 | - name: Unbind
41 | parameters:
42 | - name: user
43 | type: ByteArray
44 | - name: keys
45 | type: Array
46 | - name: AlphabetUpdate
47 | parameters:
48 | - name: id
49 | type: ByteArray
50 | - name: alphabet
51 | type: Array
52 | - name: SetConfig
53 | parameters:
54 | - name: id
55 | type: ByteArray
56 | - name: key
57 | type: ByteArray
58 | - name: value
59 | type: ByteArray
60 |
--------------------------------------------------------------------------------
/frostfs/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | FrostFS contract is a contract deployed in FrostFS mainchain.
3 |
4 | FrostFS contract is an entry point to FrostFS users. This contract stores all FrostFS
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, FrostFS can't change more than 1\3 keys at a time.
10 | FrostFS contract contains the actual list of Alphabet nodes in the sidechain.
11 |
12 | Network configuration is also stored in FrostFS 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 FrostFS contract address. The same amount of FROSTFS 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 FrostFS 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 FrostFS 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 | FrostFS 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 frostfs
95 |
--------------------------------------------------------------------------------
/frostfsid/config.yml:
--------------------------------------------------------------------------------
1 | name: "FrostFS ID"
2 | safemethods: ["key", "version"]
3 | permissions:
4 | - methods: ["update"]
5 |
--------------------------------------------------------------------------------
/frostfsid/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | FrostFSID contract is a contract deployed in FrostFS sidechain.
3 |
4 | FrostFSID contract is used to store connection between an OwnerID and its public keys.
5 | OwnerID is a 25-byte N3 wallet address that can be produced from a public key.
6 | It is one-way conversion. In simple cases, FrostFS verifies ownership by checking
7 | signature and relation between a public key and an OwnerID.
8 |
9 | In more complex cases, a user can use public keys unrelated to the OwnerID to maintain
10 | secure access to the data. FrostFSID contract stores relation between an OwnerID and
11 | arbitrary public keys. Data owner can bind a public key with its account or unbind it
12 | by invoking Bind or Unbind methods of FrostFS contract in the mainchain. After that,
13 | Alphabet nodes produce multisigned AddKey and RemoveKey invocations of FrostFSID
14 | contract.
15 |
16 | # Contract notifications
17 |
18 | FrostFSID contract does not produce notifications to process.
19 | */
20 | package frostfsid
21 |
--------------------------------------------------------------------------------
/frostfsid/frostfsid_contract.go:
--------------------------------------------------------------------------------
1 | package frostfsid
2 |
3 | import (
4 | "github.com/TrueCloudLab/frostfs-contract/common"
5 | "github.com/nspcc-dev/neo-go/pkg/interop"
6 | "github.com/nspcc-dev/neo-go/pkg/interop/contract"
7 | "github.com/nspcc-dev/neo-go/pkg/interop/iterator"
8 | "github.com/nspcc-dev/neo-go/pkg/interop/native/crypto"
9 | "github.com/nspcc-dev/neo-go/pkg/interop/native/management"
10 | "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
11 | "github.com/nspcc-dev/neo-go/pkg/interop/storage"
12 | )
13 |
14 | type (
15 | UserInfo struct {
16 | Keys [][]byte
17 | }
18 | )
19 |
20 | const (
21 | ownerSize = 1 + interop.Hash160Len + 4
22 | )
23 |
24 | const (
25 | netmapContractKey = "netmapScriptHash"
26 | containerContractKey = "containerScriptHash"
27 | notaryDisabledKey = "notary"
28 | ownerKeysPrefix = 'o'
29 | )
30 |
31 | func _deploy(data interface{}, isUpdate bool) {
32 | ctx := storage.GetContext()
33 |
34 | if isUpdate {
35 | args := data.([]interface{})
36 | common.CheckVersion(args[len(args)-1].(int))
37 | return
38 | }
39 |
40 | args := data.(struct {
41 | notaryDisabled bool
42 | addrNetmap interop.Hash160
43 | addrContainer interop.Hash160
44 | })
45 |
46 | if len(args.addrNetmap) != interop.Hash160Len || len(args.addrContainer) != interop.Hash160Len {
47 | panic("incorrect length of contract script hash")
48 | }
49 |
50 | storage.Put(ctx, netmapContractKey, args.addrNetmap)
51 | storage.Put(ctx, containerContractKey, args.addrContainer)
52 |
53 | // initialize the way to collect signatures
54 | storage.Put(ctx, notaryDisabledKey, args.notaryDisabled)
55 | if args.notaryDisabled {
56 | common.InitVote(ctx)
57 | runtime.Log("frostfsid contract notary disabled")
58 | }
59 |
60 | runtime.Log("frostfsid contract initialized")
61 | }
62 |
63 | // Update method updates contract source code and manifest. It can be invoked
64 | // only by committee.
65 | func Update(script []byte, manifest []byte, data interface{}) {
66 | if !common.HasUpdateAccess() {
67 | panic("only committee can update contract")
68 | }
69 |
70 | contract.Call(interop.Hash160(management.Hash), "update",
71 | contract.All, script, manifest, common.AppendVersion(data))
72 | runtime.Log("frostfsid contract updated")
73 | }
74 |
75 | // AddKey binds a list of the provided public keys to the OwnerID. It can be invoked only by
76 | // Alphabet nodes.
77 | //
78 | // This method panics if the OwnerID is not an ownerSize byte or the public key is not 33 byte long.
79 | // If the key is already bound, the method ignores it.
80 | func AddKey(owner []byte, keys []interop.PublicKey) {
81 | // V2 format
82 | if len(owner) != ownerSize {
83 | panic("incorrect owner")
84 | }
85 |
86 | for i := range keys {
87 | if len(keys[i]) != interop.PublicKeyCompressedLen {
88 | panic("incorrect public key")
89 | }
90 | }
91 |
92 | ctx := storage.GetContext()
93 | notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
94 |
95 | var ( // for invocation collection without notary
96 | alphabet []interop.PublicKey
97 | nodeKey []byte
98 | indirectCall bool
99 | )
100 |
101 | if notaryDisabled {
102 | alphabet = common.AlphabetNodes()
103 | nodeKey = common.InnerRingInvoker(alphabet)
104 | if len(nodeKey) == 0 {
105 | panic("invocation from non inner ring node")
106 | }
107 |
108 | indirectCall = common.FromKnownContract(
109 | ctx,
110 | runtime.GetCallingScriptHash(),
111 | containerContractKey)
112 |
113 | if indirectCall {
114 | threshold := len(alphabet)*2/3 + 1
115 | id := invokeIDKeys(owner, keys, []byte("add"))
116 |
117 | n := common.Vote(ctx, id, nodeKey)
118 | if n < threshold {
119 | return
120 | }
121 |
122 | common.RemoveVotes(ctx, id)
123 | }
124 | } else {
125 | multiaddr := common.AlphabetAddress()
126 | common.CheckAlphabetWitness(multiaddr)
127 | }
128 |
129 | ownerKey := append([]byte{ownerKeysPrefix}, owner...)
130 | for i := range keys {
131 | stKey := append(ownerKey, keys[i]...)
132 | storage.Put(ctx, stKey, []byte{1})
133 | }
134 |
135 | runtime.Log("key bound to the owner")
136 | }
137 |
138 | // RemoveKey unbinds the provided public keys from the OwnerID. It can be invoked only by
139 | // Alphabet nodes.
140 | //
141 | // This method panics if the OwnerID is not an ownerSize byte or the public key is not 33 byte long.
142 | // If the key is already unbound, the method ignores it.
143 | func RemoveKey(owner []byte, keys []interop.PublicKey) {
144 | // V2 format
145 | if len(owner) != ownerSize {
146 | panic("incorrect owner")
147 | }
148 |
149 | for i := range keys {
150 | if len(keys[i]) != interop.PublicKeyCompressedLen {
151 | panic("incorrect public key")
152 | }
153 | }
154 |
155 | ctx := storage.GetContext()
156 | notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
157 |
158 | var ( // for invocation collection without notary
159 | alphabet []interop.PublicKey
160 | nodeKey []byte
161 | )
162 |
163 | if notaryDisabled {
164 | alphabet = common.AlphabetNodes()
165 | nodeKey = common.InnerRingInvoker(alphabet)
166 | if len(nodeKey) == 0 {
167 | panic("invocation from non inner ring node")
168 | }
169 |
170 | threshold := len(alphabet)*2/3 + 1
171 | id := invokeIDKeys(owner, keys, []byte("remove"))
172 |
173 | n := common.Vote(ctx, id, nodeKey)
174 | if n < threshold {
175 | return
176 | }
177 |
178 | common.RemoveVotes(ctx, id)
179 | } else {
180 | multiaddr := common.AlphabetAddress()
181 | if !runtime.CheckWitness(multiaddr) {
182 | panic("invocation from non inner ring node")
183 | }
184 | }
185 |
186 | ownerKey := append([]byte{ownerKeysPrefix}, owner...)
187 | for i := range keys {
188 | stKey := append(ownerKey, keys[i]...)
189 | storage.Delete(ctx, stKey)
190 | }
191 | }
192 |
193 | // Key method returns a list of 33-byte public keys bound with the OwnerID.
194 | //
195 | // This method panics if the owner is not ownerSize byte long.
196 | func Key(owner []byte) [][]byte {
197 | // V2 format
198 | if len(owner) != ownerSize {
199 | panic("incorrect owner")
200 | }
201 |
202 | ctx := storage.GetReadOnlyContext()
203 |
204 | ownerKey := append([]byte{ownerKeysPrefix}, owner...)
205 | info := getUserInfo(ctx, ownerKey)
206 |
207 | return info.Keys
208 | }
209 |
210 | // Version returns the version of the contract.
211 | func Version() int {
212 | return common.Version
213 | }
214 |
215 | func getUserInfo(ctx storage.Context, key interface{}) UserInfo {
216 | it := storage.Find(ctx, key, storage.KeysOnly|storage.RemovePrefix)
217 | pubs := [][]byte{}
218 | for iterator.Next(it) {
219 | pub := iterator.Value(it).([]byte)
220 | pubs = append(pubs, pub)
221 | }
222 |
223 | return UserInfo{Keys: pubs}
224 | }
225 |
226 | func invokeIDKeys(owner []byte, keys []interop.PublicKey, prefix []byte) []byte {
227 | prefix = append(prefix, owner...)
228 | for i := range keys {
229 | prefix = append(prefix, keys[i]...)
230 | }
231 |
232 | return crypto.Sha256(prefix)
233 | }
234 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/TrueCloudLab/frostfs-contract
2 |
3 | go 1.14
4 |
5 | require (
6 | github.com/mr-tron/base58 v1.2.0
7 | github.com/nspcc-dev/neo-go v0.99.4
8 | github.com/nspcc-dev/neo-go/pkg/interop v0.0.0-20220927123257-24c107e3a262
9 | github.com/stretchr/testify v1.7.0
10 | )
11 |
--------------------------------------------------------------------------------
/netmap/config.yml:
--------------------------------------------------------------------------------
1 | name: "FrostFS Netmap"
2 | safemethods: ["innerRingList", "epoch", "netmap", "netmapCandidates", "snapshot", "snapshotByEpoch", "config", "listConfig", "version"]
3 | permissions:
4 | - methods: ["update", "newEpoch"]
5 | events:
6 | - name: AddPeer
7 | parameters:
8 | - name: nodeInfo
9 | type: ByteArray
10 | - name: AddPeerSuccess
11 | parameters:
12 | - name: publicKey
13 | type: PublicKey
14 | - name: UpdateState
15 | parameters:
16 | - name: state
17 | type: Integer
18 | - name: publicKey
19 | type: PublicKey
20 | - name: UpdateStateSuccess
21 | parameters:
22 | - name: publicKey
23 | type: PublicKey
24 | - name: state
25 | type: Integer
26 | - name: NewEpoch
27 | parameters:
28 | - name: epoch
29 | type: Integer
30 |
--------------------------------------------------------------------------------
/netmap/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Netmap contract is a contract deployed in FrostFS sidechain.
3 |
4 | Netmap contract stores and manages FrostFS 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 |
--------------------------------------------------------------------------------
/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"]
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 |
--------------------------------------------------------------------------------
/nns/namestate.go:
--------------------------------------------------------------------------------
1 | package nns
2 |
3 | import (
4 | "github.com/nspcc-dev/neo-go/pkg/interop"
5 | "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
6 | )
7 |
8 | // NameState represents domain name state.
9 | type NameState struct {
10 | Owner interop.Hash160
11 | Name string
12 | Expiration int64
13 | Admin interop.Hash160
14 | }
15 |
16 | // ensureNotExpired panics if domain name is expired.
17 | func (n NameState) ensureNotExpired() {
18 | if int64(runtime.GetTime()) >= n.Expiration {
19 | panic("name has expired")
20 | }
21 | }
22 |
23 | // checkAdmin panics if script container is not signed by the domain name admin.
24 | func (n NameState) checkAdmin() {
25 | if runtime.CheckWitness(n.Owner) {
26 | return
27 | }
28 | if n.Admin == nil || !runtime.CheckWitness(n.Admin) {
29 | panic("not witnessed by admin")
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/nns/recordtype.go:
--------------------------------------------------------------------------------
1 | package nns
2 |
3 | // RecordType is domain name service record types.
4 | type RecordType byte
5 |
6 | // Record types are defined in [RFC 1035](https://tools.ietf.org/html/rfc1035)
7 | const (
8 | // A represents address record type.
9 | A RecordType = 1
10 | // CNAME represents canonical name record type.
11 | CNAME RecordType = 5
12 | // SOA represents start of authority record type.
13 | SOA RecordType = 6
14 | // TXT represents text record type.
15 | TXT RecordType = 16
16 | )
17 |
18 | // Record types are defined in [RFC 3596](https://tools.ietf.org/html/rfc3596)
19 | const (
20 | // AAAA represents IPv6 address record type.
21 | AAAA RecordType = 28
22 | )
23 |
--------------------------------------------------------------------------------
/processing/config.yml:
--------------------------------------------------------------------------------
1 | name: "FrostFS Multi Signature Processing"
2 | safemethods: ["verify", "version"]
3 | permissions:
4 | - methods: ["update"]
5 |
--------------------------------------------------------------------------------
/processing/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Processing contract is a contract deployed in FrostFS mainchain.
3 |
4 | Processing contract pays for all multisignature transaction executions when notary
5 | service is enabled in the mainchain. Notary service prepares multisigned transactions,
6 | however they should contain sidechain GAS to be executed. It is inconvenient to
7 | ask Alphabet nodes to pay for these transactions: nodes can change over time,
8 | some nodes will spend sidechain GAS faster. It leads to economic instability.
9 |
10 | Processing contract exists to solve this issue. At the Withdraw invocation of
11 | FrostFS contract, a user pays fee directly to this contract. This fee is used to
12 | pay for Cheque invocation of FrostFS contract that returns mainchain GAS back
13 | to the user. The address of the Processing contract is used as the first signer in
14 | the multisignature transaction. Therefore, NeoVM executes Verify method of the
15 | contract and if invocation is verified, Processing contract pays for the
16 | execution.
17 |
18 | # Contract notifications
19 |
20 | Processing contract does not produce notifications to process.
21 | */
22 | package processing
23 |
--------------------------------------------------------------------------------
/processing/processing_contract.go:
--------------------------------------------------------------------------------
1 | package processing
2 |
3 | import (
4 | "github.com/TrueCloudLab/frostfs-contract/common"
5 | "github.com/nspcc-dev/neo-go/pkg/interop"
6 | "github.com/nspcc-dev/neo-go/pkg/interop/contract"
7 | "github.com/nspcc-dev/neo-go/pkg/interop/native/gas"
8 | "github.com/nspcc-dev/neo-go/pkg/interop/native/ledger"
9 | "github.com/nspcc-dev/neo-go/pkg/interop/native/management"
10 | "github.com/nspcc-dev/neo-go/pkg/interop/native/roles"
11 | "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
12 | "github.com/nspcc-dev/neo-go/pkg/interop/storage"
13 | )
14 |
15 | const (
16 | frostfsContractKey = "frostfsScriptHash"
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 interface{}) {
23 | caller := runtime.GetCallingScriptHash()
24 | if !common.BytesEqual(caller, []byte(gas.Hash)) {
25 | common.AbortWithMessage("processing contract accepts GAS only")
26 | }
27 | }
28 |
29 | func _deploy(data interface{}, isUpdate bool) {
30 | if isUpdate {
31 | args := data.([]interface{})
32 | common.CheckVersion(args[len(args)-1].(int))
33 | return
34 | }
35 |
36 | args := data.(struct {
37 | addrFrostFS interop.Hash160
38 | })
39 |
40 | ctx := storage.GetContext()
41 |
42 | if len(args.addrFrostFS) != interop.Hash160Len {
43 | panic("incorrect length of contract script hash")
44 | }
45 |
46 | storage.Put(ctx, frostfsContractKey, args.addrFrostFS)
47 |
48 | runtime.Log("processing contract initialized")
49 | }
50 |
51 | // Update method updates contract source code and manifest. It can be invoked
52 | // only by the sidechain committee.
53 | func Update(script []byte, manifest []byte, data interface{}) {
54 | blockHeight := ledger.CurrentIndex()
55 | alphabetKeys := roles.GetDesignatedByRole(roles.NeoFSAlphabet, uint32(blockHeight+1))
56 | alphabetCommittee := common.Multiaddress(alphabetKeys, true)
57 |
58 | if !runtime.CheckWitness(alphabetCommittee) {
59 | panic("only side chain committee can update contract")
60 | }
61 |
62 | contract.Call(interop.Hash160(management.Hash), "update",
63 | contract.All, script, manifest, common.AppendVersion(data))
64 | runtime.Log("processing contract updated")
65 | }
66 |
67 | // Verify method returns true if transaction contains valid multisignature of
68 | // Alphabet nodes of the Inner Ring.
69 | func Verify() bool {
70 | ctx := storage.GetContext()
71 | frostfsContractAddr := storage.Get(ctx, frostfsContractKey).(interop.Hash160)
72 | multiaddr := contract.Call(frostfsContractAddr, multiaddrMethod, contract.ReadOnly).(interop.Hash160)
73 |
74 | return runtime.CheckWitness(multiaddr)
75 | }
76 |
77 | // Version returns the version of the contract.
78 | func Version() int {
79 | return common.Version
80 | }
81 |
--------------------------------------------------------------------------------
/proxy/config.yml:
--------------------------------------------------------------------------------
1 | name: "FrostFS Notary Proxy"
2 | safemethods: ["verify", "version"]
3 | permissions:
4 | - methods: ["update"]
5 |
--------------------------------------------------------------------------------
/proxy/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Proxy contract is a contract deployed in FrostFS sidechain.
3 |
4 | Proxy contract pays for all multisignature transaction executions when notary
5 | service is enabled in the sidechain. Notary service prepares multisigned transactions,
6 | however they should contain sidechain GAS to be executed. It is inconvenient to
7 | ask Alphabet nodes to pay for these transactions: nodes can change over time,
8 | some nodes will spend sidechain GAS faster. It leads to economic instability.
9 |
10 | Proxy contract exists to solve this issue. While Alphabet contracts hold all
11 | sidechain NEO, proxy contract holds most of the sidechain GAS. Alphabet
12 | contracts emit half of the available GAS to the proxy contract. The address of the
13 | Proxy contract is used as the first signer in a multisignature transaction.
14 | Therefore, NeoVM executes Verify method of the contract; and if invocation is
15 | verified, Proxy contract pays for the execution.
16 |
17 | # Contract notifications
18 |
19 | Proxy contract does not produce notifications to process.
20 | */
21 | package proxy
22 |
--------------------------------------------------------------------------------
/proxy/proxy_contract.go:
--------------------------------------------------------------------------------
1 | package proxy
2 |
3 | import (
4 | "github.com/TrueCloudLab/frostfs-contract/common"
5 | "github.com/nspcc-dev/neo-go/pkg/interop"
6 | "github.com/nspcc-dev/neo-go/pkg/interop/contract"
7 | "github.com/nspcc-dev/neo-go/pkg/interop/native/gas"
8 | "github.com/nspcc-dev/neo-go/pkg/interop/native/management"
9 | "github.com/nspcc-dev/neo-go/pkg/interop/native/neo"
10 | "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
11 | )
12 |
13 | // OnNEP17Payment is a callback for NEP-17 compatible native GAS contract.
14 | func OnNEP17Payment(from interop.Hash160, amount int, data interface{}) {
15 | caller := runtime.GetCallingScriptHash()
16 | if !common.BytesEqual(caller, []byte(gas.Hash)) {
17 | common.AbortWithMessage("proxy contract accepts GAS only")
18 | }
19 | }
20 |
21 | func _deploy(data interface{}, isUpdate bool) {
22 | if isUpdate {
23 | args := data.([]interface{})
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 interface{}) {
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 method returns true if transaction contains valid multisignature of
44 | // Alphabet nodes of the Inner Ring.
45 | func Verify() bool {
46 | alphabet := neo.GetCommittee()
47 | sig := common.Multiaddress(alphabet, false)
48 |
49 | if !runtime.CheckWitness(sig) {
50 | sig = common.Multiaddress(alphabet, true)
51 | return runtime.CheckWitness(sig)
52 | }
53 |
54 | return true
55 | }
56 |
57 | // Version returns the version of the contract.
58 | func Version() int {
59 | return common.Version
60 | }
61 |
--------------------------------------------------------------------------------
/reputation/config.yml:
--------------------------------------------------------------------------------
1 | name: "FrostFS Reputation"
2 | safemethods: ["get", "getByID", "listByEpoch"]
3 | permissions:
4 | - methods: ["update"]
5 | events:
6 | - name: reputationPut
7 | parameters:
8 | - name: epoch
9 | type: Integer
10 | - name: peerID
11 | type: ByteArray
12 | - name: value
13 | type: ByteArray
14 |
--------------------------------------------------------------------------------
/reputation/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Reputation contract is a contract deployed in FrostFS sidechain.
3 |
4 | Inner Ring nodes produce data audit for each container during each epoch. In the end,
5 | nodes produce DataAuditResult structure that contains information about audit
6 | progress. Reputation contract provides storage for such structures and simple
7 | interface to iterate over available DataAuditResults on specified epoch.
8 |
9 | During settlement process, Alphabet nodes fetch all DataAuditResult structures
10 | from the epoch and execute balance transfers from data owners to Storage and
11 | Inner Ring nodes if data audit succeeds.
12 |
13 | # Contract notifications
14 |
15 | Reputation contract does not produce notifications to process.
16 | */
17 | package reputation
18 |
--------------------------------------------------------------------------------
/reputation/reputation_contract.go:
--------------------------------------------------------------------------------
1 | package reputation
2 |
3 | import (
4 | "github.com/TrueCloudLab/frostfs-contract/common"
5 | "github.com/nspcc-dev/neo-go/pkg/interop"
6 | "github.com/nspcc-dev/neo-go/pkg/interop/contract"
7 | "github.com/nspcc-dev/neo-go/pkg/interop/convert"
8 | "github.com/nspcc-dev/neo-go/pkg/interop/iterator"
9 | "github.com/nspcc-dev/neo-go/pkg/interop/native/management"
10 | "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
11 | "github.com/nspcc-dev/neo-go/pkg/interop/storage"
12 | )
13 |
14 | const (
15 | notaryDisabledKey = "notary"
16 | reputationValuePrefix = 'r'
17 | reputationCountPrefix = 'c'
18 | )
19 |
20 | func _deploy(data interface{}, isUpdate bool) {
21 | ctx := storage.GetContext()
22 |
23 | if isUpdate {
24 | args := data.([]interface{})
25 | common.CheckVersion(args[len(args)-1].(int))
26 | return
27 | }
28 |
29 | args := data.(struct {
30 | notaryDisabled bool
31 | })
32 |
33 | // initialize the way to collect signatures
34 | storage.Put(ctx, notaryDisabledKey, args.notaryDisabled)
35 | if args.notaryDisabled {
36 | common.InitVote(ctx)
37 | runtime.Log("reputation contract notary disabled")
38 | }
39 |
40 | runtime.Log("reputation contract initialized")
41 | }
42 |
43 | // Update method updates contract source code and manifest. It can be invoked
44 | // only by committee.
45 | func Update(script []byte, manifest []byte, data interface{}) {
46 | if !common.HasUpdateAccess() {
47 | panic("only committee can update contract")
48 | }
49 |
50 | contract.Call(interop.Hash160(management.Hash), "update",
51 | contract.All, script, manifest, common.AppendVersion(data))
52 | runtime.Log("reputation contract updated")
53 | }
54 |
55 | // Put method saves DataAuditResult in contract storage. It can be invoked only by
56 | // Inner Ring nodes. It does not require multisignature invocations.
57 | //
58 | // Epoch is the epoch number when DataAuditResult structure was generated.
59 | // PeerID contains public keys of the Inner Ring node that has produced DataAuditResult.
60 | // Value contains a stable marshaled structure of DataAuditResult.
61 | func Put(epoch int, peerID []byte, value []byte) {
62 | ctx := storage.GetContext()
63 | notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
64 |
65 | var ( // for invocation collection without notary
66 | alphabet []interop.PublicKey
67 | nodeKey []byte
68 | alphabetCall bool
69 | )
70 |
71 | if notaryDisabled {
72 | alphabet = common.AlphabetNodes()
73 | nodeKey = common.InnerRingInvoker(alphabet)
74 | alphabetCall = len(nodeKey) != 0
75 | } else {
76 | multiaddr := common.AlphabetAddress()
77 | alphabetCall = runtime.CheckWitness(multiaddr)
78 | }
79 |
80 | if !alphabetCall {
81 | runtime.Notify("reputationPut", epoch, peerID, value)
82 | return
83 | }
84 |
85 | id := storageID(epoch, peerID)
86 | if notaryDisabled {
87 | threshold := len(alphabet)*2/3 + 1
88 |
89 | n := common.Vote(ctx, id, nodeKey)
90 | if n < threshold {
91 | return
92 | }
93 |
94 | common.RemoveVotes(ctx, id)
95 | }
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 DataAuditResult structures
112 | // produced by the specified Inner Ring node 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 DataAuditResult 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 interface{} = epoch
160 |
161 | return append(buf.([]byte), peerID...)
162 | }
163 |
--------------------------------------------------------------------------------
/subnet/config.yml:
--------------------------------------------------------------------------------
1 | name: "FrostFS Subnet"
2 | safemethods: ["version"]
3 | permissions:
4 | - methods: ["update"]
5 | events:
6 | - name: Put
7 | parameters:
8 | - name: id
9 | type: ByteArray
10 | - name: ownerKey
11 | type: PublicKey
12 | - name: info
13 | type: ByteArray
14 | - name: Delete
15 | parameters:
16 | - name: id
17 | type: ByteArray
18 | - name: RemoveNode
19 | parameters:
20 | - name: subnetID
21 | type: ByteArray
22 | - name: node
23 | type: PublicKey
24 |
--------------------------------------------------------------------------------
/subnet/doc.go:
--------------------------------------------------------------------------------
1 | /*
2 | Subnet contract is a contract deployed in FrostFS sidechain.
3 |
4 | Subnet contract stores and manages FrostFS subnetwork states. It allows registering
5 | and deleting subnetworks, limiting access to them, and defining a list of the Storage
6 | Nodes that can be included in them.
7 |
8 | # Contract notifications
9 |
10 | Put notification. This notification is produced when a new subnetwork is
11 | registered by invoking Put method.
12 |
13 | Put
14 | - name: id
15 | type: ByteArray
16 | - name: ownerKey
17 | type: PublicKey
18 | - name: info
19 | type: ByteArray
20 |
21 | Delete notification. This notification is produced when some subnetwork is
22 | deleted by invoking Delete method.
23 |
24 | Delete
25 | - name: id
26 | type: ByteArray
27 |
28 | RemoveNode notification. This notification is produced when some node is deleted
29 | by invoking RemoveNode method.
30 |
31 | RemoveNode
32 | - name: subnetID
33 | type: ByteArray
34 | - name: node
35 | type: PublicKey
36 | */
37 | package subnet
38 |
--------------------------------------------------------------------------------
/subnet/subnet_contract.go:
--------------------------------------------------------------------------------
1 | package subnet
2 |
3 | import (
4 | "github.com/TrueCloudLab/frostfs-contract/common"
5 | "github.com/nspcc-dev/neo-go/pkg/interop"
6 | "github.com/nspcc-dev/neo-go/pkg/interop/contract"
7 | "github.com/nspcc-dev/neo-go/pkg/interop/iterator"
8 | "github.com/nspcc-dev/neo-go/pkg/interop/native/management"
9 | "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
10 | "github.com/nspcc-dev/neo-go/pkg/interop/storage"
11 | )
12 |
13 | const (
14 | // ErrInvalidSubnetID is thrown when subnet id is not a slice of 5 bytes.
15 | ErrInvalidSubnetID = "invalid subnet ID"
16 | // ErrInvalidGroupID is thrown when group id is not a slice of 5 bytes.
17 | ErrInvalidGroupID = "invalid group ID"
18 | // ErrInvalidOwner is thrown when owner has invalid format.
19 | ErrInvalidOwner = "invalid owner"
20 | // ErrInvalidAdmin is thrown when admin has invalid format.
21 | ErrInvalidAdmin = "invalid administrator"
22 | // ErrAlreadyExists is thrown when id already exists.
23 | ErrAlreadyExists = "subnet id already exists"
24 | // ErrNotExist is thrown when id doesn't exist.
25 | ErrNotExist = "subnet id doesn't exist"
26 | // ErrInvalidUser is thrown when user has invalid format.
27 | ErrInvalidUser = "invalid user"
28 | // ErrInvalidNode is thrown when node has invalid format.
29 | ErrInvalidNode = "invalid node key"
30 | // ErrNodeAdmNotExist is thrown when node admin is not found.
31 | ErrNodeAdmNotExist = "node admin not found"
32 | // ErrClientAdmNotExist is thrown when client admin is not found.
33 | ErrClientAdmNotExist = "client admin not found"
34 | // ErrNodeNotExist is thrown when node is not found.
35 | ErrNodeNotExist = "node not found"
36 | // ErrUserNotExist is thrown when user is not found.
37 | ErrUserNotExist = "user not found"
38 | // ErrAccessDenied is thrown when operation is denied for caller.
39 | ErrAccessDenied = "access denied"
40 | )
41 |
42 | const (
43 | nodeAdminPrefix = 'a'
44 | infoPrefix = 'i'
45 | clientAdminPrefix = 'm'
46 | nodePrefix = 'n'
47 | ownerPrefix = 'o'
48 | userPrefix = 'u'
49 | notaryDisabledKey = 'z'
50 | )
51 |
52 | const (
53 | userIDSize = 27
54 | subnetIDSize = 5
55 | groupIDSize = 5
56 | )
57 |
58 | // _deploy function sets up initial list of inner ring public keys.
59 | func _deploy(data interface{}, isUpdate bool) {
60 | if isUpdate {
61 | args := data.([]interface{})
62 | common.CheckVersion(args[len(args)-1].(int))
63 | return
64 | }
65 |
66 | args := data.(struct {
67 | notaryDisabled bool
68 | })
69 |
70 | ctx := storage.GetContext()
71 | storage.Put(ctx, []byte{notaryDisabledKey}, args.notaryDisabled)
72 | }
73 |
74 | // Update method updates contract source code and manifest. It can be invoked
75 | // only by committee.
76 | func Update(script []byte, manifest []byte, data interface{}) {
77 | if !common.HasUpdateAccess() {
78 | panic("only committee can update contract")
79 | }
80 |
81 | contract.Call(interop.Hash160(management.Hash), "update", contract.All,
82 | script, manifest, common.AppendVersion(data))
83 | runtime.Log("subnet contract updated")
84 | }
85 |
86 | // Put creates a new subnet with the specified owner and info.
87 | func Put(id []byte, ownerKey interop.PublicKey, info []byte) {
88 | // V2 format
89 | if len(id) != subnetIDSize {
90 | panic(ErrInvalidSubnetID)
91 | }
92 | if len(ownerKey) != interop.PublicKeyCompressedLen {
93 | panic(ErrInvalidOwner)
94 | }
95 |
96 | ctx := storage.GetContext()
97 | stKey := append([]byte{ownerPrefix}, id...)
98 | if storage.Get(ctx, stKey) != nil {
99 | panic(ErrAlreadyExists)
100 | }
101 |
102 | notaryDisabled := storage.Get(ctx, notaryDisabledKey).(bool)
103 | if notaryDisabled {
104 | alphabet := common.AlphabetNodes()
105 | nodeKey := common.InnerRingInvoker(alphabet)
106 | if len(nodeKey) == 0 {
107 | common.CheckWitness(ownerKey)
108 | runtime.Notify("Put", id, ownerKey, info)
109 | return
110 | }
111 |
112 | threshold := len(alphabet)*2/3 + 1
113 | id := common.InvokeID([]interface{}{ownerKey, info}, []byte("put"))
114 | n := common.Vote(ctx, id, nodeKey)
115 | if n < threshold {
116 | return
117 | }
118 |
119 | common.RemoveVotes(ctx, id)
120 | } else {
121 | common.CheckOwnerWitness(ownerKey)
122 |
123 | multiaddr := common.AlphabetAddress()
124 | common.CheckAlphabetWitness(multiaddr)
125 | }
126 |
127 | storage.Put(ctx, stKey, ownerKey)
128 | stKey[0] = infoPrefix
129 | storage.Put(ctx, stKey, info)
130 | }
131 |
132 | // Get returns info about the subnet with the specified id.
133 | func Get(id []byte) []byte {
134 | // V2 format
135 | if len(id) != subnetIDSize {
136 | panic(ErrInvalidSubnetID)
137 | }
138 |
139 | ctx := storage.GetReadOnlyContext()
140 | key := append([]byte{infoPrefix}, id...)
141 | raw := storage.Get(ctx, key)
142 | if raw == nil {
143 | panic(ErrNotExist)
144 | }
145 | return raw.([]byte)
146 | }
147 |
148 | // Delete deletes the subnet with the specified id.
149 | func Delete(id []byte) {
150 | // V2 format
151 | if len(id) != subnetIDSize {
152 | panic(ErrInvalidSubnetID)
153 | }
154 |
155 | ctx := storage.GetContext()
156 | key := append([]byte{ownerPrefix}, id...)
157 | raw := storage.Get(ctx, key)
158 | if raw == nil {
159 | return
160 | }
161 |
162 | owner := raw.([]byte)
163 | common.CheckOwnerWitness(owner)
164 |
165 | storage.Delete(ctx, key)
166 |
167 | key[0] = infoPrefix
168 | storage.Delete(ctx, key)
169 |
170 | key[0] = nodeAdminPrefix
171 | deleteByPrefix(ctx, key)
172 |
173 | key[0] = nodePrefix
174 | deleteByPrefix(ctx, key)
175 |
176 | key[0] = clientAdminPrefix
177 | deleteByPrefix(ctx, key)
178 |
179 | key[0] = userPrefix
180 | deleteByPrefix(ctx, key)
181 |
182 | runtime.Notify("Delete", id)
183 | }
184 |
185 | // AddNodeAdmin adds a new node administrator to the specified subnetwork.
186 | func AddNodeAdmin(subnetID []byte, adminKey interop.PublicKey) {
187 | // V2 format
188 | if len(subnetID) != subnetIDSize {
189 | panic(ErrInvalidSubnetID)
190 | }
191 |
192 | if len(adminKey) != interop.PublicKeyCompressedLen {
193 | panic(ErrInvalidAdmin)
194 | }
195 |
196 | ctx := storage.GetContext()
197 |
198 | stKey := append([]byte{ownerPrefix}, subnetID...)
199 |
200 | rawOwner := storage.Get(ctx, stKey)
201 | if rawOwner == nil {
202 | panic(ErrNotExist)
203 | }
204 |
205 | owner := rawOwner.([]byte)
206 | common.CheckOwnerWitness(owner)
207 |
208 | stKey[0] = nodeAdminPrefix
209 |
210 | if keyInList(ctx, adminKey, stKey) {
211 | return
212 | }
213 |
214 | putKeyInList(ctx, adminKey, stKey)
215 | }
216 |
217 | // RemoveNodeAdmin removes node administrator from the specified subnetwork.
218 | // Must be called by the subnet owner only.
219 | func RemoveNodeAdmin(subnetID []byte, adminKey interop.PublicKey) {
220 | // V2 format
221 | if len(subnetID) != subnetIDSize {
222 | panic(ErrInvalidSubnetID)
223 | }
224 |
225 | if len(adminKey) != interop.PublicKeyCompressedLen {
226 | panic(ErrInvalidAdmin)
227 | }
228 |
229 | ctx := storage.GetContext()
230 |
231 | stKey := append([]byte{ownerPrefix}, subnetID...)
232 |
233 | rawOwner := storage.Get(ctx, stKey)
234 | if rawOwner == nil {
235 | panic(ErrNotExist)
236 | }
237 |
238 | owner := rawOwner.([]byte)
239 | common.CheckOwnerWitness(owner)
240 |
241 | stKey[0] = nodeAdminPrefix
242 |
243 | if !keyInList(ctx, adminKey, stKey) {
244 | return
245 | }
246 |
247 | deleteKeyFromList(ctx, adminKey, stKey)
248 | }
249 |
250 | // AddNode adds a node to the specified subnetwork.
251 | // Must be called by the subnet's owner or the node administrator
252 | // only.
253 | func AddNode(subnetID []byte, node interop.PublicKey) {
254 | // V2 format
255 | if len(subnetID) != subnetIDSize {
256 | panic(ErrInvalidSubnetID)
257 | }
258 |
259 | if len(node) != interop.PublicKeyCompressedLen {
260 | panic(ErrInvalidNode)
261 | }
262 |
263 | ctx := storage.GetContext()
264 |
265 | stKey := append([]byte{ownerPrefix}, subnetID...)
266 |
267 | rawOwner := storage.Get(ctx, stKey)
268 | if rawOwner == nil {
269 | panic(ErrNotExist)
270 | }
271 |
272 | stKey[0] = nodeAdminPrefix
273 |
274 | owner := rawOwner.([]byte)
275 |
276 | if !calledByOwnerOrAdmin(ctx, owner, stKey) {
277 | panic(ErrAccessDenied)
278 | }
279 |
280 | stKey[0] = nodePrefix
281 |
282 | if keyInList(ctx, node, stKey) {
283 | return
284 | }
285 |
286 | putKeyInList(ctx, node, stKey)
287 | }
288 |
289 | // RemoveNode removes a node from the specified subnetwork.
290 | // Must be called by the subnet's owner or the node administrator
291 | // only.
292 | func RemoveNode(subnetID []byte, node interop.PublicKey) {
293 | // V2 format
294 | if len(subnetID) != subnetIDSize {
295 | panic(ErrInvalidSubnetID)
296 | }
297 |
298 | if len(node) != interop.PublicKeyCompressedLen {
299 | panic(ErrInvalidNode)
300 | }
301 |
302 | ctx := storage.GetContext()
303 |
304 | stKey := append([]byte{ownerPrefix}, subnetID...)
305 |
306 | rawOwner := storage.Get(ctx, stKey)
307 | if rawOwner == nil {
308 | panic(ErrNotExist)
309 | }
310 |
311 | stKey[0] = nodeAdminPrefix
312 |
313 | owner := rawOwner.([]byte)
314 |
315 | if !calledByOwnerOrAdmin(ctx, owner, stKey) {
316 | panic(ErrAccessDenied)
317 | }
318 |
319 | stKey[0] = nodePrefix
320 |
321 | if !keyInList(ctx, node, stKey) {
322 | return
323 | }
324 |
325 | storage.Delete(ctx, append(stKey, node...))
326 |
327 | runtime.Notify("RemoveNode", subnetID, node)
328 | }
329 |
330 | // NodeAllowed checks if a node is included in the
331 | // specified subnet.
332 | func NodeAllowed(subnetID []byte, node interop.PublicKey) bool {
333 | // V2 format
334 | if len(subnetID) != subnetIDSize {
335 | panic(ErrInvalidSubnetID)
336 | }
337 |
338 | if len(node) != interop.PublicKeyCompressedLen {
339 | panic(ErrInvalidNode)
340 | }
341 |
342 | ctx := storage.GetReadOnlyContext()
343 |
344 | stKey := append([]byte{ownerPrefix}, subnetID...)
345 |
346 | rawOwner := storage.Get(ctx, stKey)
347 | if rawOwner == nil {
348 | panic(ErrNotExist)
349 | }
350 |
351 | stKey[0] = nodePrefix
352 |
353 | return storage.Get(ctx, append(stKey, node...)) != nil
354 | }
355 |
356 | // AddClientAdmin adds a new client administrator of the specified group in the specified subnetwork.
357 | // Must be called by the owner only.
358 | func AddClientAdmin(subnetID []byte, groupID []byte, adminPublicKey interop.PublicKey) {
359 | // V2 format
360 | if len(subnetID) != subnetIDSize {
361 | panic(ErrInvalidSubnetID)
362 | }
363 |
364 | // V2 format
365 | if len(groupID) != groupIDSize {
366 | panic(ErrInvalidGroupID)
367 | }
368 |
369 | if len(adminPublicKey) != interop.PublicKeyCompressedLen {
370 | panic(ErrInvalidAdmin)
371 | }
372 |
373 | ctx := storage.GetContext()
374 |
375 | stKey := append([]byte{ownerPrefix}, subnetID...)
376 |
377 | rawOwner := storage.Get(ctx, stKey)
378 | if rawOwner == nil {
379 | panic(ErrNotExist)
380 | }
381 |
382 | owner := rawOwner.([]byte)
383 | common.CheckOwnerWitness(owner)
384 |
385 | stKey[0] = clientAdminPrefix
386 | stKey = append(stKey, groupID...)
387 |
388 | if keyInList(ctx, adminPublicKey, stKey) {
389 | return
390 | }
391 |
392 | putKeyInList(ctx, adminPublicKey, stKey)
393 | }
394 |
395 | // RemoveClientAdmin removes client administrator from the
396 | // specified group in the specified subnetwork.
397 | // Must be called by the owner only.
398 | func RemoveClientAdmin(subnetID []byte, groupID []byte, adminPublicKey interop.PublicKey) {
399 | // V2 format
400 | if len(subnetID) != subnetIDSize {
401 | panic(ErrInvalidSubnetID)
402 | }
403 |
404 | // V2 format
405 | if len(groupID) != groupIDSize {
406 | panic(ErrInvalidGroupID)
407 | }
408 |
409 | if len(adminPublicKey) != interop.PublicKeyCompressedLen {
410 | panic(ErrInvalidAdmin)
411 | }
412 |
413 | ctx := storage.GetContext()
414 |
415 | stKey := append([]byte{ownerPrefix}, subnetID...)
416 |
417 | rawOwner := storage.Get(ctx, stKey)
418 | if rawOwner == nil {
419 | panic(ErrNotExist)
420 | }
421 |
422 | owner := rawOwner.([]byte)
423 | common.CheckOwnerWitness(owner)
424 |
425 | stKey[0] = clientAdminPrefix
426 | stKey = append(stKey, groupID...)
427 |
428 | if !keyInList(ctx, adminPublicKey, stKey) {
429 | return
430 | }
431 |
432 | deleteKeyFromList(ctx, adminPublicKey, stKey)
433 | }
434 |
435 | // AddUser adds user to the specified subnetwork and group.
436 | // Must be called by the owner or the group's admin only.
437 | func AddUser(subnetID []byte, groupID []byte, userID []byte) {
438 | // V2 format
439 | if len(subnetID) != subnetIDSize {
440 | panic(ErrInvalidSubnetID)
441 | }
442 |
443 | // V2 format
444 | if len(userID) != userIDSize {
445 | panic(ErrInvalidUser)
446 | }
447 |
448 | // V2 format
449 | if len(groupID) != groupIDSize {
450 | panic(ErrInvalidGroupID)
451 | }
452 |
453 | ctx := storage.GetContext()
454 |
455 | stKey := append([]byte{ownerPrefix}, subnetID...)
456 |
457 | rawOwner := storage.Get(ctx, stKey)
458 | if rawOwner == nil {
459 | panic(ErrNotExist)
460 | }
461 |
462 | stKey[0] = clientAdminPrefix
463 | stKey = append(stKey, groupID...)
464 |
465 | owner := rawOwner.([]byte)
466 |
467 | if !calledByOwnerOrAdmin(ctx, owner, stKey) {
468 | panic(ErrAccessDenied)
469 | }
470 |
471 | stKey[0] = userPrefix
472 |
473 | if keyInList(ctx, userID, stKey) {
474 | return
475 | }
476 |
477 | putKeyInList(ctx, userID, stKey)
478 | }
479 |
480 | // RemoveUser removes a user from the specified subnetwork and group.
481 | // Must be called by the owner or the group's admin only.
482 | func RemoveUser(subnetID []byte, groupID []byte, userID []byte) {
483 | // V2 format
484 | if len(subnetID) != subnetIDSize {
485 | panic(ErrInvalidSubnetID)
486 | }
487 |
488 | // V2 format
489 | if len(groupID) != groupIDSize {
490 | panic(ErrInvalidGroupID)
491 | }
492 |
493 | // V2 format
494 | if len(userID) != userIDSize {
495 | panic(ErrInvalidUser)
496 | }
497 |
498 | ctx := storage.GetContext()
499 |
500 | stKey := append([]byte{ownerPrefix}, subnetID...)
501 |
502 | rawOwner := storage.Get(ctx, stKey)
503 | if rawOwner == nil {
504 | panic(ErrNotExist)
505 | }
506 |
507 | stKey[0] = clientAdminPrefix
508 | stKey = append(stKey, groupID...)
509 |
510 | owner := rawOwner.([]byte)
511 |
512 | if !calledByOwnerOrAdmin(ctx, owner, stKey) {
513 | panic(ErrAccessDenied)
514 | }
515 |
516 | stKey[0] = userPrefix
517 |
518 | if !keyInList(ctx, userID, stKey) {
519 | return
520 | }
521 |
522 | deleteKeyFromList(ctx, userID, stKey)
523 | }
524 |
525 | // UserAllowed returns bool that indicates if a node is included in the
526 | // specified subnet.
527 | func UserAllowed(subnetID []byte, user []byte) bool {
528 | // V2 format
529 | if len(subnetID) != subnetIDSize {
530 | panic(ErrInvalidSubnetID)
531 | }
532 |
533 | ctx := storage.GetContext()
534 |
535 | stKey := append([]byte{ownerPrefix}, subnetID...)
536 | if storage.Get(ctx, stKey) == nil {
537 | panic(ErrNotExist)
538 | }
539 |
540 | stKey[0] = userPrefix
541 | prefixLen := len(stKey) + groupIDSize
542 |
543 | iter := storage.Find(ctx, stKey, storage.KeysOnly)
544 | for iterator.Next(iter) {
545 | key := iterator.Value(iter).([]byte)
546 | if common.BytesEqual(user, key[prefixLen:]) {
547 | return true
548 | }
549 | }
550 |
551 | return false
552 | }
553 |
554 | // Version returns the version of the contract.
555 | func Version() int {
556 | return common.Version
557 | }
558 |
559 | func keyInList(ctx storage.Context, searchedKey interop.PublicKey, prefix []byte) bool {
560 | return storage.Get(ctx, append(prefix, searchedKey...)) != nil
561 | }
562 |
563 | func putKeyInList(ctx storage.Context, keyToPut interop.PublicKey, prefix []byte) {
564 | storage.Put(ctx, append(prefix, keyToPut...), []byte{1})
565 | }
566 |
567 | func deleteKeyFromList(ctx storage.Context, keyToDelete interop.PublicKey, prefix []byte) {
568 | storage.Delete(ctx, append(prefix, keyToDelete...))
569 | }
570 |
571 | func deleteByPrefix(ctx storage.Context, prefix []byte) {
572 | iter := storage.Find(ctx, prefix, storage.KeysOnly)
573 | for iterator.Next(iter) {
574 | k := iterator.Value(iter).([]byte)
575 | storage.Delete(ctx, k)
576 | }
577 | }
578 |
579 | func calledByOwnerOrAdmin(ctx storage.Context, owner []byte, adminPrefix []byte) bool {
580 | if runtime.CheckWitness(owner) {
581 | return true
582 | }
583 |
584 | iter := storage.Find(ctx, adminPrefix, storage.KeysOnly|storage.RemovePrefix)
585 | for iterator.Next(iter) {
586 | key := iterator.Value(iter).([]byte)
587 | if runtime.CheckWitness(key) {
588 | return true
589 | }
590 | }
591 |
592 | return false
593 | }
594 |
--------------------------------------------------------------------------------
/tests/alphabet_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "path"
5 | "testing"
6 |
7 | "github.com/TrueCloudLab/frostfs-contract/common"
8 | "github.com/TrueCloudLab/frostfs-contract/container"
9 | "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
10 | "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
11 | "github.com/nspcc-dev/neo-go/pkg/neotest"
12 | "github.com/nspcc-dev/neo-go/pkg/util"
13 | "github.com/nspcc-dev/neo-go/pkg/vm"
14 | "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
15 | "github.com/nspcc-dev/neo-go/pkg/wallet"
16 | "github.com/stretchr/testify/require"
17 | )
18 |
19 | const alphabetPath = "../alphabet"
20 |
21 | func deployAlphabetContract(t *testing.T, e *neotest.Executor, addrNetmap, addrProxy util.Uint160, name string, index, total int64) util.Uint160 {
22 | c := neotest.CompileFile(t, e.CommitteeHash, alphabetPath, path.Join(alphabetPath, "config.yml"))
23 |
24 | args := make([]interface{}, 6)
25 | args[0] = false
26 | args[1] = addrNetmap
27 | args[2] = addrProxy
28 | args[3] = name
29 | args[4] = index
30 | args[5] = total
31 |
32 | e.DeployContract(t, c, args)
33 | return c.Hash
34 | }
35 |
36 | func newAlphabetInvoker(t *testing.T) (*neotest.Executor, *neotest.ContractInvoker) {
37 | e := newExecutor(t)
38 |
39 | ctrNNS := neotest.CompileFile(t, e.CommitteeHash, nnsPath, path.Join(nnsPath, "config.yml"))
40 | ctrNetmap := neotest.CompileFile(t, e.CommitteeHash, netmapPath, path.Join(netmapPath, "config.yml"))
41 | ctrBalance := neotest.CompileFile(t, e.CommitteeHash, balancePath, path.Join(balancePath, "config.yml"))
42 | ctrContainer := neotest.CompileFile(t, e.CommitteeHash, containerPath, path.Join(containerPath, "config.yml"))
43 | ctrProxy := neotest.CompileFile(t, e.CommitteeHash, proxyPath, path.Join(proxyPath, "config.yml"))
44 |
45 | e.DeployContract(t, ctrNNS, nil)
46 | deployNetmapContract(t, e, ctrBalance.Hash, ctrContainer.Hash,
47 | container.RegistrationFeeKey, int64(containerFee),
48 | container.AliasFeeKey, int64(containerAliasFee))
49 | deployBalanceContract(t, e, ctrNetmap.Hash, ctrContainer.Hash)
50 | deployContainerContract(t, e, ctrNetmap.Hash, ctrBalance.Hash, ctrNNS.Hash)
51 | deployProxyContract(t, e, ctrNetmap.Hash)
52 | hash := deployAlphabetContract(t, e, ctrNetmap.Hash, ctrProxy.Hash, "Az", 0, 1)
53 |
54 | alphabet := getAlphabetAcc(t, e)
55 |
56 | setAlphabetRole(t, e, alphabet.PrivateKey().PublicKey().Bytes())
57 |
58 | return e, e.CommitteeInvoker(hash)
59 | }
60 |
61 | func TestEmit(t *testing.T) {
62 | _, c := newAlphabetInvoker(t)
63 |
64 | const method = "emit"
65 |
66 | alphabet := getAlphabetAcc(t, c.Executor)
67 |
68 | cCommittee := c.WithSigners(neotest.NewSingleSigner(alphabet))
69 | cCommittee.InvokeFail(t, "no gas to emit", method)
70 |
71 | transferNeoToContract(t, c)
72 |
73 | cCommittee.Invoke(t, stackitem.Null{}, method)
74 |
75 | notAlphabet := c.NewAccount(t)
76 | cNotAlphabet := c.WithSigners(notAlphabet)
77 |
78 | cNotAlphabet.InvokeFail(t, "invalid invoker", method)
79 | }
80 |
81 | func TestVote(t *testing.T) {
82 | e, c := newAlphabetInvoker(t)
83 |
84 | const method = "vote"
85 |
86 | newAlphabet := c.NewAccount(t)
87 | newAlphabetPub, ok := vm.ParseSignatureContract(newAlphabet.Script())
88 | require.True(t, ok)
89 | cNewAlphabet := c.WithSigners(newAlphabet)
90 |
91 | cNewAlphabet.InvokeFail(t, common.ErrAlphabetWitnessFailed, method, int64(0), []interface{}{newAlphabetPub})
92 | c.InvokeFail(t, "invalid epoch", method, int64(1), []interface{}{newAlphabetPub})
93 |
94 | setAlphabetRole(t, e, newAlphabetPub)
95 | transferNeoToContract(t, c)
96 |
97 | neoSH := e.NativeHash(t, nativenames.Neo)
98 | neoInvoker := c.CommitteeInvoker(neoSH)
99 |
100 | gasSH := e.NativeHash(t, nativenames.Gas)
101 | gasInvoker := e.CommitteeInvoker(gasSH)
102 |
103 | res, err := gasInvoker.TestInvoke(t, "balanceOf", gasInvoker.Committee.ScriptHash())
104 | require.NoError(t, err)
105 |
106 | // transfer some GAS to the new alphabet node
107 | gasInvoker.Invoke(t, stackitem.NewBool(true), "transfer", gasInvoker.Committee.ScriptHash(), newAlphabet.ScriptHash(), res.Top().BigInt().Int64()/2, nil)
108 |
109 | newInvoker := neoInvoker.WithSigners(newAlphabet)
110 |
111 | newInvoker.Invoke(t, stackitem.NewBool(true), "registerCandidate", newAlphabetPub)
112 | c.Invoke(t, stackitem.Null{}, method, int64(0), []interface{}{newAlphabetPub})
113 |
114 | // wait one block util
115 | // a new committee is accepted
116 | c.AddNewBlock(t)
117 |
118 | cNewAlphabet.Invoke(t, stackitem.Null{}, "emit")
119 | c.InvokeFail(t, "invalid invoker", "emit")
120 | }
121 |
122 | func transferNeoToContract(t *testing.T, invoker *neotest.ContractInvoker) {
123 | neoSH, err := invoker.Chain.GetNativeContractScriptHash(nativenames.Neo)
124 | require.NoError(t, err)
125 |
126 | neoInvoker := invoker.CommitteeInvoker(neoSH)
127 |
128 | res, err := neoInvoker.TestInvoke(t, "balanceOf", neoInvoker.Committee.ScriptHash())
129 | require.NoError(t, err)
130 |
131 | // transfer all NEO to alphabet contract
132 | neoInvoker.Invoke(t, stackitem.NewBool(true), "transfer", neoInvoker.Committee.ScriptHash(), invoker.Hash, res.Top().BigInt().Int64(), nil)
133 | }
134 |
135 | func setAlphabetRole(t *testing.T, e *neotest.Executor, new []byte) {
136 | designSH, err := e.Chain.GetNativeContractScriptHash(nativenames.Designation)
137 | require.NoError(t, err)
138 |
139 | designInvoker := e.CommitteeInvoker(designSH)
140 |
141 | // set committee as NeoFSAlphabet
142 | designInvoker.Invoke(t, stackitem.Null{}, "designateAsRole", int64(noderoles.NeoFSAlphabet), []interface{}{new})
143 | }
144 |
145 | func getAlphabetAcc(t *testing.T, e *neotest.Executor) *wallet.Account {
146 | multi, ok := e.Committee.(neotest.MultiSigner)
147 | require.True(t, ok)
148 |
149 | return multi.Single(0).Account()
150 | }
151 |
--------------------------------------------------------------------------------
/tests/balance_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "path"
5 | "testing"
6 |
7 | "github.com/nspcc-dev/neo-go/pkg/neotest"
8 | "github.com/nspcc-dev/neo-go/pkg/util"
9 | "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
10 | )
11 |
12 | const balancePath = "../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([]interface{}, 3)
18 | args[0] = false
19 | args[1] = addrNetmap
20 | args[2] = addrContainer
21 |
22 | e.DeployContract(t, c, args)
23 | return c.Hash
24 | }
25 |
26 | func balanceMint(t *testing.T, c *neotest.ContractInvoker, acc neotest.Signer, amount int64, details []byte) {
27 | c.Invoke(t, stackitem.Null{}, "mint", acc.ScriptHash(), amount, details)
28 | }
29 |
--------------------------------------------------------------------------------
/tests/container_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "bytes"
5 | "crypto/sha256"
6 | "math/big"
7 | "path"
8 | "testing"
9 |
10 | "github.com/TrueCloudLab/frostfs-contract/common"
11 | "github.com/TrueCloudLab/frostfs-contract/container"
12 | "github.com/TrueCloudLab/frostfs-contract/nns"
13 | "github.com/mr-tron/base58"
14 | "github.com/nspcc-dev/neo-go/pkg/core/interop/storage"
15 | "github.com/nspcc-dev/neo-go/pkg/encoding/address"
16 | "github.com/nspcc-dev/neo-go/pkg/neotest"
17 | "github.com/nspcc-dev/neo-go/pkg/util"
18 | "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
19 | "github.com/stretchr/testify/require"
20 | )
21 |
22 | const containerPath = "../container"
23 |
24 | const (
25 | containerFee = 0_0100_0000
26 | containerAliasFee = 0_0050_0000
27 | )
28 |
29 | func deployContainerContract(t *testing.T, e *neotest.Executor, addrNetmap, addrBalance, addrNNS util.Uint160) util.Uint160 {
30 | args := make([]interface{}, 6)
31 | args[0] = int64(0)
32 | args[1] = addrNetmap
33 | args[2] = addrBalance
34 | args[3] = util.Uint160{} // not needed for now
35 | args[4] = addrNNS
36 | args[5] = "frostfs"
37 |
38 | c := neotest.CompileFile(t, e.CommitteeHash, containerPath, path.Join(containerPath, "config.yml"))
39 | e.DeployContract(t, c, args)
40 | return c.Hash
41 | }
42 |
43 | func newContainerInvoker(t *testing.T) (*neotest.ContractInvoker, *neotest.ContractInvoker, *neotest.ContractInvoker) {
44 | e := newExecutor(t)
45 |
46 | ctrNNS := neotest.CompileFile(t, e.CommitteeHash, nnsPath, path.Join(nnsPath, "config.yml"))
47 | ctrNetmap := neotest.CompileFile(t, e.CommitteeHash, netmapPath, path.Join(netmapPath, "config.yml"))
48 | ctrBalance := neotest.CompileFile(t, e.CommitteeHash, balancePath, path.Join(balancePath, "config.yml"))
49 | ctrContainer := neotest.CompileFile(t, e.CommitteeHash, containerPath, path.Join(containerPath, "config.yml"))
50 |
51 | e.DeployContract(t, ctrNNS, nil)
52 | deployNetmapContract(t, e, ctrBalance.Hash, ctrContainer.Hash,
53 | container.RegistrationFeeKey, int64(containerFee),
54 | container.AliasFeeKey, int64(containerAliasFee))
55 | deployBalanceContract(t, e, ctrNetmap.Hash, ctrContainer.Hash)
56 | deployContainerContract(t, e, ctrNetmap.Hash, ctrBalance.Hash, ctrNNS.Hash)
57 | return e.CommitteeInvoker(ctrContainer.Hash), e.CommitteeInvoker(ctrBalance.Hash), e.CommitteeInvoker(ctrNetmap.Hash)
58 | }
59 |
60 | func setContainerOwner(c []byte, acc neotest.Signer) {
61 | owner, _ := base58.Decode(address.Uint160ToString(acc.ScriptHash()))
62 | copy(c[6:], owner)
63 | }
64 |
65 | type testContainer struct {
66 | id [32]byte
67 | value, sig, pub, token []byte
68 | }
69 |
70 | func dummyContainer(owner neotest.Signer) testContainer {
71 | value := randomBytes(100)
72 | value[1] = 0 // zero offset
73 | setContainerOwner(value, owner)
74 |
75 | return testContainer{
76 | id: sha256.Sum256(value),
77 | value: value,
78 | sig: randomBytes(64),
79 | pub: randomBytes(33),
80 | token: randomBytes(42),
81 | }
82 | }
83 |
84 | func TestContainerCount(t *testing.T) {
85 | c, cBal, _ := newContainerInvoker(t)
86 |
87 | checkCount := func(t *testing.T, expected int64) {
88 | s, err := c.TestInvoke(t, "count")
89 | require.NoError(t, err)
90 | bi := s.Pop().BigInt()
91 | require.True(t, bi.IsInt64())
92 | require.Equal(t, int64(expected), bi.Int64())
93 | }
94 |
95 | checkCount(t, 0)
96 | acc1, cnt1 := addContainer(t, c, cBal)
97 | checkCount(t, 1)
98 |
99 | _, cnt2 := addContainer(t, c, cBal)
100 | checkCount(t, 2)
101 |
102 | // Same owner.
103 | cnt3 := dummyContainer(acc1)
104 | balanceMint(t, cBal, acc1, containerFee*1, []byte{})
105 | c.Invoke(t, stackitem.Null{}, "put", cnt3.value, cnt3.sig, cnt3.pub, cnt3.token)
106 | checkContainerList(t, c, [][]byte{cnt1.id[:], cnt2.id[:], cnt3.id[:]})
107 |
108 | c.Invoke(t, stackitem.Null{}, "delete", cnt1.id[:], cnt1.sig, cnt1.token)
109 | checkCount(t, 2)
110 | checkContainerList(t, c, [][]byte{cnt2.id[:], cnt3.id[:]})
111 |
112 | c.Invoke(t, stackitem.Null{}, "delete", cnt2.id[:], cnt2.sig, cnt2.token)
113 | checkCount(t, 1)
114 | checkContainerList(t, c, [][]byte{cnt3.id[:]})
115 |
116 | c.Invoke(t, stackitem.Null{}, "delete", cnt3.id[:], cnt3.sig, cnt3.token)
117 | checkCount(t, 0)
118 | checkContainerList(t, c, [][]byte{})
119 | }
120 |
121 | func checkContainerList(t *testing.T, c *neotest.ContractInvoker, expected [][]byte) {
122 | t.Run("check with `list`", func(t *testing.T) {
123 | s, err := c.TestInvoke(t, "list", nil)
124 | require.NoError(t, err)
125 | require.Equal(t, 1, s.Len())
126 |
127 | if len(expected) == 0 {
128 | _, ok := s.Top().Item().(stackitem.Null)
129 | require.True(t, ok)
130 | return
131 | }
132 |
133 | arr, ok := s.Top().Value().([]stackitem.Item)
134 | require.True(t, ok)
135 | require.Equal(t, len(expected), len(arr))
136 |
137 | actual := make([][]byte, 0, len(expected))
138 | for i := range arr {
139 | id, ok := arr[i].Value().([]byte)
140 | require.True(t, ok)
141 | actual = append(actual, id)
142 | }
143 | require.ElementsMatch(t, expected, actual)
144 | })
145 | t.Run("check with `containersOf`", func(t *testing.T) {
146 | s, err := c.TestInvoke(t, "containersOf", nil)
147 | require.NoError(t, err)
148 | require.Equal(t, 1, s.Len())
149 |
150 | iter, ok := s.Top().Value().(*storage.Iterator)
151 | require.True(t, ok)
152 |
153 | actual := make([][]byte, 0, len(expected))
154 | for iter.Next() {
155 | id, ok := iter.Value().Value().([]byte)
156 | require.True(t, ok)
157 | actual = append(actual, id)
158 | }
159 | require.ElementsMatch(t, expected, actual)
160 | })
161 |
162 | }
163 |
164 | func TestContainerPut(t *testing.T) {
165 | c, cBal, _ := newContainerInvoker(t)
166 |
167 | acc := c.NewAccount(t)
168 | cnt := dummyContainer(acc)
169 |
170 | putArgs := []interface{}{cnt.value, cnt.sig, cnt.pub, cnt.token}
171 | c.InvokeFail(t, "insufficient balance to create container", "put", putArgs...)
172 |
173 | balanceMint(t, cBal, acc, containerFee*1, []byte{})
174 |
175 | cAcc := c.WithSigners(acc)
176 | cAcc.InvokeFail(t, common.ErrAlphabetWitnessFailed, "put", putArgs...)
177 |
178 | c.Invoke(t, stackitem.Null{}, "put", putArgs...)
179 |
180 | t.Run("with nice names", func(t *testing.T) {
181 | ctrNNS := neotest.CompileFile(t, c.CommitteeHash, nnsPath, path.Join(nnsPath, "config.yml"))
182 | nnsHash := ctrNNS.Hash
183 |
184 | balanceMint(t, cBal, acc, containerFee*1, []byte{})
185 |
186 | putArgs := []interface{}{cnt.value, cnt.sig, cnt.pub, cnt.token, "mycnt", ""}
187 | t.Run("no fee for alias", func(t *testing.T) {
188 | c.InvokeFail(t, "insufficient balance to create container", "putNamed", putArgs...)
189 | })
190 |
191 | balanceMint(t, cBal, acc, containerAliasFee*1, []byte{})
192 | c.Invoke(t, stackitem.Null{}, "putNamed", putArgs...)
193 |
194 | expected := stackitem.NewArray([]stackitem.Item{
195 | stackitem.NewByteArray([]byte(base58.Encode(cnt.id[:]))),
196 | })
197 | cNNS := c.CommitteeInvoker(nnsHash)
198 | cNNS.Invoke(t, expected, "resolve", "mycnt.frostfs", int64(nns.TXT))
199 |
200 | t.Run("name is already taken", func(t *testing.T) {
201 | c.InvokeFail(t, "name is already taken", "putNamed", putArgs...)
202 | })
203 |
204 | c.Invoke(t, stackitem.Null{}, "delete", cnt.id[:], cnt.sig, cnt.token)
205 | cNNS.Invoke(t, stackitem.Null{}, "resolve", "mycnt.frostfs", int64(nns.TXT))
206 |
207 | t.Run("register in advance", func(t *testing.T) {
208 | cnt.value[len(cnt.value)-1] = 10
209 | cnt.id = sha256.Sum256(cnt.value)
210 |
211 | cNNS.Invoke(t, true, "register",
212 | "cdn", c.CommitteeHash,
213 | "whateveriwant@world.com", int64(0), int64(0), int64(100_000), int64(0))
214 |
215 | cNNS.Invoke(t, true, "register",
216 | "domain.cdn", c.CommitteeHash,
217 | "whateveriwant@world.com", int64(0), int64(0), int64(100_000), int64(0))
218 |
219 | balanceMint(t, cBal, acc, (containerFee+containerAliasFee)*1, []byte{})
220 |
221 | putArgs := []interface{}{cnt.value, cnt.sig, cnt.pub, cnt.token, "domain", "cdn"}
222 | c2 := c.WithSigners(c.Committee, acc)
223 | c2.Invoke(t, stackitem.Null{}, "putNamed", putArgs...)
224 |
225 | expected = stackitem.NewArray([]stackitem.Item{
226 | stackitem.NewByteArray([]byte(base58.Encode(cnt.id[:])))})
227 | cNNS.Invoke(t, expected, "resolve", "domain.cdn", int64(nns.TXT))
228 | })
229 | })
230 | }
231 |
232 | func addContainer(t *testing.T, c, cBal *neotest.ContractInvoker) (neotest.Signer, testContainer) {
233 | acc := c.NewAccount(t)
234 | cnt := dummyContainer(acc)
235 |
236 | balanceMint(t, cBal, acc, containerFee*1, []byte{})
237 | c.Invoke(t, stackitem.Null{}, "put", cnt.value, cnt.sig, cnt.pub, cnt.token)
238 | return acc, cnt
239 | }
240 |
241 | func TestContainerDelete(t *testing.T) {
242 | c, cBal, _ := newContainerInvoker(t)
243 |
244 | acc, cnt := addContainer(t, c, cBal)
245 | cAcc := c.WithSigners(acc)
246 | cAcc.InvokeFail(t, common.ErrAlphabetWitnessFailed, "delete",
247 | cnt.id[:], cnt.sig, cnt.token)
248 |
249 | c.Invoke(t, stackitem.Null{}, "delete", cnt.id[:], cnt.sig, cnt.token)
250 |
251 | t.Run("missing container", func(t *testing.T) {
252 | id := cnt.id
253 | id[0] ^= 0xFF
254 | c.Invoke(t, stackitem.Null{}, "delete", cnt.id[:], cnt.sig, cnt.token)
255 | })
256 |
257 | c.InvokeFail(t, container.NotFoundError, "get", cnt.id[:])
258 | }
259 |
260 | func TestContainerOwner(t *testing.T) {
261 | c, cBal, _ := newContainerInvoker(t)
262 |
263 | acc, cnt := addContainer(t, c, cBal)
264 |
265 | t.Run("missing container", func(t *testing.T) {
266 | id := cnt.id
267 | id[0] ^= 0xFF
268 | c.InvokeFail(t, container.NotFoundError, "owner", id[:])
269 | })
270 |
271 | owner, _ := base58.Decode(address.Uint160ToString(acc.ScriptHash()))
272 | c.Invoke(t, stackitem.NewBuffer(owner), "owner", cnt.id[:])
273 | }
274 |
275 | func TestContainerGet(t *testing.T) {
276 | c, cBal, _ := newContainerInvoker(t)
277 |
278 | _, cnt := addContainer(t, c, cBal)
279 |
280 | t.Run("missing container", func(t *testing.T) {
281 | id := cnt.id
282 | id[0] ^= 0xFF
283 | c.InvokeFail(t, container.NotFoundError, "get", id[:])
284 | })
285 |
286 | expected := stackitem.NewStruct([]stackitem.Item{
287 | stackitem.NewByteArray(cnt.value),
288 | stackitem.NewByteArray(cnt.sig),
289 | stackitem.NewByteArray(cnt.pub),
290 | stackitem.NewByteArray(cnt.token),
291 | })
292 | c.Invoke(t, expected, "get", cnt.id[:])
293 | }
294 |
295 | type eacl struct {
296 | value []byte
297 | sig []byte
298 | pub []byte
299 | token []byte
300 | }
301 |
302 | func dummyEACL(containerID [32]byte) eacl {
303 | e := make([]byte, 50)
304 | copy(e[6:], containerID[:])
305 | return eacl{
306 | value: e,
307 | sig: randomBytes(64),
308 | pub: randomBytes(33),
309 | token: randomBytes(42),
310 | }
311 | }
312 |
313 | func TestContainerSetEACL(t *testing.T) {
314 | c, cBal, _ := newContainerInvoker(t)
315 |
316 | acc, cnt := addContainer(t, c, cBal)
317 |
318 | t.Run("missing container", func(t *testing.T) {
319 | id := cnt.id
320 | id[0] ^= 0xFF
321 | e := dummyEACL(id)
322 | c.InvokeFail(t, container.NotFoundError, "setEACL", e.value, e.sig, e.pub, e.token)
323 | })
324 |
325 | e := dummyEACL(cnt.id)
326 | setArgs := []interface{}{e.value, e.sig, e.pub, e.token}
327 | cAcc := c.WithSigners(acc)
328 | cAcc.InvokeFail(t, common.ErrAlphabetWitnessFailed, "setEACL", setArgs...)
329 |
330 | c.Invoke(t, stackitem.Null{}, "setEACL", setArgs...)
331 |
332 | expected := stackitem.NewStruct([]stackitem.Item{
333 | stackitem.NewByteArray(e.value),
334 | stackitem.NewByteArray(e.sig),
335 | stackitem.NewByteArray(e.pub),
336 | stackitem.NewByteArray(e.token),
337 | })
338 | c.Invoke(t, expected, "eACL", cnt.id[:])
339 | }
340 |
341 | func TestContainerSizeEstimation(t *testing.T) {
342 | c, cBal, cNm := newContainerInvoker(t)
343 |
344 | _, cnt := addContainer(t, c, cBal)
345 | nodes := []testNodeInfo{
346 | newStorageNode(t, c),
347 | newStorageNode(t, c),
348 | newStorageNode(t, c),
349 | }
350 | for i := range nodes {
351 | cNm.WithSigners(nodes[i].signer).Invoke(t, stackitem.Null{}, "addPeer", nodes[i].raw)
352 | cNm.Invoke(t, stackitem.Null{}, "addPeerIR", nodes[i].raw)
353 | }
354 |
355 | // putContainerSize retrieves storage nodes from the previous snapshot,
356 | // so epoch must be incremented twice.
357 | cNm.Invoke(t, stackitem.Null{}, "newEpoch", int64(1))
358 | cNm.Invoke(t, stackitem.Null{}, "newEpoch", int64(2))
359 |
360 | t.Run("must be witnessed by key in the argument", func(t *testing.T) {
361 | c.WithSigners(nodes[1].signer).InvokeFail(t, common.ErrWitnessFailed, "putContainerSize",
362 | int64(2), cnt.id[:], int64(123), nodes[0].pub)
363 | })
364 |
365 | c.WithSigners(nodes[0].signer).Invoke(t, stackitem.Null{}, "putContainerSize",
366 | int64(2), cnt.id[:], int64(123), nodes[0].pub)
367 | estimations := []estimation{{nodes[0].pub, 123}}
368 | checkEstimations(t, c, 2, cnt, estimations...)
369 |
370 | c.WithSigners(nodes[1].signer).Invoke(t, stackitem.Null{}, "putContainerSize",
371 | int64(2), cnt.id[:], int64(42), nodes[1].pub)
372 | estimations = append(estimations, estimation{nodes[1].pub, int64(42)})
373 | checkEstimations(t, c, 2, cnt, estimations...)
374 |
375 | t.Run("add estimation for a different epoch", func(t *testing.T) {
376 | c.WithSigners(nodes[2].signer).Invoke(t, stackitem.Null{}, "putContainerSize",
377 | int64(1), cnt.id[:], int64(777), nodes[2].pub)
378 | checkEstimations(t, c, 1, cnt, estimation{nodes[2].pub, 777})
379 | checkEstimations(t, c, 2, cnt, estimations...)
380 | })
381 |
382 | c.WithSigners(nodes[2].signer).Invoke(t, stackitem.Null{}, "putContainerSize",
383 | int64(3), cnt.id[:], int64(888), nodes[2].pub)
384 | checkEstimations(t, c, 3, cnt, estimation{nodes[2].pub, 888})
385 |
386 | // Remove old estimations.
387 | for i := int64(1); i <= container.CleanupDelta; i++ {
388 | cNm.Invoke(t, stackitem.Null{}, "newEpoch", 2+i)
389 | checkEstimations(t, c, 2, cnt, estimations...)
390 | checkEstimations(t, c, 3, cnt, estimation{nodes[2].pub, 888})
391 | }
392 |
393 | epoch := int64(2 + container.CleanupDelta + 1)
394 | cNm.Invoke(t, stackitem.Null{}, "newEpoch", epoch)
395 | checkEstimations(t, c, 2, cnt, estimations...) // not yet removed
396 | checkEstimations(t, c, 3, cnt, estimation{nodes[2].pub, 888})
397 |
398 | c.WithSigners(nodes[1].signer).Invoke(t, stackitem.Null{}, "putContainerSize",
399 | epoch, cnt.id[:], int64(999), nodes[1].pub)
400 |
401 | checkEstimations(t, c, 2, cnt, estimations[:1]...)
402 | checkEstimations(t, c, epoch, cnt, estimation{nodes[1].pub, int64(999)})
403 |
404 | // Estimation from node 0 should be cleaned during epoch tick.
405 | for i := int64(1); i <= container.TotalCleanupDelta-container.CleanupDelta; i++ {
406 | cNm.Invoke(t, stackitem.Null{}, "newEpoch", epoch+i)
407 | }
408 | checkEstimations(t, c, 2, cnt)
409 | checkEstimations(t, c, epoch, cnt, estimation{nodes[1].pub, int64(999)})
410 | }
411 |
412 | type estimation struct {
413 | from []byte
414 | size int64
415 | }
416 |
417 | func checkEstimations(t *testing.T, c *neotest.ContractInvoker, epoch int64, cnt testContainer, estimations ...estimation) {
418 | // Check that listed estimations match expected
419 | listEstimations := getListEstimations(t, c, epoch, cnt)
420 | requireEstimationsMatch(t, estimations, listEstimations)
421 |
422 | // Check that iterated estimations match expected
423 | iterEstimations := getIterEstimations(t, c, epoch)
424 | requireEstimationsMatch(t, estimations, iterEstimations)
425 | }
426 |
427 | func getListEstimations(t *testing.T, c *neotest.ContractInvoker, epoch int64, cnt testContainer) []estimation {
428 | s, err := c.TestInvoke(t, "listContainerSizes", epoch)
429 | require.NoError(t, err)
430 |
431 | var id []byte
432 |
433 | // When there are no estimations, listContainerSizes can also return nothing.
434 | item := s.Top().Item()
435 | switch it := item.(type) {
436 | case stackitem.Null:
437 | require.Equal(t, stackitem.Null{}, it)
438 | return make([]estimation, 0)
439 | case *stackitem.Array:
440 | id, err = it.Value().([]stackitem.Item)[0].TryBytes()
441 | require.NoError(t, err)
442 | default:
443 | require.FailNow(t, "invalid return type for listContainerSizes")
444 | }
445 |
446 | s, err = c.TestInvoke(t, "getContainerSize", id)
447 | require.NoError(t, err)
448 |
449 | // Here and below we assume that all estimations in the contract are related to our container
450 | sizes := s.Top().Array()
451 | require.Equal(t, cnt.id[:], sizes[0].Value())
452 |
453 | return convertStackToEstimations(sizes[1].Value().([]stackitem.Item))
454 | }
455 |
456 | func getIterEstimations(t *testing.T, c *neotest.ContractInvoker, epoch int64) []estimation {
457 | iterStack, err := c.TestInvoke(t, "iterateContainerSizes", epoch)
458 | require.NoError(t, err)
459 | iter := iterStack.Pop().Value().(*storage.Iterator)
460 |
461 | // Iterator contains pairs: key + estimation (as stack item), we extract estimations only
462 | pairs := iteratorToArray(iter)
463 | estimationItems := make([]stackitem.Item, len(pairs))
464 | for i, pair := range pairs {
465 | pairItems := pair.Value().([]stackitem.Item)
466 | estimationItems[i] = pairItems[1]
467 | }
468 |
469 | return convertStackToEstimations(estimationItems)
470 | }
471 |
472 | func convertStackToEstimations(stackItems []stackitem.Item) []estimation {
473 | estimations := make([]estimation, 0, len(stackItems))
474 | for _, item := range stackItems {
475 | value := item.Value().([]stackitem.Item)
476 | from := value[0].Value().([]byte)
477 | size := value[1].Value().(*big.Int)
478 |
479 | estimation := estimation{from: from, size: size.Int64()}
480 | estimations = append(estimations, estimation)
481 | }
482 | return estimations
483 | }
484 |
485 | func requireEstimationsMatch(t *testing.T, expected []estimation, actual []estimation) {
486 | require.Equal(t, len(expected), len(actual))
487 | for _, e := range expected {
488 | found := false
489 | for _, a := range actual {
490 | if found = bytes.Equal(e.from, a.from); found {
491 | require.Equal(t, e.size, a.size)
492 | break
493 | }
494 | }
495 | require.True(t, found, "expected estimation from %x to be present", e.from)
496 | }
497 | }
498 |
--------------------------------------------------------------------------------
/tests/frostfs_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "bytes"
5 | "path"
6 | "sort"
7 | "testing"
8 |
9 | "github.com/TrueCloudLab/frostfs-contract/frostfs"
10 | "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
11 | "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
12 | "github.com/nspcc-dev/neo-go/pkg/neotest"
13 | "github.com/nspcc-dev/neo-go/pkg/smartcontract"
14 | "github.com/nspcc-dev/neo-go/pkg/util"
15 | "github.com/nspcc-dev/neo-go/pkg/vm"
16 | "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
17 | "github.com/nspcc-dev/neo-go/pkg/wallet"
18 | "github.com/stretchr/testify/require"
19 | )
20 |
21 | const frostfsPath = "../frostfs"
22 |
23 | func deployFrostFSContract(t *testing.T, e *neotest.Executor, addrProc util.Uint160,
24 | pubs keys.PublicKeys, config ...interface{}) util.Uint160 {
25 | args := make([]interface{}, 5)
26 | args[0] = false
27 | args[1] = addrProc
28 |
29 | arr := make([]interface{}, len(pubs))
30 | for i := range pubs {
31 | arr[i] = pubs[i].Bytes()
32 | }
33 | args[2] = arr
34 | args[3] = append([]interface{}{}, config...)
35 |
36 | c := neotest.CompileFile(t, e.CommitteeHash, frostfsPath, path.Join(frostfsPath, "config.yml"))
37 | e.DeployContract(t, c, args)
38 | return c.Hash
39 | }
40 |
41 | func newFrostFSInvoker(t *testing.T, n int, config ...interface{}) (*neotest.ContractInvoker, neotest.Signer, keys.PublicKeys) {
42 | e := newExecutor(t)
43 |
44 | accounts := make([]*wallet.Account, n)
45 | for i := 0; i < n; i++ {
46 | acc, err := wallet.NewAccount()
47 | require.NoError(t, err)
48 |
49 | accounts[i] = acc
50 | }
51 |
52 | sort.Slice(accounts, func(i, j int) bool {
53 | p1 := accounts[i].PrivateKey().PublicKey()
54 | p2 := accounts[j].PrivateKey().PublicKey()
55 | return p1.Cmp(p2) == -1
56 | })
57 |
58 | pubs := make(keys.PublicKeys, n)
59 | for i := range accounts {
60 | pubs[i] = accounts[i].PrivateKey().PublicKey()
61 | }
62 |
63 | m := smartcontract.GetMajorityHonestNodeCount(len(accounts))
64 | for i := range accounts {
65 | require.NoError(t, accounts[i].ConvertMultisig(m, pubs.Copy()))
66 | }
67 |
68 | alphabet := neotest.NewMultiSigner(accounts...)
69 | h := deployFrostFSContract(t, e, util.Uint160{}, pubs, config...)
70 |
71 | gasHash, err := e.Chain.GetNativeContractScriptHash(nativenames.Gas)
72 | require.NoError(t, err)
73 |
74 | vc := e.CommitteeInvoker(gasHash).WithSigners(e.Validator)
75 | vc.Invoke(t, true, "transfer",
76 | e.Validator.ScriptHash(), alphabet.ScriptHash(),
77 | int64(10_0000_0000), nil)
78 |
79 | return e.CommitteeInvoker(h).WithSigners(alphabet), alphabet, pubs
80 | }
81 |
82 | func TestFrostFS_AlphabetList(t *testing.T) {
83 | const alphabetSize = 4
84 |
85 | e, _, pubs := newFrostFSInvoker(t, alphabetSize)
86 | arr := make([]stackitem.Item, len(pubs))
87 | for i := range arr {
88 | arr[i] = stackitem.NewStruct([]stackitem.Item{
89 | stackitem.NewByteArray(pubs[i].Bytes()),
90 | })
91 | }
92 |
93 | e.Invoke(t, stackitem.NewArray(arr), "alphabetList")
94 | }
95 |
96 | func TestFrostFS_InnerRingCandidate(t *testing.T) {
97 | e, _, _ := newFrostFSInvoker(t, 4, frostfs.CandidateFeeConfigKey, int64(10))
98 |
99 | const candidateCount = 3
100 |
101 | accs := make([]neotest.Signer, candidateCount)
102 | for i := range accs {
103 | accs[i] = e.NewAccount(t)
104 | }
105 |
106 | arr := make([]stackitem.Item, candidateCount)
107 | pubs := make([][]byte, candidateCount)
108 | sort.Slice(accs, func(i, j int) bool {
109 | s1 := accs[i].Script()
110 | s2 := accs[j].Script()
111 | return bytes.Compare(s1, s2) == -1
112 | })
113 |
114 | for i, acc := range accs {
115 | cAcc := e.WithSigners(acc)
116 | pub, ok := vm.ParseSignatureContract(acc.Script())
117 | require.True(t, ok)
118 | cAcc.Invoke(t, stackitem.Null{}, "innerRingCandidateAdd", pub)
119 | cAcc.InvokeFail(t, "candidate already in the list", "innerRingCandidateAdd", pub)
120 |
121 | pubs[i] = pub
122 | arr[i] = stackitem.NewStruct([]stackitem.Item{stackitem.NewBuffer(pub)})
123 | }
124 |
125 | e.Invoke(t, stackitem.NewArray(arr), "innerRingCandidates")
126 |
127 | cAcc := e.WithSigners(accs[1])
128 | cAcc.Invoke(t, stackitem.Null{}, "innerRingCandidateRemove", pubs[1])
129 | e.Invoke(t, stackitem.NewArray([]stackitem.Item{arr[0], arr[2]}), "innerRingCandidates")
130 |
131 | cAcc = e.WithSigners(accs[2])
132 | cAcc.Invoke(t, stackitem.Null{}, "innerRingCandidateRemove", pubs[2])
133 | e.Invoke(t, stackitem.NewArray([]stackitem.Item{arr[0]}), "innerRingCandidates")
134 |
135 | cAcc = e.WithSigners(accs[0])
136 | cAcc.Invoke(t, stackitem.Null{}, "innerRingCandidateRemove", pubs[0])
137 | e.Invoke(t, stackitem.NewArray([]stackitem.Item{}), "innerRingCandidates")
138 | }
139 |
--------------------------------------------------------------------------------
/tests/frostfsid_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "bytes"
5 | "path"
6 | "sort"
7 | "testing"
8 |
9 | "github.com/TrueCloudLab/frostfs-contract/container"
10 | "github.com/mr-tron/base58"
11 | "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
12 | "github.com/nspcc-dev/neo-go/pkg/encoding/address"
13 | "github.com/nspcc-dev/neo-go/pkg/neotest"
14 | "github.com/nspcc-dev/neo-go/pkg/util"
15 | "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
16 | "github.com/stretchr/testify/require"
17 | )
18 |
19 | const frostfsidPath = "../frostfsid"
20 |
21 | func deployFrostFSIDContract(t *testing.T, e *neotest.Executor, addrNetmap, addrContainer util.Uint160) util.Uint160 {
22 | args := make([]interface{}, 5)
23 | args[0] = false
24 | args[1] = addrNetmap
25 | args[2] = addrContainer
26 |
27 | c := neotest.CompileFile(t, e.CommitteeHash, frostfsidPath, path.Join(frostfsidPath, "config.yml"))
28 | e.DeployContract(t, c, args)
29 | return c.Hash
30 | }
31 |
32 | func newFrostFSIDInvoker(t *testing.T) *neotest.ContractInvoker {
33 | e := newExecutor(t)
34 |
35 | ctrNNS := neotest.CompileFile(t, e.CommitteeHash, nnsPath, path.Join(nnsPath, "config.yml"))
36 | ctrNetmap := neotest.CompileFile(t, e.CommitteeHash, netmapPath, path.Join(netmapPath, "config.yml"))
37 | ctrBalance := neotest.CompileFile(t, e.CommitteeHash, balancePath, path.Join(balancePath, "config.yml"))
38 | ctrContainer := neotest.CompileFile(t, e.CommitteeHash, containerPath, path.Join(containerPath, "config.yml"))
39 |
40 | e.DeployContract(t, ctrNNS, nil)
41 | deployNetmapContract(t, e, ctrBalance.Hash, ctrContainer.Hash,
42 | container.RegistrationFeeKey, int64(containerFee),
43 | container.AliasFeeKey, int64(containerAliasFee))
44 | deployBalanceContract(t, e, ctrNetmap.Hash, ctrContainer.Hash)
45 | deployContainerContract(t, e, ctrNetmap.Hash, ctrBalance.Hash, ctrNNS.Hash)
46 | h := deployFrostFSIDContract(t, e, ctrNetmap.Hash, ctrContainer.Hash)
47 | return e.CommitteeInvoker(h)
48 | }
49 |
50 | func TestFrostFSID_AddKey(t *testing.T) {
51 | e := newFrostFSIDInvoker(t)
52 |
53 | pubs := make([][]byte, 6)
54 | for i := range pubs {
55 | p, err := keys.NewPrivateKey()
56 | require.NoError(t, err)
57 | pubs[i] = p.PublicKey().Bytes()
58 | }
59 | acc := e.NewAccount(t)
60 | owner, _ := base58.Decode(address.Uint160ToString(acc.ScriptHash()))
61 | e.Invoke(t, stackitem.Null{}, "addKey", owner,
62 | []interface{}{pubs[0], pubs[1]})
63 |
64 | sort.Slice(pubs[:2], func(i, j int) bool {
65 | return bytes.Compare(pubs[i], pubs[j]) == -1
66 | })
67 | arr := []stackitem.Item{
68 | stackitem.NewBuffer(pubs[0]),
69 | stackitem.NewBuffer(pubs[1]),
70 | }
71 | e.Invoke(t, stackitem.NewArray(arr), "key", owner)
72 |
73 | t.Run("multiple addKey per block", func(t *testing.T) {
74 | tx1 := e.PrepareInvoke(t, "addKey", owner, []interface{}{pubs[2]})
75 | tx2 := e.PrepareInvoke(t, "addKey", owner, []interface{}{pubs[3], pubs[4]})
76 | e.AddNewBlock(t, tx1, tx2)
77 | e.CheckHalt(t, tx1.Hash(), stackitem.Null{})
78 | e.CheckHalt(t, tx2.Hash(), stackitem.Null{})
79 |
80 | sort.Slice(pubs[:5], func(i, j int) bool {
81 | return bytes.Compare(pubs[i], pubs[j]) == -1
82 | })
83 | arr = []stackitem.Item{
84 | stackitem.NewBuffer(pubs[0]),
85 | stackitem.NewBuffer(pubs[1]),
86 | stackitem.NewBuffer(pubs[2]),
87 | stackitem.NewBuffer(pubs[3]),
88 | stackitem.NewBuffer(pubs[4]),
89 | }
90 | e.Invoke(t, stackitem.NewArray(arr), "key", owner)
91 | })
92 |
93 | e.Invoke(t, stackitem.Null{}, "removeKey", owner,
94 | []interface{}{pubs[1], pubs[5]})
95 | arr = []stackitem.Item{
96 | stackitem.NewBuffer(pubs[0]),
97 | stackitem.NewBuffer(pubs[2]),
98 | stackitem.NewBuffer(pubs[3]),
99 | stackitem.NewBuffer(pubs[4]),
100 | }
101 | e.Invoke(t, stackitem.NewArray(arr), "key", owner)
102 |
103 | t.Run("multiple removeKey per block", func(t *testing.T) {
104 | tx1 := e.PrepareInvoke(t, "removeKey", owner, []interface{}{pubs[2]})
105 | tx2 := e.PrepareInvoke(t, "removeKey", owner, []interface{}{pubs[0], pubs[4]})
106 | e.AddNewBlock(t, tx1, tx2)
107 | e.CheckHalt(t, tx1.Hash(), stackitem.Null{})
108 | e.CheckHalt(t, tx2.Hash(), stackitem.Null{})
109 |
110 | arr = []stackitem.Item{stackitem.NewBuffer(pubs[3])}
111 | e.Invoke(t, stackitem.NewArray(arr), "key", owner)
112 | })
113 | }
114 |
--------------------------------------------------------------------------------
/tests/helpers.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "math/rand"
5 | )
6 |
7 | func randomBytes(n int) []byte {
8 | a := make([]byte, n)
9 | rand.Read(a)
10 | return a
11 | }
12 |
--------------------------------------------------------------------------------
/tests/netmap_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "math/big"
5 | "math/rand"
6 | "path"
7 | "strings"
8 | "testing"
9 |
10 | "github.com/TrueCloudLab/frostfs-contract/common"
11 | "github.com/TrueCloudLab/frostfs-contract/container"
12 | "github.com/TrueCloudLab/frostfs-contract/netmap"
13 | "github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
14 | "github.com/nspcc-dev/neo-go/pkg/neotest"
15 | "github.com/nspcc-dev/neo-go/pkg/util"
16 | "github.com/nspcc-dev/neo-go/pkg/vm"
17 | "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
18 | "github.com/stretchr/testify/require"
19 | )
20 |
21 | const netmapPath = "../netmap"
22 |
23 | func deployNetmapContract(t *testing.T, e *neotest.Executor, addrBalance, addrContainer util.Uint160, config ...interface{}) util.Uint160 {
24 | _, pubs, ok := vm.ParseMultiSigContract(e.Committee.Script())
25 | require.True(t, ok)
26 |
27 | args := make([]interface{}, 5)
28 | args[0] = false
29 | args[1] = addrBalance
30 | args[2] = addrContainer
31 | args[3] = []interface{}{pubs[0]}
32 | args[4] = append([]interface{}{}, config...)
33 |
34 | c := neotest.CompileFile(t, e.CommitteeHash, netmapPath, path.Join(netmapPath, "config.yml"))
35 | e.DeployContract(t, c, args)
36 | return c.Hash
37 | }
38 |
39 | func newNetmapInvoker(t *testing.T, config ...interface{}) *neotest.ContractInvoker {
40 | e := newExecutor(t)
41 |
42 | ctrNNS := neotest.CompileFile(t, e.CommitteeHash, nnsPath, path.Join(nnsPath, "config.yml"))
43 | ctrNetmap := neotest.CompileFile(t, e.CommitteeHash, netmapPath, path.Join(netmapPath, "config.yml"))
44 | ctrBalance := neotest.CompileFile(t, e.CommitteeHash, balancePath, path.Join(balancePath, "config.yml"))
45 | ctrContainer := neotest.CompileFile(t, e.CommitteeHash, containerPath, path.Join(containerPath, "config.yml"))
46 |
47 | e.DeployContract(t, ctrNNS, nil)
48 | deployContainerContract(t, e, ctrNetmap.Hash, ctrBalance.Hash, ctrNNS.Hash)
49 | deployBalanceContract(t, e, ctrNetmap.Hash, ctrContainer.Hash)
50 | deployNetmapContract(t, e, ctrBalance.Hash, ctrContainer.Hash, config...)
51 | return e.CommitteeInvoker(ctrNetmap.Hash)
52 | }
53 |
54 | func TestDeploySetConfig(t *testing.T) {
55 | c := newNetmapInvoker(t, "SomeKey", "TheValue", container.AliasFeeKey, int64(123))
56 | c.Invoke(t, "TheValue", "config", "SomeKey")
57 | c.Invoke(t, stackitem.NewByteArray(bigint.ToBytes(big.NewInt(123))),
58 | "config", container.AliasFeeKey)
59 | }
60 |
61 | type testNodeInfo struct {
62 | signer neotest.SingleSigner
63 | pub []byte
64 | raw []byte
65 | state netmap.NodeState
66 | }
67 |
68 | func dummyNodeInfo(acc neotest.Signer) testNodeInfo {
69 | ni := make([]byte, 66)
70 | rand.Read(ni)
71 |
72 | s := acc.(neotest.SingleSigner)
73 | pub := s.Account().PrivateKey().PublicKey().Bytes()
74 | copy(ni[2:], pub)
75 | return testNodeInfo{
76 | signer: s,
77 | pub: pub,
78 | raw: ni,
79 | state: netmap.NodeStateOnline,
80 | }
81 | }
82 |
83 | func newStorageNode(t *testing.T, c *neotest.ContractInvoker) testNodeInfo {
84 | return dummyNodeInfo(c.NewAccount(t))
85 | }
86 |
87 | func TestAddPeer(t *testing.T) {
88 | c := newNetmapInvoker(t)
89 |
90 | acc := c.NewAccount(t)
91 | cAcc := c.WithSigners(acc)
92 | dummyInfo := dummyNodeInfo(acc)
93 |
94 | acc1 := c.NewAccount(t)
95 | cAcc1 := c.WithSigners(acc1)
96 | cAcc1.InvokeFail(t, common.ErrWitnessFailed, "addPeer", dummyInfo.raw)
97 |
98 | h := cAcc.Invoke(t, stackitem.Null{}, "addPeer", dummyInfo.raw)
99 | aer := cAcc.CheckHalt(t, h)
100 | require.Equal(t, 0, len(aer.Events))
101 |
102 | dummyInfo.raw[0] ^= 0xFF
103 | h = cAcc.Invoke(t, stackitem.Null{}, "addPeer", dummyInfo.raw)
104 | aer = cAcc.CheckHalt(t, h)
105 | require.Equal(t, 0, len(aer.Events))
106 |
107 | c.InvokeFail(t, common.ErrWitnessFailed, "addPeer", dummyInfo.raw)
108 | c.Invoke(t, stackitem.Null{}, "addPeerIR", dummyInfo.raw)
109 | }
110 |
111 | func TestNewEpoch(t *testing.T) {
112 | rand.Seed(42)
113 |
114 | const epochCount = netmap.DefaultSnapshotCount * 2
115 |
116 | cNm := newNetmapInvoker(t)
117 | nodes := make([][]testNodeInfo, epochCount)
118 | for i := range nodes {
119 | size := rand.Int()%5 + 1
120 | arr := make([]testNodeInfo, size)
121 | for j := 0; j < size; j++ {
122 | arr[j] = newStorageNode(t, cNm)
123 | }
124 | nodes[i] = arr
125 | }
126 |
127 | for i := 0; i < epochCount; i++ {
128 | for _, tn := range nodes[i] {
129 | cNm.WithSigners(tn.signer).Invoke(t, stackitem.Null{}, "addPeer", tn.raw)
130 | cNm.Invoke(t, stackitem.Null{}, "addPeerIR", tn.raw)
131 | }
132 |
133 | if i > 0 {
134 | // Remove random nodes from the previous netmap.
135 | current := make([]testNodeInfo, 0, len(nodes[i])+len(nodes[i-1]))
136 | current = append(current, nodes[i]...)
137 |
138 | for j := range nodes[i-1] {
139 | if rand.Int()%3 == 0 {
140 | cNm.Invoke(t, stackitem.Null{}, "updateStateIR",
141 | int64(netmap.NodeStateOffline), nodes[i-1][j].pub)
142 | } else {
143 | current = append(current, nodes[i-1][j])
144 | }
145 | }
146 | nodes[i] = current
147 | }
148 | cNm.Invoke(t, stackitem.Null{}, "newEpoch", i+1)
149 |
150 | t.Logf("Epoch: %d, Netmap()", i)
151 | s, err := cNm.TestInvoke(t, "netmap")
152 | require.NoError(t, err)
153 | require.Equal(t, 1, s.Len())
154 | checkSnapshot(t, s, nodes[i])
155 |
156 | for j := 0; j <= i && j < netmap.DefaultSnapshotCount; j++ {
157 | t.Logf("Epoch: %d, diff: %d", i, j)
158 | checkSnapshotAt(t, j, cNm, nodes[i-j])
159 | }
160 |
161 | _, err = cNm.TestInvoke(t, "snapshot", netmap.DefaultSnapshotCount)
162 | require.Error(t, err)
163 | require.True(t, strings.Contains(err.Error(), "incorrect diff"))
164 |
165 | _, err = cNm.TestInvoke(t, "snapshot", -1)
166 | require.Error(t, err)
167 | require.True(t, strings.Contains(err.Error(), "incorrect diff"))
168 | }
169 | }
170 |
171 | func TestUpdateSnapshotCount(t *testing.T) {
172 | rand.Seed(42)
173 |
174 | require.True(t, netmap.DefaultSnapshotCount > 5) // sanity check, adjust tests if false.
175 |
176 | prepare := func(t *testing.T, cNm *neotest.ContractInvoker, epochCount int) [][]testNodeInfo {
177 | nodes := make([][]testNodeInfo, epochCount)
178 | nodes[0] = []testNodeInfo{newStorageNode(t, cNm)}
179 | cNm.Invoke(t, stackitem.Null{}, "addPeerIR", nodes[0][0].raw)
180 | cNm.Invoke(t, stackitem.Null{}, "newEpoch", 1)
181 | for i := 1; i < len(nodes); i++ {
182 | sn := newStorageNode(t, cNm)
183 | nodes[i] = append(nodes[i-1], sn)
184 | cNm.Invoke(t, stackitem.Null{}, "addPeerIR", sn.raw)
185 | cNm.Invoke(t, stackitem.Null{}, "newEpoch", i+1)
186 | }
187 | return nodes
188 | }
189 |
190 | t.Run("increase size, extend with nil", func(t *testing.T) {
191 | // Before: S-old .. S
192 | // After : S-old .. S nil nil ...
193 | const epochCount = netmap.DefaultSnapshotCount / 2
194 |
195 | cNm := newNetmapInvoker(t)
196 | nodes := prepare(t, cNm, epochCount)
197 |
198 | const newCount = netmap.DefaultSnapshotCount + 3
199 | cNm.Invoke(t, stackitem.Null{}, "updateSnapshotCount", newCount)
200 |
201 | s, err := cNm.TestInvoke(t, "netmap")
202 | require.NoError(t, err)
203 | require.Equal(t, 1, s.Len())
204 | checkSnapshot(t, s, nodes[epochCount-1])
205 | for i := 0; i < epochCount; i++ {
206 | checkSnapshotAt(t, i, cNm, nodes[epochCount-i-1])
207 | }
208 | for i := epochCount; i < newCount; i++ {
209 | checkSnapshotAt(t, i, cNm, nil)
210 | }
211 | _, err = cNm.TestInvoke(t, "snapshot", int64(newCount))
212 | require.Error(t, err)
213 | })
214 | t.Run("increase size, copy old snapshots", func(t *testing.T) {
215 | // Before: S-x .. S S-old ...
216 | // After : S-x .. S nil nil S-old ...
217 | const epochCount = netmap.DefaultSnapshotCount + netmap.DefaultSnapshotCount/2
218 |
219 | cNm := newNetmapInvoker(t)
220 | nodes := prepare(t, cNm, epochCount)
221 |
222 | const newCount = netmap.DefaultSnapshotCount + 3
223 | cNm.Invoke(t, stackitem.Null{}, "updateSnapshotCount", newCount)
224 |
225 | s, err := cNm.TestInvoke(t, "netmap")
226 | require.NoError(t, err)
227 | require.Equal(t, 1, s.Len())
228 | checkSnapshot(t, s, nodes[epochCount-1])
229 | for i := 0; i < newCount-3; i++ {
230 | checkSnapshotAt(t, i, cNm, nodes[epochCount-i-1])
231 | }
232 | for i := newCount - 3; i < newCount; i++ {
233 | checkSnapshotAt(t, i, cNm, nil)
234 | }
235 | _, err = cNm.TestInvoke(t, "snapshot", int64(newCount))
236 | require.Error(t, err)
237 | })
238 | t.Run("decrease size, small decrease", func(t *testing.T) {
239 | // Before: S-x .. S S-old ... ...
240 | // After : S-x .. S S-new ...
241 | const epochCount = netmap.DefaultSnapshotCount + netmap.DefaultSnapshotCount/2
242 |
243 | cNm := newNetmapInvoker(t)
244 | nodes := prepare(t, cNm, epochCount)
245 |
246 | const newCount = netmap.DefaultSnapshotCount/2 + 2
247 | cNm.Invoke(t, stackitem.Null{}, "updateSnapshotCount", newCount)
248 |
249 | s, err := cNm.TestInvoke(t, "netmap")
250 | require.NoError(t, err)
251 | require.Equal(t, 1, s.Len())
252 | checkSnapshot(t, s, nodes[epochCount-1])
253 | for i := 0; i < newCount; i++ {
254 | checkSnapshotAt(t, i, cNm, nodes[epochCount-i-1])
255 | }
256 | _, err = cNm.TestInvoke(t, "snapshot", int64(newCount))
257 | require.Error(t, err)
258 | })
259 | t.Run("decrease size, big decrease", func(t *testing.T) {
260 | // Before: S-x ... ... S S-old ... ...
261 | // After : S-new ... S
262 | const epochCount = netmap.DefaultSnapshotCount + netmap.DefaultSnapshotCount/2
263 |
264 | cNm := newNetmapInvoker(t)
265 | nodes := prepare(t, cNm, epochCount)
266 |
267 | const newCount = netmap.DefaultSnapshotCount/2 - 2
268 | cNm.Invoke(t, stackitem.Null{}, "updateSnapshotCount", newCount)
269 |
270 | s, err := cNm.TestInvoke(t, "netmap")
271 | require.NoError(t, err)
272 | require.Equal(t, 1, s.Len())
273 | checkSnapshot(t, s, nodes[epochCount-1])
274 | for i := 0; i < newCount; i++ {
275 | checkSnapshotAt(t, i, cNm, nodes[epochCount-i-1])
276 | }
277 | _, err = cNm.TestInvoke(t, "snapshot", int64(newCount))
278 | require.Error(t, err)
279 | })
280 | }
281 |
282 | func checkSnapshotAt(t *testing.T, epoch int, cNm *neotest.ContractInvoker, nodes []testNodeInfo) {
283 | s, err := cNm.TestInvoke(t, "snapshot", int64(epoch))
284 | require.NoError(t, err)
285 | require.Equal(t, 1, s.Len())
286 | checkSnapshot(t, s, nodes)
287 | }
288 |
289 | func checkSnapshot(t *testing.T, s *vm.Stack, nodes []testNodeInfo) {
290 | arr, ok := s.Pop().Value().([]stackitem.Item)
291 | require.True(t, ok, "expected array")
292 | require.Equal(t, len(nodes), len(arr), "expected %d nodes", len(nodes))
293 |
294 | actual := make([]netmap.Node, len(nodes))
295 | expected := make([]netmap.Node, len(nodes))
296 | for i := range nodes {
297 | n, ok := arr[i].Value().([]stackitem.Item)
298 | require.True(t, ok, "expected node struct")
299 | require.Equalf(t, 2, len(n), "expected %d field(s)", 2)
300 |
301 | require.IsType(t, []byte{}, n[0].Value())
302 |
303 | state, err := n[1].TryInteger()
304 | require.NoError(t, err)
305 |
306 | actual[i].BLOB = n[0].Value().([]byte)
307 | actual[i].State = netmap.NodeState(state.Int64())
308 | expected[i].BLOB = nodes[i].raw
309 | expected[i].State = nodes[i].state
310 | }
311 |
312 | require.ElementsMatch(t, expected, actual, "snapshot is different")
313 | }
314 |
315 | func TestUpdateStateIR(t *testing.T) {
316 | cNm := newNetmapInvoker(t)
317 |
318 | acc := cNm.NewAccount(t)
319 | pub := acc.(neotest.SingleSigner).Account().PrivateKey().PublicKey().Bytes()
320 |
321 | t.Run("can't move online, need addPeerIR", func(t *testing.T) {
322 | cNm.InvokeFail(t, "peer is missing", "updateStateIR", int64(netmap.NodeStateOnline), pub)
323 | })
324 |
325 | dummyInfo := dummyNodeInfo(acc)
326 | cNm.Invoke(t, stackitem.Null{}, "addPeerIR", dummyInfo.raw)
327 |
328 | acc1 := cNm.NewAccount(t)
329 | dummyInfo1 := dummyNodeInfo(acc1)
330 | cNm.Invoke(t, stackitem.Null{}, "addPeerIR", dummyInfo1.raw)
331 |
332 | t.Run("must be signed by the alphabet", func(t *testing.T) {
333 | cAcc := cNm.WithSigners(acc)
334 | cAcc.InvokeFail(t, common.ErrAlphabetWitnessFailed, "updateStateIR", int64(netmap.NodeStateOffline), pub)
335 | })
336 | t.Run("invalid state", func(t *testing.T) {
337 | cNm.InvokeFail(t, "unsupported state", "updateStateIR", int64(42), pub)
338 | })
339 |
340 | checkNetmapCandidates(t, cNm, 2)
341 |
342 | // Move the first node offline.
343 | cNm.Invoke(t, stackitem.Null{}, "updateStateIR", int64(netmap.NodeStateOffline), pub)
344 | checkNetmapCandidates(t, cNm, 1)
345 |
346 | checkState := func(expected netmap.NodeState) {
347 | arr := checkNetmapCandidates(t, cNm, 1)
348 | nn := arr[0].Value().([]stackitem.Item)
349 | state, err := nn[1].TryInteger()
350 | require.NoError(t, err)
351 | require.Equal(t, int64(expected), state.Int64())
352 | }
353 |
354 | // Move the second node in the maintenance state.
355 | pub1 := acc1.(neotest.SingleSigner).Account().PrivateKey().PublicKey().Bytes()
356 | t.Run("maintenance -> add peer", func(t *testing.T) {
357 | cNm.Invoke(t, stackitem.Null{}, "updateStateIR", int64(netmap.NodeStateMaintenance), pub1)
358 | checkState(netmap.NodeStateMaintenance)
359 | cNm.Invoke(t, stackitem.Null{}, "addPeerIR", dummyInfo1.raw)
360 | checkState(netmap.NodeStateOnline)
361 | })
362 | t.Run("maintenance -> online", func(t *testing.T) {
363 | cNm.Invoke(t, stackitem.Null{}, "updateStateIR", int64(netmap.NodeStateMaintenance), pub1)
364 | checkState(netmap.NodeStateMaintenance)
365 | cNm.Invoke(t, stackitem.Null{}, "updateStateIR", int64(netmap.NodeStateOnline), pub1)
366 | checkState(netmap.NodeStateOnline)
367 | })
368 | }
369 |
370 | func TestUpdateState(t *testing.T) {
371 | cNm := newNetmapInvoker(t)
372 |
373 | accs := []neotest.Signer{cNm.NewAccount(t), cNm.NewAccount(t)}
374 | pubs := make([][]byte, len(accs))
375 | for i := range accs {
376 | dummyInfo := dummyNodeInfo(accs[i])
377 | cNm.Invoke(t, stackitem.Null{}, "addPeerIR", dummyInfo.raw)
378 | pubs[i] = accs[i].(neotest.SingleSigner).Account().PrivateKey().PublicKey().Bytes()
379 | }
380 |
381 | t.Run("missing witness", func(t *testing.T) {
382 | cAcc := cNm.WithSigners(accs[0])
383 | cNm.InvokeFail(t, common.ErrWitnessFailed, "updateState", int64(2), pubs[0])
384 | cAcc.InvokeFail(t, common.ErrAlphabetWitnessFailed, "updateState", int64(2), pubs[0])
385 | cAcc.InvokeFail(t, common.ErrWitnessFailed, "updateState", int64(2), pubs[1])
386 | })
387 |
388 | checkNetmapCandidates(t, cNm, 2)
389 |
390 | cBoth := cNm.WithSigners(accs[0], cNm.Committee)
391 |
392 | cBoth.Invoke(t, stackitem.Null{}, "updateState", int64(2), pubs[0])
393 | checkNetmapCandidates(t, cNm, 1)
394 |
395 | t.Run("remove already removed node", func(t *testing.T) {
396 | cBoth.Invoke(t, stackitem.Null{}, "updateState", int64(2), pubs[0])
397 | checkNetmapCandidates(t, cNm, 1)
398 | })
399 |
400 | cBoth = cNm.WithSigners(accs[1], cNm.Committee)
401 | cBoth.Invoke(t, stackitem.Null{}, "updateState", int64(2), pubs[1])
402 | checkNetmapCandidates(t, cNm, 0)
403 | }
404 |
405 | func checkNetmapCandidates(t *testing.T, c *neotest.ContractInvoker, size int) []stackitem.Item {
406 | s, err := c.TestInvoke(t, "netmapCandidates")
407 | require.NoError(t, err)
408 | require.Equal(t, 1, s.Len())
409 |
410 | arr, ok := s.Pop().Value().([]stackitem.Item)
411 | require.True(t, ok)
412 | require.Equal(t, size, len(arr))
413 | return arr
414 | }
415 |
--------------------------------------------------------------------------------
/tests/nns_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "fmt"
5 | "math/big"
6 | "path"
7 | "strings"
8 | "testing"
9 | "time"
10 |
11 | "github.com/TrueCloudLab/frostfs-contract/nns"
12 | "github.com/nspcc-dev/neo-go/pkg/core/interop/storage"
13 | "github.com/nspcc-dev/neo-go/pkg/neotest"
14 | "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
15 | "github.com/stretchr/testify/require"
16 | )
17 |
18 | const nnsPath = "../nns"
19 |
20 | const msPerYear = 365 * 24 * time.Hour / time.Millisecond
21 |
22 | func newNNSInvoker(t *testing.T, addRoot bool) *neotest.ContractInvoker {
23 | e := newExecutor(t)
24 | ctr := neotest.CompileFile(t, e.CommitteeHash, nnsPath, path.Join(nnsPath, "config.yml"))
25 | e.DeployContract(t, ctr, nil)
26 |
27 | c := e.CommitteeInvoker(ctr.Hash)
28 | if addRoot {
29 | // Set expiration big enough to pass all tests.
30 | refresh, retry, expire, ttl := int64(101), int64(102), int64(msPerYear/1000*100), int64(104)
31 | c.Invoke(t, true, "register",
32 | "com", c.CommitteeHash,
33 | "myemail@nspcc.ru", refresh, retry, expire, ttl)
34 | }
35 | return c
36 | }
37 |
38 | func TestNNSGeneric(t *testing.T) {
39 | c := newNNSInvoker(t, false)
40 |
41 | c.Invoke(t, "NNS", "symbol")
42 | c.Invoke(t, 0, "decimals")
43 | c.Invoke(t, 0, "totalSupply")
44 | }
45 |
46 | func TestNNSRegisterTLD(t *testing.T) {
47 | c := newNNSInvoker(t, false)
48 |
49 | refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
50 |
51 | c.InvokeFail(t, "invalid domain name format", "register",
52 | "0com", c.CommitteeHash,
53 | "email@nspcc.ru", refresh, retry, expire, ttl)
54 |
55 | acc := c.NewAccount(t)
56 | cAcc := c.WithSigners(acc)
57 | cAcc.InvokeFail(t, "not witnessed by committee", "register",
58 | "com", acc.ScriptHash(),
59 | "email@nspcc.ru", refresh, retry, expire, ttl)
60 |
61 | c.Invoke(t, true, "register",
62 | "com", c.CommitteeHash,
63 | "email@nspcc.ru", refresh, retry, expire, ttl)
64 |
65 | c.InvokeFail(t, "TLD already exists", "register",
66 | "com", c.CommitteeHash,
67 | "email@nspcc.ru", refresh, retry, expire, ttl)
68 | }
69 |
70 | func TestNNSRegister(t *testing.T) {
71 | c := newNNSInvoker(t, false)
72 |
73 | accTop := c.NewAccount(t)
74 | refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
75 | c1 := c.WithSigners(c.Committee, accTop)
76 | c1.Invoke(t, true, "register",
77 | "com", accTop.ScriptHash(),
78 | "myemail@nspcc.ru", refresh, retry, expire, ttl)
79 |
80 | acc := c.NewAccount(t)
81 | c2 := c.WithSigners(c.Committee, acc)
82 | c2.InvokeFail(t, "not witnessed by admin", "register",
83 | "testdomain.com", acc.ScriptHash(),
84 | "myemail@nspcc.ru", refresh, retry, expire, ttl)
85 |
86 | c3 := c.WithSigners(accTop, acc)
87 | t.Run("domain names with hyphen", func(t *testing.T) {
88 | c3.InvokeFail(t, "invalid domain name format", "register",
89 | "-testdomain.com", acc.ScriptHash(),
90 | "myemail@nspcc.ru", refresh, retry, expire, ttl)
91 | c3.InvokeFail(t, "invalid domain name format", "register",
92 | "testdomain-.com", acc.ScriptHash(),
93 | "myemail@nspcc.ru", refresh, retry, expire, ttl)
94 | c3.Invoke(t, true, "register",
95 | "test-domain.com", acc.ScriptHash(),
96 | "myemail@nspcc.ru", refresh, retry, expire, ttl)
97 | })
98 | c3.Invoke(t, true, "register",
99 | "testdomain.com", acc.ScriptHash(),
100 | "myemail@nspcc.ru", refresh, retry, expire, ttl)
101 |
102 | b := c.TopBlock(t)
103 | expected := stackitem.NewArray([]stackitem.Item{stackitem.NewBuffer(
104 | []byte(fmt.Sprintf("testdomain.com myemail@nspcc.ru %d %d %d %d %d",
105 | b.Timestamp, refresh, retry, expire, ttl)))})
106 | c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.SOA))
107 |
108 | cAcc := c.WithSigners(acc)
109 | cAcc.Invoke(t, stackitem.Null{}, "addRecord",
110 | "testdomain.com", int64(nns.TXT), "first TXT record")
111 | cAcc.InvokeFail(t, "record already exists", "addRecord",
112 | "testdomain.com", int64(nns.TXT), "first TXT record")
113 | cAcc.Invoke(t, stackitem.Null{}, "addRecord",
114 | "testdomain.com", int64(nns.TXT), "second TXT record")
115 |
116 | expected = stackitem.NewArray([]stackitem.Item{
117 | stackitem.NewByteArray([]byte("first TXT record")),
118 | stackitem.NewByteArray([]byte("second TXT record"))})
119 | c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.TXT))
120 |
121 | cAcc.Invoke(t, stackitem.Null{}, "setRecord",
122 | "testdomain.com", int64(nns.TXT), int64(0), "replaced first")
123 |
124 | expected = stackitem.NewArray([]stackitem.Item{
125 | stackitem.NewByteArray([]byte("replaced first")),
126 | stackitem.NewByteArray([]byte("second TXT record"))})
127 | c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.TXT))
128 | }
129 |
130 | func TestTLDRecord(t *testing.T) {
131 | c := newNNSInvoker(t, true)
132 | c.Invoke(t, stackitem.Null{}, "addRecord",
133 | "com", int64(nns.A), "1.2.3.4")
134 |
135 | result := []stackitem.Item{stackitem.NewByteArray([]byte("1.2.3.4"))}
136 | c.Invoke(t, result, "resolve", "com", int64(nns.A))
137 | }
138 |
139 | func TestNNSRegisterMulti(t *testing.T) {
140 | c := newNNSInvoker(t, true)
141 |
142 | newArgs := func(domain string, account neotest.Signer) []interface{} {
143 | return []interface{}{
144 | domain, account.ScriptHash(), "doesnt@matter.com",
145 | int64(101), int64(102), int64(103), int64(104),
146 | }
147 | }
148 | acc := c.NewAccount(t)
149 | cBoth := c.WithSigners(c.Committee, acc)
150 | args := newArgs("neo.com", acc)
151 | cBoth.Invoke(t, true, "register", args...)
152 |
153 | c1 := c.WithSigners(acc)
154 | t.Run("parent domain is missing", func(t *testing.T) {
155 | msg := "one of the parent domains is not registered"
156 | args[0] = "testnet.fs.neo.com"
157 | c1.InvokeFail(t, msg, "register", args...)
158 | })
159 |
160 | args[0] = "fs.neo.com"
161 | c1.Invoke(t, true, "register", args...)
162 |
163 | args[0] = "testnet.fs.neo.com"
164 | c1.Invoke(t, true, "register", args...)
165 |
166 | acc2 := c.NewAccount(t)
167 | c2 := c.WithSigners(c.Committee, acc2)
168 | args = newArgs("mainnet.fs.neo.com", acc2)
169 | c2.InvokeFail(t, "not witnessed by admin", "register", args...)
170 |
171 | c1.Invoke(t, stackitem.Null{}, "addRecord",
172 | "something.mainnet.fs.neo.com", int64(nns.A), "1.2.3.4")
173 | c1.Invoke(t, stackitem.Null{}, "addRecord",
174 | "another.fs.neo.com", int64(nns.A), "4.3.2.1")
175 |
176 | c2 = c.WithSigners(acc, acc2)
177 | c2.InvokeFail(t, "parent domain has conflicting records: something.mainnet.fs.neo.com",
178 | "register", args...)
179 |
180 | c1.Invoke(t, stackitem.Null{}, "deleteRecords",
181 | "something.mainnet.fs.neo.com", int64(nns.A))
182 | c2.Invoke(t, true, "register", args...)
183 |
184 | c2 = c.WithSigners(acc2)
185 | c2.Invoke(t, stackitem.Null{}, "addRecord",
186 | "cdn.mainnet.fs.neo.com", int64(nns.A), "166.15.14.13")
187 | result := stackitem.NewArray([]stackitem.Item{
188 | stackitem.NewByteArray([]byte("166.15.14.13"))})
189 | c2.Invoke(t, result, "resolve", "cdn.mainnet.fs.neo.com", int64(nns.A))
190 | }
191 |
192 | func TestNNSUpdateSOA(t *testing.T) {
193 | c := newNNSInvoker(t, true)
194 |
195 | refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
196 | c.Invoke(t, true, "register",
197 | "testdomain.com", c.CommitteeHash,
198 | "myemail@nspcc.ru", refresh, retry, expire, ttl)
199 |
200 | refresh *= 2
201 | retry *= 2
202 | expire *= 2
203 | ttl *= 2
204 | c.Invoke(t, stackitem.Null{}, "updateSOA",
205 | "testdomain.com", "newemail@nspcc.ru", refresh, retry, expire, ttl)
206 |
207 | b := c.TopBlock(t)
208 | expected := stackitem.NewArray([]stackitem.Item{stackitem.NewBuffer(
209 | []byte(fmt.Sprintf("testdomain.com newemail@nspcc.ru %d %d %d %d %d",
210 | b.Timestamp, refresh, retry, expire, ttl)))})
211 | c.Invoke(t, expected, "getRecords", "testdomain.com", int64(nns.SOA))
212 | }
213 |
214 | func TestNNSGetAllRecords(t *testing.T) {
215 | c := newNNSInvoker(t, true)
216 |
217 | refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
218 | c.Invoke(t, true, "register",
219 | "testdomain.com", c.CommitteeHash,
220 | "myemail@nspcc.ru", refresh, retry, expire, ttl)
221 |
222 | c.Invoke(t, stackitem.Null{}, "addRecord", "testdomain.com", int64(nns.TXT), "first TXT record")
223 | c.Invoke(t, stackitem.Null{}, "addRecord", "testdomain.com", int64(nns.A), "1.2.3.4")
224 |
225 | b := c.TopBlock(t)
226 | expSOA := fmt.Sprintf("testdomain.com myemail@nspcc.ru %d %d %d %d %d",
227 | b.Timestamp, refresh, retry, expire, ttl)
228 |
229 | s, err := c.TestInvoke(t, "getAllRecords", "testdomain.com")
230 | require.NoError(t, err)
231 |
232 | iter := s.Pop().Value().(*storage.Iterator)
233 | require.True(t, iter.Next())
234 | require.Equal(t, stackitem.NewStruct([]stackitem.Item{
235 | stackitem.Make("testdomain.com"), stackitem.Make(int64(nns.A)),
236 | stackitem.Make("1.2.3.4"), stackitem.Make(new(big.Int)),
237 | }), iter.Value())
238 |
239 | require.True(t, iter.Next())
240 | require.Equal(t, stackitem.NewStruct([]stackitem.Item{
241 | stackitem.Make("testdomain.com"), stackitem.Make(int64(nns.SOA)),
242 | stackitem.NewBuffer([]byte(expSOA)), stackitem.Make(new(big.Int)),
243 | }), iter.Value())
244 |
245 | require.True(t, iter.Next())
246 | require.Equal(t, stackitem.NewStruct([]stackitem.Item{
247 | stackitem.Make("testdomain.com"), stackitem.Make(int64(nns.TXT)),
248 | stackitem.Make("first TXT record"), stackitem.Make(new(big.Int)),
249 | }), iter.Value())
250 |
251 | require.False(t, iter.Next())
252 | }
253 |
254 | func TestExpiration(t *testing.T) {
255 | c := newNNSInvoker(t, true)
256 |
257 | refresh, retry, expire, ttl := int64(101), int64(102), int64(msPerYear/1000*10), int64(104)
258 | c.Invoke(t, true, "register",
259 | "testdomain.com", c.CommitteeHash,
260 | "myemail@nspcc.ru", refresh, retry, expire, ttl)
261 |
262 | checkProperties := func(t *testing.T, expiration uint64) {
263 | expected := stackitem.NewMapWithValue([]stackitem.MapElement{
264 | {Key: stackitem.Make("name"), Value: stackitem.Make("testdomain.com")},
265 | {Key: stackitem.Make("expiration"), Value: stackitem.Make(expiration)}})
266 | s, err := c.TestInvoke(t, "properties", "testdomain.com")
267 | require.NoError(t, err)
268 | require.Equal(t, expected.Value(), s.Top().Item().Value())
269 | }
270 |
271 | top := c.TopBlock(t)
272 | expiration := top.Timestamp + uint64(expire*1000)
273 | checkProperties(t, expiration)
274 |
275 | b := c.NewUnsignedBlock(t)
276 | b.Timestamp = expiration - 2 // test invoke is done with +1 timestamp
277 | require.NoError(t, c.Chain.AddBlock(c.SignBlock(b)))
278 | checkProperties(t, expiration)
279 |
280 | b = c.NewUnsignedBlock(t)
281 | b.Timestamp = expiration - 1
282 | require.NoError(t, c.Chain.AddBlock(c.SignBlock(b)))
283 |
284 | _, err := c.TestInvoke(t, "properties", "testdomain.com")
285 | require.Error(t, err)
286 | require.True(t, strings.Contains(err.Error(), "name has expired"))
287 |
288 | c.InvokeFail(t, "name has expired", "getAllRecords", "testdomain.com")
289 | c.InvokeFail(t, "name has expired", "ownerOf", "testdomain.com")
290 | }
291 |
292 | func TestNNSSetAdmin(t *testing.T) {
293 | c := newNNSInvoker(t, true)
294 |
295 | refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
296 | c.Invoke(t, true, "register",
297 | "testdomain.com", c.CommitteeHash,
298 | "myemail@nspcc.ru", refresh, retry, expire, ttl)
299 |
300 | acc := c.NewAccount(t)
301 | cAcc := c.WithSigners(acc)
302 | cAcc.InvokeFail(t, "not witnessed by admin", "addRecord",
303 | "testdomain.com", int64(nns.TXT), "won't be added")
304 |
305 | c1 := c.WithSigners(c.Committee, acc)
306 | c1.Invoke(t, stackitem.Null{}, "setAdmin", "testdomain.com", acc.ScriptHash())
307 |
308 | cAcc.Invoke(t, stackitem.Null{}, "addRecord",
309 | "testdomain.com", int64(nns.TXT), "will be added")
310 | }
311 |
312 | func TestNNSIsAvailable(t *testing.T) {
313 | c := newNNSInvoker(t, false)
314 |
315 | c.Invoke(t, true, "isAvailable", "com")
316 | c.InvokeFail(t, "TLD not found", "isAvailable", "domain.com")
317 |
318 | refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
319 | c.Invoke(t, true, "register",
320 | "com", c.CommitteeHash,
321 | "myemail@nspcc.ru", refresh, retry, expire, ttl)
322 |
323 | c.Invoke(t, false, "isAvailable", "com")
324 | c.Invoke(t, true, "isAvailable", "domain.com")
325 |
326 | acc := c.NewAccount(t)
327 | c1 := c.WithSigners(c.Committee, acc)
328 | c1.Invoke(t, true, "register",
329 | "domain.com", acc.ScriptHash(),
330 | "myemail@nspcc.ru", refresh, retry, expire, ttl)
331 |
332 | c.Invoke(t, false, "isAvailable", "domain.com")
333 | }
334 |
335 | func TestNNSRenew(t *testing.T) {
336 | c := newNNSInvoker(t, true)
337 |
338 | acc := c.NewAccount(t)
339 | c1 := c.WithSigners(c.Committee, acc)
340 | refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
341 | c1.Invoke(t, true, "register",
342 | "testdomain.com", c.CommitteeHash,
343 | "myemail@nspcc.ru", refresh, retry, expire, ttl)
344 |
345 | const msPerYear = 365 * 24 * time.Hour / time.Millisecond
346 | b := c.TopBlock(t)
347 | ts := b.Timestamp + uint64(expire*1000) + uint64(msPerYear)
348 |
349 | cAcc := c.WithSigners(acc)
350 | cAcc.InvokeFail(t, "not witnessed by admin", "renew", "testdomain.com")
351 | c1.Invoke(t, ts, "renew", "testdomain.com")
352 | expected := stackitem.NewMapWithValue([]stackitem.MapElement{
353 | {Key: stackitem.Make("name"), Value: stackitem.Make("testdomain.com")},
354 | {Key: stackitem.Make("expiration"), Value: stackitem.Make(ts)}})
355 | cAcc.Invoke(t, expected, "properties", "testdomain.com")
356 | }
357 |
358 | func TestNNSResolve(t *testing.T) {
359 | c := newNNSInvoker(t, true)
360 |
361 | refresh, retry, expire, ttl := int64(101), int64(102), int64(103), int64(104)
362 | c.Invoke(t, true, "register",
363 | "test.com", c.CommitteeHash,
364 | "myemail@nspcc.ru", refresh, retry, expire, ttl)
365 |
366 | c.Invoke(t, stackitem.Null{}, "addRecord",
367 | "test.com", int64(nns.TXT), "expected result")
368 |
369 | records := stackitem.NewArray([]stackitem.Item{stackitem.Make("expected result")})
370 | c.Invoke(t, records, "resolve", "test.com", int64(nns.TXT))
371 | c.Invoke(t, records, "resolve", "test.com.", int64(nns.TXT))
372 | c.InvokeFail(t, "invalid domain name format", "resolve", "test.com..", int64(nns.TXT))
373 | }
374 |
--------------------------------------------------------------------------------
/tests/processing_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "path"
5 | "testing"
6 |
7 | "github.com/nspcc-dev/neo-go/pkg/neotest"
8 | "github.com/nspcc-dev/neo-go/pkg/util"
9 | "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
10 | )
11 |
12 | const processingPath = "../processing"
13 |
14 | func deployProcessingContract(t *testing.T, e *neotest.Executor, addrFrostFS util.Uint160) util.Uint160 {
15 | c := neotest.CompileFile(t, e.CommitteeHash, processingPath, path.Join(processingPath, "config.yml"))
16 |
17 | args := make([]interface{}, 1)
18 | args[0] = addrFrostFS
19 |
20 | e.DeployContract(t, c, args)
21 | return c.Hash
22 | }
23 |
24 | func newProcessingInvoker(t *testing.T) (*neotest.ContractInvoker, neotest.Signer) {
25 | frostfsInvoker, irMultiAcc, _ := newFrostFSInvoker(t, 2)
26 | hash := deployProcessingContract(t, frostfsInvoker.Executor, frostfsInvoker.Hash)
27 |
28 | return frostfsInvoker.CommitteeInvoker(hash), irMultiAcc
29 | }
30 |
31 | func TestVerify_Processing(t *testing.T) {
32 | c, irMultiAcc := newProcessingInvoker(t)
33 |
34 | const method = "verify"
35 |
36 | cIR := c.WithSigners(irMultiAcc)
37 |
38 | cIR.Invoke(t, stackitem.NewBool(true), method)
39 | c.Invoke(t, stackitem.NewBool(false), method)
40 | }
41 |
--------------------------------------------------------------------------------
/tests/proxy_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "path"
5 | "testing"
6 |
7 | "github.com/nspcc-dev/neo-go/pkg/neotest"
8 | "github.com/nspcc-dev/neo-go/pkg/util"
9 | "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
10 | )
11 |
12 | const proxyPath = "../proxy"
13 |
14 | func deployProxyContract(t *testing.T, e *neotest.Executor, addrNetmap util.Uint160) util.Uint160 {
15 | args := make([]interface{}, 1)
16 | args[0] = addrNetmap
17 |
18 | c := neotest.CompileFile(t, e.CommitteeHash, proxyPath, path.Join(proxyPath, "config.yml"))
19 | e.DeployContract(t, c, args)
20 | return c.Hash
21 | }
22 |
23 | func newProxyInvoker(t *testing.T) *neotest.ContractInvoker {
24 | e := newExecutor(t)
25 |
26 | ctrNetmap := neotest.CompileFile(t, e.CommitteeHash, netmapPath, path.Join(netmapPath, "config.yml"))
27 | ctrBalance := neotest.CompileFile(t, e.CommitteeHash, balancePath, path.Join(balancePath, "config.yml"))
28 | ctrContainer := neotest.CompileFile(t, e.CommitteeHash, containerPath, path.Join(containerPath, "config.yml"))
29 | ctrProxy := neotest.CompileFile(t, e.CommitteeHash, proxyPath, path.Join(proxyPath, "config.yml"))
30 |
31 | deployNetmapContract(t, e, ctrBalance.Hash, ctrContainer.Hash)
32 | deployProxyContract(t, e, ctrNetmap.Hash)
33 |
34 | return e.CommitteeInvoker(ctrProxy.Hash)
35 | }
36 |
37 | func TestVerify(t *testing.T) {
38 | e := newProxyInvoker(t)
39 |
40 | const method = "verify"
41 |
42 | e.Invoke(t, stackitem.NewBool(true), method)
43 |
44 | notAlphabet := e.NewAccount(t)
45 | cNotAlphabet := e.WithSigners(notAlphabet)
46 |
47 | cNotAlphabet.Invoke(t, stackitem.NewBool(false), method)
48 | }
49 |
--------------------------------------------------------------------------------
/tests/reputation_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "path"
5 | "testing"
6 |
7 | "github.com/nspcc-dev/neo-go/pkg/neotest"
8 | "github.com/nspcc-dev/neo-go/pkg/neotest/chain"
9 | "github.com/nspcc-dev/neo-go/pkg/util"
10 | "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
11 | )
12 |
13 | const reputationPath = "../reputation"
14 |
15 | func deployReputationContract(t *testing.T, e *neotest.Executor) util.Uint160 {
16 | c := neotest.CompileFile(t, e.CommitteeHash, reputationPath,
17 | path.Join(reputationPath, "config.yml"))
18 |
19 | args := make([]interface{}, 1)
20 | args[0] = false
21 |
22 | e.DeployContract(t, c, args)
23 | return c.Hash
24 | }
25 |
26 | func newReputationInvoker(t *testing.T) *neotest.ContractInvoker {
27 | bc, acc := chain.NewSingle(t)
28 | e := neotest.NewExecutor(t, bc, acc, acc)
29 | h := deployReputationContract(t, e)
30 | return e.CommitteeInvoker(h)
31 | }
32 |
33 | func TestReputation_Put(t *testing.T) {
34 | e := newReputationInvoker(t)
35 |
36 | peerID := []byte{1, 2, 3}
37 | e.Invoke(t, stackitem.Null{}, "put", int64(1), peerID, []byte{4})
38 |
39 | t.Run("concurrent invocations", func(t *testing.T) {
40 | repValue1 := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
41 | repValue2 := []byte{10, 20, 30, 40, 50, 60, 70, 80}
42 | tx1 := e.PrepareInvoke(t, "put", int64(1), peerID, repValue1)
43 | tx2 := e.PrepareInvoke(t, "put", int64(1), peerID, repValue2)
44 |
45 | e.AddNewBlock(t, tx1, tx2)
46 | e.CheckHalt(t, tx1.Hash(), stackitem.Null{})
47 | e.CheckHalt(t, tx2.Hash(), stackitem.Null{})
48 |
49 | t.Run("get all", func(t *testing.T) {
50 | result := stackitem.NewArray([]stackitem.Item{
51 | stackitem.NewBuffer([]byte{4}),
52 | stackitem.NewBuffer(repValue1),
53 | stackitem.NewBuffer(repValue2),
54 | })
55 | e.Invoke(t, result, "get", int64(1), peerID)
56 | })
57 | })
58 | }
59 |
60 | func TestReputation_ListByEpoch(t *testing.T) {
61 | e := newReputationInvoker(t)
62 |
63 | peerIDs := []string{"peer1", "peer2"}
64 | e.Invoke(t, stackitem.Null{}, "put", int64(1), peerIDs[0], []byte{1})
65 | e.Invoke(t, stackitem.Null{}, "put", int64(1), peerIDs[0], []byte{2})
66 | e.Invoke(t, stackitem.Null{}, "put", int64(2), peerIDs[1], []byte{3})
67 | e.Invoke(t, stackitem.Null{}, "put", int64(2), peerIDs[0], []byte{4})
68 | e.Invoke(t, stackitem.Null{}, "put", int64(2), peerIDs[1], []byte{5})
69 |
70 | result := stackitem.NewArray([]stackitem.Item{
71 | stackitem.NewBuffer(append([]byte{1}, peerIDs[0]...)),
72 | })
73 | e.Invoke(t, result, "listByEpoch", int64(1))
74 |
75 | result = stackitem.NewArray([]stackitem.Item{
76 | stackitem.NewBuffer(append([]byte{2}, peerIDs[0]...)),
77 | stackitem.NewBuffer(append([]byte{2}, peerIDs[1]...)),
78 | })
79 | e.Invoke(t, result, "listByEpoch", int64(2))
80 | }
81 |
--------------------------------------------------------------------------------
/tests/subnet_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "encoding/binary"
5 | "path"
6 | "testing"
7 |
8 | "github.com/TrueCloudLab/frostfs-contract/common"
9 | "github.com/TrueCloudLab/frostfs-contract/subnet"
10 | "github.com/nspcc-dev/neo-go/pkg/neotest"
11 | "github.com/nspcc-dev/neo-go/pkg/util"
12 | "github.com/nspcc-dev/neo-go/pkg/vm"
13 | "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
14 | "github.com/stretchr/testify/require"
15 | )
16 |
17 | const subnetPath = "../subnet"
18 |
19 | func deploySubnetContract(t *testing.T, e *neotest.Executor) util.Uint160 {
20 | c := neotest.CompileFile(t, e.CommitteeHash, subnetPath, path.Join(subnetPath, "config.yml"))
21 | args := []interface{}{false}
22 | e.DeployContract(t, c, args)
23 | return c.Hash
24 | }
25 |
26 | func newSubnetInvoker(t *testing.T) *neotest.ContractInvoker {
27 | e := newExecutor(t)
28 | h := deploySubnetContract(t, e)
29 | return e.CommitteeInvoker(h)
30 | }
31 |
32 | func TestSubnet_Version(t *testing.T) {
33 | e := newSubnetInvoker(t)
34 | e.Invoke(t, common.Version, "version")
35 | }
36 |
37 | func TestSubnet_Put(t *testing.T) {
38 | e := newSubnetInvoker(t)
39 |
40 | acc := e.NewAccount(t)
41 | pub, ok := vm.ParseSignatureContract(acc.Script())
42 | require.True(t, ok)
43 |
44 | id := make([]byte, 5)
45 | binary.LittleEndian.PutUint32(id, 123)
46 | info := randomBytes(10)
47 |
48 | e.InvokeFail(t, common.ErrWitnessFailed, "put", id, pub, info)
49 |
50 | cAcc := e.WithSigners(acc)
51 | cAcc.InvokeFail(t, common.ErrAlphabetWitnessFailed, "put", id, pub, info)
52 |
53 | cBoth := e.WithSigners(e.Committee, acc)
54 | cBoth.InvokeFail(t, subnet.ErrInvalidSubnetID, "put", []byte{1, 2, 3}, pub, info)
55 | cBoth.InvokeFail(t, subnet.ErrInvalidOwner, "put", id, pub[10:], info)
56 | cBoth.Invoke(t, stackitem.Null{}, "put", id, pub, info)
57 | cAcc.Invoke(t, stackitem.NewBuffer(info), "get", id)
58 | cBoth.InvokeFail(t, subnet.ErrAlreadyExists, "put", id, pub, info)
59 | }
60 |
61 | func TestSubnet_Delete(t *testing.T) {
62 | e := newSubnetInvoker(t)
63 |
64 | id, owner := createSubnet(t, e)
65 |
66 | e.InvokeFail(t, common.ErrWitnessFailed, "delete", id)
67 |
68 | cAcc := e.WithSigners(owner)
69 | cAcc.InvokeFail(t, subnet.ErrInvalidSubnetID, "delete", []byte{1, 1, 1, 1})
70 | cAcc.Invoke(t, stackitem.Null{}, "delete", []byte{1, 1, 1, 1, 1})
71 | cAcc.Invoke(t, stackitem.Null{}, "delete", id)
72 | cAcc.InvokeFail(t, subnet.ErrNotExist, "get", id)
73 | }
74 |
75 | func TestSubnet_AddNodeAdmin(t *testing.T) {
76 | e := newSubnetInvoker(t)
77 |
78 | id, owner := createSubnet(t, e)
79 |
80 | adm := e.NewAccount(t)
81 | admPub, ok := vm.ParseSignatureContract(adm.Script())
82 | require.True(t, ok)
83 |
84 | const method = "addNodeAdmin"
85 |
86 | e.InvokeFail(t, subnet.ErrInvalidSubnetID, method, []byte{0, 0, 0, 0}, admPub)
87 | e.InvokeFail(t, subnet.ErrInvalidAdmin, method, id, admPub[1:])
88 | e.InvokeFail(t, subnet.ErrNotExist, method, []byte{0, 0, 0, 0, 0}, admPub)
89 |
90 | cAdm := e.WithSigners(adm)
91 | cAdm.InvokeFail(t, common.ErrOwnerWitnessFailed, method, id, admPub)
92 |
93 | cOwner := e.WithSigners(owner)
94 | cOwner.Invoke(t, stackitem.Null{}, method, id, admPub)
95 |
96 | cOwner.Invoke(t, stackitem.Null{}, method, id, admPub)
97 | }
98 |
99 | func TestSubnet_RemoveNodeAdmin(t *testing.T) {
100 | e := newSubnetInvoker(t)
101 |
102 | id, owner := createSubnet(t, e)
103 |
104 | adm := e.NewAccount(t)
105 | admPub, ok := vm.ParseSignatureContract(adm.Script())
106 | require.True(t, ok)
107 |
108 | const method = "removeNodeAdmin"
109 |
110 | e.InvokeFail(t, subnet.ErrInvalidSubnetID, method, []byte{0, 0, 0, 0}, admPub)
111 | e.InvokeFail(t, subnet.ErrInvalidAdmin, method, id, admPub[1:])
112 | e.InvokeFail(t, subnet.ErrNotExist, method, []byte{0, 0, 0, 0, 0}, admPub)
113 |
114 | cAdm := e.WithSigners(adm)
115 | cAdm.InvokeFail(t, common.ErrOwnerWitnessFailed, method, id, admPub)
116 |
117 | cOwner := e.WithSigners(owner)
118 |
119 | cOwner.Invoke(t, stackitem.Null{}, method, id, admPub)
120 | cOwner.Invoke(t, stackitem.Null{}, "addNodeAdmin", id, admPub)
121 | cOwner.Invoke(t, stackitem.Null{}, method, id, admPub)
122 | cOwner.Invoke(t, stackitem.Null{}, method, id, admPub)
123 | }
124 |
125 | func TestSubnet_AddNode(t *testing.T) {
126 | e := newSubnetInvoker(t)
127 |
128 | id, owner := createSubnet(t, e)
129 |
130 | node := e.NewAccount(t)
131 | nodePub, ok := vm.ParseSignatureContract(node.Script())
132 | require.True(t, ok)
133 |
134 | const method = "addNode"
135 |
136 | cOwn := e.WithSigners(owner)
137 | cOwn.InvokeFail(t, subnet.ErrInvalidSubnetID, method, []byte{0, 0, 0, 0}, nodePub)
138 | cOwn.InvokeFail(t, subnet.ErrInvalidNode, method, id, nodePub[1:])
139 | cOwn.InvokeFail(t, subnet.ErrNotExist, method, []byte{0, 0, 0, 0, 0}, nodePub)
140 |
141 | cOwn.Invoke(t, stackitem.Null{}, method, id, nodePub)
142 | cOwn.Invoke(t, stackitem.Null{}, method, id, nodePub)
143 | }
144 |
145 | func TestSubnet_RemoveNode(t *testing.T) {
146 | e := newSubnetInvoker(t)
147 |
148 | id, owner := createSubnet(t, e)
149 |
150 | node := e.NewAccount(t)
151 | nodePub, ok := vm.ParseSignatureContract(node.Script())
152 | require.True(t, ok)
153 |
154 | adm := e.NewAccount(t)
155 | admPub, ok := vm.ParseSignatureContract(adm.Script())
156 | require.True(t, ok)
157 |
158 | const method = "removeNode"
159 |
160 | cOwn := e.WithSigners(owner)
161 | cOwn.InvokeFail(t, subnet.ErrInvalidSubnetID, method, []byte{0, 0, 0, 0}, nodePub)
162 | cOwn.InvokeFail(t, subnet.ErrInvalidNode, method, id, nodePub[1:])
163 | cOwn.InvokeFail(t, subnet.ErrNotExist, method, []byte{0, 0, 0, 0, 0}, nodePub)
164 | cOwn.Invoke(t, stackitem.Null{}, method, id, nodePub)
165 |
166 | cOwn.Invoke(t, stackitem.Null{}, "addNode", id, nodePub)
167 | cOwn.Invoke(t, stackitem.Null{}, method, id, nodePub)
168 |
169 | cAdm := cOwn.WithSigners(adm)
170 |
171 | cOwn.Invoke(t, stackitem.Null{}, "addNodeAdmin", id, admPub)
172 | cAdm.Invoke(t, stackitem.Null{}, method, id, nodePub)
173 | }
174 |
175 | func TestSubnet_NodeAllowed(t *testing.T) {
176 | e := newSubnetInvoker(t)
177 |
178 | id, owner := createSubnet(t, e)
179 |
180 | node := e.NewAccount(t)
181 | nodePub, ok := vm.ParseSignatureContract(node.Script())
182 | require.True(t, ok)
183 |
184 | const method = "nodeAllowed"
185 |
186 | cOwn := e.WithSigners(owner)
187 | cOwn.InvokeFail(t, subnet.ErrInvalidSubnetID, method, []byte{0, 0, 0, 0}, nodePub)
188 | cOwn.InvokeFail(t, subnet.ErrInvalidNode, method, id, nodePub[1:])
189 | cOwn.InvokeFail(t, subnet.ErrNotExist, method, []byte{0, 0, 0, 0, 0}, nodePub)
190 | cOwn.Invoke(t, stackitem.NewBool(false), method, id, nodePub)
191 |
192 | cOwn.Invoke(t, stackitem.Null{}, "addNode", id, nodePub)
193 | cOwn.Invoke(t, stackitem.NewBool(true), method, id, nodePub)
194 | }
195 |
196 | func TestSubnet_AddClientAdmin(t *testing.T) {
197 | e := newSubnetInvoker(t)
198 |
199 | id, owner := createSubnet(t, e)
200 |
201 | adm := e.NewAccount(t)
202 | admPub, ok := vm.ParseSignatureContract(adm.Script())
203 | require.True(t, ok)
204 |
205 | const method = "addClientAdmin"
206 |
207 | groupId := randomBytes(5)
208 |
209 | cOwn := e.WithSigners(owner)
210 | cOwn.InvokeFail(t, subnet.ErrInvalidSubnetID, method, []byte{0, 0, 0, 0}, groupId, admPub)
211 | cOwn.InvokeFail(t, subnet.ErrInvalidAdmin, method, id, groupId, admPub[1:])
212 | cOwn.InvokeFail(t, subnet.ErrNotExist, method, []byte{0, 0, 0, 0, 0}, groupId, admPub)
213 | cOwn.Invoke(t, stackitem.Null{}, method, id, groupId, admPub)
214 | cOwn.Invoke(t, stackitem.Null{}, method, id, groupId, admPub)
215 | }
216 |
217 | func TestSubnet_RemoveClientAdmin(t *testing.T) {
218 | e := newSubnetInvoker(t)
219 |
220 | id, owner := createSubnet(t, e)
221 |
222 | adm := e.NewAccount(t)
223 | admPub, ok := vm.ParseSignatureContract(adm.Script())
224 | require.True(t, ok)
225 |
226 | const method = "removeClientAdmin"
227 |
228 | groupId := randomBytes(5)
229 |
230 | cOwn := e.WithSigners(owner)
231 | cOwn.InvokeFail(t, subnet.ErrInvalidSubnetID, method, []byte{0, 0, 0, 0}, groupId, admPub)
232 | cOwn.InvokeFail(t, subnet.ErrInvalidAdmin, method, id, groupId, admPub[1:])
233 | cOwn.InvokeFail(t, subnet.ErrNotExist, method, []byte{0, 0, 0, 0, 0}, groupId, admPub)
234 | cOwn.Invoke(t, stackitem.Null{}, method, id, groupId, admPub)
235 | cOwn.Invoke(t, stackitem.Null{}, "addClientAdmin", id, groupId, admPub)
236 | cOwn.Invoke(t, stackitem.Null{}, method, id, groupId, admPub)
237 | }
238 |
239 | func TestSubnet_AddUser(t *testing.T) {
240 | e := newSubnetInvoker(t)
241 |
242 | id, owner := createSubnet(t, e)
243 |
244 | adm := e.NewAccount(t)
245 | admPub, ok := vm.ParseSignatureContract(adm.Script())
246 | require.True(t, ok)
247 |
248 | user := randomBytes(27)
249 |
250 | groupId := randomBytes(5)
251 |
252 | const method = "addUser"
253 |
254 | cOwn := e.WithSigners(owner)
255 | cOwn.InvokeFail(t, subnet.ErrInvalidSubnetID, method, []byte{0, 0, 0, 0}, groupId, user)
256 | cOwn.InvokeFail(t, subnet.ErrNotExist, method, []byte{0, 0, 0, 0, 0}, groupId, user)
257 |
258 | cOwn.Invoke(t, stackitem.Null{}, "addClientAdmin", id, groupId, admPub)
259 |
260 | cAdm := e.WithSigners(adm)
261 | cAdm.Invoke(t, stackitem.Null{}, method, id, groupId, user)
262 | cOwn.Invoke(t, stackitem.Null{}, method, id, groupId, user)
263 | }
264 |
265 | func TestSubnet_RemoveUser(t *testing.T) {
266 | e := newSubnetInvoker(t)
267 |
268 | id, owner := createSubnet(t, e)
269 |
270 | groupId := randomBytes(5)
271 | user := randomBytes(27)
272 |
273 | adm := e.NewAccount(t)
274 | admPub, ok := vm.ParseSignatureContract(adm.Script())
275 | require.True(t, ok)
276 |
277 | const method = "removeUser"
278 |
279 | cOwn := e.WithSigners(owner)
280 | cOwn.InvokeFail(t, subnet.ErrInvalidSubnetID, method, []byte{0, 0, 0, 0}, groupId, user)
281 | cOwn.InvokeFail(t, subnet.ErrNotExist, method, []byte{0, 0, 0, 0, 0}, groupId, user)
282 |
283 | cOwn.Invoke(t, stackitem.Null{}, method, id, groupId, user)
284 | cOwn.Invoke(t, stackitem.Null{}, "addUser", id, groupId, user)
285 | cOwn.Invoke(t, stackitem.Null{}, method, id, groupId, user)
286 |
287 | cAdm := cOwn.WithSigners(adm)
288 |
289 | cOwn.Invoke(t, stackitem.Null{}, "addClientAdmin", id, groupId, admPub)
290 | cAdm.Invoke(t, stackitem.Null{}, method, id, groupId, user)
291 | }
292 |
293 | func TestSubnet_UserAllowed(t *testing.T) {
294 | e := newSubnetInvoker(t)
295 |
296 | id, owner := createSubnet(t, e)
297 |
298 | groupId := randomBytes(5)
299 |
300 | user := randomBytes(27)
301 |
302 | const method = "userAllowed"
303 |
304 | cOwn := e.WithSigners(owner)
305 | cOwn.InvokeFail(t, subnet.ErrNotExist, method, []byte{0, 0, 0, 0, 0}, user)
306 |
307 | cOwn.Invoke(t, stackitem.NewBool(false), method, id, user)
308 | cOwn.Invoke(t, stackitem.Null{}, "addUser", id, groupId, user)
309 | cOwn.Invoke(t, stackitem.NewBool(true), method, id, user)
310 | }
311 |
312 | func createSubnet(t *testing.T, e *neotest.ContractInvoker) (id []byte, owner neotest.Signer) {
313 | var (
314 | ok bool
315 | pub []byte
316 | )
317 |
318 | owner = e.NewAccount(t)
319 | pub, ok = vm.ParseSignatureContract(owner.Script())
320 | require.True(t, ok)
321 |
322 | id = make([]byte, 5)
323 | binary.LittleEndian.PutUint32(id, 123)
324 | info := randomBytes(10)
325 |
326 | cBoth := e.WithSigners(e.Committee, owner)
327 | cBoth.Invoke(t, stackitem.Null{}, "put", id, pub, info)
328 |
329 | return
330 | }
331 |
--------------------------------------------------------------------------------
/tests/util.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/nspcc-dev/neo-go/pkg/core/interop/storage"
7 | "github.com/nspcc-dev/neo-go/pkg/neotest"
8 | "github.com/nspcc-dev/neo-go/pkg/neotest/chain"
9 | "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
10 | )
11 |
12 | func iteratorToArray(iter *storage.Iterator) []stackitem.Item {
13 | stackItems := make([]stackitem.Item, 0)
14 | for iter.Next() {
15 | stackItems = append(stackItems, iter.Value())
16 | }
17 | return stackItems
18 | }
19 |
20 | func newExecutor(t *testing.T) *neotest.Executor {
21 | bc, acc := chain.NewSingle(t)
22 | return neotest.NewExecutor(t, bc, acc, acc)
23 | }
24 |
--------------------------------------------------------------------------------
/tests/version_test.go:
--------------------------------------------------------------------------------
1 | package tests
2 |
3 | import (
4 | "os"
5 | "strconv"
6 | "strings"
7 | "testing"
8 |
9 | "github.com/TrueCloudLab/frostfs-contract/common"
10 | "github.com/stretchr/testify/require"
11 | )
12 |
13 | func TestVersion(t *testing.T) {
14 | data, err := os.ReadFile("../VERSION")
15 | require.NoError(t, err)
16 |
17 | v := strings.TrimPrefix(string(data), "v")
18 | parts := strings.Split(strings.TrimSpace(v), ".")
19 | require.Len(t, parts, 3)
20 |
21 | var ver [3]int
22 | for i := range parts {
23 | ver[i], err = strconv.Atoi(parts[i])
24 | require.NoError(t, err)
25 | }
26 |
27 | require.Equal(t, common.Version, ver[0]*1_000_000+ver[1]*1_000+ver[2],
28 | "version from common package is different from the one in VERSION file")
29 | }
30 |
--------------------------------------------------------------------------------