├── .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 | 5 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 28 | 32 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | 50 | 56 | 59 | 60 | 67 | 68 | 69 | 70 | 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 | FrostFS 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 | --------------------------------------------------------------------------------