Compare commits
1 commit
master
...
carpawell/
Author | SHA1 | Date | |
---|---|---|---|
|
0c0a433b84 |
1470 changed files with 54064 additions and 85591 deletions
|
@ -1,4 +1,4 @@
|
||||||
FROM golang:1.23 AS builder
|
FROM golang:1.18 as builder
|
||||||
ARG BUILD=now
|
ARG BUILD=now
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ARG REPO=repository
|
ARG REPO=repository
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
FROM golang:1.23
|
|
||||||
|
|
||||||
WORKDIR /tmp
|
|
||||||
|
|
||||||
# Install apt packages
|
|
||||||
RUN apt-get update && apt-get install --no-install-recommends -y \
|
|
||||||
pip \
|
|
||||||
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Dash → Bash
|
|
||||||
RUN echo "dash dash/sh boolean false" | debconf-set-selections
|
|
||||||
RUN DEBIAN_FRONTEND=noninteractive dpkg-reconfigure dash
|
|
||||||
|
|
||||||
RUN useradd -u 1234 -d /home/ci -m ci
|
|
||||||
USER ci
|
|
||||||
|
|
||||||
ENV PATH="$PATH:/home/ci/.local/bin"
|
|
||||||
|
|
||||||
COPY .pre-commit-config.yaml .
|
|
||||||
|
|
||||||
RUN pip install "pre-commit==3.1.1" \
|
|
||||||
&& git init . \
|
|
||||||
&& pre-commit install-hooks \
|
|
||||||
&& rm -rf /tmp/*
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM golang:1.23 AS builder
|
FROM golang:1.18 as builder
|
||||||
ARG BUILD=now
|
ARG BUILD=now
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ARG REPO=repository
|
ARG REPO=repository
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM golang:1.23 AS builder
|
FROM golang:1.18 as builder
|
||||||
ARG BUILD=now
|
ARG BUILD=now
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ARG REPO=repository
|
ARG REPO=repository
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM golang:1.23 AS builder
|
FROM golang:1.18 as builder
|
||||||
ARG BUILD=now
|
ARG BUILD=now
|
||||||
ARG VERSION=dev
|
ARG VERSION=dev
|
||||||
ARG REPO=repository
|
ARG REPO=repository
|
||||||
|
|
19
.docker/Dockerfile.storage-testnet
Normal file
19
.docker/Dockerfile.storage-testnet
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
FROM golang:1.18 as builder
|
||||||
|
ARG BUILD=now
|
||||||
|
ARG VERSION=dev
|
||||||
|
ARG REPO=repository
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . /src
|
||||||
|
|
||||||
|
RUN make bin/frostfs-node
|
||||||
|
|
||||||
|
# Executable image
|
||||||
|
FROM alpine AS frostfs-node
|
||||||
|
RUN apk add --no-cache bash
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
COPY --from=builder /src/bin/frostfs-node /bin/frostfs-node
|
||||||
|
COPY --from=builder /src/config/testnet/config.yml /config.yml
|
||||||
|
|
||||||
|
CMD ["frostfs-node", "--config", "/config.yml"]
|
|
@ -5,5 +5,4 @@ docker-compose.yml
|
||||||
Dockerfile
|
Dockerfile
|
||||||
temp
|
temp
|
||||||
.dockerignore
|
.dockerignore
|
||||||
docker
|
docker
|
||||||
.cache
|
|
|
@ -1,41 +0,0 @@
|
||||||
name: Build
|
|
||||||
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
name: Build Components
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
go_versions: [ '1.22', '1.23' ]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
# Allows to fetch all history for all branches and tags.
|
|
||||||
# Need this for proper versioning.
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '${{ matrix.go_versions }}'
|
|
||||||
|
|
||||||
- name: Build CLI
|
|
||||||
run: make bin/frostfs-cli
|
|
||||||
- run: bin/frostfs-cli --version
|
|
||||||
|
|
||||||
- name: Build NODE
|
|
||||||
run: make bin/frostfs-node
|
|
||||||
|
|
||||||
- name: Build IR
|
|
||||||
run: make bin/frostfs-ir
|
|
||||||
|
|
||||||
- name: Build ADM
|
|
||||||
run: make bin/frostfs-adm
|
|
||||||
- run: bin/frostfs-adm --version
|
|
||||||
|
|
||||||
- name: Build LENS
|
|
||||||
run: make bin/frostfs-lens
|
|
||||||
- run: bin/frostfs-lens --version
|
|
|
@ -1,21 +0,0 @@
|
||||||
name: DCO action
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
dco:
|
|
||||||
name: DCO
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '1.22'
|
|
||||||
|
|
||||||
- name: Run commit format checker
|
|
||||||
uses: https://git.frostfs.info/TrueCloudLab/dco-go@v3
|
|
||||||
with:
|
|
||||||
from: 'origin/${{ github.event.pull_request.base.ref }}'
|
|
|
@ -1,25 +0,0 @@
|
||||||
name: Pre-commit hooks
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
precommit:
|
|
||||||
name: Pre-commit
|
|
||||||
env:
|
|
||||||
# Skip pre-commit hooks which are executed by other actions.
|
|
||||||
SKIP: make-lint,go-staticcheck-repo-mod,go-unit-tests,gofumpt
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
# If we use actions/setup-python from either Github or Gitea,
|
|
||||||
# the line above fails with a cryptic error about not being able to find python.
|
|
||||||
# So install everything manually.
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: 1.23
|
|
||||||
- name: Set up Python
|
|
||||||
run: |
|
|
||||||
apt update
|
|
||||||
apt install -y pre-commit
|
|
||||||
- name: Run pre-commit
|
|
||||||
run: pre-commit run --color=always --hook-stage manual --all-files
|
|
|
@ -1,111 +0,0 @@
|
||||||
name: Tests and linters
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
name: Lint
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '1.23'
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Install linters
|
|
||||||
run: make lint-install
|
|
||||||
|
|
||||||
- name: Run linters
|
|
||||||
run: make lint
|
|
||||||
|
|
||||||
tests:
|
|
||||||
name: Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
go_versions: [ '1.22', '1.23' ]
|
|
||||||
fail-fast: false
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '${{ matrix.go_versions }}'
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: make test
|
|
||||||
|
|
||||||
tests-race:
|
|
||||||
name: Tests with -race
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '1.22'
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: go test ./... -count=1 -race
|
|
||||||
|
|
||||||
staticcheck:
|
|
||||||
name: Staticcheck
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '1.23'
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Install staticcheck
|
|
||||||
run: make staticcheck-install
|
|
||||||
|
|
||||||
- name: Run staticcheck
|
|
||||||
run: make staticcheck-run
|
|
||||||
|
|
||||||
gopls:
|
|
||||||
name: gopls check
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '1.22'
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Install gopls
|
|
||||||
run: make gopls-install
|
|
||||||
|
|
||||||
- name: Run gopls
|
|
||||||
run: make gopls-run
|
|
||||||
|
|
||||||
fumpt:
|
|
||||||
name: Run gofumpt
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '1.23'
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Install gofumpt
|
|
||||||
run: make fumpt-install
|
|
||||||
|
|
||||||
- name: Run gofumpt
|
|
||||||
run: |
|
|
||||||
make fumpt
|
|
||||||
git diff --exit-code --quiet
|
|
|
@ -1,22 +0,0 @@
|
||||||
name: Vulncheck
|
|
||||||
on: [pull_request]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
vulncheck:
|
|
||||||
name: Vulncheck
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: '1.23'
|
|
||||||
|
|
||||||
- name: Install govulncheck
|
|
||||||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
|
||||||
|
|
||||||
- name: Run govulncheck
|
|
||||||
run: govulncheck ./...
|
|
1
.gitattributes
vendored
1
.gitattributes
vendored
|
@ -1,3 +1,2 @@
|
||||||
/**/*.pb.go -diff -merge
|
/**/*.pb.go -diff -merge
|
||||||
/**/*.pb.go linguist-generated=true
|
/**/*.pb.go linguist-generated=true
|
||||||
/go.sum -diff
|
|
||||||
|
|
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
* @carpawell @fyrchik @acid-ant
|
|
@ -2,7 +2,7 @@
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
title: ''
|
title: ''
|
||||||
labels: community, triage, bug
|
labels: community, triage
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@ -18,11 +18,8 @@ assignees: ''
|
||||||
If suggesting a change/improvement, explain the difference from current behavior -->
|
If suggesting a change/improvement, explain the difference from current behavior -->
|
||||||
|
|
||||||
## Possible Solution
|
## Possible Solution
|
||||||
<!-- Not obligatory
|
<!-- Not obligatory, but suggest a fix/reason for the bug,
|
||||||
If no reason/fix/additions for the bug can be suggested,
|
or ideas how to implement the addition or change -->
|
||||||
uncomment the following phrase:
|
|
||||||
|
|
||||||
No fix can be suggested by a QA engineer. Further solutions shall be up to developers. -->
|
|
||||||
|
|
||||||
## Steps to Reproduce (for bugs)
|
## Steps to Reproduce (for bugs)
|
||||||
<!-- Provide a link to a live example, or an unambiguous set of steps
|
<!-- Provide a link to a live example, or an unambiguous set of steps
|
||||||
|
@ -44,3 +41,10 @@ assignees: ''
|
||||||
* Version used:
|
* Version used:
|
||||||
* Server setup and configuration:
|
* Server setup and configuration:
|
||||||
* Operating System and version (`uname -a`):
|
* Operating System and version (`uname -a`):
|
||||||
|
|
||||||
|
## Don't forget to add labels!
|
||||||
|
- component label (`frostfs-adm`, `frostfs-storage`, ...)
|
||||||
|
- `goodfirstissue`, `helpwanted` if needed
|
||||||
|
- does this issue belong to an epic?
|
||||||
|
- priority (`P0`-`P4`) if already triaged
|
||||||
|
- quarter label (`202XQY`) if possible
|
0
.forgejo/logo.svg → .github/logo.svg
vendored
0
.forgejo/logo.svg → .github/logo.svg
vendored
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
29
.github/workflows/changelog.yml
vendored
Normal file
29
.github/workflows/changelog.yml
vendored
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
name: CHANGELOG check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- support/**
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Check for updates
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Get changed CHANGELOG
|
||||||
|
id: changelog-diff
|
||||||
|
uses: tj-actions/changed-files@v29
|
||||||
|
with:
|
||||||
|
files: CHANGELOG.md
|
||||||
|
|
||||||
|
- name: Fail if changelog not updated
|
||||||
|
if: steps.changelog-diff.outputs.any_changed == 'false'
|
||||||
|
uses: actions/github-script@v3
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
core.setFailed('CHANGELOG.md has not been updated')
|
37
.github/workflows/config-update.yml
vendored
Normal file
37
.github/workflows/config-update.yml
vendored
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
name: Configuration check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- support/**
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: config-check
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Get changed config-related files
|
||||||
|
id: config-diff
|
||||||
|
uses: tj-actions/changed-files@v29
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
config/**
|
||||||
|
cmd/neofs-node/config/**
|
||||||
|
|
||||||
|
- name: Get changed doc files
|
||||||
|
id: docs-diff
|
||||||
|
uses: tj-actions/changed-files@v29
|
||||||
|
with:
|
||||||
|
files: docs/**
|
||||||
|
|
||||||
|
- name: Fail if config files are changed but the documentation is not updated
|
||||||
|
if: steps.config-diff.outputs.any_changed == 'true' && steps.docs-diff.outputs.any_changed == 'false'
|
||||||
|
uses: actions/github-script@v3
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
core.setFailed('Documentation has not been updated')
|
22
.github/workflows/dco.yml
vendored
Normal file
22
.github/workflows/dco.yml
vendored
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
name: DCO check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- support/**
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
commits_check_job:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Commits Check
|
||||||
|
steps:
|
||||||
|
- name: Get PR Commits
|
||||||
|
id: 'get-pr-commits'
|
||||||
|
uses: tim-actions/get-pr-commits@master
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: DCO Check
|
||||||
|
uses: tim-actions/dco@master
|
||||||
|
with:
|
||||||
|
commits: ${{ steps.get-pr-commits.outputs.commits }}
|
60
.github/workflows/go.yml
vendored
Normal file
60
.github/workflows/go.yml
vendored
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
name: frostfs-node tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- support/**
|
||||||
|
paths-ignore:
|
||||||
|
- '*.md'
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- support/**
|
||||||
|
paths-ignore:
|
||||||
|
- '*.md'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
go: [ '1.18.x', '1.19.x' ]
|
||||||
|
steps:
|
||||||
|
- name: Setup go
|
||||||
|
uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: ${{ matrix.go }}
|
||||||
|
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Cache go mod
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ matrix.go }}-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-${{ matrix.go }}-
|
||||||
|
|
||||||
|
- name: Run go test
|
||||||
|
run: go test -coverprofile=coverage.txt -covermode=atomic ./...
|
||||||
|
|
||||||
|
- name: Codecov
|
||||||
|
env:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
run: bash <(curl -s https://codecov.io/bash)
|
||||||
|
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-20.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/setup-go@v3
|
||||||
|
with:
|
||||||
|
go-version: 1.19
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v3
|
||||||
|
with:
|
||||||
|
version: v1.50.0
|
||||||
|
args: --timeout=5m
|
||||||
|
only-new-issues: true
|
20
.gitignore
vendored
20
.gitignore
vendored
|
@ -30,22 +30,4 @@ testfile
|
||||||
.neofs-cli.yml
|
.neofs-cli.yml
|
||||||
|
|
||||||
# debhelpers
|
# debhelpers
|
||||||
debian/*debhelper*
|
**/.debhelper
|
||||||
|
|
||||||
# logfiles
|
|
||||||
debian/*.log
|
|
||||||
|
|
||||||
# .substvars
|
|
||||||
debian/*.substvars
|
|
||||||
|
|
||||||
# .bash-completion
|
|
||||||
debian/*.bash-completion
|
|
||||||
|
|
||||||
# Install folders and files
|
|
||||||
debian/frostfs-cli/
|
|
||||||
debian/frostfs-ir/
|
|
||||||
debian/files
|
|
||||||
debian/frostfs-storage/
|
|
||||||
debian/changelog
|
|
||||||
man/
|
|
||||||
debs/
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
# options for analysis running
|
# options for analysis running
|
||||||
run:
|
run:
|
||||||
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
||||||
timeout: 20m
|
timeout: 5m
|
||||||
|
|
||||||
# include test files or not, default is true
|
# include test files or not, default is true
|
||||||
tests: false
|
tests: false
|
||||||
|
@ -12,8 +12,7 @@ run:
|
||||||
# output configuration options
|
# output configuration options
|
||||||
output:
|
output:
|
||||||
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
|
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
|
||||||
formats:
|
format: tab
|
||||||
- format: tab
|
|
||||||
|
|
||||||
# all available settings of specific linters
|
# all available settings of specific linters
|
||||||
linters-settings:
|
linters-settings:
|
||||||
|
@ -25,32 +24,6 @@ linters-settings:
|
||||||
govet:
|
govet:
|
||||||
# report about shadowed variables
|
# report about shadowed variables
|
||||||
check-shadowing: false
|
check-shadowing: false
|
||||||
staticcheck:
|
|
||||||
checks: ["all", "-SA1019"] # TODO Enable SA1019 after deprecated warning are fixed.
|
|
||||||
funlen:
|
|
||||||
lines: 80 # default 60
|
|
||||||
statements: 60 # default 40
|
|
||||||
gocognit:
|
|
||||||
min-complexity: 40 # default 30
|
|
||||||
importas:
|
|
||||||
no-unaliased: true
|
|
||||||
no-extra-aliases: false
|
|
||||||
alias:
|
|
||||||
pkg: git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object
|
|
||||||
alias: objectSDK
|
|
||||||
unused:
|
|
||||||
field-writes-are-uses: false
|
|
||||||
exported-fields-are-used: false
|
|
||||||
local-variables-are-used: false
|
|
||||||
custom:
|
|
||||||
truecloudlab-linters:
|
|
||||||
path: bin/linters/external_linters.so
|
|
||||||
original-url: git.frostfs.info/TrueCloudLab/linters.git
|
|
||||||
settings:
|
|
||||||
noliteral:
|
|
||||||
target-methods : ["reportFlushError", "reportError"]
|
|
||||||
disable-packages: ["codes", "err", "res","exec"]
|
|
||||||
constants-package: "git.frostfs.info/TrueCloudLab/frostfs-node/internal/logs"
|
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
enable:
|
enable:
|
||||||
|
@ -71,23 +44,13 @@ linters:
|
||||||
- bidichk
|
- bidichk
|
||||||
- durationcheck
|
- durationcheck
|
||||||
- exhaustive
|
- exhaustive
|
||||||
- copyloopvar
|
- exportloopref
|
||||||
- gofmt
|
- gofmt
|
||||||
- goimports
|
- goimports
|
||||||
- misspell
|
- misspell
|
||||||
- predeclared
|
- predeclared
|
||||||
- reassign
|
- reassign
|
||||||
- whitespace
|
- whitespace
|
||||||
- containedctx
|
|
||||||
- funlen
|
|
||||||
- gocognit
|
|
||||||
- contextcheck
|
|
||||||
- importas
|
|
||||||
- truecloudlab-linters
|
|
||||||
- perfsprint
|
|
||||||
- testifylint
|
|
||||||
- protogetter
|
|
||||||
- intrange
|
|
||||||
- tenv
|
|
||||||
disable-all: true
|
disable-all: true
|
||||||
fast: false
|
fast: false
|
||||||
|
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
ci:
|
|
||||||
autofix_prs: false
|
|
||||||
|
|
||||||
repos:
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v4.5.0
|
|
||||||
hooks:
|
|
||||||
- id: check-added-large-files
|
|
||||||
- id: check-case-conflict
|
|
||||||
- id: check-executables-have-shebangs
|
|
||||||
- id: check-shebang-scripts-are-executable
|
|
||||||
- id: check-merge-conflict
|
|
||||||
- id: check-json
|
|
||||||
- id: check-xml
|
|
||||||
- id: check-yaml
|
|
||||||
- id: trailing-whitespace
|
|
||||||
args: [--markdown-linebreak-ext=md]
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
exclude: "(.key|.svg)$"
|
|
||||||
|
|
||||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
|
||||||
rev: v0.9.0.6
|
|
||||||
hooks:
|
|
||||||
- id: shellcheck
|
|
||||||
|
|
||||||
- repo: local
|
|
||||||
hooks:
|
|
||||||
- id: make-lint
|
|
||||||
name: Run Make Lint
|
|
||||||
entry: make lint
|
|
||||||
language: system
|
|
||||||
pass_filenames: false
|
|
||||||
|
|
||||||
- repo: local
|
|
||||||
hooks:
|
|
||||||
- id: go-unit-tests
|
|
||||||
name: go unit tests
|
|
||||||
entry: make test GOFLAGS=''
|
|
||||||
pass_filenames: false
|
|
||||||
types: [go]
|
|
||||||
language: system
|
|
||||||
|
|
||||||
- repo: local
|
|
||||||
hooks:
|
|
||||||
- id: gofumpt
|
|
||||||
name: gofumpt
|
|
||||||
entry: make fumpt
|
|
||||||
pass_filenames: false
|
|
||||||
types: [go]
|
|
||||||
language: system
|
|
||||||
|
|
||||||
- repo: https://github.com/TekWizely/pre-commit-golang
|
|
||||||
rev: v1.0.0-rc.1
|
|
||||||
hooks:
|
|
||||||
- id: go-staticcheck-repo-mod
|
|
||||||
- id: go-mod-tidy
|
|
1760
CHANGELOG.md
1760
CHANGELOG.md
File diff suppressed because it is too large
Load diff
|
@ -3,8 +3,8 @@
|
||||||
First, thank you for contributing! We love and encourage pull requests from
|
First, thank you for contributing! We love and encourage pull requests from
|
||||||
everyone. Please follow the guidelines:
|
everyone. Please follow the guidelines:
|
||||||
|
|
||||||
- Check the open [issues](https://git.frostfs.info/TrueCloudLab/frostfs-node/issues) and
|
- Check the open [issues](https://github.com/TrueCloudLab/frostfs-node/issues) and
|
||||||
[pull requests](https://git.frostfs.info/TrueCloudLab/frostfs-node/pulls) for existing
|
[pull requests](https://github.com/TrueCloudLab/frostfs-node/pulls) for existing
|
||||||
discussions.
|
discussions.
|
||||||
|
|
||||||
- Open an issue first, to discuss a new feature or enhancement.
|
- Open an issue first, to discuss a new feature or enhancement.
|
||||||
|
@ -27,19 +27,19 @@ Start by forking the `frostfs-node` repository, make changes in a branch and the
|
||||||
send a pull request. We encourage pull requests to discuss code changes. Here
|
send a pull request. We encourage pull requests to discuss code changes. Here
|
||||||
are the steps in details:
|
are the steps in details:
|
||||||
|
|
||||||
### Set up your Forgejo repository
|
### Set up your GitHub Repository
|
||||||
Fork [FrostFS node upstream](https://git.frostfs.info/TrueCloudLab/frostfs-node) source
|
Fork [FrostFS node upstream](https://github.com/TrueCloudLab/frostfs-node/fork) source
|
||||||
repository to your own personal repository. Copy the URL of your fork (you will
|
repository to your own personal repository. Copy the URL of your fork (you will
|
||||||
need it for the `git clone` command below).
|
need it for the `git clone` command below).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ git clone https://git.frostfs.info/TrueCloudLab/frostfs-node
|
$ git clone https://github.com/TrueCloudLab/frostfs-node
|
||||||
```
|
```
|
||||||
|
|
||||||
### Set up git remote as ``upstream``
|
### Set up git remote as ``upstream``
|
||||||
```sh
|
```sh
|
||||||
$ cd frostfs-node
|
$ cd neofs-node
|
||||||
$ git remote add upstream https://git.frostfs.info/TrueCloudLab/frostfs-node
|
$ git remote add upstream https://github.com/TrueCloudLab/frostfs-node
|
||||||
$ git fetch upstream
|
$ git fetch upstream
|
||||||
$ git merge upstream/master
|
$ git merge upstream/master
|
||||||
...
|
...
|
||||||
|
@ -58,7 +58,7 @@ $ git checkout -b feature/123-something_awesome
|
||||||
After your code changes, make sure
|
After your code changes, make sure
|
||||||
|
|
||||||
- To add test cases for the new code.
|
- To add test cases for the new code.
|
||||||
- To run `make lint` and `make staticcheck-run`
|
- To run `make lint`
|
||||||
- To squash your commits into a single commit or a series of logically separated
|
- To squash your commits into a single commit or a series of logically separated
|
||||||
commits run `git rebase -i`. It's okay to force update your pull request.
|
commits run `git rebase -i`. It's okay to force update your pull request.
|
||||||
- To run `make test` and `make all` completes.
|
- To run `make test` and `make all` completes.
|
||||||
|
@ -89,8 +89,8 @@ $ git push origin feature/123-something_awesome
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create a Pull Request
|
### Create a Pull Request
|
||||||
Pull requests can be created via Forgejo. Refer to [this
|
Pull requests can be created via GitHub. Refer to [this
|
||||||
document](https://docs.codeberg.org/collaborating/pull-requests-and-git-flow/) for
|
document](https://help.github.com/articles/creating-a-pull-request/) for
|
||||||
detailed steps on how to create a pull request. After a Pull Request gets peer
|
detailed steps on how to create a pull request. After a Pull Request gets peer
|
||||||
reviewed and approved, it will be merged.
|
reviewed and approved, it will be merged.
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ In alphabetical order:
|
||||||
- Alexey Vanin
|
- Alexey Vanin
|
||||||
- Anastasia Prasolova
|
- Anastasia Prasolova
|
||||||
- Anatoly Bogatyrev
|
- Anatoly Bogatyrev
|
||||||
- Evgeny Kulikov
|
- Evgeny Kulikov
|
||||||
- Evgeny Stratonikov
|
- Evgeny Stratonikov
|
||||||
- Leonard Liubich
|
- Leonard Liubich
|
||||||
- Sergei Liubich
|
- Sergei Liubich
|
||||||
|
|
208
Makefile
Executable file → Normal file
208
Makefile
Executable file → Normal file
|
@ -4,19 +4,11 @@ SHELL = bash
|
||||||
REPO ?= $(shell go list -m)
|
REPO ?= $(shell go list -m)
|
||||||
VERSION ?= $(shell git describe --tags --dirty --match "v*" --always --abbrev=8 2>/dev/null || cat VERSION 2>/dev/null || echo "develop")
|
VERSION ?= $(shell git describe --tags --dirty --match "v*" --always --abbrev=8 2>/dev/null || cat VERSION 2>/dev/null || echo "develop")
|
||||||
|
|
||||||
HUB_IMAGE ?= git.frostfs.info/truecloudlab/frostfs
|
HUB_IMAGE ?= truecloudlab/frostfs
|
||||||
HUB_TAG ?= "$(shell echo ${VERSION} | sed 's/^v//')"
|
HUB_TAG ?= "$(shell echo ${VERSION} | sed 's/^v//')"
|
||||||
|
|
||||||
GO_VERSION ?= 1.22
|
GO_VERSION ?= 1.19
|
||||||
LINT_VERSION ?= 1.62.0
|
LINT_VERSION ?= 1.50.0
|
||||||
TRUECLOUDLAB_LINT_VERSION ?= 0.0.8
|
|
||||||
PROTOC_VERSION ?= 25.0
|
|
||||||
PROTOGEN_FROSTFS_VERSION ?= $(shell go list -f '{{.Version}}' -m git.frostfs.info/TrueCloudLab/frostfs-sdk-go)
|
|
||||||
PROTOC_OS_VERSION=osx-x86_64
|
|
||||||
ifeq ($(shell uname), Linux)
|
|
||||||
PROTOC_OS_VERSION=linux-x86_64
|
|
||||||
endif
|
|
||||||
STATICCHECK_VERSION ?= 2024.1.1
|
|
||||||
ARCH = amd64
|
ARCH = amd64
|
||||||
|
|
||||||
BIN = bin
|
BIN = bin
|
||||||
|
@ -24,35 +16,17 @@ RELEASE = release
|
||||||
DIRS = $(BIN) $(RELEASE)
|
DIRS = $(BIN) $(RELEASE)
|
||||||
|
|
||||||
# List of binaries to build.
|
# List of binaries to build.
|
||||||
CMDS = $(notdir $(basename $(wildcard cmd/frostfs-*)))
|
CMDS = $(notdir $(basename $(wildcard cmd/*)))
|
||||||
BINS = $(addprefix $(BIN)/, $(CMDS))
|
BINS = $(addprefix $(BIN)/, $(CMDS))
|
||||||
|
|
||||||
OUTPUT_LINT_DIR ?= $(abspath $(BIN))/linters
|
# .deb package versioning
|
||||||
LINT_DIR = $(OUTPUT_LINT_DIR)/golangci-lint-$(LINT_VERSION)-v$(TRUECLOUDLAB_LINT_VERSION)
|
OS_RELEASE = $(shell lsb_release -cs)
|
||||||
TMP_DIR := .cache
|
PKG_VERSION ?= $(shell echo $(VERSION) | sed "s/^v//" | \
|
||||||
PROTOBUF_DIR ?= $(abspath $(BIN))/protobuf
|
sed -E "s/(.*)-(g[a-fA-F0-9]{6,8})(.*)/\1\3~\2/" | \
|
||||||
PROTOC_DIR ?= $(PROTOBUF_DIR)/protoc-v$(PROTOC_VERSION)
|
sed "s/-/~/")-${OS_RELEASE}
|
||||||
PROTOGEN_FROSTFS_DIR ?= $(PROTOBUF_DIR)/protogen-$(PROTOGEN_FROSTFS_VERSION)
|
|
||||||
STATICCHECK_DIR ?= $(abspath $(BIN))/staticcheck
|
|
||||||
STATICCHECK_VERSION_DIR ?= $(STATICCHECK_DIR)/$(STATICCHECK_VERSION)
|
|
||||||
|
|
||||||
SOURCES = $(shell find . -type f -name "*.go" -print)
|
.PHONY: help all images dep clean fmts fmt imports test lint docker/lint
|
||||||
|
prepare-release debpackage
|
||||||
GOFUMPT_VERSION ?= v0.7.0
|
|
||||||
GOFUMPT_DIR ?= $(abspath $(BIN))/gofumpt
|
|
||||||
GOFUMPT_VERSION_DIR ?= $(GOFUMPT_DIR)/$(GOFUMPT_VERSION)
|
|
||||||
|
|
||||||
GOPLS_VERSION ?= v0.15.1
|
|
||||||
GOPLS_DIR ?= $(abspath $(BIN))/gopls
|
|
||||||
GOPLS_VERSION_DIR ?= $(GOPLS_DIR)/$(GOPLS_VERSION)
|
|
||||||
GOPLS_TEMP_FILE := $(shell mktemp)
|
|
||||||
|
|
||||||
FROSTFS_CONTRACTS_PATH=$(abspath ./../frostfs-contract)
|
|
||||||
LOCODE_DB_PATH=$(abspath ./.cache/locode_db)
|
|
||||||
LOCODE_DB_VERSION=v0.4.0
|
|
||||||
|
|
||||||
.PHONY: help all images dep clean fmts fumpt imports test lint docker/lint
|
|
||||||
prepare-release pre-commit unpre-commit
|
|
||||||
|
|
||||||
# To build a specific binary, use it's name prefix with bin/ as a target
|
# To build a specific binary, use it's name prefix with bin/ as a target
|
||||||
# For example `make bin/frostfs-node` will build only storage node binary
|
# For example `make bin/frostfs-node` will build only storage node binary
|
||||||
|
@ -91,37 +65,24 @@ dep:
|
||||||
CGO_ENABLED=0 \
|
CGO_ENABLED=0 \
|
||||||
go mod tidy -v && echo OK
|
go mod tidy -v && echo OK
|
||||||
|
|
||||||
# Build export-metrics
|
|
||||||
export-metrics: dep
|
|
||||||
@printf "⇒ Build export-metrics\n"
|
|
||||||
CGO_ENABLED=0 \
|
|
||||||
go build -v -trimpath -o bin/export-metrics ./scripts/export-metrics
|
|
||||||
|
|
||||||
# Regenerate proto files:
|
# Regenerate proto files:
|
||||||
protoc:
|
protoc:
|
||||||
@if [ ! -d "$(PROTOC_DIR)" ] || [ ! -d "$(PROTOGEN_FROSTFS_DIR)" ]; then \
|
@GOPRIVATE=github.com/TrueCloudLab go mod vendor
|
||||||
make protoc-install; \
|
# Install specific version for protobuf lib
|
||||||
fi
|
@go list -f '{{.Path}}/...@{{.Version}}' -m github.com/golang/protobuf | xargs go install -v
|
||||||
@for f in `find . -type f -name '*.proto' -not -path './bin/*'`; do \
|
@GOBIN=$(abspath $(BIN)) go install -mod=mod -v github.com/TrueCloudLab/frostfs-api-go/v2/util/protogen
|
||||||
|
# Protoc generate
|
||||||
|
@for f in `find . -type f -name '*.proto' -not -path './vendor/*'`; do \
|
||||||
echo "⇒ Processing $$f "; \
|
echo "⇒ Processing $$f "; \
|
||||||
$(PROTOC_DIR)/bin/protoc \
|
protoc \
|
||||||
--proto_path=.:$(PROTOC_DIR)/include:/usr/local/include \
|
--proto_path=.:./vendor:/usr/local/include \
|
||||||
--plugin=protoc-gen-go-frostfs=$(PROTOGEN_FROSTFS_DIR)/protogen \
|
--plugin=protoc-gen-go-frostfs=$(BIN)/protogen \
|
||||||
--go-frostfs_out=. --go-frostfs_opt=paths=source_relative \
|
--go-frostfs_out=. --go-frostfs_opt=paths=source_relative \
|
||||||
|
--go_out=. --go_opt=paths=source_relative \
|
||||||
--go-grpc_opt=require_unimplemented_servers=false \
|
--go-grpc_opt=require_unimplemented_servers=false \
|
||||||
--go-grpc_out=. --go-grpc_opt=paths=source_relative $$f; \
|
--go-grpc_out=. --go-grpc_opt=paths=source_relative $$f; \
|
||||||
done
|
done
|
||||||
|
rm -rf vendor
|
||||||
# Install protoc
|
|
||||||
protoc-install:
|
|
||||||
@rm -rf $(PROTOBUF_DIR)
|
|
||||||
@mkdir $(PROTOBUF_DIR)
|
|
||||||
@echo "⇒ Installing protoc... "
|
|
||||||
@wget -q -O $(PROTOBUF_DIR)/protoc-$(PROTOC_VERSION).zip 'https://github.com/protocolbuffers/protobuf/releases/download/v$(PROTOC_VERSION)/protoc-$(PROTOC_VERSION)-$(PROTOC_OS_VERSION).zip'
|
|
||||||
@unzip -q -o $(PROTOBUF_DIR)/protoc-$(PROTOC_VERSION).zip -d $(PROTOC_DIR)
|
|
||||||
@rm $(PROTOBUF_DIR)/protoc-$(PROTOC_VERSION).zip
|
|
||||||
@echo "⇒ Instaling protogen FrostFS plugin..."
|
|
||||||
@GOBIN=$(PROTOGEN_FROSTFS_DIR) go install -mod=mod -v git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/util/protogen@$(PROTOGEN_FROSTFS_VERSION)
|
|
||||||
|
|
||||||
# Build FrostFS component's docker image
|
# Build FrostFS component's docker image
|
||||||
image-%:
|
image-%:
|
||||||
|
@ -134,7 +95,7 @@ image-%:
|
||||||
-t $(HUB_IMAGE)-$*:$(HUB_TAG) .
|
-t $(HUB_IMAGE)-$*:$(HUB_TAG) .
|
||||||
|
|
||||||
# Build all Docker images
|
# Build all Docker images
|
||||||
images: image-storage image-ir image-cli image-adm
|
images: image-storage image-ir image-cli image-adm image-storage-testnet
|
||||||
|
|
||||||
# Build dirty local Docker images
|
# Build dirty local Docker images
|
||||||
dirty-images: image-dirty-storage image-dirty-ir image-dirty-cli image-dirty-adm
|
dirty-images: image-dirty-storage image-dirty-ir image-dirty-cli image-dirty-adm
|
||||||
|
@ -150,86 +111,26 @@ docker/%:
|
||||||
|
|
||||||
|
|
||||||
# Run all code formatters
|
# Run all code formatters
|
||||||
fmts: fumpt imports
|
fmts: fmt imports
|
||||||
|
|
||||||
|
# Reformat code
|
||||||
|
fmt:
|
||||||
|
@echo "⇒ Processing gofmt check"
|
||||||
|
@gofmt -s -w cmd/ pkg/ misc/
|
||||||
|
|
||||||
# Reformat imports
|
# Reformat imports
|
||||||
imports:
|
imports:
|
||||||
@echo "⇒ Processing goimports check"
|
@echo "⇒ Processing goimports check"
|
||||||
@goimports -w cmd/ pkg/ misc/
|
@goimports -w cmd/ pkg/ misc/
|
||||||
|
|
||||||
# Install gofumpt
|
|
||||||
fumpt-install:
|
|
||||||
@rm -rf $(GOFUMPT_DIR)
|
|
||||||
@mkdir $(GOFUMPT_DIR)
|
|
||||||
@GOBIN=$(GOFUMPT_VERSION_DIR) go install mvdan.cc/gofumpt@$(GOFUMPT_VERSION)
|
|
||||||
|
|
||||||
# Run gofumpt
|
|
||||||
fumpt:
|
|
||||||
@if [ ! -d "$(GOFUMPT_VERSION_DIR)" ]; then \
|
|
||||||
make fumpt-install; \
|
|
||||||
fi
|
|
||||||
@echo "⇒ Processing gofumpt check"
|
|
||||||
$(GOFUMPT_VERSION_DIR)/gofumpt -l -w cmd/ pkg/ misc/
|
|
||||||
|
|
||||||
# Run Unit Test with go test
|
# Run Unit Test with go test
|
||||||
test: GOFLAGS ?= "-count=1"
|
|
||||||
test:
|
test:
|
||||||
@echo "⇒ Running go test"
|
@echo "⇒ Running go test"
|
||||||
@GOFLAGS="$(GOFLAGS)" go test ./...
|
@go test ./...
|
||||||
|
|
||||||
# Run pre-commit
|
|
||||||
pre-commit-run:
|
|
||||||
@pre-commit run -a --hook-stage manual
|
|
||||||
|
|
||||||
# Install linters
|
|
||||||
lint-install:
|
|
||||||
@rm -rf $(OUTPUT_LINT_DIR)
|
|
||||||
@mkdir $(OUTPUT_LINT_DIR)
|
|
||||||
@mkdir -p $(TMP_DIR)
|
|
||||||
@rm -rf $(TMP_DIR)/linters
|
|
||||||
@git -c advice.detachedHead=false clone --branch v$(TRUECLOUDLAB_LINT_VERSION) https://git.frostfs.info/TrueCloudLab/linters.git $(TMP_DIR)/linters
|
|
||||||
@@make -C $(TMP_DIR)/linters lib CGO_ENABLED=1 OUT_DIR=$(OUTPUT_LINT_DIR)
|
|
||||||
@rm -rf $(TMP_DIR)/linters
|
|
||||||
@rmdir $(TMP_DIR) 2>/dev/null || true
|
|
||||||
@CGO_ENABLED=1 GOBIN=$(LINT_DIR) go install -trimpath github.com/golangci/golangci-lint/cmd/golangci-lint@v$(LINT_VERSION)
|
|
||||||
|
|
||||||
# Run linters
|
# Run linters
|
||||||
lint:
|
lint:
|
||||||
@if [ ! -d "$(LINT_DIR)" ]; then \
|
@golangci-lint --timeout=5m run
|
||||||
make lint-install; \
|
|
||||||
fi
|
|
||||||
$(LINT_DIR)/golangci-lint run
|
|
||||||
|
|
||||||
# Install staticcheck
|
|
||||||
staticcheck-install:
|
|
||||||
@rm -rf $(STATICCHECK_DIR)
|
|
||||||
@mkdir $(STATICCHECK_DIR)
|
|
||||||
@GOBIN=$(STATICCHECK_VERSION_DIR) go install honnef.co/go/tools/cmd/staticcheck@$(STATICCHECK_VERSION)
|
|
||||||
|
|
||||||
# Run staticcheck
|
|
||||||
staticcheck-run:
|
|
||||||
@if [ ! -d "$(STATICCHECK_VERSION_DIR)" ]; then \
|
|
||||||
make staticcheck-install; \
|
|
||||||
fi
|
|
||||||
@$(STATICCHECK_VERSION_DIR)/staticcheck ./...
|
|
||||||
|
|
||||||
# Install gopls
|
|
||||||
gopls-install:
|
|
||||||
@rm -rf $(GOPLS_DIR)
|
|
||||||
@mkdir $(GOPLS_DIR)
|
|
||||||
@GOBIN=$(GOPLS_VERSION_DIR) go install golang.org/x/tools/gopls@$(GOPLS_VERSION)
|
|
||||||
|
|
||||||
# Run gopls
|
|
||||||
gopls-run:
|
|
||||||
@if [ ! -d "$(GOPLS_VERSION_DIR)" ]; then \
|
|
||||||
make gopls-install; \
|
|
||||||
fi
|
|
||||||
$(GOPLS_VERSION_DIR)/gopls check $(SOURCES) 2>&1 >$(GOPLS_TEMP_FILE)
|
|
||||||
@if [[ $$(wc -l < $(GOPLS_TEMP_FILE)) -ne 0 ]]; then \
|
|
||||||
cat $(GOPLS_TEMP_FILE); \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
rm $(GOPLS_TEMP_FILE)
|
|
||||||
|
|
||||||
# Run linters in Docker
|
# Run linters in Docker
|
||||||
docker/lint:
|
docker/lint:
|
||||||
|
@ -239,49 +140,24 @@ docker/lint:
|
||||||
--env HOME=/src \
|
--env HOME=/src \
|
||||||
golangci/golangci-lint:v$(LINT_VERSION) bash -c 'cd /src/ && make lint'
|
golangci/golangci-lint:v$(LINT_VERSION) bash -c 'cd /src/ && make lint'
|
||||||
|
|
||||||
# Activate pre-commit hooks
|
|
||||||
pre-commit:
|
|
||||||
pre-commit install -t pre-commit -t commit-msg
|
|
||||||
|
|
||||||
# Deactivate pre-commit hooks
|
|
||||||
unpre-commit:
|
|
||||||
pre-commit uninstall -t pre-commit -t commit-msg
|
|
||||||
|
|
||||||
# Print version
|
# Print version
|
||||||
version:
|
version:
|
||||||
@echo $(VERSION)
|
@echo $(VERSION)
|
||||||
|
|
||||||
# Delete built artifacts
|
|
||||||
clean:
|
clean:
|
||||||
|
rm -rf vendor
|
||||||
rm -rf .cache
|
rm -rf .cache
|
||||||
rm -rf $(BIN)
|
rm -rf $(BIN)
|
||||||
rm -rf $(RELEASE)
|
rm -rf $(RELEASE)
|
||||||
|
|
||||||
# Download locode database
|
# Package for Debian
|
||||||
locode-download:
|
debpackage:
|
||||||
mkdir -p $(TMP_DIR)
|
dch --package frostfs-node \
|
||||||
@wget -q -O ./$(TMP_DIR)/locode_db.gz 'https://git.frostfs.info/TrueCloudLab/frostfs-locode-db/releases/download/${LOCODE_DB_VERSION}/locode_db.gz'
|
--controlmaint \
|
||||||
gzip -dfk ./$(TMP_DIR)/locode_db.gz
|
--newversion $(PKG_VERSION) \
|
||||||
|
--distribution $(OS_RELEASE) \
|
||||||
|
"Please see CHANGELOG.md for code changes for $(VERSION)"
|
||||||
|
dpkg-buildpackage --no-sign -b
|
||||||
|
|
||||||
# Start dev environment
|
debclean:
|
||||||
env-up: all
|
dh clean
|
||||||
docker compose -f dev/docker-compose.yml up -d
|
|
||||||
@if [ ! -d "$(FROSTFS_CONTRACTS_PATH)" ]; then \
|
|
||||||
echo "Frostfs contracts not found"; exit 1; \
|
|
||||||
fi
|
|
||||||
${BIN}/frostfs-adm --config ./dev/adm/frostfs-adm.yml morph init --contracts ${FROSTFS_CONTRACTS_PATH}
|
|
||||||
${BIN}/frostfs-adm --config ./dev/adm/frostfs-adm.yml morph refill-gas --storage-wallet ./dev/storage/wallet01.json --gas 10.0
|
|
||||||
${BIN}/frostfs-adm --config ./dev/adm/frostfs-adm.yml morph refill-gas --storage-wallet ./dev/storage/wallet02.json --gas 10.0
|
|
||||||
${BIN}/frostfs-adm --config ./dev/adm/frostfs-adm.yml morph refill-gas --storage-wallet ./dev/storage/wallet03.json --gas 10.0
|
|
||||||
${BIN}/frostfs-adm --config ./dev/adm/frostfs-adm.yml morph refill-gas --storage-wallet ./dev/storage/wallet04.json --gas 10.0
|
|
||||||
@if [ ! -f "$(LOCODE_DB_PATH)" ]; then \
|
|
||||||
make locode-download; \
|
|
||||||
fi
|
|
||||||
mkdir -p ./$(TMP_DIR)/state
|
|
||||||
mkdir -p ./$(TMP_DIR)/storage
|
|
||||||
|
|
||||||
# Shutdown dev environment
|
|
||||||
env-down:
|
|
||||||
docker compose -f dev/docker-compose.yml down -v
|
|
||||||
rm -rf ./$(TMP_DIR)/state
|
|
||||||
rm -rf ./$(TMP_DIR)/storage
|
|
||||||
|
|
60
README.md
60
README.md
|
@ -1,5 +1,5 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./.forgejo/logo.svg" width="500px" alt="FrostFS">
|
<img src="./.github/logo.svg" width="500px" alt="FrostFS">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
@ -7,8 +7,9 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
[![Report](https://goreportcard.com/badge/git.frostfs.info/TrueCloudLab/frostfs-node)](https://goreportcard.com/report/git.frostfs.info/TrueCloudLab/frostfs-node)
|
[![Report](https://goreportcard.com/badge/github.com/TrueCloudLab/frostfs-node)](https://goreportcard.com/report/github.com/TrueCloudLab/frostfs-node)
|
||||||
![Release (latest)](https://git.frostfs.info/TrueCloudLab/frostfs-node/badges/release.svg)
|
![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/TrueCloudLab/frostfs-node?sort=semver)
|
||||||
|
![License](https://img.shields.io/github/license/TrueCloudLab/frostfs-node.svg?style=popout)
|
||||||
|
|
||||||
# Overview
|
# Overview
|
||||||
|
|
||||||
|
@ -30,10 +31,10 @@ dApps directly from
|
||||||
code level. This way dApps are not limited to on-chain storage and can
|
code level. This way dApps are not limited to on-chain storage and can
|
||||||
manipulate large amounts of data without paying a prohibitive price.
|
manipulate large amounts of data without paying a prohibitive price.
|
||||||
|
|
||||||
FrostFS has a native [gRPC API](https://git.frostfs.info/TrueCloudLab/frostfs-api) and has
|
FrostFS has a native [gRPC API](https://github.com/TrueCloudLab/frostfs-api) and has
|
||||||
protocol gateways for popular protocols such as [AWS
|
protocol gateways for popular protocols such as [AWS
|
||||||
S3](https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw),
|
S3](https://github.com/TrueCloudLab/frostfs-s3-gw),
|
||||||
[HTTP](https://git.frostfs.info/TrueCloudLab/frostfs-http-gw),
|
[HTTP](https://github.com/TrueCloudLab/frostfs-http-gw),
|
||||||
[FUSE](https://wikipedia.org/wiki/Filesystem_in_Userspace) and
|
[FUSE](https://wikipedia.org/wiki/Filesystem_in_Userspace) and
|
||||||
[sFTP](https://en.wikipedia.org/wiki/SSH_File_Transfer_Protocol) allowing
|
[sFTP](https://en.wikipedia.org/wiki/SSH_File_Transfer_Protocol) allowing
|
||||||
developers to integrate applications without rewriting their code.
|
developers to integrate applications without rewriting their code.
|
||||||
|
@ -44,11 +45,11 @@ Now, we only support GNU/Linux on amd64 CPUs with AVX/AVX2 instructions. More
|
||||||
platforms will be officially supported after release `1.0`.
|
platforms will be officially supported after release `1.0`.
|
||||||
|
|
||||||
The latest version of frostfs-node works with frostfs-contract
|
The latest version of frostfs-node works with frostfs-contract
|
||||||
[v0.19.2](https://git.frostfs.info/TrueCloudLab/frostfs-contract/releases/tag/v0.19.2).
|
[v0.16.0](https://github.com/TrueCloudLab/frostfs-contract/releases/tag/v0.16.0).
|
||||||
|
|
||||||
# Building
|
# Building
|
||||||
|
|
||||||
To make all binaries you need Go 1.22+ and `make`:
|
To make all binaries you need Go 1.18+ and `make`:
|
||||||
```
|
```
|
||||||
make all
|
make all
|
||||||
```
|
```
|
||||||
|
@ -70,50 +71,11 @@ make docker/bin/frostfs-<name> # build a specific binary
|
||||||
|
|
||||||
## Docker images
|
## Docker images
|
||||||
|
|
||||||
To make docker images suitable for use in [frostfs-dev-env](https://git.frostfs.info/TrueCloudLab/frostfs-dev-env/) use:
|
To make docker images suitable for use in [frostfs-dev-env](https://github.com/TrueCloudLab/frostfs-dev-env/) use:
|
||||||
```
|
```
|
||||||
make images
|
make images
|
||||||
```
|
```
|
||||||
|
|
||||||
# Debugging
|
|
||||||
|
|
||||||
## VSCode
|
|
||||||
|
|
||||||
To run and debug single node cluster with VSCode:
|
|
||||||
|
|
||||||
1. Clone and build [frostfs-contract](https://git.frostfs.info/TrueCloudLab/frostfs-contract) repository to the same directory level as `frostfs-node`. For example:
|
|
||||||
|
|
||||||
```
|
|
||||||
/
|
|
||||||
├── src
|
|
||||||
├── frostfs-node
|
|
||||||
└── frostfs-contract
|
|
||||||
```
|
|
||||||
See `frostfs-contract`'s README.md for build instructions.
|
|
||||||
|
|
||||||
2. Copy `launch.json` and `tasks.json` from `dev/.vscode-example` directory to `.vscode` directory. If you already have such files in `.vscode` directory, then merge them manually.
|
|
||||||
|
|
||||||
3. Go to **Run and Debug** (`Ctrl+Shift+D`) and start `IR+Storage node` configuration.
|
|
||||||
|
|
||||||
4. To create container and put object into it run (container and object IDs will be different):
|
|
||||||
|
|
||||||
```
|
|
||||||
./bin/frostfs-cli container create -r 127.0.0.1:8080 --wallet ./dev/wallet.json --policy "REP 1 IN X CBF 1 SELECT 1 FROM * AS X" --await
|
|
||||||
Enter password > <- press ENTER, the is no password for wallet
|
|
||||||
CID: CfPhEuHQ2PRvM4gfBQDC4dWZY3NccovyfcnEdiq2ixju
|
|
||||||
|
|
||||||
./bin/frostfs-cli object put -r 127.0.0.1:8080 --wallet ./dev/wallet.json --file README.md --cid CfPhEuHQ2PRvM4gfBQDC4dWZY3NccovyfcnEdiq2ixju
|
|
||||||
Enter password >
|
|
||||||
4300 / 4300 [===========================================================================================================================================================================================================] 100.00% 0s
|
|
||||||
[README.md] Object successfully stored
|
|
||||||
OID: 78sohnudVMnPsczXqsTUcvezosan2YDNVZwDE8Kq5YwU
|
|
||||||
CID: CfPhEuHQ2PRvM4gfBQDC4dWZY3NccovyfcnEdiq2ixju
|
|
||||||
|
|
||||||
./bin/frostfs-cli object get -r 127.0.0.1:8080 --wallet ./dev/wallet.json --cid CfPhEuHQ2PRvM4gfBQDC4dWZY3NccovyfcnEdiq2ixju --oid 78sohnudVMnPsczXqsTUcvezosan2YDNVZwDE8Kq5YwU
|
|
||||||
...
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
Feel free to contribute to this project after reading the [contributing
|
Feel free to contribute to this project after reading the [contributing
|
||||||
|
@ -124,7 +86,7 @@ the feature/topic you are going to implement.
|
||||||
|
|
||||||
# Credits
|
# Credits
|
||||||
|
|
||||||
FrostFS is maintained by [True Cloud Lab](https://git.frostfs.info/TrueCloudLab/) with the help and
|
FrostFS is maintained by [True Cloud Lab](https://github.com/TrueCloudLab/) with the help and
|
||||||
contributions from community members.
|
contributions from community members.
|
||||||
|
|
||||||
Please see [CREDITS](CREDITS.md) for details.
|
Please see [CREDITS](CREDITS.md) for details.
|
||||||
|
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
||||||
v0.42.0
|
v0.35.0
|
||||||
|
|
|
@ -3,22 +3,23 @@
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Admin tool provides an easier way to deploy and maintain private installation
|
Admin tool provides an easier way to deploy and maintain private installation
|
||||||
of FrostFS. Private installation provides a set of N3 consensus nodes, FrostFS
|
of FrostFS. Private installation provides a set of N3 consensus nodes, FrostFS
|
||||||
Alphabet, and Storage nodes. Admin tool generates consensus keys, initializes
|
Alphabet, and Storage nodes. Admin tool generates consensus keys, initializes
|
||||||
the sidechain, and provides functions to update the network and register new
|
the sidechain, and provides functions to update the network and register new
|
||||||
Storage nodes.
|
Storage nodes.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
To build binary locally, use `make bin/frostfs-adm` command.
|
To build binary locally, use `make bin/frostfs-adm` command.
|
||||||
|
|
||||||
For clean build inside a docker container, use `make docker/bin/frostfs-adm`.
|
For clean build inside a docker container, use `make docker/bin/frostfs-adm`.
|
||||||
|
|
||||||
Build docker image with `make image-adm`.
|
Build docker image with `make image-adm`.
|
||||||
|
|
||||||
At FrostFS private install deployment, frostfs-adm requires compiled FrostFS
|
At FrostFS private install deployment, frostfs-adm requires compiled FrostFS
|
||||||
contracts. Find them in the latest release of
|
contracts. Find them in the latest release of
|
||||||
[frostfs-contract repository](https://git.frostfs.info/TrueCloudLab/frostfs-contract/releases).
|
[frostfs-contract repository](https://github.com/TrueCloudLab/frostfs-contract/releases).
|
||||||
|
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
|
@ -26,7 +27,7 @@ contracts. Find them in the latest release of
|
||||||
|
|
||||||
Config section provides `init` command that creates a configuration file for
|
Config section provides `init` command that creates a configuration file for
|
||||||
private installation deployment and updates. Config file is optional, all
|
private installation deployment and updates. Config file is optional, all
|
||||||
parameters can be passed by arguments or read from standard input (wallet
|
parameters can be passed by arguments or read from standard input (wallet
|
||||||
passwords).
|
passwords).
|
||||||
|
|
||||||
Config example:
|
Config example:
|
||||||
|
@ -36,7 +37,9 @@ alphabet-wallets: /path # path to consensus node / alphabet wallets s
|
||||||
network:
|
network:
|
||||||
max_object_size: 67108864 # max size of a single FrostFS object, bytes
|
max_object_size: 67108864 # max size of a single FrostFS object, bytes
|
||||||
epoch_duration: 240 # duration of a FrostFS epoch in blocks, consider block generation frequency in the sidechain
|
epoch_duration: 240 # duration of a FrostFS epoch in blocks, consider block generation frequency in the sidechain
|
||||||
|
basic_income_rate: 0 # basic income rate, for private consider 0
|
||||||
fee:
|
fee:
|
||||||
|
audit: 0 # network audit fee, for private installation consider 0
|
||||||
candidate: 0 # inner ring candidate registration fee, for private installation consider 0
|
candidate: 0 # inner ring candidate registration fee, for private installation consider 0
|
||||||
container: 0 # container creation fee, for private installation consider 0
|
container: 0 # container creation fee, for private installation consider 0
|
||||||
container_alias: 0 # container nice-name registration fee, for private installation consider 0
|
container_alias: 0 # container nice-name registration fee, for private installation consider 0
|
||||||
|
@ -55,15 +58,14 @@ credentials: # passwords for consensus node / alphabet wallets
|
||||||
|
|
||||||
#### Network deployment
|
#### Network deployment
|
||||||
|
|
||||||
- `generate-alphabet` generates a set of wallets for consensus and
|
- `generate-alphabet` generates a set of wallets for consensus and
|
||||||
Alphabet nodes. The list of the name for alphabet wallets(no gaps between names allowed, order is important):
|
Alphabet nodes.
|
||||||
- az, buky, vedi, glagoli, dobro, yest, zhivete, dzelo, zemlja, izhe, izhei, gerv, kako, ljudi, mislete, nash, on, pokoj, rtsi, slovo, tverdo, uk
|
|
||||||
|
|
||||||
- `init` initializes the sidechain by deploying smart contracts and
|
- `init` initializes the sidechain by deploying smart contracts and
|
||||||
setting provided FrostFS network configuration.
|
setting provided FrostFS network configuration.
|
||||||
|
|
||||||
- `generate-storage-wallet` generates a wallet for the Storage node that
|
- `generate-storage-wallet` generates a wallet for the Storage node that
|
||||||
is ready for deployment. It also transfers a bit of sidechain GAS, so this
|
is ready for deployment. It also transfers a bit of sidechain GAS, so this
|
||||||
wallet can be used for FrostFS bootstrap.
|
wallet can be used for FrostFS bootstrap.
|
||||||
|
|
||||||
#### Network maintenance
|
#### Network maintenance
|
||||||
|
@ -73,7 +75,7 @@ credentials: # passwords for consensus node / alphabet wallets
|
||||||
- `force-new-epoch` increments FrostFS epoch number and executes new epoch
|
- `force-new-epoch` increments FrostFS epoch number and executes new epoch
|
||||||
handlers in FrostFS nodes.
|
handlers in FrostFS nodes.
|
||||||
|
|
||||||
- `refill-gas` transfers sidechain GAS to the specified wallet.
|
- `refill-gas` transfers sidechain GAS to the specified wallet.
|
||||||
|
|
||||||
- `update-contracts` updates contracts to a new version.
|
- `update-contracts` updates contracts to a new version.
|
||||||
|
|
||||||
|
@ -85,7 +87,7 @@ info. These commands **do not migrate actual objects**.
|
||||||
- `dump-containers` saves all containers and metadata registered in the container
|
- `dump-containers` saves all containers and metadata registered in the container
|
||||||
contract to a file.
|
contract to a file.
|
||||||
|
|
||||||
- `restore-containers` restores previously saved containers by their repeated registration in
|
- `restore-containers` restores previously saved containers by their repeated registration in
|
||||||
the container contract.
|
the container contract.
|
||||||
|
|
||||||
- `list-containers` output all containers ids.
|
- `list-containers` output all containers ids.
|
||||||
|
|
|
@ -2,23 +2,22 @@
|
||||||
|
|
||||||
This is a short guide on how to deploy a private FrostFS storage network on bare
|
This is a short guide on how to deploy a private FrostFS storage network on bare
|
||||||
metal without docker images. This guide does not cover details on how to start
|
metal without docker images. This guide does not cover details on how to start
|
||||||
consensus, Alphabet, or Storage nodes. This guide covers only `frostfs-adm`
|
consensus, Alphabet, or Storage nodes. This guide covers only `frostfs-adm`
|
||||||
related configuration details.
|
related configuration details.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
To follow this guide you need:
|
To follow this guide you need:
|
||||||
- latest released version of [neo-go](https://github.com/nspcc-dev/neo-go/releases) (v0.97.2 at the moment),
|
- latest released version of [neo-go](https://github.com/nspcc-dev/neo-go/releases) (v0.97.2 at the moment),
|
||||||
- latest released version of [frostfs-adm](https://git.frostfs.info/TrueCloudLab/frostfs-node/releases) utility (v0.42.9 at the moment),
|
- latest released version of [frostfs-adm](https://github.com/TrueCloudLab/frostfs-node/releases) utility (v0.25.1 at the moment),
|
||||||
- latest released version of compiled [frostfs-contract](https://git.frostfs.info/TrueCloudLab/frostfs-contract/releases) (v0.19.2 at the moment).
|
- latest released version of compiled [frostfs-contract](https://github.com/TrueCloudLab/frostfs-contract/releases) (v0.11.0 at the moment).
|
||||||
|
|
||||||
## Step 1: Prepare network configuration
|
## Step 1: Prepare network configuration
|
||||||
|
|
||||||
To start a network, you need a set of consensus nodes, the same number of
|
To start a network, you need a set of consensus nodes, the same number of
|
||||||
Alphabet nodes and any number of Storage nodes. While the number of Storage
|
Alphabet nodes and any number of Storage nodes. While the number of Storage
|
||||||
nodes can be scaled almost infinitely, the number of consensus and Alphabet
|
nodes can be scaled almost infinitely, the number of consensus and Alphabet
|
||||||
nodes can't be changed so easily right now. Consider this before going any further.
|
nodes can't be changed so easily right now. Consider this before going any further.
|
||||||
Note also that there is an upper limit on the number of alphabet nodes (currently 22).
|
|
||||||
|
|
||||||
It is easier to use`frostfs-adm` with a predefined configuration. First, create
|
It is easier to use`frostfs-adm` with a predefined configuration. First, create
|
||||||
a network configuration file. In this example, there is going to be only one
|
a network configuration file. In this example, there is going to be only one
|
||||||
|
@ -28,15 +27,15 @@ consensus / Alphabet node in the network.
|
||||||
$ frostfs-adm config init --path foo.network.yml
|
$ frostfs-adm config init --path foo.network.yml
|
||||||
Initial config file saved to foo.network.yml
|
Initial config file saved to foo.network.yml
|
||||||
|
|
||||||
$ cat foo.network.yml
|
$ cat foo.network.yml
|
||||||
rpc-endpoint: https://neo.rpc.node:30333
|
rpc-endpoint: https://neo.rpc.node:30333
|
||||||
alphabet-wallets: /home/user/deploy/alphabet-wallets
|
alphabet-wallets: /home/user/deploy/alphabet-wallets
|
||||||
network:
|
network:
|
||||||
max_object_size: 67108864
|
max_object_size: 67108864
|
||||||
epoch_duration: 240
|
epoch_duration: 240
|
||||||
max_ec_data_count: 12
|
basic_income_rate: 0
|
||||||
max_ec_parity_count: 4
|
|
||||||
fee:
|
fee:
|
||||||
|
audit: 0
|
||||||
candidate: 0
|
candidate: 0
|
||||||
container: 0
|
container: 0
|
||||||
withdraw: 0
|
withdraw: 0
|
||||||
|
@ -44,17 +43,17 @@ credentials:
|
||||||
az: hunter2
|
az: hunter2
|
||||||
```
|
```
|
||||||
|
|
||||||
For private installation, it is recommended to set all **fees** and **basic
|
For private installation, it is recommended to set all **fees** and **basic
|
||||||
income rate** to 0.
|
income rate** to 0.
|
||||||
|
|
||||||
As for **epoch duration**, consider consensus node block generation frequency.
|
As for **epoch duration**, consider consensus node block generation frequency.
|
||||||
With default 15 seconds per block, 240 blocks are going to be a 1-hour epoch.
|
With default 15 seconds per block, 240 blocks are going to be a 1-hour epoch.
|
||||||
|
|
||||||
For **max object size**, 67108864 (64 MiB) or 134217728 (128 MiB) should provide
|
For **max object size**, 67108864 (64 MiB) or 134217728 (128 MiB) should provide
|
||||||
good chunk distribution in most cases.
|
good chunk distribution in most cases.
|
||||||
|
|
||||||
With this config, generate wallets (private keys) of consensus nodes. The same
|
With this config, generate wallets (private keys) of consensus nodes. The same
|
||||||
wallets will be used for Alphabet nodes. Make sure, that dir for alphabet
|
wallets will be used for Alphabet nodes. Make sure, that dir for alphabet
|
||||||
wallets already exists.
|
wallets already exists.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -64,25 +63,20 @@ alphabet-wallets: /home/user/deploy/alphabet-wallets
|
||||||
wallet[0]: hunter2
|
wallet[0]: hunter2
|
||||||
```
|
```
|
||||||
|
|
||||||
This command generates wallets with the following names:
|
|
||||||
- az, buky, vedi, glagoli, dobro, yest, zhivete, dzelo, zemlja, izhe, izhei, gerv, kako, ljudi, mislete, nash, on, pokoj, rtsi, slovo, tverdo, uk
|
|
||||||
|
|
||||||
No gaps between names allowed, order is important.
|
|
||||||
|
|
||||||
Do not lose wallet files and network config. Store it in an encrypted backed up
|
Do not lose wallet files and network config. Store it in an encrypted backed up
|
||||||
storage.
|
storage.
|
||||||
|
|
||||||
## Step 2: Launch consensus nodes
|
## Step 2: Launch consensus nodes
|
||||||
|
|
||||||
Configure blockchain nodes with the generated wallets from the previous step.
|
Configure blockchain nodes with the generated wallets from the previous step.
|
||||||
Config examples can be found in
|
Config examples can be found in
|
||||||
[neo-go repository](https://github.com/nspcc-dev/neo-go/tree/master/config).
|
[neo-go repository](https://github.com/nspcc-dev/neo-go/tree/master/config).
|
||||||
|
|
||||||
Gather public keys from **all** generated wallets. We are interested in the first
|
Gather public keys from **all** generated wallets. We are interested in the first
|
||||||
`simple signature contract` public key.
|
`simple signature contract` public key.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ neo-go wallet dump-keys -w alphabet-wallets/az.json
|
$ neo-go wallet dump-keys -w alphabet-wallets/az.json
|
||||||
NitdS4k4f1Hh5mbLJhAswBK3WC2gQgPN1o (simple signature contract):
|
NitdS4k4f1Hh5mbLJhAswBK3WC2gQgPN1o (simple signature contract):
|
||||||
02c1cc85f9c856dbe2d02017349bcb7b4e5defa78b8056a09b3240ba2a8c078869
|
02c1cc85f9c856dbe2d02017349bcb7b4e5defa78b8056a09b3240ba2a8c078869
|
||||||
|
|
||||||
|
@ -93,10 +87,10 @@ NiMKabp3ddi3xShmLAXhTfbnuWb4cSJT6E (1 out of 1 multisig contract):
|
||||||
02c1cc85f9c856dbe2d02017349bcb7b4e5defa78b8056a09b3240ba2a8c078869
|
02c1cc85f9c856dbe2d02017349bcb7b4e5defa78b8056a09b3240ba2a8c078869
|
||||||
```
|
```
|
||||||
|
|
||||||
Put the list of public keys into `ProtocolConfiguration.StandbyCommittee`
|
Put the list of public keys into `ProtocolConfiguration.StandbyCommittee`
|
||||||
section. Specify the wallet path and the password in `ApplicationConfiguration.P2PNotary`
|
section. Specify the wallet path and the password in `ApplicationConfiguration.P2PNotary`
|
||||||
and `ApplicationConfiguration.UnlockWallet` sections. If config includes
|
and `ApplicationConfiguration.UnlockWallet` sections. If config includes
|
||||||
`ProtocolConfiguration.NativeActivations` section, add notary
|
`ProtocolConfiguration.NativeActivations` section, add notary
|
||||||
contract `Notary: [0]`.
|
contract `Notary: [0]`.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
@ -127,7 +121,7 @@ and possible overload issues.
|
||||||
Use archive with compiled FrostFS contracts to initialize the sidechain.
|
Use archive with compiled FrostFS contracts to initialize the sidechain.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ tar -xzvf frostfs-contract-v0.11.0.tar.gz
|
$ tar -xzvf frostfs-contract-v0.11.0.tar.gz
|
||||||
|
|
||||||
$ ./frostfs-adm -c foo.network.yml morph init --contracts ./frostfs-contract-v0.11.0
|
$ ./frostfs-adm -c foo.network.yml morph init --contracts ./frostfs-contract-v0.11.0
|
||||||
Stage 1: transfer GAS to alphabet nodes.
|
Stage 1: transfer GAS to alphabet nodes.
|
||||||
|
@ -147,18 +141,20 @@ Waiting for transactions to persist...
|
||||||
Stage 7: set addresses in NNS.
|
Stage 7: set addresses in NNS.
|
||||||
Waiting for transactions to persist...
|
Waiting for transactions to persist...
|
||||||
NNS: Set alphabet0.frostfs -> f692dfb4d43a15b464eb51a7041160fb29c44b6a
|
NNS: Set alphabet0.frostfs -> f692dfb4d43a15b464eb51a7041160fb29c44b6a
|
||||||
|
NNS: Set audit.frostfs -> 7df847b993affb3852074345a7c2bd622171ee0d
|
||||||
NNS: Set balance.frostfs -> 103519b3067a66307080a66570c0491ee8f68879
|
NNS: Set balance.frostfs -> 103519b3067a66307080a66570c0491ee8f68879
|
||||||
NNS: Set container.frostfs -> cae60bdd689d185901e495352d0247752ce50846
|
NNS: Set container.frostfs -> cae60bdd689d185901e495352d0247752ce50846
|
||||||
NNS: Set frostfsid.frostfs -> c421fb60a3895865a8f24d197d6a80ef686041d2
|
NNS: Set frostfsid.frostfs -> c421fb60a3895865a8f24d197d6a80ef686041d2
|
||||||
NNS: Set netmap.frostfs -> 894eb854632f50fb124412ce7951ebc00763525e
|
NNS: Set netmap.frostfs -> 894eb854632f50fb124412ce7951ebc00763525e
|
||||||
NNS: Set proxy.frostfs -> ac6e6fe4b373d0ca0ca4969d1e58fa0988724e7d
|
NNS: Set proxy.frostfs -> ac6e6fe4b373d0ca0ca4969d1e58fa0988724e7d
|
||||||
|
NNS: Set reputation.frostfs -> 6eda57c9d93d990573646762d1fea327ce41191f
|
||||||
Waiting for transactions to persist...
|
Waiting for transactions to persist...
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step 4: Launch Alphabet nodes
|
## Step 4: Launch Alphabet nodes
|
||||||
|
|
||||||
Configure Alphabet nodes with the wallets generated in step 1. For
|
Configure Alphabet nodes with the wallets generated in step 1. For
|
||||||
`morph.validators` use a list of public keys from
|
`morph.validators` use a list of public keys from
|
||||||
`ProtocolConfiguration.StandbyCommittee`.
|
`ProtocolConfiguration.StandbyCommittee`.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
@ -182,10 +178,10 @@ Generate a new wallet for a Storage node.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ frostfs-adm -c foo.network.yml morph generate-storage-wallet --storage-wallet ./sn01.json --initial-gas 10.0
|
$ frostfs-adm -c foo.network.yml morph generate-storage-wallet --storage-wallet ./sn01.json --initial-gas 10.0
|
||||||
New password >
|
New password >
|
||||||
Waiting for transactions to persist...
|
Waiting for transactions to persist...
|
||||||
|
|
||||||
$ neo-go wallet dump-keys -w sn01.json
|
$ neo-go wallet dump-keys -w sn01.json
|
||||||
Ngr7p8Z9S22XDH6VkUG9oXobv8zZRAWwwv (simple signature contract):
|
Ngr7p8Z9S22XDH6VkUG9oXobv8zZRAWwwv (simple signature contract):
|
||||||
0355eccb72cd46f09a3e5237eaa0f4949cceb5ecfa5a225bd3bb9fd021c4d75b85
|
0355eccb72cd46f09a3e5237eaa0f4949cceb5ecfa5a225bd3bb9fd021c4d75b85
|
||||||
```
|
```
|
||||||
|
@ -209,7 +205,7 @@ Current epoch: 8, increase to 9.
|
||||||
Waiting for transactions to persist...
|
Waiting for transactions to persist...
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
After that, FrostFS Storage is ready to work. You can access it directly or
|
After that, FrostFS Storage is ready to work. You can access it directly or
|
||||||
with protocol gates.
|
with protocol gates.
|
||||||
|
|
39
cmd/frostfs-adm/docs/subnetwork-creation.md
Normal file
39
cmd/frostfs-adm/docs/subnetwork-creation.md
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# FrostFS subnetwork creation
|
||||||
|
|
||||||
|
This is a short guide on how to create FrostFS subnetworks. This guide
|
||||||
|
considers that the sidechain and the inner ring (alphabet nodes) have already been
|
||||||
|
deployed and the sidechain contains a deployed `subnet` contract.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
To follow this guide, you need:
|
||||||
|
- neo-go sidechain RPC endpoint;
|
||||||
|
- latest released version of [frostfs-adm](https://github.com/TrueCloudLab/frostfs-node/releases);
|
||||||
|
- wallet with FrostFS account.
|
||||||
|
|
||||||
|
## Creation
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ frostfs-adm morph subnet create \
|
||||||
|
-r <side_chain_RPC_endpoint> \
|
||||||
|
-w </path/to/owner/wallet> \
|
||||||
|
--notary
|
||||||
|
Create subnet request sent successfully. ID: 4223489767.
|
||||||
|
```
|
||||||
|
|
||||||
|
**NOTE:** in notary-enabled environment you should have a sufficient
|
||||||
|
notary deposit (not expired, with enough GAS balance). Your subnet ID
|
||||||
|
will differ from the example.
|
||||||
|
|
||||||
|
The default account in the wallet that has been passed with `-w` flag is the owner
|
||||||
|
of the just created subnetwork.
|
||||||
|
|
||||||
|
You can check if your subnetwork was created successfully:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ frostfs-adm morph subnet get \
|
||||||
|
-r <side_chain_RPC_endpoint> \
|
||||||
|
--subnet <subnet_ID>
|
||||||
|
Owner: NUc734PMJXiqa2J9jRtvskU3kCdyyuSN8Q
|
||||||
|
```
|
||||||
|
Your owner will differ from the example.
|
137
cmd/frostfs-adm/docs/subnetwork-usage.md
Normal file
137
cmd/frostfs-adm/docs/subnetwork-usage.md
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
# Managing Subnetworks
|
||||||
|
|
||||||
|
This is a short guide on how to manage FrostFS subnetworks. This guide
|
||||||
|
considers that the sidechain and the inner ring (alphabet nodes) have already been
|
||||||
|
deployed, and the sidechain contains a deployed `subnet` contract.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- neo-go sidechain RPC endpoint;
|
||||||
|
- latest released version of [frostfs-adm](https://github.com/TrueCloudLab/frostfs-node/releases);
|
||||||
|
- [created](subnetwork-creation.md) subnetwork;
|
||||||
|
- wallet with the account that owns the subnetwork;
|
||||||
|
- public key of the Storage Node;
|
||||||
|
- public keys of the node and client administrators;
|
||||||
|
- owner IDs of the FrostFS users.
|
||||||
|
|
||||||
|
## Add node administrator
|
||||||
|
|
||||||
|
Node administrators are accounts that can manage (add and delete nodes)
|
||||||
|
the whitelist of the nodes which can be included to a subnetwork. Only the subnet
|
||||||
|
owner is allowed to add and remove node administrators from the subnetwork.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ frostfs-adm morph subnet admin add \
|
||||||
|
-r <side_chain_RPC_endpoint> \
|
||||||
|
-w </path/to/owner/wallet> \
|
||||||
|
--admin <HEX_admin_public_key> \
|
||||||
|
--subnet <subnet_ID>
|
||||||
|
Add admin request sent successfully.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Add node
|
||||||
|
|
||||||
|
Adding a node to a subnetwork means that the node becomes able to service
|
||||||
|
containers that have been created in that subnetwork. Addition only changes
|
||||||
|
the list of the allowed nodes. Node is not required to be bootstrapped at the
|
||||||
|
moment of its inclusion.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ frostfs-adm morph subnet node add \
|
||||||
|
-r <side_chain_RPC_endpoint> \
|
||||||
|
-w </path/to/node_admin/wallet> \
|
||||||
|
--node <HEX_node_public_key> \
|
||||||
|
--subnet <subnet_ID>
|
||||||
|
Add node request sent successfully.
|
||||||
|
```
|
||||||
|
|
||||||
|
**NOTE:** the owner of the subnetwork is also allowed to add nodes.
|
||||||
|
|
||||||
|
## Add client administrator
|
||||||
|
|
||||||
|
Client administrators are accounts that can manage (add and delete
|
||||||
|
nodes) the whitelist of the clients that can create containers in the
|
||||||
|
subnetwork. Only the subnet owner is allowed to add and remove client
|
||||||
|
administrators from the subnetwork.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ frostfs-adm morph subnet admin add \
|
||||||
|
-r <side_chain_RPC_endpoint> \
|
||||||
|
-w </path/to/owner/wallet> \
|
||||||
|
--admin <HEX_admin_public_key> \
|
||||||
|
--subnet <subnet_ID> \
|
||||||
|
--client \
|
||||||
|
--group <group_ID>
|
||||||
|
Add admin request sent successfully.
|
||||||
|
```
|
||||||
|
|
||||||
|
**NOTE:** you do not need to create a group explicitly, it will be created
|
||||||
|
right after the first client admin is added. Group ID is a 4-byte
|
||||||
|
positive integer number.
|
||||||
|
|
||||||
|
## Add client
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ frostfs-adm morph subnet client add \
|
||||||
|
-r <side_chain_RPC_endpoint> \
|
||||||
|
-w </path/to/client_admin/wallet> \
|
||||||
|
--client <client_ownerID> \
|
||||||
|
--subnet <subnet_ID> \
|
||||||
|
--group <group_ID>
|
||||||
|
Add client request sent successfully.
|
||||||
|
```
|
||||||
|
|
||||||
|
**NOTE:** the owner of the subnetwork is also allowed to add clients. This is
|
||||||
|
the only one command that accepts `ownerID`, not the public key.
|
||||||
|
Administrator can manage only their group (a group where that administrator
|
||||||
|
has been added by the subnet owner).
|
||||||
|
|
||||||
|
# Bootstrapping Storage Node
|
||||||
|
|
||||||
|
After a subnetwork [is created](subnetwork-creation.md) and a node is included into it, the
|
||||||
|
node could be bootstrapped and service subnetwork containers.
|
||||||
|
|
||||||
|
For bootstrapping, you need to specify the ID of the subnetwork in the node's
|
||||||
|
configuration:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
...
|
||||||
|
node:
|
||||||
|
...
|
||||||
|
subnet:
|
||||||
|
entries: # list of IDs of subnets to enter in a text format of FrostFS API protocol (overrides corresponding attributes)
|
||||||
|
- <subnetwork_ID>
|
||||||
|
...
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
**NOTE:** specifying subnetwork that is denied for the node is not an error:
|
||||||
|
that configuration value would be ignored. You do not need to specify zero
|
||||||
|
(with 0 ID) subnetwork: its inclusion is implicit. On the contrary, to exclude
|
||||||
|
a node from the default zero subnetwork, you need to specify it explicitly:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
...
|
||||||
|
node:
|
||||||
|
...
|
||||||
|
subnet:
|
||||||
|
exit_zero: true # toggle entrance to zero subnet (overrides corresponding attribute and occurrence in `entries`)
|
||||||
|
...
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
# Creating container in non-zero subnetwork
|
||||||
|
|
||||||
|
Creating containers without using `--subnet` flag is equivalent to
|
||||||
|
creating container in the zero subnetwork.
|
||||||
|
|
||||||
|
To create a container in a private network, your wallet must be added to
|
||||||
|
the client whitelist by the client admins or the subnet owners:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ frostfs-cli container create \
|
||||||
|
--policy 'REP 1' \
|
||||||
|
-w </path/to/wallet> \
|
||||||
|
-r s01.frostfs.devenv:8080 \
|
||||||
|
--subnet <subnet_ID>
|
||||||
|
```
|
|
@ -1,42 +0,0 @@
|
||||||
package commonflags
|
|
||||||
|
|
||||||
const (
|
|
||||||
ConfigFlag = "config"
|
|
||||||
ConfigFlagShorthand = "c"
|
|
||||||
ConfigFlagUsage = "Config file"
|
|
||||||
|
|
||||||
ConfigDirFlag = "config-dir"
|
|
||||||
ConfigDirFlagUsage = "Config directory"
|
|
||||||
|
|
||||||
Verbose = "verbose"
|
|
||||||
VerboseShorthand = "v"
|
|
||||||
VerboseUsage = "Verbose output"
|
|
||||||
|
|
||||||
EndpointFlag = "rpc-endpoint"
|
|
||||||
EndpointFlagDesc = "N3 RPC node endpoint"
|
|
||||||
EndpointFlagShort = "r"
|
|
||||||
|
|
||||||
AlphabetWalletsFlag = "alphabet-wallets"
|
|
||||||
AlphabetWalletsFlagDesc = "Path to alphabet wallets dir"
|
|
||||||
|
|
||||||
LocalDumpFlag = "local-dump"
|
|
||||||
ContractsInitFlag = "contracts"
|
|
||||||
ContractsInitFlagDesc = "Path to archive with compiled FrostFS contracts (the default is to fetch the latest release from the official repository)"
|
|
||||||
ContractsURLFlag = "contracts-url"
|
|
||||||
ContractsURLFlagDesc = "URL to archive with compiled FrostFS contracts"
|
|
||||||
EpochDurationInitFlag = "network.epoch_duration"
|
|
||||||
MaxObjectSizeInitFlag = "network.max_object_size"
|
|
||||||
MaxECDataCountFlag = "network.max_ec_data_count"
|
|
||||||
MaxECParityCounFlag = "network.max_ec_parity_count"
|
|
||||||
RefillGasAmountFlag = "gas"
|
|
||||||
StorageWalletFlag = "storage-wallet"
|
|
||||||
ContainerFeeInitFlag = "network.fee.container"
|
|
||||||
ContainerAliasFeeInitFlag = "network.fee.container_alias"
|
|
||||||
CandidateFeeInitFlag = "network.fee.candidate"
|
|
||||||
WithdrawFeeInitFlag = "network.fee.withdraw"
|
|
||||||
MaintenanceModeAllowedInitFlag = "network.maintenance_mode_allowed"
|
|
||||||
HomomorphicHashDisabledInitFlag = "network.homomorphic_hash_disabled"
|
|
||||||
CustomZoneFlag = "domain"
|
|
||||||
AlphabetSizeFlag = "size"
|
|
||||||
AllFlag = "all"
|
|
||||||
)
|
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/innerring"
|
"github.com/TrueCloudLab/frostfs-node/pkg/innerring"
|
||||||
"github.com/nspcc-dev/neo-go/cli/input"
|
"github.com/nspcc-dev/neo-go/cli/input"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
@ -18,11 +18,11 @@ type configTemplate struct {
|
||||||
AlphabetDir string
|
AlphabetDir string
|
||||||
MaxObjectSize int
|
MaxObjectSize int
|
||||||
EpochDuration int
|
EpochDuration int
|
||||||
|
BasicIncomeRate int
|
||||||
|
AuditFee int
|
||||||
CandidateFee int
|
CandidateFee int
|
||||||
ContainerFee int
|
ContainerFee int
|
||||||
ContainerAliasFee int
|
ContainerAliasFee int
|
||||||
MaxECDataCount int
|
|
||||||
MaxECParityCount int
|
|
||||||
WithdrawFee int
|
WithdrawFee int
|
||||||
Glagolitics []string
|
Glagolitics []string
|
||||||
HomomorphicHashDisabled bool
|
HomomorphicHashDisabled bool
|
||||||
|
@ -33,10 +33,10 @@ alphabet-wallets: {{ .AlphabetDir}}
|
||||||
network:
|
network:
|
||||||
max_object_size: {{ .MaxObjectSize}}
|
max_object_size: {{ .MaxObjectSize}}
|
||||||
epoch_duration: {{ .EpochDuration}}
|
epoch_duration: {{ .EpochDuration}}
|
||||||
max_ec_data_count: {{ .MaxECDataCount}}
|
basic_income_rate: {{ .BasicIncomeRate}}
|
||||||
max_ec_parity_count: {{ .MaxECParityCount}}
|
|
||||||
homomorphic_hash_disabled: {{ .HomomorphicHashDisabled}}
|
homomorphic_hash_disabled: {{ .HomomorphicHashDisabled}}
|
||||||
fee:
|
fee:
|
||||||
|
audit: {{ .AuditFee}}
|
||||||
candidate: {{ .CandidateFee}}
|
candidate: {{ .CandidateFee}}
|
||||||
container: {{ .ContainerFee}}
|
container: {{ .ContainerFee}}
|
||||||
container_alias: {{ .ContainerAliasFee }}
|
container_alias: {{ .ContainerAliasFee }}
|
||||||
|
@ -47,19 +47,19 @@ credentials:
|
||||||
{{.}}: password{{end}}
|
{{.}}: password{{end}}
|
||||||
`
|
`
|
||||||
|
|
||||||
func initConfig(cmd *cobra.Command, _ []string) error {
|
func initConfig(cmd *cobra.Command, args []string) error {
|
||||||
configPath, err := readConfigPathFromArgs(cmd)
|
configPath, err := readConfigPathFromArgs(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
pathDir := filepath.Dir(configPath)
|
pathDir := filepath.Dir(configPath)
|
||||||
err = os.MkdirAll(pathDir, 0o700)
|
err = os.MkdirAll(pathDir, 0700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create dir %s: %w", pathDir, err)
|
return fmt.Errorf("create dir %s: %w", pathDir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.OpenFile(configPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC|os.O_SYNC, 0o600)
|
f, err := os.OpenFile(configPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC|os.O_SYNC, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("open %s: %w", configPath, err)
|
return fmt.Errorf("open %s: %w", configPath, err)
|
||||||
}
|
}
|
||||||
|
@ -110,10 +110,10 @@ func generateConfigExample(appDir string, credSize int) (string, error) {
|
||||||
tmpl := configTemplate{
|
tmpl := configTemplate{
|
||||||
Endpoint: "https://neo.rpc.node:30333",
|
Endpoint: "https://neo.rpc.node:30333",
|
||||||
MaxObjectSize: 67108864, // 64 MiB
|
MaxObjectSize: 67108864, // 64 MiB
|
||||||
MaxECDataCount: 12, // Tested with 16-node networks, assuming 12 data + 4 parity nodes.
|
|
||||||
MaxECParityCount: 4, // Maximum 4 parity chunks, typically <= 3 for most policies.
|
|
||||||
EpochDuration: 240, // 1 hour with 15s per block
|
EpochDuration: 240, // 1 hour with 15s per block
|
||||||
|
BasicIncomeRate: 1_0000_0000, // 0.0001 GAS per GiB (Fixed12)
|
||||||
HomomorphicHashDisabled: false, // object homomorphic hash is enabled
|
HomomorphicHashDisabled: false, // object homomorphic hash is enabled
|
||||||
|
AuditFee: 1_0000, // 0.00000001 GAS per audit (Fixed12)
|
||||||
CandidateFee: 100_0000_0000, // 100.0 GAS (Fixed8)
|
CandidateFee: 100_0000_0000, // 100.0 GAS (Fixed8)
|
||||||
ContainerFee: 1000, // 0.000000001 * 7 GAS per container (Fixed12)
|
ContainerFee: 1000, // 0.000000001 * 7 GAS per container (Fixed12)
|
||||||
ContainerAliasFee: 500, // ContainerFee / 2
|
ContainerAliasFee: 500, // ContainerFee / 2
|
||||||
|
@ -128,7 +128,7 @@ func generateConfigExample(appDir string, credSize int) (string, error) {
|
||||||
tmpl.AlphabetDir = filepath.Join(appDir, "alphabet-wallets")
|
tmpl.AlphabetDir = filepath.Join(appDir, "alphabet-wallets")
|
||||||
|
|
||||||
var i innerring.GlagoliticLetter
|
var i innerring.GlagoliticLetter
|
||||||
for i = range innerring.GlagoliticLetter(credSize) {
|
for i = 0; i < innerring.GlagoliticLetter(credSize); i++ {
|
||||||
tmpl.Glagolitics = append(tmpl.Glagolitics, i.String())
|
tmpl.Glagolitics = append(tmpl.Glagolitics, i.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/innerring"
|
"github.com/TrueCloudLab/frostfs-node/pkg/innerring"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
@ -27,9 +27,9 @@ func TestGenerateConfigExample(t *testing.T) {
|
||||||
require.Equal(t, "https://neo.rpc.node:30333", v.GetString("rpc-endpoint"))
|
require.Equal(t, "https://neo.rpc.node:30333", v.GetString("rpc-endpoint"))
|
||||||
require.Equal(t, filepath.Join(appDir, "alphabet-wallets"), v.GetString("alphabet-wallets"))
|
require.Equal(t, filepath.Join(appDir, "alphabet-wallets"), v.GetString("alphabet-wallets"))
|
||||||
require.Equal(t, 67108864, v.GetInt("network.max_object_size"))
|
require.Equal(t, 67108864, v.GetInt("network.max_object_size"))
|
||||||
require.Equal(t, 12, v.GetInt("network.max_ec_data_count"))
|
|
||||||
require.Equal(t, 4, v.GetInt("network.max_ec_parity_count"))
|
|
||||||
require.Equal(t, 240, v.GetInt("network.epoch_duration"))
|
require.Equal(t, 240, v.GetInt("network.epoch_duration"))
|
||||||
|
require.Equal(t, 100000000, v.GetInt("network.basic_income_rate"))
|
||||||
|
require.Equal(t, 10000, v.GetInt("network.fee.audit"))
|
||||||
require.Equal(t, 10000000000, v.GetInt("network.fee.candidate"))
|
require.Equal(t, 10000000000, v.GetInt("network.fee.candidate"))
|
||||||
require.Equal(t, 1000, v.GetInt("network.fee.container"))
|
require.Equal(t, 1000, v.GetInt("network.fee.container"))
|
||||||
require.Equal(t, 100000000, v.GetInt("network.fee.withdraw"))
|
require.Equal(t, 100000000, v.GetInt("network.fee.withdraw"))
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
package metabase
|
|
||||||
|
|
||||||
import "github.com/spf13/cobra"
|
|
||||||
|
|
||||||
// RootCmd is a root command of config section.
|
|
||||||
var RootCmd = &cobra.Command{
|
|
||||||
Use: "metabase",
|
|
||||||
Short: "Section for metabase commands",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
RootCmd.AddCommand(UpgradeCmd)
|
|
||||||
|
|
||||||
initUpgradeCommand()
|
|
||||||
}
|
|
|
@ -1,150 +0,0 @@
|
||||||
package metabase
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-node/config"
|
|
||||||
engineconfig "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-node/config/engine"
|
|
||||||
shardconfig "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-node/config/engine/shard"
|
|
||||||
morphconfig "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-node/config/morph"
|
|
||||||
nodeconfig "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-node/config/node"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
|
|
||||||
meta "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/metabase"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
|
|
||||||
morphcontainer "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/container"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
noCompactFlag = "no-compact"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errNoPathsFound = errors.New("no metabase paths found")
|
|
||||||
errNoMorphEndpointsFound = errors.New("no morph endpoints found")
|
|
||||||
)
|
|
||||||
|
|
||||||
var UpgradeCmd = &cobra.Command{
|
|
||||||
Use: "upgrade",
|
|
||||||
Short: "Upgrade metabase to latest version",
|
|
||||||
RunE: upgrade,
|
|
||||||
}
|
|
||||||
|
|
||||||
func upgrade(cmd *cobra.Command, _ []string) error {
|
|
||||||
configFile, err := cmd.Flags().GetString(commonflags.ConfigFlag)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
configDir, err := cmd.Flags().GetString(commonflags.ConfigDirFlag)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
appCfg := config.New(configFile, configDir, config.EnvPrefix)
|
|
||||||
paths, err := getMetabasePaths(appCfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(paths) == 0 {
|
|
||||||
return errNoPathsFound
|
|
||||||
}
|
|
||||||
cmd.Println("found", len(paths), "metabases:")
|
|
||||||
for i, path := range paths {
|
|
||||||
cmd.Println(i+1, ":", path)
|
|
||||||
}
|
|
||||||
mc, err := createMorphClient(cmd.Context(), appCfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer mc.Close()
|
|
||||||
civ, err := createContainerInfoProvider(mc)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
noCompact, _ := cmd.Flags().GetBool(noCompactFlag)
|
|
||||||
result := make(map[string]bool)
|
|
||||||
var resultGuard sync.Mutex
|
|
||||||
eg, ctx := errgroup.WithContext(cmd.Context())
|
|
||||||
for _, path := range paths {
|
|
||||||
eg.Go(func() error {
|
|
||||||
var success bool
|
|
||||||
cmd.Println("upgrading metabase", path, "...")
|
|
||||||
if err := meta.Upgrade(ctx, path, !noCompact, civ, func(a ...any) {
|
|
||||||
cmd.Println(append([]any{time.Now().Format(time.RFC3339), ":", path, ":"}, a...)...)
|
|
||||||
}); err != nil {
|
|
||||||
cmd.Println("error: failed to upgrade metabase", path, ":", err)
|
|
||||||
} else {
|
|
||||||
success = true
|
|
||||||
cmd.Println("metabase", path, "upgraded successfully")
|
|
||||||
}
|
|
||||||
resultGuard.Lock()
|
|
||||||
result[path] = success
|
|
||||||
resultGuard.Unlock()
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if err := eg.Wait(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for mb, ok := range result {
|
|
||||||
if ok {
|
|
||||||
cmd.Println(mb, ": success")
|
|
||||||
} else {
|
|
||||||
cmd.Println(mb, ": failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMetabasePaths(appCfg *config.Config) ([]string, error) {
|
|
||||||
var paths []string
|
|
||||||
if err := engineconfig.IterateShards(appCfg, false, func(sc *shardconfig.Config) error {
|
|
||||||
paths = append(paths, sc.Metabase().Path())
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
return nil, fmt.Errorf("get metabase paths: %w", err)
|
|
||||||
}
|
|
||||||
return paths, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createMorphClient(ctx context.Context, appCfg *config.Config) (*client.Client, error) {
|
|
||||||
addresses := morphconfig.RPCEndpoint(appCfg)
|
|
||||||
if len(addresses) == 0 {
|
|
||||||
return nil, errNoMorphEndpointsFound
|
|
||||||
}
|
|
||||||
key := nodeconfig.Key(appCfg)
|
|
||||||
cli, err := client.New(ctx,
|
|
||||||
key,
|
|
||||||
client.WithDialTimeout(morphconfig.DialTimeout(appCfg)),
|
|
||||||
client.WithEndpoints(addresses...),
|
|
||||||
client.WithSwitchInterval(morphconfig.SwitchInterval(appCfg)),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("create morph client:%w", err)
|
|
||||||
}
|
|
||||||
return cli, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createContainerInfoProvider(cli *client.Client) (container.InfoProvider, error) {
|
|
||||||
sh, err := cli.NNSContractAddress(client.NNSContainerContractName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("resolve container contract hash: %w", err)
|
|
||||||
}
|
|
||||||
cc, err := morphcontainer.NewFromMorph(cli, sh, 0, morphcontainer.TryNotary())
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("create morph container client: %w", err)
|
|
||||||
}
|
|
||||||
return container.NewInfoProvider(func() (container.Source, error) {
|
|
||||||
return morphcontainer.AsContainerSource(cc), nil
|
|
||||||
}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func initUpgradeCommand() {
|
|
||||||
flags := UpgradeCmd.Flags()
|
|
||||||
flags.Bool(noCompactFlag, false, "Do not compact upgraded metabase file")
|
|
||||||
}
|
|
|
@ -1,268 +0,0 @@
|
||||||
package ape
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
|
||||||
parseutil "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/util"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
apechain "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
namespaceTarget = "namespace"
|
|
||||||
containerTarget = "container"
|
|
||||||
userTarget = "user"
|
|
||||||
groupTarget = "group"
|
|
||||||
jsonFlag = "json"
|
|
||||||
jsonFlagDesc = "Output rule chains in JSON format"
|
|
||||||
chainIDFlag = "chain-id"
|
|
||||||
chainIDDesc = "Rule chain ID"
|
|
||||||
ruleFlag = "rule"
|
|
||||||
ruleFlagDesc = "Rule chain in text format"
|
|
||||||
pathFlag = "path"
|
|
||||||
pathFlagDesc = "path to encoded chain in JSON or binary format"
|
|
||||||
targetNameFlag = "target-name"
|
|
||||||
targetNameDesc = "Resource name in APE resource name format"
|
|
||||||
targetTypeFlag = "target-type"
|
|
||||||
targetTypeDesc = "Resource type(container/namespace)"
|
|
||||||
addrAdminFlag = "addr"
|
|
||||||
addrAdminDesc = "The address of the admins wallet"
|
|
||||||
chainNameFlag = "chain-name"
|
|
||||||
chainNameFlagDesc = "Chain name(ingress|s3)"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
addRuleChainCmd = &cobra.Command{
|
|
||||||
Use: "add-rule-chain",
|
|
||||||
Short: "Add rule chain",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
},
|
|
||||||
Run: addRuleChain,
|
|
||||||
}
|
|
||||||
|
|
||||||
removeRuleChainCmd = &cobra.Command{
|
|
||||||
Use: "rm-rule-chain",
|
|
||||||
Short: "Remove rule chain",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
},
|
|
||||||
Run: removeRuleChain,
|
|
||||||
}
|
|
||||||
|
|
||||||
listRuleChainsCmd = &cobra.Command{
|
|
||||||
Use: "list-rule-chains",
|
|
||||||
Short: "List rule chains",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
Run: listRuleChains,
|
|
||||||
}
|
|
||||||
|
|
||||||
setAdminCmd = &cobra.Command{
|
|
||||||
Use: "set-admin",
|
|
||||||
Short: "Set admin",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
},
|
|
||||||
Run: setAdmin,
|
|
||||||
}
|
|
||||||
|
|
||||||
getAdminCmd = &cobra.Command{
|
|
||||||
Use: "get-admin",
|
|
||||||
Short: "Get admin",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
Run: getAdmin,
|
|
||||||
}
|
|
||||||
|
|
||||||
listTargetsCmd = &cobra.Command{
|
|
||||||
Use: "list-targets",
|
|
||||||
Short: "List targets",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
Run: listTargets,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func initAddRuleChainCmd() {
|
|
||||||
Cmd.AddCommand(addRuleChainCmd)
|
|
||||||
|
|
||||||
addRuleChainCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
addRuleChainCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
|
|
||||||
addRuleChainCmd.Flags().String(targetTypeFlag, "", targetTypeDesc)
|
|
||||||
_ = addRuleChainCmd.MarkFlagRequired(targetTypeFlag)
|
|
||||||
addRuleChainCmd.Flags().String(targetNameFlag, "", targetNameDesc)
|
|
||||||
_ = addRuleChainCmd.MarkFlagRequired(targetNameFlag)
|
|
||||||
|
|
||||||
addRuleChainCmd.Flags().String(chainIDFlag, "", chainIDDesc)
|
|
||||||
_ = addRuleChainCmd.MarkFlagRequired(chainIDFlag)
|
|
||||||
addRuleChainCmd.Flags().StringArray(ruleFlag, []string{}, ruleFlagDesc)
|
|
||||||
addRuleChainCmd.Flags().String(pathFlag, "", pathFlagDesc)
|
|
||||||
addRuleChainCmd.Flags().String(chainNameFlag, ingress, chainNameFlagDesc)
|
|
||||||
addRuleChainCmd.MarkFlagsMutuallyExclusive(ruleFlag, pathFlag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initRemoveRuleChainCmd() {
|
|
||||||
Cmd.AddCommand(removeRuleChainCmd)
|
|
||||||
|
|
||||||
removeRuleChainCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
removeRuleChainCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
|
|
||||||
removeRuleChainCmd.Flags().String(targetTypeFlag, "", targetTypeDesc)
|
|
||||||
_ = removeRuleChainCmd.MarkFlagRequired(targetTypeFlag)
|
|
||||||
removeRuleChainCmd.Flags().String(targetNameFlag, "", targetNameDesc)
|
|
||||||
_ = removeRuleChainCmd.MarkFlagRequired(targetNameFlag)
|
|
||||||
removeRuleChainCmd.Flags().String(chainIDFlag, "", chainIDDesc)
|
|
||||||
removeRuleChainCmd.Flags().String(chainNameFlag, ingress, chainNameFlagDesc)
|
|
||||||
removeRuleChainCmd.Flags().Bool(commonflags.AllFlag, false, "Remove all chains for target")
|
|
||||||
removeRuleChainCmd.MarkFlagsMutuallyExclusive(commonflags.AllFlag, chainIDFlag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initListRuleChainsCmd() {
|
|
||||||
Cmd.AddCommand(listRuleChainsCmd)
|
|
||||||
|
|
||||||
listRuleChainsCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
listRuleChainsCmd.Flags().StringP(targetTypeFlag, "t", "", targetTypeDesc)
|
|
||||||
_ = listRuleChainsCmd.MarkFlagRequired(targetTypeFlag)
|
|
||||||
listRuleChainsCmd.Flags().String(targetNameFlag, "", targetNameDesc)
|
|
||||||
_ = listRuleChainsCmd.MarkFlagRequired(targetNameFlag)
|
|
||||||
listRuleChainsCmd.Flags().Bool(jsonFlag, false, jsonFlagDesc)
|
|
||||||
listRuleChainsCmd.Flags().String(chainNameFlag, ingress, chainNameFlagDesc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initSetAdminCmd() {
|
|
||||||
Cmd.AddCommand(setAdminCmd)
|
|
||||||
|
|
||||||
setAdminCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
setAdminCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
setAdminCmd.Flags().String(addrAdminFlag, "", addrAdminDesc)
|
|
||||||
_ = setAdminCmd.MarkFlagRequired(addrAdminFlag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initGetAdminCmd() {
|
|
||||||
Cmd.AddCommand(getAdminCmd)
|
|
||||||
|
|
||||||
getAdminCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initListTargetsCmd() {
|
|
||||||
Cmd.AddCommand(listTargetsCmd)
|
|
||||||
|
|
||||||
listTargetsCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
listTargetsCmd.Flags().StringP(targetTypeFlag, "t", "", targetTypeDesc)
|
|
||||||
_ = listTargetsCmd.MarkFlagRequired(targetTypeFlag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func addRuleChain(cmd *cobra.Command, _ []string) {
|
|
||||||
chain := parseChain(cmd)
|
|
||||||
target := parseTarget(cmd)
|
|
||||||
pci, ac := newPolicyContractInterface(cmd)
|
|
||||||
h, vub, err := pci.AddMorphRuleChain(parseChainName(cmd), target, chain)
|
|
||||||
cmd.Println("Waiting for transaction to persist...")
|
|
||||||
_, err = ac.Wait(h, vub, err)
|
|
||||||
commonCmd.ExitOnErr(cmd, "add rule chain error: %w", err)
|
|
||||||
cmd.Println("Rule chain added successfully")
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeRuleChain(cmd *cobra.Command, _ []string) {
|
|
||||||
target := parseTarget(cmd)
|
|
||||||
pci, ac := newPolicyContractInterface(cmd)
|
|
||||||
removeAll, _ := cmd.Flags().GetBool(commonflags.AllFlag)
|
|
||||||
if removeAll {
|
|
||||||
h, vub, err := pci.RemoveMorphRuleChainsByTarget(parseChainName(cmd), target)
|
|
||||||
cmd.Println("Waiting for transaction to persist...")
|
|
||||||
_, err = ac.Wait(h, vub, err)
|
|
||||||
commonCmd.ExitOnErr(cmd, "remove rule chain error: %w", err)
|
|
||||||
cmd.Println("All chains for target removed successfully")
|
|
||||||
} else {
|
|
||||||
chainID := parseChainID(cmd)
|
|
||||||
h, vub, err := pci.RemoveMorphRuleChain(parseChainName(cmd), target, chainID)
|
|
||||||
cmd.Println("Waiting for transaction to persist...")
|
|
||||||
_, err = ac.Wait(h, vub, err)
|
|
||||||
commonCmd.ExitOnErr(cmd, "remove rule chain error: %w", err)
|
|
||||||
cmd.Println("Rule chain removed successfully")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func listRuleChains(cmd *cobra.Command, _ []string) {
|
|
||||||
target := parseTarget(cmd)
|
|
||||||
pci, _ := newPolicyContractReaderInterface(cmd)
|
|
||||||
chains, err := pci.ListMorphRuleChains(parseChainName(cmd), target)
|
|
||||||
commonCmd.ExitOnErr(cmd, "list rule chains error: %w", err)
|
|
||||||
if len(chains) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON, _ := cmd.Flags().GetBool(jsonFlag)
|
|
||||||
if toJSON {
|
|
||||||
prettyJSONFormat(cmd, chains)
|
|
||||||
} else {
|
|
||||||
for _, c := range chains {
|
|
||||||
parseutil.PrintHumanReadableAPEChain(cmd, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setAdmin(cmd *cobra.Command, _ []string) {
|
|
||||||
s, _ := cmd.Flags().GetString(addrAdminFlag)
|
|
||||||
addr, err := util.Uint160DecodeStringLE(s)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't decode admin addr: %w", err)
|
|
||||||
pci, ac := newPolicyContractInterface(cmd)
|
|
||||||
h, vub, err := pci.SetAdmin(addr)
|
|
||||||
cmd.Println("Waiting for transaction to persist...")
|
|
||||||
_, err = ac.Wait(h, vub, err)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't set admin: %w", err)
|
|
||||||
cmd.Println("Admin set successfully")
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAdmin(cmd *cobra.Command, _ []string) {
|
|
||||||
pci, _ := newPolicyContractReaderInterface(cmd)
|
|
||||||
addr, err := pci.GetAdmin()
|
|
||||||
commonCmd.ExitOnErr(cmd, "unable to get admin: %w", err)
|
|
||||||
cmd.Println(addr.StringLE())
|
|
||||||
}
|
|
||||||
|
|
||||||
func listTargets(cmd *cobra.Command, _ []string) {
|
|
||||||
typ, err := parseTargetType(cmd)
|
|
||||||
commonCmd.ExitOnErr(cmd, "parse target type error: %w", err)
|
|
||||||
pci, inv := newPolicyContractReaderInterface(cmd)
|
|
||||||
|
|
||||||
sid, it, err := pci.ListTargetsIterator(typ)
|
|
||||||
commonCmd.ExitOnErr(cmd, "list targets error: %w", err)
|
|
||||||
items, err := inv.TraverseIterator(sid, &it, 0)
|
|
||||||
for err == nil && len(items) != 0 {
|
|
||||||
for _, item := range items {
|
|
||||||
bts, err := item.TryBytes()
|
|
||||||
commonCmd.ExitOnErr(cmd, "list targets error: %w", err)
|
|
||||||
if len(bts) == 0 {
|
|
||||||
cmd.Println("(no name)")
|
|
||||||
} else {
|
|
||||||
cmd.Println(string(bts))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items, err = inv.TraverseIterator(sid, &it, 0)
|
|
||||||
commonCmd.ExitOnErr(cmd, "unable to list targets: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func prettyJSONFormat(cmd *cobra.Command, chains []*apechain.Chain) {
|
|
||||||
wr := bytes.NewBufferString("")
|
|
||||||
data, err := json.Marshal(chains)
|
|
||||||
if err == nil {
|
|
||||||
err = json.Indent(wr, data, "", " ")
|
|
||||||
}
|
|
||||||
commonCmd.ExitOnErr(cmd, "print rule chain error: %w", err)
|
|
||||||
cmd.Println(wr)
|
|
||||||
}
|
|
|
@ -1,154 +0,0 @@
|
||||||
package ape
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper"
|
|
||||||
parseutil "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/util"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
apechain "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
|
||||||
policyengine "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
|
|
||||||
morph "git.frostfs.info/TrueCloudLab/policy-engine/pkg/morph/policy"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
ingress = "ingress"
|
|
||||||
s3 = "s3"
|
|
||||||
)
|
|
||||||
|
|
||||||
var mChainName = map[string]apechain.Name{
|
|
||||||
ingress: apechain.Ingress,
|
|
||||||
s3: apechain.S3,
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
errUnknownTargetType = errors.New("unknown target type")
|
|
||||||
errChainIDCannotBeEmpty = errors.New("chain id cannot be empty")
|
|
||||||
errRuleIsNotParsed = errors.New("rule is not passed")
|
|
||||||
errUnsupportedChainName = errors.New("unsupported chain name")
|
|
||||||
)
|
|
||||||
|
|
||||||
func parseTarget(cmd *cobra.Command) policyengine.Target {
|
|
||||||
name, _ := cmd.Flags().GetString(targetNameFlag)
|
|
||||||
typ, err := parseTargetType(cmd)
|
|
||||||
|
|
||||||
// interpret "root" namespace as empty
|
|
||||||
if typ == policyengine.Namespace && name == "root" {
|
|
||||||
name = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
commonCmd.ExitOnErr(cmd, "read target type error: %w", err)
|
|
||||||
|
|
||||||
return policyengine.Target{
|
|
||||||
Name: name,
|
|
||||||
Type: typ,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseTargetType(cmd *cobra.Command) (policyengine.TargetType, error) {
|
|
||||||
typ, _ := cmd.Flags().GetString(targetTypeFlag)
|
|
||||||
switch typ {
|
|
||||||
case namespaceTarget:
|
|
||||||
return policyengine.Namespace, nil
|
|
||||||
case containerTarget:
|
|
||||||
return policyengine.Container, nil
|
|
||||||
case userTarget:
|
|
||||||
return policyengine.User, nil
|
|
||||||
case groupTarget:
|
|
||||||
return policyengine.Group, nil
|
|
||||||
}
|
|
||||||
return -1, errUnknownTargetType
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseChainID(cmd *cobra.Command) apechain.ID {
|
|
||||||
chainID, _ := cmd.Flags().GetString(chainIDFlag)
|
|
||||||
if chainID == "" {
|
|
||||||
commonCmd.ExitOnErr(cmd, "read chain id error: %w",
|
|
||||||
errChainIDCannotBeEmpty)
|
|
||||||
}
|
|
||||||
return apechain.ID(chainID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseChain(cmd *cobra.Command) *apechain.Chain {
|
|
||||||
chain := new(apechain.Chain)
|
|
||||||
|
|
||||||
if rules, _ := cmd.Flags().GetStringArray(ruleFlag); len(rules) > 0 {
|
|
||||||
commonCmd.ExitOnErr(cmd, "parser error: %w", parseutil.ParseAPEChain(chain, rules))
|
|
||||||
} else if encPath, _ := cmd.Flags().GetString(pathFlag); encPath != "" {
|
|
||||||
commonCmd.ExitOnErr(cmd, "decode binary or json error: %w", parseutil.ParseAPEChainBinaryOrJSON(chain, encPath))
|
|
||||||
} else {
|
|
||||||
commonCmd.ExitOnErr(cmd, "parser error: %w", errRuleIsNotParsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
chain.ID = parseChainID(cmd)
|
|
||||||
|
|
||||||
cmd.Println("Parsed chain:")
|
|
||||||
parseutil.PrintHumanReadableAPEChain(cmd, chain)
|
|
||||||
|
|
||||||
return chain
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseChainName(cmd *cobra.Command) apechain.Name {
|
|
||||||
chainName, _ := cmd.Flags().GetString(chainNameFlag)
|
|
||||||
apeChainName, ok := mChainName[strings.ToLower(chainName)]
|
|
||||||
if !ok {
|
|
||||||
commonCmd.ExitOnErr(cmd, "", errUnsupportedChainName)
|
|
||||||
}
|
|
||||||
return apeChainName
|
|
||||||
}
|
|
||||||
|
|
||||||
// invokerAdapter adapats invoker.Invoker to ContractStorageInvoker interface.
|
|
||||||
type invokerAdapter struct {
|
|
||||||
*invoker.Invoker
|
|
||||||
rpcActor invoker.RPCInvoke
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *invokerAdapter) GetRPCInvoker() invoker.RPCInvoke {
|
|
||||||
return n.rpcActor
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPolicyContractReaderInterface(cmd *cobra.Command) (*morph.ContractStorageReader, *invoker.Invoker) {
|
|
||||||
c, err := helper.GetN3Client(viper.GetViper())
|
|
||||||
commonCmd.ExitOnErr(cmd, "unable to create NEO rpc client: %w", err)
|
|
||||||
|
|
||||||
inv := invoker.New(c, nil)
|
|
||||||
var ch util.Uint160
|
|
||||||
r := management.NewReader(inv)
|
|
||||||
nnsCs, err := helper.GetContractByID(r, 1)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't get NNS contract state: %w", err)
|
|
||||||
|
|
||||||
ch, err = helper.NNSResolveHash(inv, nnsCs.Hash, helper.DomainOf(constants.PolicyContract))
|
|
||||||
commonCmd.ExitOnErr(cmd, "unable to resolve policy contract hash: %w", err)
|
|
||||||
|
|
||||||
invokerAdapter := &invokerAdapter{
|
|
||||||
Invoker: inv,
|
|
||||||
rpcActor: c,
|
|
||||||
}
|
|
||||||
|
|
||||||
return morph.NewContractStorageReader(invokerAdapter, ch), inv
|
|
||||||
}
|
|
||||||
|
|
||||||
func newPolicyContractInterface(cmd *cobra.Command) (*morph.ContractStorage, *helper.LocalActor) {
|
|
||||||
c, err := helper.GetN3Client(viper.GetViper())
|
|
||||||
commonCmd.ExitOnErr(cmd, "unable to create NEO rpc client: %w", err)
|
|
||||||
|
|
||||||
ac, err := helper.NewLocalActor(cmd, c, constants.ConsensusAccountName)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't create actor: %w", err)
|
|
||||||
|
|
||||||
var ch util.Uint160
|
|
||||||
r := management.NewReader(ac.Invoker)
|
|
||||||
nnsCs, err := helper.GetContractByID(r, 1)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't get NNS contract state: %w", err)
|
|
||||||
|
|
||||||
ch, err = helper.NNSResolveHash(ac.Invoker, nnsCs.Hash, helper.DomainOf(constants.PolicyContract))
|
|
||||||
commonCmd.ExitOnErr(cmd, "unable to resolve policy contract hash: %w", err)
|
|
||||||
|
|
||||||
return morph.NewContractStorage(ac, ch), ac
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
package ape
|
|
||||||
|
|
||||||
import "github.com/spf13/cobra"
|
|
||||||
|
|
||||||
var Cmd = &cobra.Command{
|
|
||||||
Use: "ape",
|
|
||||||
Short: "Section for APE configuration commands",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
initAddRuleChainCmd()
|
|
||||||
initRemoveRuleChainCmd()
|
|
||||||
initListRuleChainsCmd()
|
|
||||||
initSetAdminCmd()
|
|
||||||
initGetAdminCmd()
|
|
||||||
initListTargetsCmd()
|
|
||||||
}
|
|
241
cmd/frostfs-adm/internal/modules/morph/balance.go
Normal file
241
cmd/frostfs-adm/internal/modules/morph/balance.go
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/elliptic"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-contract/nns"
|
||||||
|
"github.com/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/gas"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/rolemgmt"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type accBalancePair struct {
|
||||||
|
scriptHash util.Uint160
|
||||||
|
balance *big.Int
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
dumpBalancesStorageFlag = "storage"
|
||||||
|
dumpBalancesAlphabetFlag = "alphabet"
|
||||||
|
dumpBalancesProxyFlag = "proxy"
|
||||||
|
dumpBalancesUseScriptHashFlag = "script-hash"
|
||||||
|
|
||||||
|
// notaryEnabled signifies whether contracts were deployed in a notary-enabled environment.
|
||||||
|
// The setting is here to simplify testing and building the command for testnet (notary currently disabled).
|
||||||
|
// It will be removed eventually.
|
||||||
|
notaryEnabled = true
|
||||||
|
)
|
||||||
|
|
||||||
|
func dumpBalances(cmd *cobra.Command, _ []string) error {
|
||||||
|
var (
|
||||||
|
dumpStorage, _ = cmd.Flags().GetBool(dumpBalancesStorageFlag)
|
||||||
|
dumpAlphabet, _ = cmd.Flags().GetBool(dumpBalancesAlphabetFlag)
|
||||||
|
dumpProxy, _ = cmd.Flags().GetBool(dumpBalancesProxyFlag)
|
||||||
|
nnsCs *state.Contract
|
||||||
|
nmHash util.Uint160
|
||||||
|
)
|
||||||
|
|
||||||
|
c, err := getN3Client(viper.GetViper())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
inv := invoker.New(c, nil)
|
||||||
|
|
||||||
|
if !notaryEnabled || dumpStorage || dumpAlphabet || dumpProxy {
|
||||||
|
nnsCs, err = c.GetContractStateByID(1)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't get NNS contract info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nmHash, err = nnsResolveHash(inv, nnsCs.Hash, netmapContract+".frostfs")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't get netmap contract hash: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
irList, err := fetchIRNodes(c, nmHash, rolemgmt.Hash)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fetchBalances(inv, gas.Hash, irList); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printBalances(cmd, "Inner ring nodes balances:", irList)
|
||||||
|
|
||||||
|
if dumpStorage {
|
||||||
|
arr, err := unwrap.Array(inv.Call(nmHash, "netmap"))
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("can't fetch the list of storage nodes")
|
||||||
|
}
|
||||||
|
|
||||||
|
snList := make([]accBalancePair, len(arr))
|
||||||
|
for i := range arr {
|
||||||
|
node, ok := arr[i].Value().([]stackitem.Item)
|
||||||
|
if !ok || len(node) == 0 {
|
||||||
|
return errors.New("can't parse the list of storage nodes")
|
||||||
|
}
|
||||||
|
bs, err := node[0].TryBytes()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("can't parse the list of storage nodes")
|
||||||
|
}
|
||||||
|
var ni netmap.NodeInfo
|
||||||
|
if err := ni.Unmarshal(bs); err != nil {
|
||||||
|
return fmt.Errorf("can't parse the list of storage nodes: %w", err)
|
||||||
|
}
|
||||||
|
pub, err := keys.NewPublicKeyFromBytes(ni.PublicKey(), elliptic.P256())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't parse storage node public key: %w", err)
|
||||||
|
}
|
||||||
|
snList[i].scriptHash = pub.GetScriptHash()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fetchBalances(inv, gas.Hash, snList); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printBalances(cmd, "\nStorage node balances:", snList)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dumpProxy {
|
||||||
|
h, err := nnsResolveHash(inv, nnsCs.Hash, proxyContract+".frostfs")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't get hash of the proxy contract: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyList := []accBalancePair{{scriptHash: h}}
|
||||||
|
if err := fetchBalances(inv, gas.Hash, proxyList); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printBalances(cmd, "\nProxy contract balance:", proxyList)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dumpAlphabet {
|
||||||
|
alphaList := make([]accBalancePair, len(irList))
|
||||||
|
|
||||||
|
w := io.NewBufBinWriter()
|
||||||
|
for i := range alphaList {
|
||||||
|
emit.AppCall(w.BinWriter, nnsCs.Hash, "resolve", callflag.ReadOnly,
|
||||||
|
getAlphabetNNSDomain(i),
|
||||||
|
int64(nns.TXT))
|
||||||
|
}
|
||||||
|
if w.Err != nil {
|
||||||
|
panic(w.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
alphaRes, err := c.InvokeScript(w.Bytes(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't fetch info from NNS: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range alphaList {
|
||||||
|
h, err := parseNNSResolveResult(alphaRes.Stack[i])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't fetch the alphabet contract #%d hash: %w", i, err)
|
||||||
|
}
|
||||||
|
alphaList[i].scriptHash = h
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fetchBalances(inv, gas.Hash, alphaList); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printBalances(cmd, "\nAlphabet contracts balances:", alphaList)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchIRNodes(c Client, nmHash, desigHash util.Uint160) ([]accBalancePair, error) {
|
||||||
|
var irList []accBalancePair
|
||||||
|
|
||||||
|
inv := invoker.New(c, nil)
|
||||||
|
|
||||||
|
if notaryEnabled {
|
||||||
|
height, err := c.GetBlockCount()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't get block height: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
arr, err := getDesignatedByRole(inv, desigHash, noderoles.NeoFSAlphabet, height)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("can't fetch list of IR nodes from the netmap contract")
|
||||||
|
}
|
||||||
|
|
||||||
|
irList = make([]accBalancePair, len(arr))
|
||||||
|
for i := range arr {
|
||||||
|
irList[i].scriptHash = arr[i].GetScriptHash()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
arr, err := unwrap.ArrayOfBytes(inv.Call(nmHash, "innerRingList"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("can't fetch list of IR nodes from the netmap contract")
|
||||||
|
}
|
||||||
|
|
||||||
|
irList = make([]accBalancePair, len(arr))
|
||||||
|
for i := range arr {
|
||||||
|
pub, err := keys.NewPublicKeyFromBytes(arr[i], elliptic.P256())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't parse IR node public key: %w", err)
|
||||||
|
}
|
||||||
|
irList[i].scriptHash = pub.GetScriptHash()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return irList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printBalances(cmd *cobra.Command, prefix string, accounts []accBalancePair) {
|
||||||
|
useScriptHash, _ := cmd.Flags().GetBool(dumpBalancesUseScriptHashFlag)
|
||||||
|
|
||||||
|
cmd.Println(prefix)
|
||||||
|
for i := range accounts {
|
||||||
|
var addr string
|
||||||
|
if useScriptHash {
|
||||||
|
addr = accounts[i].scriptHash.StringLE()
|
||||||
|
} else {
|
||||||
|
addr = address.Uint160ToString(accounts[i].scriptHash)
|
||||||
|
}
|
||||||
|
cmd.Printf("%s: %s\n", addr, fixedn.ToString(accounts[i].balance, 8))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchBalances(c *invoker.Invoker, gasHash util.Uint160, accounts []accBalancePair) error {
|
||||||
|
w := io.NewBufBinWriter()
|
||||||
|
for i := range accounts {
|
||||||
|
emit.AppCall(w.BinWriter, gasHash, "balanceOf", callflag.ReadStates, accounts[i].scriptHash)
|
||||||
|
}
|
||||||
|
if w.Err != nil {
|
||||||
|
panic(w.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := c.Run(w.Bytes())
|
||||||
|
if err != nil || res.State != vmstate.Halt.String() || len(res.Stack) != len(accounts) {
|
||||||
|
return errors.New("can't fetch account balances")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range accounts {
|
||||||
|
bal, err := res.Stack[i].TryInteger()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't parse account balance: %w", err)
|
||||||
|
}
|
||||||
|
accounts[i].balance = bal
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,246 +0,0 @@
|
||||||
package balance
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/elliptic"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/gas"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/rolemgmt"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
type accBalancePair struct {
|
|
||||||
scriptHash util.Uint160
|
|
||||||
balance *big.Int
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
dumpBalancesStorageFlag = "storage"
|
|
||||||
dumpBalancesAlphabetFlag = "alphabet"
|
|
||||||
dumpBalancesProxyFlag = "proxy"
|
|
||||||
dumpBalancesUseScriptHashFlag = "script-hash"
|
|
||||||
)
|
|
||||||
|
|
||||||
func dumpBalances(cmd *cobra.Command, _ []string) error {
|
|
||||||
var (
|
|
||||||
dumpStorage, _ = cmd.Flags().GetBool(dumpBalancesStorageFlag)
|
|
||||||
dumpAlphabet, _ = cmd.Flags().GetBool(dumpBalancesAlphabetFlag)
|
|
||||||
dumpProxy, _ = cmd.Flags().GetBool(dumpBalancesProxyFlag)
|
|
||||||
nnsCs *state.Contract
|
|
||||||
nmHash util.Uint160
|
|
||||||
)
|
|
||||||
|
|
||||||
c, err := helper.GetN3Client(viper.GetViper())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
inv := invoker.New(c, nil)
|
|
||||||
|
|
||||||
if dumpStorage || dumpAlphabet || dumpProxy {
|
|
||||||
r := management.NewReader(inv)
|
|
||||||
nnsCs, err = helper.GetContractByID(r, 1)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get NNS contract info: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nmHash, err = helper.NNSResolveHash(inv, nnsCs.Hash, helper.DomainOf(constants.NetmapContract))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get netmap contract hash: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
irList, err := fetchIRNodes(c, rolemgmt.Hash)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := fetchBalances(inv, gas.Hash, irList); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
printBalances(cmd, "Inner ring nodes balances:", irList)
|
|
||||||
|
|
||||||
if dumpStorage {
|
|
||||||
if err := printStorageNodeBalances(cmd, inv, nmHash); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if dumpProxy {
|
|
||||||
if err := printProxyContractBalance(cmd, inv, nnsCs.Hash); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if dumpAlphabet {
|
|
||||||
if err := printAlphabetContractBalances(cmd, c, inv, len(irList), nnsCs.Hash); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func printStorageNodeBalances(cmd *cobra.Command, inv *invoker.Invoker, nmHash util.Uint160) error {
|
|
||||||
arr, err := unwrap.Array(inv.Call(nmHash, "netmap"))
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("can't fetch the list of storage nodes")
|
|
||||||
}
|
|
||||||
|
|
||||||
snList := make([]accBalancePair, len(arr))
|
|
||||||
for i := range arr {
|
|
||||||
node, ok := arr[i].Value().([]stackitem.Item)
|
|
||||||
if !ok || len(node) == 0 {
|
|
||||||
return errors.New("can't parse the list of storage nodes")
|
|
||||||
}
|
|
||||||
bs, err := node[0].TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("can't parse the list of storage nodes")
|
|
||||||
}
|
|
||||||
var ni netmap.NodeInfo
|
|
||||||
if err := ni.Unmarshal(bs); err != nil {
|
|
||||||
return fmt.Errorf("can't parse the list of storage nodes: %w", err)
|
|
||||||
}
|
|
||||||
pub, err := keys.NewPublicKeyFromBytes(ni.PublicKey(), elliptic.P256())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't parse storage node public key: %w", err)
|
|
||||||
}
|
|
||||||
snList[i].scriptHash = pub.GetScriptHash()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := fetchBalances(inv, gas.Hash, snList); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
printBalances(cmd, "\nStorage node balances:", snList)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func printProxyContractBalance(cmd *cobra.Command, inv *invoker.Invoker, nnsHash util.Uint160) error {
|
|
||||||
h, err := helper.NNSResolveHash(inv, nnsHash, helper.DomainOf(constants.ProxyContract))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get hash of the proxy contract: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyList := []accBalancePair{{scriptHash: h}}
|
|
||||||
if err := fetchBalances(inv, gas.Hash, proxyList); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
printBalances(cmd, "\nProxy contract balance:", proxyList)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func printAlphabetContractBalances(cmd *cobra.Command, c helper.Client, inv *invoker.Invoker, count int, nnsHash util.Uint160) error {
|
|
||||||
alphaList := make([]accBalancePair, count)
|
|
||||||
|
|
||||||
w := io.NewBufBinWriter()
|
|
||||||
for i := range alphaList {
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "resolve", callflag.ReadOnly,
|
|
||||||
helper.GetAlphabetNNSDomain(i),
|
|
||||||
int64(nns.TXT))
|
|
||||||
}
|
|
||||||
if w.Err != nil {
|
|
||||||
panic(w.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
alphaRes, err := c.InvokeScript(w.Bytes(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't fetch info from NNS: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range alphaList {
|
|
||||||
h, err := helper.ParseNNSResolveResult(alphaRes.Stack[i])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't fetch the alphabet contract #%d hash: %w", i, err)
|
|
||||||
}
|
|
||||||
alphaList[i].scriptHash = h
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := fetchBalances(inv, gas.Hash, alphaList); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
printBalances(cmd, "\nAlphabet contracts balances:", alphaList)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchIRNodes(c helper.Client, desigHash util.Uint160) ([]accBalancePair, error) {
|
|
||||||
inv := invoker.New(c, nil)
|
|
||||||
|
|
||||||
height, err := c.GetBlockCount()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't get block height: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
arr, err := helper.GetDesignatedByRole(inv, desigHash, noderoles.NeoFSAlphabet, height)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("can't fetch list of IR nodes from the netmap contract")
|
|
||||||
}
|
|
||||||
|
|
||||||
irList := make([]accBalancePair, len(arr))
|
|
||||||
for i := range arr {
|
|
||||||
irList[i].scriptHash = arr[i].GetScriptHash()
|
|
||||||
}
|
|
||||||
return irList, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func printBalances(cmd *cobra.Command, prefix string, accounts []accBalancePair) {
|
|
||||||
useScriptHash, _ := cmd.Flags().GetBool(dumpBalancesUseScriptHashFlag)
|
|
||||||
|
|
||||||
cmd.Println(prefix)
|
|
||||||
for i := range accounts {
|
|
||||||
var addr string
|
|
||||||
if useScriptHash {
|
|
||||||
addr = accounts[i].scriptHash.StringLE()
|
|
||||||
} else {
|
|
||||||
addr = address.Uint160ToString(accounts[i].scriptHash)
|
|
||||||
}
|
|
||||||
cmd.Printf("%s: %s\n", addr, fixedn.ToString(accounts[i].balance, 8))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchBalances(c *invoker.Invoker, gasHash util.Uint160, accounts []accBalancePair) error {
|
|
||||||
w := io.NewBufBinWriter()
|
|
||||||
for i := range accounts {
|
|
||||||
emit.AppCall(w.BinWriter, gasHash, "balanceOf", callflag.ReadStates, accounts[i].scriptHash)
|
|
||||||
}
|
|
||||||
if w.Err != nil {
|
|
||||||
panic(w.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := c.Run(w.Bytes())
|
|
||||||
if err != nil || res.State != vmstate.Halt.String() || len(res.Stack) != len(accounts) {
|
|
||||||
return errors.New("can't fetch account balances")
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range accounts {
|
|
||||||
bal, err := res.Stack[i].TryInteger()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't parse account balance: %w", err)
|
|
||||||
}
|
|
||||||
accounts[i].balance = bal
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
package balance
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
var DumpCmd = &cobra.Command{
|
|
||||||
Use: "dump-balances",
|
|
||||||
Short: "Dump GAS balances",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
RunE: dumpBalances,
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDumpBalancesCmd() {
|
|
||||||
DumpCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
DumpCmd.Flags().BoolP(dumpBalancesStorageFlag, "s", false, "Dump balances of storage nodes from the current netmap")
|
|
||||||
DumpCmd.Flags().BoolP(dumpBalancesAlphabetFlag, "a", false, "Dump balances of alphabet contracts")
|
|
||||||
DumpCmd.Flags().BoolP(dumpBalancesProxyFlag, "p", false, "Dump balances of the proxy contract")
|
|
||||||
DumpCmd.Flags().Bool(dumpBalancesUseScriptHashFlag, false, "Use script-hash format for addresses")
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
initDumpBalancesCmd()
|
|
||||||
}
|
|
192
cmd/frostfs-adm/internal/modules/morph/config.go
Normal file
192
cmd/frostfs-adm/internal/modules/morph/config.go
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
const forceConfigSet = "force"
|
||||||
|
|
||||||
|
func dumpNetworkConfig(cmd *cobra.Command, _ []string) error {
|
||||||
|
c, err := getN3Client(viper.GetViper())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't create N3 client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inv := invoker.New(c, nil)
|
||||||
|
|
||||||
|
cs, err := c.GetContractStateByID(1)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't get NNS contract info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nmHash, err := nnsResolveHash(inv, cs.Hash, netmapContract+".frostfs")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't get netmap contract hash: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
arr, err := unwrap.Array(inv.Call(nmHash, "listConfig"))
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("can't fetch list of network config keys from the netmap contract")
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
tw := tabwriter.NewWriter(buf, 0, 2, 2, ' ', 0)
|
||||||
|
|
||||||
|
for _, param := range arr {
|
||||||
|
tuple, ok := param.Value().([]stackitem.Item)
|
||||||
|
if !ok || len(tuple) != 2 {
|
||||||
|
return errors.New("invalid ListConfig response from netmap contract")
|
||||||
|
}
|
||||||
|
|
||||||
|
k, err := tuple[0].TryBytes()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("invalid config key from netmap contract")
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := tuple[1].TryBytes()
|
||||||
|
if err != nil {
|
||||||
|
return invalidConfigValueErr(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch string(k) {
|
||||||
|
case netmapAuditFeeKey, netmapBasicIncomeRateKey,
|
||||||
|
netmapContainerFeeKey, netmapContainerAliasFeeKey,
|
||||||
|
netmapEigenTrustIterationsKey,
|
||||||
|
netmapEpochKey, netmapInnerRingCandidateFeeKey,
|
||||||
|
netmapMaxObjectSizeKey, netmapWithdrawFeeKey:
|
||||||
|
nbuf := make([]byte, 8)
|
||||||
|
copy(nbuf[:], v)
|
||||||
|
n := binary.LittleEndian.Uint64(nbuf)
|
||||||
|
_, _ = tw.Write([]byte(fmt.Sprintf("%s:\t%d (int)\n", k, n)))
|
||||||
|
case netmapEigenTrustAlphaKey:
|
||||||
|
_, _ = tw.Write([]byte(fmt.Sprintf("%s:\t%s (str)\n", k, v)))
|
||||||
|
case netmapHomomorphicHashDisabledKey, netmapMaintenanceAllowedKey:
|
||||||
|
vBool, err := tuple[1].TryBool()
|
||||||
|
if err != nil {
|
||||||
|
return invalidConfigValueErr(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = tw.Write([]byte(fmt.Sprintf("%s:\t%t (bool)\n", k, vBool)))
|
||||||
|
default:
|
||||||
|
_, _ = tw.Write([]byte(fmt.Sprintf("%s:\t%s (hex)\n", k, hex.EncodeToString(v))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = tw.Flush()
|
||||||
|
cmd.Print(buf.String())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setConfigCmd(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return errors.New("empty config pairs")
|
||||||
|
}
|
||||||
|
|
||||||
|
wCtx, err := newInitializeContext(cmd, viper.GetViper())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't initialize context: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cs, err := wCtx.Client.GetContractStateByID(1)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't get NNS contract info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nmHash, err := nnsResolveHash(wCtx.ReadOnlyInvoker, cs.Hash, netmapContract+".frostfs")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't get netmap contract hash: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
forceFlag, _ := cmd.Flags().GetBool(forceConfigSet)
|
||||||
|
|
||||||
|
bw := io.NewBufBinWriter()
|
||||||
|
for _, arg := range args {
|
||||||
|
k, v, err := parseConfigPair(arg, forceFlag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// In NeoFS this is done via Notary contract. Here, however, we can form the
|
||||||
|
// transaction locally. The first `nil` argument is required only for notary
|
||||||
|
// disabled environment which is not supported by that command.
|
||||||
|
emit.AppCall(bw.BinWriter, nmHash, "setConfig", callflag.All, nil, k, v)
|
||||||
|
if bw.Err != nil {
|
||||||
|
return fmt.Errorf("can't form raw transaction: %w", bw.Err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = wCtx.sendConsensusTx(bw.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return wCtx.awaitTx()
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseConfigPair(kvStr string, force bool) (key string, val interface{}, err error) {
|
||||||
|
kv := strings.SplitN(kvStr, "=", 2)
|
||||||
|
if len(kv) != 2 {
|
||||||
|
return "", nil, fmt.Errorf("invalid parameter format: must be 'key=val', got: %s", kvStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
key = kv[0]
|
||||||
|
valRaw := kv[1]
|
||||||
|
|
||||||
|
switch key {
|
||||||
|
case netmapAuditFeeKey, netmapBasicIncomeRateKey,
|
||||||
|
netmapContainerFeeKey, netmapContainerAliasFeeKey,
|
||||||
|
netmapEigenTrustIterationsKey,
|
||||||
|
netmapEpochKey, netmapInnerRingCandidateFeeKey,
|
||||||
|
netmapMaxObjectSizeKey, netmapWithdrawFeeKey:
|
||||||
|
val, err = strconv.ParseInt(valRaw, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("could not parse %s's value '%s' as int: %w", key, valRaw, err)
|
||||||
|
}
|
||||||
|
case netmapEigenTrustAlphaKey:
|
||||||
|
// just check that it could
|
||||||
|
// be parsed correctly
|
||||||
|
_, err = strconv.ParseFloat(kv[1], 64)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("could not parse %s's value '%s' as float: %w", key, valRaw, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
val = valRaw
|
||||||
|
case netmapHomomorphicHashDisabledKey, netmapMaintenanceAllowedKey:
|
||||||
|
val, err = strconv.ParseBool(valRaw)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("could not parse %s's value '%s' as bool: %w", key, valRaw, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if !force {
|
||||||
|
return "", nil, fmt.Errorf(
|
||||||
|
"'%s' key is not well-known, use '--%s' flag if want to set it anyway",
|
||||||
|
key, forceConfigSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
val = valRaw
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidConfigValueErr(key []byte) error {
|
||||||
|
return fmt.Errorf("invalid %s config value from netmap contract", key)
|
||||||
|
}
|
|
@ -1,219 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"text/tabwriter"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/netmap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
const forceConfigSet = "force"
|
|
||||||
|
|
||||||
func dumpNetworkConfig(cmd *cobra.Command, _ []string) error {
|
|
||||||
c, err := helper.GetN3Client(viper.GetViper())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't create N3 client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
inv := invoker.New(c, nil)
|
|
||||||
r := management.NewReader(inv)
|
|
||||||
|
|
||||||
cs, err := helper.GetContractByID(r, 1)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get NNS contract info: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nmHash, err := helper.NNSResolveHash(inv, cs.Hash, helper.DomainOf(constants.NetmapContract))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get netmap contract hash: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
arr, err := unwrap.Array(inv.Call(nmHash, "listConfig"))
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("can't fetch list of network config keys from the netmap contract")
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
tw := tabwriter.NewWriter(buf, 0, 2, 2, ' ', 0)
|
|
||||||
|
|
||||||
m, err := helper.ParseConfigFromNetmapContract(arr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for k, v := range m {
|
|
||||||
switch k {
|
|
||||||
case netmap.ContainerFeeConfig, netmap.ContainerAliasFeeConfig,
|
|
||||||
netmap.EpochDurationConfig, netmap.IrCandidateFeeConfig,
|
|
||||||
netmap.MaxObjectSizeConfig, netmap.WithdrawFeeConfig,
|
|
||||||
netmap.MaxECDataCountConfig, netmap.MaxECParityCountConfig:
|
|
||||||
nbuf := make([]byte, 8)
|
|
||||||
copy(nbuf[:], v)
|
|
||||||
n := binary.LittleEndian.Uint64(nbuf)
|
|
||||||
_, _ = tw.Write([]byte(fmt.Sprintf("%s:\t%d (int)\n", k, n)))
|
|
||||||
case netmap.HomomorphicHashingDisabledKey, netmap.MaintenanceModeAllowedConfig:
|
|
||||||
if len(v) == 0 || len(v) > 1 {
|
|
||||||
return helper.InvalidConfigValueErr(k)
|
|
||||||
}
|
|
||||||
_, _ = tw.Write([]byte(fmt.Sprintf("%s:\t%t (bool)\n", k, v[0] == 1)))
|
|
||||||
default:
|
|
||||||
_, _ = tw.Write([]byte(fmt.Sprintf("%s:\t%s (hex)\n", k, hex.EncodeToString(v))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = tw.Flush()
|
|
||||||
cmd.Print(buf.String())
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetConfigCmd(cmd *cobra.Command, args []string) error {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return errors.New("empty config pairs")
|
|
||||||
}
|
|
||||||
|
|
||||||
wCtx, err := helper.NewInitializeContext(cmd, viper.GetViper())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't initialize context: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r := management.NewReader(wCtx.ReadOnlyInvoker)
|
|
||||||
cs, err := helper.GetContractByID(r, 1)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get NNS contract info: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nmHash, err := helper.NNSResolveHash(wCtx.ReadOnlyInvoker, cs.Hash, helper.DomainOf(constants.NetmapContract))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get netmap contract hash: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
forceFlag, _ := cmd.Flags().GetBool(forceConfigSet)
|
|
||||||
bw := io.NewBufBinWriter()
|
|
||||||
prm := make(map[string]any)
|
|
||||||
for _, arg := range args {
|
|
||||||
k, v, err := parseConfigPair(arg, forceFlag)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
prm[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validateConfig(prm, forceFlag); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range prm {
|
|
||||||
// In NeoFS this is done via Notary contract. Here, however, we can form the
|
|
||||||
// transaction locally. The first `nil` argument is required only for notary
|
|
||||||
// disabled environment which is not supported by that command.
|
|
||||||
emit.AppCall(bw.BinWriter, nmHash, "setConfig", callflag.All, nil, k, v)
|
|
||||||
if bw.Err != nil {
|
|
||||||
return fmt.Errorf("can't form raw transaction: %w", bw.Err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = wCtx.SendConsensusTx(bw.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return wCtx.AwaitTx()
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxECSum = 256
|
|
||||||
|
|
||||||
func validateConfig(args map[string]any, forceFlag bool) error {
|
|
||||||
var sumEC int64
|
|
||||||
_, okData := args[netmap.MaxECDataCountConfig]
|
|
||||||
_, okParity := args[netmap.MaxECParityCountConfig]
|
|
||||||
if okData != okParity {
|
|
||||||
return fmt.Errorf("both %s and %s must be present in the configuration",
|
|
||||||
netmap.MaxECDataCountConfig, netmap.MaxECParityCountConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range args {
|
|
||||||
switch k {
|
|
||||||
case netmap.ContainerFeeConfig, netmap.ContainerAliasFeeConfig,
|
|
||||||
netmap.EpochDurationConfig, netmap.IrCandidateFeeConfig,
|
|
||||||
netmap.MaxObjectSizeConfig, netmap.WithdrawFeeConfig,
|
|
||||||
netmap.MaxECDataCountConfig, netmap.MaxECParityCountConfig:
|
|
||||||
value, ok := v.(int64)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("%s has an invalid type. Expected type: int", k)
|
|
||||||
}
|
|
||||||
|
|
||||||
if value < 0 {
|
|
||||||
return fmt.Errorf("%s must be >= 0, got %v", k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
if k == netmap.MaxECDataCountConfig || k == netmap.MaxECParityCountConfig {
|
|
||||||
sumEC += value
|
|
||||||
}
|
|
||||||
case netmap.HomomorphicHashingDisabledKey, netmap.MaintenanceModeAllowedConfig:
|
|
||||||
_, ok := v.(bool)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("%s has an invalid type. Expected type: bool", k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if sumEC > maxECSum && !forceFlag {
|
|
||||||
return fmt.Errorf("the sum of %s and %s must be <= %d, got %d",
|
|
||||||
netmap.MaxECDataCountConfig, netmap.MaxECParityCountConfig, maxECSum, sumEC)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseConfigPair(kvStr string, force bool) (key string, val any, err error) {
|
|
||||||
k, v, found := strings.Cut(kvStr, "=")
|
|
||||||
if !found {
|
|
||||||
return "", nil, fmt.Errorf("invalid parameter format: must be 'key=val', got: %s", kvStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
key = k
|
|
||||||
valRaw := v
|
|
||||||
|
|
||||||
switch key {
|
|
||||||
case netmap.ContainerFeeConfig, netmap.ContainerAliasFeeConfig,
|
|
||||||
netmap.EpochDurationConfig, netmap.IrCandidateFeeConfig,
|
|
||||||
netmap.MaxObjectSizeConfig, netmap.WithdrawFeeConfig,
|
|
||||||
netmap.MaxECDataCountConfig, netmap.MaxECParityCountConfig:
|
|
||||||
val, err = strconv.ParseInt(valRaw, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("could not parse %s's value '%s' as int: %w", key, valRaw, err)
|
|
||||||
}
|
|
||||||
case netmap.HomomorphicHashingDisabledKey, netmap.MaintenanceModeAllowedConfig:
|
|
||||||
val, err = strconv.ParseBool(valRaw)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("could not parse %s's value '%s' as bool: %w", key, valRaw, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
if !force {
|
|
||||||
return "", nil, fmt.Errorf(
|
|
||||||
"'%s' key is not well-known, use '--%s' flag if want to set it anyway",
|
|
||||||
key, forceConfigSet)
|
|
||||||
}
|
|
||||||
|
|
||||||
val = valRaw
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/netmap"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_ValidateConfig(t *testing.T) {
|
|
||||||
testArgs := make(map[string]any)
|
|
||||||
|
|
||||||
testArgs[netmap.MaxECDataCountConfig] = int64(11)
|
|
||||||
require.Error(t, validateConfig(testArgs, false))
|
|
||||||
|
|
||||||
testArgs[netmap.MaxECParityCountConfig] = int64(256)
|
|
||||||
require.Error(t, validateConfig(testArgs, false))
|
|
||||||
require.NoError(t, validateConfig(testArgs, true))
|
|
||||||
|
|
||||||
testArgs[netmap.MaxECParityCountConfig] = int64(-1)
|
|
||||||
require.Error(t, validateConfig(testArgs, false))
|
|
||||||
|
|
||||||
testArgs[netmap.MaxECParityCountConfig] = int64(55)
|
|
||||||
require.NoError(t, validateConfig(testArgs, false))
|
|
||||||
|
|
||||||
testArgs[netmap.HomomorphicHashingDisabledKey] = "1"
|
|
||||||
require.Error(t, validateConfig(testArgs, false))
|
|
||||||
|
|
||||||
testArgs[netmap.HomomorphicHashingDisabledKey] = true
|
|
||||||
require.NoError(t, validateConfig(testArgs, false))
|
|
||||||
|
|
||||||
testArgs["not-well-known-configuration-key"] = "key"
|
|
||||||
require.NoError(t, validateConfig(testArgs, false))
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
SetCmd = &cobra.Command{
|
|
||||||
Use: "set-config key1=val1 [key2=val2 ...]",
|
|
||||||
DisableFlagsInUseLine: true,
|
|
||||||
Short: "Add/update global config value in the FrostFS network",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
Args: cobra.MinimumNArgs(1),
|
|
||||||
RunE: SetConfigCmd,
|
|
||||||
}
|
|
||||||
|
|
||||||
DumpCmd = &cobra.Command{
|
|
||||||
Use: "dump-config",
|
|
||||||
Short: "Dump FrostFS network config",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
RunE: dumpNetworkConfig,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func initSetConfigCmd() {
|
|
||||||
SetCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
SetCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
SetCmd.Flags().Bool(forceConfigSet, false, "Force setting not well-known configuration key")
|
|
||||||
SetCmd.Flags().String(commonflags.LocalDumpFlag, "", "Path to the blocks dump file")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDumpNetworkConfigCmd() {
|
|
||||||
DumpCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
initSetConfigCmd()
|
|
||||||
initDumpNetworkConfigCmd()
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
package constants
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
const (
|
|
||||||
ConsensusAccountName = "consensus"
|
|
||||||
ProtoConfigPath = "protocol"
|
|
||||||
|
|
||||||
// MaxAlphabetNodes is the maximum number of candidates allowed, which is currently limited by the size
|
|
||||||
// of the invocation script.
|
|
||||||
// See: https://github.com/nspcc-dev/neo-go/blob/740488f7f35e367eaa99a71c0a609c315fe2b0fc/pkg/core/transaction/witness.go#L10
|
|
||||||
MaxAlphabetNodes = 22
|
|
||||||
|
|
||||||
SingleAccountName = "single"
|
|
||||||
CommitteeAccountName = "committee"
|
|
||||||
|
|
||||||
NNSContract = "nns"
|
|
||||||
FrostfsContract = "frostfs" // not deployed in side-chain.
|
|
||||||
ProcessingContract = "processing" // not deployed in side-chain.
|
|
||||||
AlphabetContract = "alphabet"
|
|
||||||
BalanceContract = "balance"
|
|
||||||
ContainerContract = "container"
|
|
||||||
FrostfsIDContract = "frostfsid"
|
|
||||||
NetmapContract = "netmap"
|
|
||||||
PolicyContract = "policy"
|
|
||||||
ProxyContract = "proxy"
|
|
||||||
|
|
||||||
ContractWalletFilename = "contract.json"
|
|
||||||
ContractWalletPasswordKey = "contract"
|
|
||||||
|
|
||||||
FrostfsOpsEmail = "ops@frostfs.info"
|
|
||||||
NNSRefreshDefVal = int64(3600)
|
|
||||||
NNSRetryDefVal = int64(600)
|
|
||||||
NNSTtlDefVal = int64(3600)
|
|
||||||
|
|
||||||
DefaultExpirationTime = 10 * 365 * 24 * time.Hour / time.Second
|
|
||||||
|
|
||||||
DeployMethodName = "deploy"
|
|
||||||
UpdateMethodName = "update"
|
|
||||||
|
|
||||||
TestContractPassword = "grouppass"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ContractList = []string{
|
|
||||||
BalanceContract,
|
|
||||||
ContainerContract,
|
|
||||||
FrostfsIDContract,
|
|
||||||
NetmapContract,
|
|
||||||
PolicyContract,
|
|
||||||
ProxyContract,
|
|
||||||
}
|
|
||||||
|
|
||||||
FullContractList = append([]string{
|
|
||||||
FrostfsContract,
|
|
||||||
ProcessingContract,
|
|
||||||
NNSContract,
|
|
||||||
AlphabetContract,
|
|
||||||
}, ContractList...)
|
|
||||||
)
|
|
|
@ -1,20 +1,16 @@
|
||||||
package container
|
package morph
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper"
|
|
||||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
@ -26,19 +22,18 @@ import (
|
||||||
|
|
||||||
var errInvalidContainerResponse = errors.New("invalid response from container contract")
|
var errInvalidContainerResponse = errors.New("invalid response from container contract")
|
||||||
|
|
||||||
func getContainerContractHash(cmd *cobra.Command, inv *invoker.Invoker) (util.Uint160, error) {
|
func getContainerContractHash(cmd *cobra.Command, inv *invoker.Invoker, c Client) (util.Uint160, error) {
|
||||||
s, err := cmd.Flags().GetString(containerContractFlag)
|
s, err := cmd.Flags().GetString(containerContractFlag)
|
||||||
var ch util.Uint160
|
var ch util.Uint160
|
||||||
if err == nil {
|
if err == nil {
|
||||||
ch, err = util.Uint160DecodeStringLE(s)
|
ch, err = util.Uint160DecodeStringLE(s)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
r := management.NewReader(inv)
|
nnsCs, err := c.GetContractStateByID(1)
|
||||||
nnsCs, err := helper.GetContractByID(r, 1)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.Uint160{}, fmt.Errorf("can't get NNS contract state: %w", err)
|
return util.Uint160{}, fmt.Errorf("can't get NNS contract state: %w", err)
|
||||||
}
|
}
|
||||||
ch, err = helper.NNSResolveHash(inv, nnsCs.Hash, helper.DomainOf(constants.ContainerContract))
|
ch, err = nnsResolveHash(inv, nnsCs.Hash, containerContract+".frostfs")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.Uint160{}, err
|
return util.Uint160{}, err
|
||||||
}
|
}
|
||||||
|
@ -46,28 +41,16 @@ func getContainerContractHash(cmd *cobra.Command, inv *invoker.Invoker) (util.Ui
|
||||||
return ch, nil
|
return ch, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func iterateContainerList(inv *invoker.Invoker, ch util.Uint160, f func([]byte) error) error {
|
func getContainersList(inv *invoker.Invoker, ch util.Uint160) ([][]byte, error) {
|
||||||
sid, r, err := unwrap.SessionIterator(inv.Call(ch, "containersOf", ""))
|
res, err := inv.Call(ch, "list", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %v", errInvalidContainerResponse, err)
|
return nil, fmt.Errorf("%w: %v", errInvalidContainerResponse, err)
|
||||||
}
|
}
|
||||||
// Nothing bad, except live session on the server, do not report to the user.
|
itm, err := unwrap.Item(res, err)
|
||||||
defer func() { _ = inv.TerminateSession(sid) }()
|
if _, ok := itm.(stackitem.Null); !ok {
|
||||||
|
return unwrap.ArrayOfBytes(res, err)
|
||||||
items, err := inv.TraverseIterator(sid, &r, 0)
|
|
||||||
for err == nil && len(items) != 0 {
|
|
||||||
for j := range items {
|
|
||||||
b, err := items[j].TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: %v", errInvalidContainerResponse, err)
|
|
||||||
}
|
|
||||||
if err := f(b); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items, err = inv.TraverseIterator(sid, &r, 0)
|
|
||||||
}
|
}
|
||||||
return err
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func dumpContainers(cmd *cobra.Command, _ []string) error {
|
func dumpContainers(cmd *cobra.Command, _ []string) error {
|
||||||
|
@ -76,108 +59,97 @@ func dumpContainers(cmd *cobra.Command, _ []string) error {
|
||||||
return fmt.Errorf("invalid filename: %w", err)
|
return fmt.Errorf("invalid filename: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := helper.GetN3Client(viper.GetViper())
|
c, err := getN3Client(viper.GetViper())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("can't create N3 client: %w", err)
|
return fmt.Errorf("can't create N3 client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
inv := invoker.New(c, nil)
|
inv := invoker.New(c, nil)
|
||||||
|
|
||||||
ch, err := getContainerContractHash(cmd, inv)
|
ch, err := getContainerContractHash(cmd, inv, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to get contaract hash: %w", err)
|
return fmt.Errorf("unable to get contaract hash: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cids, err := getContainersList(inv, ch)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", errInvalidContainerResponse, err)
|
||||||
|
}
|
||||||
|
|
||||||
isOK, err := getCIDFilterFunc(cmd)
|
isOK, err := getCIDFilterFunc(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.OpenFile(filename, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0o660)
|
var containers []*Container
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
_, err = f.Write([]byte{'['})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
written := 0
|
|
||||||
enc := json.NewEncoder(f)
|
|
||||||
bw := io.NewBufBinWriter()
|
bw := io.NewBufBinWriter()
|
||||||
iterErr := iterateContainerList(inv, ch, func(id []byte) error {
|
for _, id := range cids {
|
||||||
if !isOK(id) {
|
if !isOK(id) {
|
||||||
return nil
|
continue
|
||||||
}
|
}
|
||||||
|
bw.Reset()
|
||||||
cnt, err := dumpSingleContainer(bw, ch, inv, id)
|
emit.AppCall(bw.BinWriter, ch, "get", callflag.All, id)
|
||||||
|
emit.AppCall(bw.BinWriter, ch, "eACL", callflag.All, id)
|
||||||
|
res, err := inv.Run(bw.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("can't get container info: %w", err)
|
||||||
|
}
|
||||||
|
if len(res.Stack) != 2 {
|
||||||
|
return fmt.Errorf("%w: expected 2 items on stack", errInvalidContainerResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writing directly to the file is ok, because json.Encoder does no internal buffering.
|
cnt := new(Container)
|
||||||
if written != 0 {
|
err = cnt.FromStackItem(res.Stack[0])
|
||||||
_, err = f.Write([]byte{','})
|
if err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("%w: %v", errInvalidContainerResponse, err)
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
written++
|
ea := new(EACL)
|
||||||
return enc.Encode(cnt)
|
err = ea.FromStackItem(res.Stack[1])
|
||||||
})
|
if err != nil {
|
||||||
if iterErr != nil {
|
return fmt.Errorf("%w: %v", errInvalidContainerResponse, err)
|
||||||
return iterErr
|
}
|
||||||
|
if len(ea.Value) != 0 {
|
||||||
|
cnt.EACL = ea
|
||||||
|
}
|
||||||
|
|
||||||
|
containers = append(containers, cnt)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = f.Write([]byte{']'})
|
out, err := json.Marshal(containers)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func dumpSingleContainer(bw *io.BufBinWriter, ch util.Uint160, inv *invoker.Invoker, id []byte) (*Container, error) {
|
|
||||||
bw.Reset()
|
|
||||||
emit.AppCall(bw.BinWriter, ch, "get", callflag.All, id)
|
|
||||||
res, err := inv.Run(bw.Bytes())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("can't get container info: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
if len(res.Stack) != 1 {
|
return os.WriteFile(filename, out, 0o660)
|
||||||
return nil, fmt.Errorf("%w: expected 1 items on stack", errInvalidContainerResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
cnt := new(Container)
|
|
||||||
err = cnt.FromStackItem(res.Stack[0])
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%w: %v", errInvalidContainerResponse, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cnt, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func listContainers(cmd *cobra.Command, _ []string) error {
|
func listContainers(cmd *cobra.Command, _ []string) error {
|
||||||
c, err := helper.GetN3Client(viper.GetViper())
|
c, err := getN3Client(viper.GetViper())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("can't create N3 client: %w", err)
|
return fmt.Errorf("can't create N3 client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
inv := invoker.New(c, nil)
|
inv := invoker.New(c, nil)
|
||||||
|
|
||||||
ch, err := getContainerContractHash(cmd, inv)
|
ch, err := getContainerContractHash(cmd, inv, c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to get contaract hash: %w", err)
|
return fmt.Errorf("unable to get contaract hash: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return iterateContainerList(inv, ch, func(id []byte) error {
|
cids, err := getContainersList(inv, ch)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", errInvalidContainerResponse, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range cids {
|
||||||
var idCnr cid.ID
|
var idCnr cid.ID
|
||||||
err = idCnr.Decode(id)
|
err = idCnr.Decode(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to decode container id: %w", err)
|
return fmt.Errorf("unable to decode container id: %w", err)
|
||||||
}
|
}
|
||||||
cmd.Println(idCnr)
|
cmd.Println(idCnr)
|
||||||
return nil
|
}
|
||||||
})
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreContainers(cmd *cobra.Command, _ []string) error {
|
func restoreContainers(cmd *cobra.Command, _ []string) error {
|
||||||
|
@ -186,20 +158,31 @@ func restoreContainers(cmd *cobra.Command, _ []string) error {
|
||||||
return fmt.Errorf("invalid filename: %w", err)
|
return fmt.Errorf("invalid filename: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
wCtx, err := helper.NewInitializeContext(cmd, viper.GetViper())
|
wCtx, err := newInitializeContext(cmd, viper.GetViper())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer wCtx.Close()
|
defer wCtx.close()
|
||||||
|
|
||||||
containers, err := parseContainers(filename)
|
nnsCs, err := wCtx.Client.GetContractStateByID(1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("can't get NNS contract state: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ch, err := fetchContainerContractHash(wCtx)
|
ch, err := nnsResolveHash(wCtx.ReadOnlyInvoker, nnsCs.Hash, containerContract+".frostfs")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("can't fetch container contract hash: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't read dump file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var containers []Container
|
||||||
|
err = json.Unmarshal(data, &containers)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't parse dump file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
isOK, err := getCIDFilterFunc(cmd)
|
isOK, err := getCIDFilterFunc(cmd)
|
||||||
|
@ -207,15 +190,6 @@ func restoreContainers(cmd *cobra.Command, _ []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = restoreOrPutContainers(containers, isOK, cmd, wCtx, ch)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return wCtx.AwaitTx()
|
|
||||||
}
|
|
||||||
|
|
||||||
func restoreOrPutContainers(containers []Container, isOK func([]byte) bool, cmd *cobra.Command, wCtx *helper.InitializeContext, ch util.Uint160) error {
|
|
||||||
bw := io.NewBufBinWriter()
|
bw := io.NewBufBinWriter()
|
||||||
for _, cnt := range containers {
|
for _, cnt := range containers {
|
||||||
hv := hash.Sha256(cnt.Value)
|
hv := hash.Sha256(cnt.Value)
|
||||||
|
@ -223,84 +197,43 @@ func restoreOrPutContainers(containers []Container, isOK func([]byte) bool, cmd
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
bw.Reset()
|
bw.Reset()
|
||||||
restored, err := isContainerRestored(cmd, wCtx, ch, bw, hv)
|
emit.AppCall(bw.BinWriter, ch, "get", callflag.All, hv.BytesBE())
|
||||||
|
res, err := wCtx.Client.InvokeScript(bw.Bytes(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("can't check if container is already restored: %w", err)
|
||||||
}
|
}
|
||||||
if restored {
|
if len(res.Stack) == 0 {
|
||||||
|
return errors.New("empty stack")
|
||||||
|
}
|
||||||
|
|
||||||
|
old := new(Container)
|
||||||
|
if err := old.FromStackItem(res.Stack[0]); err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", errInvalidContainerResponse, err)
|
||||||
|
}
|
||||||
|
if len(old.Value) != 0 {
|
||||||
|
var id cid.ID
|
||||||
|
id.SetSHA256(hv)
|
||||||
|
cmd.Printf("Container %s is already deployed.\n", id)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
bw.Reset()
|
bw.Reset()
|
||||||
|
emit.AppCall(bw.BinWriter, ch, "put", callflag.All,
|
||||||
putContainer(bw, ch, cnt)
|
cnt.Value, cnt.Signature, cnt.PublicKey, cnt.Token)
|
||||||
|
if ea := cnt.EACL; ea != nil {
|
||||||
|
emit.AppCall(bw.BinWriter, ch, "setEACL", callflag.All,
|
||||||
|
ea.Value, ea.Signature, ea.PublicKey, ea.Token)
|
||||||
|
}
|
||||||
if bw.Err != nil {
|
if bw.Err != nil {
|
||||||
panic(bw.Err)
|
panic(bw.Err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := wCtx.SendConsensusTx(bw.Bytes()); err != nil {
|
if err := wCtx.sendConsensusTx(bw.Bytes()); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func putContainer(bw *io.BufBinWriter, ch util.Uint160, cnt Container) {
|
return wCtx.awaitTx()
|
||||||
emit.AppCall(bw.BinWriter, ch, "put", callflag.All,
|
|
||||||
cnt.Value, cnt.Signature, cnt.PublicKey, cnt.Token)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isContainerRestored(cmd *cobra.Command, wCtx *helper.InitializeContext, containerHash util.Uint160, bw *io.BufBinWriter, hashValue util.Uint256) (bool, error) {
|
|
||||||
emit.AppCall(bw.BinWriter, containerHash, "get", callflag.All, hashValue.BytesBE())
|
|
||||||
res, err := wCtx.Client.InvokeScript(bw.Bytes(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("can't check if container is already restored: %w", err)
|
|
||||||
}
|
|
||||||
if len(res.Stack) == 0 {
|
|
||||||
return false, errors.New("empty stack")
|
|
||||||
}
|
|
||||||
|
|
||||||
old := new(Container)
|
|
||||||
if err := old.FromStackItem(res.Stack[0]); err != nil {
|
|
||||||
return false, fmt.Errorf("%w: %v", errInvalidContainerResponse, err)
|
|
||||||
}
|
|
||||||
if len(old.Value) != 0 {
|
|
||||||
var id cid.ID
|
|
||||||
id.SetSHA256(hashValue)
|
|
||||||
cmd.Printf("Container %s is already deployed.\n", id)
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseContainers(filename string) ([]Container, error) {
|
|
||||||
data, err := os.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't read dump file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var containers []Container
|
|
||||||
err = json.Unmarshal(data, &containers)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't parse dump file: %w", err)
|
|
||||||
}
|
|
||||||
return containers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchContainerContractHash(wCtx *helper.InitializeContext) (util.Uint160, error) {
|
|
||||||
r := management.NewReader(wCtx.ReadOnlyInvoker)
|
|
||||||
nnsCs, err := helper.GetContractByID(r, 1)
|
|
||||||
if err != nil {
|
|
||||||
return util.Uint160{}, fmt.Errorf("can't get NNS contract state: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ch, err := helper.NNSResolveHash(wCtx.ReadOnlyInvoker, nnsCs.Hash, helper.DomainOf(constants.ContainerContract))
|
|
||||||
if err != nil {
|
|
||||||
return util.Uint160{}, fmt.Errorf("can't fetch container contract hash: %w", err)
|
|
||||||
}
|
|
||||||
return ch, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Container represents container struct in contract storage.
|
// Container represents container struct in contract storage.
|
||||||
|
@ -309,6 +242,15 @@ type Container struct {
|
||||||
Signature []byte `json:"signature"`
|
Signature []byte `json:"signature"`
|
||||||
PublicKey []byte `json:"public_key"`
|
PublicKey []byte `json:"public_key"`
|
||||||
Token []byte `json:"token"`
|
Token []byte `json:"token"`
|
||||||
|
EACL *EACL `json:"eacl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// EACL represents extended ACL struct in contract storage.
|
||||||
|
type EACL struct {
|
||||||
|
Value []byte `json:"value"`
|
||||||
|
Signature []byte `json:"signature"`
|
||||||
|
PublicKey []byte `json:"public_key"`
|
||||||
|
Token []byte `json:"token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToStackItem implements stackitem.Convertible.
|
// ToStackItem implements stackitem.Convertible.
|
||||||
|
@ -355,6 +297,50 @@ func (c *Container) FromStackItem(item stackitem.Item) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToStackItem implements stackitem.Convertible.
|
||||||
|
func (c *EACL) ToStackItem() (stackitem.Item, error) {
|
||||||
|
return stackitem.NewStruct([]stackitem.Item{
|
||||||
|
stackitem.NewByteArray(c.Value),
|
||||||
|
stackitem.NewByteArray(c.Signature),
|
||||||
|
stackitem.NewByteArray(c.PublicKey),
|
||||||
|
stackitem.NewByteArray(c.Token),
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromStackItem implements stackitem.Convertible.
|
||||||
|
func (c *EACL) FromStackItem(item stackitem.Item) error {
|
||||||
|
arr, ok := item.Value().([]stackitem.Item)
|
||||||
|
if !ok || len(arr) != 4 {
|
||||||
|
return errors.New("invalid stack item type")
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := arr[0].TryBytes()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("invalid eACL value")
|
||||||
|
}
|
||||||
|
|
||||||
|
sig, err := arr[1].TryBytes()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("invalid eACL signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub, err := arr[2].TryBytes()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("invalid eACL public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
tok, err := arr[3].TryBytes()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("invalid eACL token")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Value = value
|
||||||
|
c.Signature = sig
|
||||||
|
c.PublicKey = pub
|
||||||
|
c.Token = tok
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// getCIDFilterFunc returns filtering function for container IDs.
|
// getCIDFilterFunc returns filtering function for container IDs.
|
||||||
// Raw byte slices are used because it works with structures returned
|
// Raw byte slices are used because it works with structures returned
|
||||||
// from contract.
|
// from contract.
|
||||||
|
@ -381,7 +367,7 @@ func getCIDFilterFunc(cmd *cobra.Command) (func([]byte) bool, error) {
|
||||||
var id cid.ID
|
var id cid.ID
|
||||||
id.SetSHA256(v)
|
id.SetSHA256(v)
|
||||||
idStr := id.EncodeToString()
|
idStr := id.EncodeToString()
|
||||||
_, found := slices.BinarySearch(rawIDs, idStr)
|
n := sort.Search(len(rawIDs), func(i int) bool { return rawIDs[i] >= idStr })
|
||||||
return found
|
return n < len(rawIDs) && rawIDs[n] == idStr
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
|
@ -1,68 +0,0 @@
|
||||||
package container
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
containerDumpFlag = "dump"
|
|
||||||
containerContractFlag = "container-contract"
|
|
||||||
containerIDsFlag = "cid"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
DumpCmd = &cobra.Command{
|
|
||||||
Use: "dump-containers",
|
|
||||||
Short: "Dump FrostFS containers to file",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
RunE: dumpContainers,
|
|
||||||
}
|
|
||||||
|
|
||||||
RestoreCmd = &cobra.Command{
|
|
||||||
Use: "restore-containers",
|
|
||||||
Short: "Restore FrostFS containers from file",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
RunE: restoreContainers,
|
|
||||||
}
|
|
||||||
|
|
||||||
ListCmd = &cobra.Command{
|
|
||||||
Use: "list-containers",
|
|
||||||
Short: "List FrostFS containers",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
RunE: listContainers,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func initListContainersCmd() {
|
|
||||||
ListCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
ListCmd.Flags().String(containerContractFlag, "", "Container contract hash (for networks without NNS)")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initRestoreContainersCmd() {
|
|
||||||
RestoreCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
RestoreCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
RestoreCmd.Flags().String(containerDumpFlag, "", "File to restore containers from")
|
|
||||||
RestoreCmd.Flags().StringSlice(containerIDsFlag, nil, "Containers to restore")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initDumpContainersCmd() {
|
|
||||||
DumpCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
DumpCmd.Flags().String(containerDumpFlag, "", "File where to save dumped containers")
|
|
||||||
DumpCmd.Flags().String(containerContractFlag, "", "Container contract hash (for networks without NNS)")
|
|
||||||
DumpCmd.Flags().StringSlice(containerIDsFlag, nil, "Containers to dump")
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
initDumpContainersCmd()
|
|
||||||
initRestoreContainersCmd()
|
|
||||||
initListContainersCmd()
|
|
||||||
}
|
|
|
@ -1,237 +0,0 @@
|
||||||
package contract
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper"
|
|
||||||
"github.com/nspcc-dev/neo-go/cli/cmdargs"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/services/rpcsrv/params"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
contractPathFlag = "contract"
|
|
||||||
updateFlag = "update"
|
|
||||||
)
|
|
||||||
|
|
||||||
var DeployCmd = &cobra.Command{
|
|
||||||
Use: "deploy",
|
|
||||||
Short: "Deploy additional smart-contracts",
|
|
||||||
Long: `Deploy additional smart-contract which are not related to core.
|
|
||||||
All contracts are deployed by the committee, so access to the alphabet wallets is required.
|
|
||||||
Optionally, arguments can be provided to be passed to a contract's _deploy function.
|
|
||||||
The syntax is the same as for 'neo-go contract testinvokefunction' command.
|
|
||||||
Compiled contract file name must contain '_contract.nef' suffix.
|
|
||||||
Contract's manifest file name must be 'config.json'.
|
|
||||||
NNS name is taken by stripping '_contract.nef' from the NEF file (similar to frostfs contracts).`,
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
RunE: deployContractCmd,
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
ff := DeployCmd.Flags()
|
|
||||||
|
|
||||||
ff.String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
_ = DeployCmd.MarkFlagFilename(commonflags.AlphabetWalletsFlag)
|
|
||||||
|
|
||||||
ff.StringP(commonflags.EndpointFlag, "r", "", commonflags.EndpointFlagDesc)
|
|
||||||
ff.String(contractPathFlag, "", "Path to the contract directory")
|
|
||||||
_ = DeployCmd.MarkFlagFilename(contractPathFlag)
|
|
||||||
|
|
||||||
ff.Bool(updateFlag, false, "Update an existing contract")
|
|
||||||
ff.String(commonflags.CustomZoneFlag, "frostfs", "Custom zone for NNS")
|
|
||||||
}
|
|
||||||
|
|
||||||
func deployContractCmd(cmd *cobra.Command, args []string) error {
|
|
||||||
v := viper.GetViper()
|
|
||||||
c, err := helper.NewInitializeContext(cmd, v)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("initialization error: %w", err)
|
|
||||||
}
|
|
||||||
defer c.Close()
|
|
||||||
|
|
||||||
ctrPath, _ := cmd.Flags().GetString(contractPathFlag)
|
|
||||||
ctrName, err := probeContractName(ctrPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cs, err := helper.ReadContract(ctrPath, ctrName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r := management.NewReader(c.ReadOnlyInvoker)
|
|
||||||
nnsCs, err := helper.GetContractByID(r, 1)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't fetch NNS contract state: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
callHash := management.Hash
|
|
||||||
method := constants.DeployMethodName
|
|
||||||
zone, _ := cmd.Flags().GetString(commonflags.CustomZoneFlag)
|
|
||||||
domain := ctrName + "." + zone
|
|
||||||
isUpdate, _ := cmd.Flags().GetBool(updateFlag)
|
|
||||||
if isUpdate {
|
|
||||||
cs.Hash, err = helper.NNSResolveHash(c.ReadOnlyInvoker, nnsCs.Hash, domain)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't fetch contract hash from NNS: %w", err)
|
|
||||||
}
|
|
||||||
callHash = cs.Hash
|
|
||||||
method = constants.UpdateMethodName
|
|
||||||
} else {
|
|
||||||
cs.Hash = state.CreateContractHash(
|
|
||||||
c.CommitteeAcc.Contract.ScriptHash(),
|
|
||||||
cs.NEF.Checksum,
|
|
||||||
cs.Manifest.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
writer := io.NewBufBinWriter()
|
|
||||||
if err := emitDeploymentArguments(writer.BinWriter, args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
emit.Bytes(writer.BinWriter, cs.RawManifest)
|
|
||||||
emit.Bytes(writer.BinWriter, cs.RawNEF)
|
|
||||||
emit.Int(writer.BinWriter, 3)
|
|
||||||
emit.Opcodes(writer.BinWriter, opcode.PACK)
|
|
||||||
emit.AppCallNoArgs(writer.BinWriter, callHash, method, callflag.All)
|
|
||||||
emit.Opcodes(writer.BinWriter, opcode.DROP) // contract state on stack
|
|
||||||
if !isUpdate {
|
|
||||||
err := registerNNS(nnsCs, c, zone, domain, cs, writer)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if writer.Err != nil {
|
|
||||||
panic(fmt.Errorf("BUG: can't create deployment script: %w", writer.Err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.SendCommitteeTx(writer.Bytes(), false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.AwaitTx()
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerNNS(nnsCs *state.Contract, c *helper.InitializeContext, zone string, domain string, cs *helper.ContractState, writer *io.BufBinWriter) error {
|
|
||||||
bw := io.NewBufBinWriter()
|
|
||||||
emit.Instruction(bw.BinWriter, opcode.INITSSLOT, []byte{1})
|
|
||||||
emit.AppCall(bw.BinWriter, nnsCs.Hash, "getPrice", callflag.All)
|
|
||||||
emit.Opcodes(bw.BinWriter, opcode.STSFLD0)
|
|
||||||
emit.AppCall(bw.BinWriter, nnsCs.Hash, "setPrice", callflag.All, 1)
|
|
||||||
|
|
||||||
start := bw.Len()
|
|
||||||
needRecord := false
|
|
||||||
|
|
||||||
ok, err := c.NNSRootRegistered(nnsCs.Hash, zone)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if !ok {
|
|
||||||
needRecord = true
|
|
||||||
|
|
||||||
emit.AppCall(bw.BinWriter, nnsCs.Hash, "register", callflag.All,
|
|
||||||
zone, c.CommitteeAcc.Contract.ScriptHash(),
|
|
||||||
constants.FrostfsOpsEmail, constants.NNSRefreshDefVal, constants.NNSRetryDefVal,
|
|
||||||
int64(constants.DefaultExpirationTime), constants.NNSTtlDefVal)
|
|
||||||
emit.Opcodes(bw.BinWriter, opcode.ASSERT)
|
|
||||||
|
|
||||||
emit.AppCall(bw.BinWriter, nnsCs.Hash, "register", callflag.All,
|
|
||||||
domain, c.CommitteeAcc.Contract.ScriptHash(),
|
|
||||||
constants.FrostfsOpsEmail, constants.NNSRefreshDefVal, constants.NNSRetryDefVal,
|
|
||||||
int64(constants.DefaultExpirationTime), constants.NNSTtlDefVal)
|
|
||||||
emit.Opcodes(bw.BinWriter, opcode.ASSERT)
|
|
||||||
} else {
|
|
||||||
s, ok, err := c.NNSRegisterDomainScript(nnsCs.Hash, cs.Hash, domain)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
needRecord = !ok
|
|
||||||
if len(s) != 0 {
|
|
||||||
bw.WriteBytes(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if needRecord {
|
|
||||||
emit.AppCall(bw.BinWriter, nnsCs.Hash, "deleteRecords", callflag.All, domain, int64(nns.TXT))
|
|
||||||
emit.AppCall(bw.BinWriter, nnsCs.Hash, "addRecord", callflag.All,
|
|
||||||
domain, int64(nns.TXT), address.Uint160ToString(cs.Hash))
|
|
||||||
}
|
|
||||||
|
|
||||||
if bw.Err != nil {
|
|
||||||
panic(fmt.Errorf("BUG: can't create deployment script: %w", writer.Err))
|
|
||||||
} else if bw.Len() != start {
|
|
||||||
writer.WriteBytes(bw.Bytes())
|
|
||||||
emit.Opcodes(writer.BinWriter, opcode.LDSFLD0, opcode.PUSH1, opcode.PACK)
|
|
||||||
emit.AppCallNoArgs(writer.BinWriter, nnsCs.Hash, "setPrice", callflag.All)
|
|
||||||
|
|
||||||
if needRecord {
|
|
||||||
c.Command.Printf("NNS: Set %s -> %s\n", domain, cs.Hash.StringLE())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func emitDeploymentArguments(w *io.BinWriter, args []string) error {
|
|
||||||
_, ps, err := cmdargs.ParseParams(args, true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ps) == 0 {
|
|
||||||
emit.Opcodes(w, opcode.NEWARRAY0)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(ps) != 1 {
|
|
||||||
return fmt.Errorf("at most one argument is expected for deploy, got %d", len(ps))
|
|
||||||
}
|
|
||||||
|
|
||||||
// We could emit this directly, but round-trip through JSON is more robust.
|
|
||||||
// This a CLI, so optimizing the conversion is not worth the effort.
|
|
||||||
data, err := json.Marshal(ps)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var pp params.Params
|
|
||||||
if err := json.Unmarshal(data, &pp); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return params.ExpandArrayIntoScript(w, pp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func probeContractName(ctrPath string) (string, error) {
|
|
||||||
ds, err := os.ReadDir(ctrPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("can't read directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var ctrName string
|
|
||||||
for i := range ds {
|
|
||||||
if strings.HasSuffix(ds[i].Name(), "_contract.nef") {
|
|
||||||
ctrName = strings.TrimSuffix(ds[i].Name(), "_contract.nef")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctrName == "" {
|
|
||||||
return "", fmt.Errorf("can't find any NEF files in %s", ctrPath)
|
|
||||||
}
|
|
||||||
return ctrName, nil
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
package contract
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
DumpHashesCmd = &cobra.Command{
|
|
||||||
Use: "dump-hashes",
|
|
||||||
Short: "Dump deployed contract hashes",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
RunE: dumpContractHashes,
|
|
||||||
}
|
|
||||||
UpdateCmd = &cobra.Command{
|
|
||||||
Use: "update-contracts",
|
|
||||||
Short: "Update FrostFS contracts",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
RunE: updateContracts,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func initDumpContractHashesCmd() {
|
|
||||||
DumpHashesCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
DumpHashesCmd.Flags().String(commonflags.CustomZoneFlag, "", "Custom zone to search.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initUpdateContractsCmd() {
|
|
||||||
UpdateCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
UpdateCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
UpdateCmd.Flags().String(commonflags.ContractsInitFlag, "", commonflags.ContractsInitFlagDesc)
|
|
||||||
UpdateCmd.Flags().String(commonflags.ContractsURLFlag, "", commonflags.ContractsURLFlagDesc)
|
|
||||||
UpdateCmd.MarkFlagsMutuallyExclusive(commonflags.ContractsInitFlag, commonflags.ContractsURLFlag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
initDumpContractHashesCmd()
|
|
||||||
initUpdateContractsCmd()
|
|
||||||
}
|
|
|
@ -1,197 +0,0 @@
|
||||||
package contract
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-contract/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper"
|
|
||||||
morphClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
|
||||||
io2 "github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
||||||
neoUtil "github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errMissingNNSRecord = errors.New("missing NNS record")
|
|
||||||
|
|
||||||
func updateContracts(cmd *cobra.Command, _ []string) error {
|
|
||||||
wCtx, err := helper.NewInitializeContext(cmd, viper.GetViper())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("initialization error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := helper.DeployNNS(wCtx, constants.UpdateMethodName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return updateContractsInternal(wCtx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateContractsInternal(c *helper.InitializeContext) error {
|
|
||||||
alphaCs := c.GetContract(constants.AlphabetContract)
|
|
||||||
|
|
||||||
nnsCs, err := c.NNSContractState()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
nnsHash := nnsCs.Hash
|
|
||||||
|
|
||||||
w := io2.NewBufBinWriter()
|
|
||||||
|
|
||||||
// Update script size for a single-node committee is close to the maximum allowed size of 65535.
|
|
||||||
// Because of this we want to reuse alphabet contract NEF and manifest for different updates.
|
|
||||||
// The generated script is as following.
|
|
||||||
// 1. Initialize static slot for alphabet NEF.
|
|
||||||
// 2. Store NEF into the static slot.
|
|
||||||
// 3. Push parameters for each alphabet contract on stack.
|
|
||||||
// 4. Add contract group to the manifest.
|
|
||||||
// 5. For each alphabet contract, invoke `update` using parameters on stack and
|
|
||||||
// NEF from step 2 and manifest from step 4.
|
|
||||||
emit.Instruction(w.BinWriter, opcode.INITSSLOT, []byte{1})
|
|
||||||
emit.Bytes(w.BinWriter, alphaCs.RawNEF)
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.STSFLD0)
|
|
||||||
|
|
||||||
keysParam, err := deployAlphabetAccounts(c, nnsHash, w, alphaCs)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Reset()
|
|
||||||
|
|
||||||
if err = deployOrUpdateContracts(c, w, nnsHash, keysParam); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
groupKey := c.ContractWallet.Accounts[0].PrivateKey().PublicKey()
|
|
||||||
_, _, err = c.EmitUpdateNNSGroupScript(w, nnsHash, groupKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.Command.Printf("NNS: Set %s -> %s\n", morphClient.NNSGroupKeyName, hex.EncodeToString(groupKey.Bytes()))
|
|
||||||
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.LDSFLD0)
|
|
||||||
emit.Int(w.BinWriter, 1)
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.PACK)
|
|
||||||
emit.AppCallNoArgs(w.BinWriter, nnsHash, "setPrice", callflag.All)
|
|
||||||
|
|
||||||
if err := c.SendCommitteeTx(w.Bytes(), false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.AwaitTx()
|
|
||||||
}
|
|
||||||
|
|
||||||
func deployAlphabetAccounts(c *helper.InitializeContext, nnsHash neoUtil.Uint160, w *io2.BufBinWriter, alphaCs *helper.ContractState) ([]any, error) {
|
|
||||||
var keysParam []any
|
|
||||||
|
|
||||||
baseGroups := alphaCs.Manifest.Groups
|
|
||||||
|
|
||||||
// alphabet contracts should be deployed by individual nodes to get different hashes.
|
|
||||||
for i, acc := range c.Accounts {
|
|
||||||
ctrHash, err := helper.NNSResolveHash(c.ReadOnlyInvoker, nnsHash, helper.GetAlphabetNNSDomain(i))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't resolve hash for contract update: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
keysParam = append(keysParam, acc.PrivateKey().PublicKey().Bytes())
|
|
||||||
|
|
||||||
params := c.GetAlphabetDeployItems(i, len(c.Wallets))
|
|
||||||
emit.Array(w.BinWriter, params...)
|
|
||||||
|
|
||||||
alphaCs.Manifest.Groups = baseGroups
|
|
||||||
err = helper.AddManifestGroup(c.ContractWallet, ctrHash, alphaCs)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't sign manifest group: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
emit.Bytes(w.BinWriter, alphaCs.RawManifest)
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.LDSFLD0)
|
|
||||||
emit.Int(w.BinWriter, 3)
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.PACK)
|
|
||||||
emit.AppCallNoArgs(w.BinWriter, ctrHash, constants.UpdateMethodName, callflag.All)
|
|
||||||
}
|
|
||||||
if err := c.SendCommitteeTx(w.Bytes(), false); err != nil {
|
|
||||||
if !strings.Contains(err.Error(), common.ErrAlreadyUpdated) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c.Command.Println("Alphabet contracts are already updated.")
|
|
||||||
}
|
|
||||||
|
|
||||||
return keysParam, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func deployOrUpdateContracts(c *helper.InitializeContext, w *io2.BufBinWriter, nnsHash neoUtil.Uint160, keysParam []any) error {
|
|
||||||
emit.Instruction(w.BinWriter, opcode.INITSSLOT, []byte{1})
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "getPrice", callflag.All)
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.STSFLD0)
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "setPrice", callflag.All, 1)
|
|
||||||
|
|
||||||
for _, ctrName := range constants.ContractList {
|
|
||||||
cs := c.GetContract(ctrName)
|
|
||||||
|
|
||||||
method := constants.UpdateMethodName
|
|
||||||
ctrHash, err := helper.NNSResolveHash(c.ReadOnlyInvoker, nnsHash, helper.DomainOf(ctrName))
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, errMissingNNSRecord) {
|
|
||||||
// if contract not found we deploy it instead of update
|
|
||||||
method = constants.DeployMethodName
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("can't resolve hash for contract update: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = helper.AddManifestGroup(c.ContractWallet, ctrHash, cs)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't sign manifest group: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
invokeHash := management.Hash
|
|
||||||
if method == constants.UpdateMethodName {
|
|
||||||
invokeHash = ctrHash
|
|
||||||
}
|
|
||||||
|
|
||||||
args, err := helper.GetContractDeployData(c, ctrName, keysParam, constants.UpdateMethodName)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: getting update params: %v", ctrName, err)
|
|
||||||
}
|
|
||||||
params := helper.GetContractDeployParameters(cs, args)
|
|
||||||
res, err := c.CommitteeAct.MakeCall(invokeHash, method, params...)
|
|
||||||
if err != nil {
|
|
||||||
if method != constants.UpdateMethodName || !strings.Contains(err.Error(), common.ErrAlreadyUpdated) {
|
|
||||||
return fmt.Errorf("deploy contract: %w", err)
|
|
||||||
}
|
|
||||||
c.Command.Printf("%s contract is already updated.\n", ctrName)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteBytes(res.Script)
|
|
||||||
|
|
||||||
if method == constants.DeployMethodName {
|
|
||||||
// same actions are done in InitializeContext.setNNS, can be unified
|
|
||||||
domain := ctrName + ".frostfs"
|
|
||||||
script, ok, err := c.NNSRegisterDomainScript(nnsHash, cs.Hash, domain)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !ok {
|
|
||||||
w.WriteBytes(script)
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "deleteRecords", callflag.All, domain, int64(nns.TXT))
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "addRecord", callflag.All,
|
|
||||||
domain, int64(nns.TXT), cs.Hash.StringLE())
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "addRecord", callflag.All,
|
|
||||||
domain, int64(nns.TXT), address.Uint160ToString(cs.Hash))
|
|
||||||
}
|
|
||||||
c.Command.Printf("NNS: Set %s -> %s\n", domain, cs.Hash.StringLE())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
224
cmd/frostfs-adm/internal/modules/morph/deploy.go
Normal file
224
cmd/frostfs-adm/internal/modules/morph/deploy.go
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-contract/nns"
|
||||||
|
"github.com/nspcc-dev/neo-go/cli/cmdargs"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/services/rpcsrv/params"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
contractPathFlag = "contract"
|
||||||
|
updateFlag = "update"
|
||||||
|
customZoneFlag = "domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
var deployCmd = &cobra.Command{
|
||||||
|
Use: "deploy",
|
||||||
|
Short: "Deploy additional smart-contracts",
|
||||||
|
Long: `Deploy additional smart-contract which are not related to core.
|
||||||
|
All contracts are deployed by the committee, so access to the alphabet wallets is required.
|
||||||
|
Optionally, arguments can be provided to be passed to a contract's _deploy function.
|
||||||
|
The syntax is the same as for 'neo-go contract testinvokefunction' command.
|
||||||
|
Compiled contract file name must contain '_contract.nef' suffix.
|
||||||
|
Contract's manifest file name must be 'config.json'.
|
||||||
|
NNS name is taken by stripping '_contract.nef' from the NEF file (similar to frostfs contracts).`,
|
||||||
|
PreRun: func(cmd *cobra.Command, _ []string) {
|
||||||
|
_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
||||||
|
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
||||||
|
},
|
||||||
|
RunE: deployContractCmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ff := deployCmd.Flags()
|
||||||
|
|
||||||
|
ff.String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
||||||
|
_ = deployCmd.MarkFlagFilename(alphabetWalletsFlag)
|
||||||
|
|
||||||
|
ff.StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
||||||
|
ff.String(contractPathFlag, "", "Path to the contract directory")
|
||||||
|
_ = deployCmd.MarkFlagFilename(contractPathFlag)
|
||||||
|
|
||||||
|
ff.Bool(updateFlag, false, "Update an existing contract")
|
||||||
|
ff.String(customZoneFlag, "frostfs", "Custom zone for NNS")
|
||||||
|
}
|
||||||
|
|
||||||
|
func deployContractCmd(cmd *cobra.Command, args []string) error {
|
||||||
|
v := viper.GetViper()
|
||||||
|
c, err := newInitializeContext(cmd, v)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("initialization error: %w", err)
|
||||||
|
}
|
||||||
|
defer c.close()
|
||||||
|
|
||||||
|
ctrPath, _ := cmd.Flags().GetString(contractPathFlag)
|
||||||
|
ctrName, err := probeContractName(ctrPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cs, err := readContract(ctrPath, ctrName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
nnsCs, err := c.Client.GetContractStateByID(1)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't fetch NNS contract state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
callHash := management.Hash
|
||||||
|
method := deployMethodName
|
||||||
|
zone, _ := cmd.Flags().GetString(customZoneFlag)
|
||||||
|
domain := ctrName + "." + zone
|
||||||
|
isUpdate, _ := cmd.Flags().GetBool(updateFlag)
|
||||||
|
if isUpdate {
|
||||||
|
cs.Hash, err = nnsResolveHash(c.ReadOnlyInvoker, nnsCs.Hash, domain)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't fetch contract hash from NNS: %w", err)
|
||||||
|
}
|
||||||
|
callHash = cs.Hash
|
||||||
|
method = updateMethodName
|
||||||
|
} else {
|
||||||
|
cs.Hash = state.CreateContractHash(
|
||||||
|
c.CommitteeAcc.Contract.ScriptHash(),
|
||||||
|
cs.NEF.Checksum,
|
||||||
|
cs.Manifest.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
w := io.NewBufBinWriter()
|
||||||
|
if err := emitDeploymentArguments(w.BinWriter, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
emit.Bytes(w.BinWriter, cs.RawManifest)
|
||||||
|
emit.Bytes(w.BinWriter, cs.RawNEF)
|
||||||
|
emit.Int(w.BinWriter, 3)
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.PACK)
|
||||||
|
emit.AppCallNoArgs(w.BinWriter, callHash, method, callflag.All)
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.DROP) // contract state on stack
|
||||||
|
if !isUpdate {
|
||||||
|
bw := io.NewBufBinWriter()
|
||||||
|
emit.Instruction(bw.BinWriter, opcode.INITSSLOT, []byte{1})
|
||||||
|
emit.AppCall(bw.BinWriter, nnsCs.Hash, "getPrice", callflag.All)
|
||||||
|
emit.Opcodes(bw.BinWriter, opcode.STSFLD0)
|
||||||
|
emit.AppCall(bw.BinWriter, nnsCs.Hash, "setPrice", callflag.All, 1)
|
||||||
|
|
||||||
|
start := bw.Len()
|
||||||
|
needRecord := false
|
||||||
|
|
||||||
|
ok, err := c.nnsRootRegistered(nnsCs.Hash, zone)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !ok {
|
||||||
|
needRecord = true
|
||||||
|
|
||||||
|
emit.AppCall(bw.BinWriter, nnsCs.Hash, "register", callflag.All,
|
||||||
|
zone, c.CommitteeAcc.Contract.ScriptHash(),
|
||||||
|
"ops@nspcc.ru", int64(3600), int64(600), int64(defaultExpirationTime), int64(3600))
|
||||||
|
emit.Opcodes(bw.BinWriter, opcode.ASSERT)
|
||||||
|
|
||||||
|
emit.AppCall(bw.BinWriter, nnsCs.Hash, "register", callflag.All,
|
||||||
|
domain, c.CommitteeAcc.Contract.ScriptHash(),
|
||||||
|
"ops@nspcc.ru", int64(3600), int64(600), int64(defaultExpirationTime), int64(3600))
|
||||||
|
emit.Opcodes(bw.BinWriter, opcode.ASSERT)
|
||||||
|
} else {
|
||||||
|
s, ok, err := c.nnsRegisterDomainScript(nnsCs.Hash, cs.Hash, domain)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
needRecord = !ok
|
||||||
|
if len(s) != 0 {
|
||||||
|
bw.WriteBytes(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if needRecord {
|
||||||
|
emit.AppCall(bw.BinWriter, nnsCs.Hash, "deleteRecords", callflag.All, domain, int64(nns.TXT))
|
||||||
|
emit.AppCall(bw.BinWriter, nnsCs.Hash, "addRecord", callflag.All,
|
||||||
|
domain, int64(nns.TXT), address.Uint160ToString(cs.Hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
if bw.Err != nil {
|
||||||
|
panic(fmt.Errorf("BUG: can't create deployment script: %w", w.Err))
|
||||||
|
} else if bw.Len() != start {
|
||||||
|
w.WriteBytes(bw.Bytes())
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.LDSFLD0, opcode.PUSH1, opcode.PACK)
|
||||||
|
emit.AppCallNoArgs(w.BinWriter, nnsCs.Hash, "setPrice", callflag.All)
|
||||||
|
|
||||||
|
if needRecord {
|
||||||
|
c.Command.Printf("NNS: Set %s -> %s\n", domain, cs.Hash.StringLE())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.Err != nil {
|
||||||
|
panic(fmt.Errorf("BUG: can't create deployment script: %w", w.Err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.sendCommitteeTx(w.Bytes(), false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.awaitTx()
|
||||||
|
}
|
||||||
|
|
||||||
|
func emitDeploymentArguments(w *io.BinWriter, args []string) error {
|
||||||
|
_, ps, err := cmdargs.ParseParams(args, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ps) == 0 {
|
||||||
|
emit.Opcodes(w, opcode.NEWARRAY0)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ps) != 1 {
|
||||||
|
return fmt.Errorf("at most one argument is expected for deploy, got %d", len(ps))
|
||||||
|
}
|
||||||
|
|
||||||
|
// We could emit this directly, but round-trip through JSON is more robust.
|
||||||
|
// This a CLI, so optimizing the conversion is not worth the effort.
|
||||||
|
data, err := json.Marshal(ps)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var pp params.Params
|
||||||
|
if err := json.Unmarshal(data, &pp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return params.ExpandArrayIntoScript(w, pp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func probeContractName(ctrPath string) (string, error) {
|
||||||
|
ds, err := os.ReadDir(ctrPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("can't read directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ctrName string
|
||||||
|
for i := range ds {
|
||||||
|
if strings.HasSuffix(ds[i].Name(), "_contract.nef") {
|
||||||
|
ctrName = strings.TrimSuffix(ds[i].Name(), "_contract.nef")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctrName == "" {
|
||||||
|
return "", fmt.Errorf("can't find any NEF files in %s", ctrPath)
|
||||||
|
}
|
||||||
|
return ctrName, nil
|
||||||
|
}
|
40
cmd/frostfs-adm/internal/modules/morph/download.go
Normal file
40
cmd/frostfs-adm/internal/modules/morph/download.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/go-github/v39/github"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func downloadContractsFromGithub(cmd *cobra.Command) (io.ReadCloser, error) {
|
||||||
|
gcl := github.NewClient(nil)
|
||||||
|
release, _, err := gcl.Repositories.GetLatestRelease(context.Background(), "nspcc-dev", "frostfs-contract")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't fetch release info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Printf("Found %s (%s), downloading...\n", release.GetTagName(), release.GetName())
|
||||||
|
|
||||||
|
var url string
|
||||||
|
for _, a := range release.Assets {
|
||||||
|
if strings.HasPrefix(a.GetName(), "frostfs-contract") {
|
||||||
|
url = a.GetBrowserDownloadURL()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if url == "" {
|
||||||
|
return nil, errors.New("can't find contracts archive in release assets")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't fetch contracts archive: %w", err)
|
||||||
|
}
|
||||||
|
return resp.Body, nil
|
||||||
|
}
|
|
@ -1,22 +1,15 @@
|
||||||
package contract
|
package morph
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
|
"github.com/TrueCloudLab/frostfs-contract/nns"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper"
|
|
||||||
morphClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
|
@ -36,27 +29,26 @@ type contractDumpInfo struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func dumpContractHashes(cmd *cobra.Command, _ []string) error {
|
func dumpContractHashes(cmd *cobra.Command, _ []string) error {
|
||||||
c, err := helper.GetN3Client(viper.GetViper())
|
c, err := getN3Client(viper.GetViper())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("can't create N3 client: %w", err)
|
return fmt.Errorf("can't create N3 client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
r := management.NewReader(invoker.New(c, nil))
|
cs, err := c.GetContractStateByID(1)
|
||||||
cs, err := helper.GetContractByID(r, 1)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
zone, _ := cmd.Flags().GetString(commonflags.CustomZoneFlag)
|
zone, _ := cmd.Flags().GetString(customZoneFlag)
|
||||||
if zone != "" {
|
if zone != "" {
|
||||||
return dumpCustomZoneHashes(cmd, cs.Hash, zone, c)
|
return dumpCustomZoneHashes(cmd, cs.Hash, zone, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
infos := []contractDumpInfo{{name: constants.NNSContract, hash: cs.Hash}}
|
infos := []contractDumpInfo{{name: nnsContract, hash: cs.Hash}}
|
||||||
|
|
||||||
irSize := 0
|
irSize := 0
|
||||||
for ; irSize < lastGlagoliticLetter; irSize++ {
|
for ; irSize < lastGlagoliticLetter; irSize++ {
|
||||||
ok, err := helper.NNSIsAvailable(c, cs.Hash, helper.GetAlphabetNNSDomain(irSize))
|
ok, err := nnsIsAvailable(c, cs.Hash, getAlphabetNNSDomain(irSize))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if ok {
|
} else if ok {
|
||||||
|
@ -68,9 +60,9 @@ func dumpContractHashes(cmd *cobra.Command, _ []string) error {
|
||||||
|
|
||||||
if irSize != 0 {
|
if irSize != 0 {
|
||||||
bw.Reset()
|
bw.Reset()
|
||||||
for i := range irSize {
|
for i := 0; i < irSize; i++ {
|
||||||
emit.AppCall(bw.BinWriter, cs.Hash, "resolve", callflag.ReadOnly,
|
emit.AppCall(bw.BinWriter, cs.Hash, "resolve", callflag.ReadOnly,
|
||||||
helper.GetAlphabetNNSDomain(i),
|
getAlphabetNNSDomain(i),
|
||||||
int64(nns.TXT))
|
int64(nns.TXT))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,19 +71,19 @@ func dumpContractHashes(cmd *cobra.Command, _ []string) error {
|
||||||
return fmt.Errorf("can't fetch info from NNS: %w", err)
|
return fmt.Errorf("can't fetch info from NNS: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range irSize {
|
for i := 0; i < irSize; i++ {
|
||||||
info := contractDumpInfo{name: fmt.Sprintf("alphabet %d", i)}
|
info := contractDumpInfo{name: fmt.Sprintf("alphabet %d", i)}
|
||||||
if h, err := helper.ParseNNSResolveResult(alphaRes.Stack[i]); err == nil {
|
if h, err := parseNNSResolveResult(alphaRes.Stack[i]); err == nil {
|
||||||
info.hash = h
|
info.hash = h
|
||||||
}
|
}
|
||||||
infos = append(infos, info)
|
infos = append(infos, info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ctrName := range constants.ContractList {
|
for _, ctrName := range contractList {
|
||||||
bw.Reset()
|
bw.Reset()
|
||||||
emit.AppCall(bw.BinWriter, cs.Hash, "resolve", callflag.ReadOnly,
|
emit.AppCall(bw.BinWriter, cs.Hash, "resolve", callflag.ReadOnly,
|
||||||
helper.DomainOf(ctrName), int64(nns.TXT))
|
ctrName+".frostfs", int64(nns.TXT))
|
||||||
|
|
||||||
res, err := c.InvokeScript(bw.Bytes(), nil)
|
res, err := c.InvokeScript(bw.Bytes(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -100,7 +92,7 @@ func dumpContractHashes(cmd *cobra.Command, _ []string) error {
|
||||||
|
|
||||||
info := contractDumpInfo{name: ctrName}
|
info := contractDumpInfo{name: ctrName}
|
||||||
if len(res.Stack) != 0 {
|
if len(res.Stack) != 0 {
|
||||||
if h, err := helper.ParseNNSResolveResult(res.Stack[0]); err == nil {
|
if h, err := parseNNSResolveResult(res.Stack[0]); err == nil {
|
||||||
info.hash = h
|
info.hash = h
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,32 +105,33 @@ func dumpContractHashes(cmd *cobra.Command, _ []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func dumpCustomZoneHashes(cmd *cobra.Command, nnsHash util.Uint160, zone string, c helper.Client) error {
|
func dumpCustomZoneHashes(cmd *cobra.Command, nnsHash util.Uint160, zone string, c Client) error {
|
||||||
const nnsMaxTokens = 100
|
const nnsMaxTokens = 100
|
||||||
|
|
||||||
inv := invoker.New(c, nil)
|
inv := invoker.New(c, nil)
|
||||||
|
|
||||||
|
arr, err := unwrap.Array(inv.CallAndExpandIterator(nnsHash, "tokens", nnsMaxTokens))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't get a list of NNS domains: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(zone, ".") {
|
if !strings.HasPrefix(zone, ".") {
|
||||||
zone = "." + zone
|
zone = "." + zone
|
||||||
}
|
}
|
||||||
|
|
||||||
var infos []contractDumpInfo
|
var infos []contractDumpInfo
|
||||||
processItem := func(item stackitem.Item) {
|
for i := range arr {
|
||||||
bs, err := item.TryBytes()
|
bs, err := arr[i].TryBytes()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cmd.PrintErrf("Invalid NNS record: %v\n", err)
|
continue
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !bytes.HasSuffix(bs, []byte(zone)) || bytes.HasPrefix(bs, []byte(morphClient.NNSGroupKeyName)) {
|
if !bytes.HasSuffix(bs, []byte(zone)) {
|
||||||
// Related https://github.com/nspcc-dev/neofs-contract/issues/316.
|
continue
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h, err := helper.NNSResolveHash(inv, nnsHash, string(bs))
|
h, err := nnsResolveHash(inv, nnsHash, string(bs))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cmd.PrintErrf("Could not resolve name %s: %v\n", string(bs), err)
|
continue
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
infos = append(infos, contractDumpInfo{
|
infos = append(infos, contractDumpInfo{
|
||||||
|
@ -147,48 +140,6 @@ func dumpCustomZoneHashes(cmd *cobra.Command, nnsHash util.Uint160, zone string,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
script, err := smartcontract.CreateCallAndPrefetchIteratorScript(nnsHash, "tokens", nnsMaxTokens)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("create prefetch script: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
arr, sessionID, iter, err := unwrap.ArrayAndSessionIterator(inv.Run(script))
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, unwrap.ErrNoSessionID) {
|
|
||||||
items, err := unwrap.Array(inv.CallAndExpandIterator(nnsHash, "tokens", nnsMaxTokens))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get a list of NNS domains: %w", err)
|
|
||||||
}
|
|
||||||
if len(items) == nnsMaxTokens {
|
|
||||||
cmd.PrintErrln("Provided RPC endpoint doesn't support sessions, some hashes might be lost.")
|
|
||||||
}
|
|
||||||
for i := range items {
|
|
||||||
processItem(items[i])
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for i := range arr {
|
|
||||||
processItem(arr[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
_ = inv.TerminateSession(sessionID)
|
|
||||||
}()
|
|
||||||
|
|
||||||
items, err := inv.TraverseIterator(sessionID, &iter, 0)
|
|
||||||
for err == nil && len(items) != 0 {
|
|
||||||
for i := range items {
|
|
||||||
processItem(items[i])
|
|
||||||
}
|
|
||||||
items, err = inv.TraverseIterator(sessionID, &iter, 0)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error during NNS domains iteration: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fillContractVersion(cmd, c, infos)
|
fillContractVersion(cmd, c, infos)
|
||||||
printContractInfo(cmd, infos)
|
printContractInfo(cmd, infos)
|
||||||
|
|
||||||
|
@ -227,7 +178,7 @@ func printContractInfo(cmd *cobra.Command, infos []contractDumpInfo) {
|
||||||
cmd.Print(buf.String())
|
cmd.Print(buf.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func fillContractVersion(cmd *cobra.Command, c helper.Client, infos []contractDumpInfo) {
|
func fillContractVersion(cmd *cobra.Command, c Client, infos []contractDumpInfo) {
|
||||||
bw := io.NewBufBinWriter()
|
bw := io.NewBufBinWriter()
|
||||||
sub := io.NewBufBinWriter()
|
sub := io.NewBufBinWriter()
|
||||||
for i := range infos {
|
for i := range infos {
|
57
cmd/frostfs-adm/internal/modules/morph/epoch.go
Normal file
57
cmd/frostfs-adm/internal/modules/morph/epoch.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func forceNewEpochCmd(cmd *cobra.Command, args []string) error {
|
||||||
|
wCtx, err := newInitializeContext(cmd, viper.GetViper())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't to initialize context: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cs, err := wCtx.Client.GetContractStateByID(1)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't get NNS contract info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nmHash, err := nnsResolveHash(wCtx.ReadOnlyInvoker, cs.Hash, netmapContract+".frostfs")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't get netmap contract hash: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bw := io.NewBufBinWriter()
|
||||||
|
if err := emitNewEpochCall(bw, wCtx, nmHash); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wCtx.sendConsensusTx(bw.Bytes()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return wCtx.awaitTx()
|
||||||
|
}
|
||||||
|
|
||||||
|
func emitNewEpochCall(bw *io.BufBinWriter, wCtx *initializeContext, nmHash util.Uint160) error {
|
||||||
|
curr, err := unwrap.Int64(wCtx.ReadOnlyInvoker.Call(nmHash, "epoch"))
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("can't fetch current epoch from the netmap contract")
|
||||||
|
}
|
||||||
|
|
||||||
|
newEpoch := curr + 1
|
||||||
|
wCtx.Command.Printf("Current epoch: %d, increase to %d.\n", curr, newEpoch)
|
||||||
|
|
||||||
|
// In NeoFS this is done via Notary contract. Here, however, we can form the
|
||||||
|
// transaction locally.
|
||||||
|
emit.AppCall(bw.BinWriter, nmHash, "newEpoch", callflag.All, newEpoch)
|
||||||
|
return bw.Err
|
||||||
|
}
|
|
@ -1,538 +0,0 @@
|
||||||
package frostfsid
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
frostfsidclient "git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client"
|
|
||||||
frostfsidrpclient "git.frostfs.info/TrueCloudLab/frostfs-contract/rpcclient/frostfsid"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
const iteratorBatchSize = 1
|
|
||||||
|
|
||||||
const (
|
|
||||||
namespaceFlag = "namespace"
|
|
||||||
subjectNameFlag = "subject-name"
|
|
||||||
subjectKeyFlag = "subject-key"
|
|
||||||
subjectAddressFlag = "subject-address"
|
|
||||||
includeNamesFlag = "include-names"
|
|
||||||
groupNameFlag = "group-name"
|
|
||||||
groupIDFlag = "group-id"
|
|
||||||
|
|
||||||
rootNamespacePlaceholder = "<root>"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
Cmd = &cobra.Command{
|
|
||||||
Use: "frostfsid",
|
|
||||||
Short: "Section for frostfsid interactions commands",
|
|
||||||
}
|
|
||||||
|
|
||||||
frostfsidCreateNamespaceCmd = &cobra.Command{
|
|
||||||
Use: "create-namespace",
|
|
||||||
Short: "Create new namespace in frostfsid contract",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
Run: frostfsidCreateNamespace,
|
|
||||||
}
|
|
||||||
|
|
||||||
frostfsidListNamespacesCmd = &cobra.Command{
|
|
||||||
Use: "list-namespaces",
|
|
||||||
Short: "List all namespaces in frostfsid",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
Run: frostfsidListNamespaces,
|
|
||||||
}
|
|
||||||
|
|
||||||
frostfsidCreateSubjectCmd = &cobra.Command{
|
|
||||||
Use: "create-subject",
|
|
||||||
Short: "Create subject in frostfsid contract",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
Run: frostfsidCreateSubject,
|
|
||||||
}
|
|
||||||
|
|
||||||
frostfsidDeleteSubjectCmd = &cobra.Command{
|
|
||||||
Use: "delete-subject",
|
|
||||||
Short: "Delete subject from frostfsid contract",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
Run: frostfsidDeleteSubject,
|
|
||||||
}
|
|
||||||
|
|
||||||
frostfsidListSubjectsCmd = &cobra.Command{
|
|
||||||
Use: "list-subjects",
|
|
||||||
Short: "List subjects in namespace",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
Run: frostfsidListSubjects,
|
|
||||||
}
|
|
||||||
|
|
||||||
frostfsidCreateGroupCmd = &cobra.Command{
|
|
||||||
Use: "create-group",
|
|
||||||
Short: "Create group in frostfsid contract",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
Run: frostfsidCreateGroup,
|
|
||||||
}
|
|
||||||
|
|
||||||
frostfsidDeleteGroupCmd = &cobra.Command{
|
|
||||||
Use: "delete-group",
|
|
||||||
Short: "Delete group from frostfsid contract",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
Run: frostfsidDeleteGroup,
|
|
||||||
}
|
|
||||||
|
|
||||||
frostfsidListGroupsCmd = &cobra.Command{
|
|
||||||
Use: "list-groups",
|
|
||||||
Short: "List groups in namespace",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
Run: frostfsidListGroups,
|
|
||||||
}
|
|
||||||
|
|
||||||
frostfsidAddSubjectToGroupCmd = &cobra.Command{
|
|
||||||
Use: "add-subject-to-group",
|
|
||||||
Short: "Add subject to group",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
Run: frostfsidAddSubjectToGroup,
|
|
||||||
}
|
|
||||||
|
|
||||||
frostfsidRemoveSubjectFromGroupCmd = &cobra.Command{
|
|
||||||
Use: "remove-subject-from-group",
|
|
||||||
Short: "Remove subject from group",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
Run: frostfsidRemoveSubjectFromGroup,
|
|
||||||
}
|
|
||||||
|
|
||||||
frostfsidListGroupSubjectsCmd = &cobra.Command{
|
|
||||||
Use: "list-group-subjects",
|
|
||||||
Short: "List subjects in group",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
},
|
|
||||||
Run: frostfsidListGroupSubjects,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func initFrostfsIDCreateNamespaceCmd() {
|
|
||||||
Cmd.AddCommand(frostfsidCreateNamespaceCmd)
|
|
||||||
frostfsidCreateNamespaceCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
frostfsidCreateNamespaceCmd.Flags().String(namespaceFlag, "", "Namespace name to create")
|
|
||||||
frostfsidCreateNamespaceCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
_ = frostfsidCreateNamespaceCmd.MarkFlagRequired(namespaceFlag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initFrostfsIDListNamespacesCmd() {
|
|
||||||
Cmd.AddCommand(frostfsidListNamespacesCmd)
|
|
||||||
frostfsidListNamespacesCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
frostfsidListNamespacesCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initFrostfsIDCreateSubjectCmd() {
|
|
||||||
Cmd.AddCommand(frostfsidCreateSubjectCmd)
|
|
||||||
frostfsidCreateSubjectCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
frostfsidCreateSubjectCmd.Flags().String(namespaceFlag, "", "Namespace where create subject")
|
|
||||||
frostfsidCreateSubjectCmd.Flags().String(subjectNameFlag, "", "Subject name, must be unique in namespace")
|
|
||||||
frostfsidCreateSubjectCmd.Flags().String(subjectKeyFlag, "", "Subject hex-encoded public key")
|
|
||||||
frostfsidCreateSubjectCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initFrostfsIDDeleteSubjectCmd() {
|
|
||||||
Cmd.AddCommand(frostfsidDeleteSubjectCmd)
|
|
||||||
frostfsidDeleteSubjectCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
frostfsidDeleteSubjectCmd.Flags().String(subjectAddressFlag, "", "Subject address")
|
|
||||||
frostfsidDeleteSubjectCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initFrostfsIDListSubjectsCmd() {
|
|
||||||
Cmd.AddCommand(frostfsidListSubjectsCmd)
|
|
||||||
frostfsidListSubjectsCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
frostfsidListSubjectsCmd.Flags().String(namespaceFlag, "", "Namespace to list subjects")
|
|
||||||
frostfsidListSubjectsCmd.Flags().Bool(includeNamesFlag, false, "Whether include subject name (require additional requests)")
|
|
||||||
frostfsidListSubjectsCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initFrostfsIDCreateGroupCmd() {
|
|
||||||
Cmd.AddCommand(frostfsidCreateGroupCmd)
|
|
||||||
frostfsidCreateGroupCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
frostfsidCreateGroupCmd.Flags().String(namespaceFlag, "", "Namespace where create group")
|
|
||||||
frostfsidCreateGroupCmd.Flags().String(groupNameFlag, "", "Group name, must be unique in namespace")
|
|
||||||
frostfsidCreateGroupCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
_ = frostfsidCreateGroupCmd.MarkFlagRequired(groupNameFlag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initFrostfsIDDeleteGroupCmd() {
|
|
||||||
Cmd.AddCommand(frostfsidDeleteGroupCmd)
|
|
||||||
frostfsidDeleteGroupCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
frostfsidDeleteGroupCmd.Flags().String(namespaceFlag, "", "Namespace to delete group")
|
|
||||||
frostfsidDeleteGroupCmd.Flags().Int64(groupIDFlag, 0, "Group id")
|
|
||||||
frostfsidDeleteGroupCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initFrostfsIDListGroupsCmd() {
|
|
||||||
Cmd.AddCommand(frostfsidListGroupsCmd)
|
|
||||||
frostfsidListGroupsCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
frostfsidListGroupsCmd.Flags().String(namespaceFlag, "", "Namespace to list groups")
|
|
||||||
frostfsidListGroupsCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initFrostfsIDAddSubjectToGroupCmd() {
|
|
||||||
Cmd.AddCommand(frostfsidAddSubjectToGroupCmd)
|
|
||||||
frostfsidAddSubjectToGroupCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
frostfsidAddSubjectToGroupCmd.Flags().String(subjectAddressFlag, "", "Subject address")
|
|
||||||
frostfsidAddSubjectToGroupCmd.Flags().Int64(groupIDFlag, 0, "Group id")
|
|
||||||
frostfsidAddSubjectToGroupCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initFrostfsIDRemoveSubjectFromGroupCmd() {
|
|
||||||
Cmd.AddCommand(frostfsidRemoveSubjectFromGroupCmd)
|
|
||||||
frostfsidRemoveSubjectFromGroupCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
frostfsidRemoveSubjectFromGroupCmd.Flags().String(subjectAddressFlag, "", "Subject address")
|
|
||||||
frostfsidRemoveSubjectFromGroupCmd.Flags().Int64(groupIDFlag, 0, "Group id")
|
|
||||||
frostfsidRemoveSubjectFromGroupCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initFrostfsIDListGroupSubjectsCmd() {
|
|
||||||
Cmd.AddCommand(frostfsidListGroupSubjectsCmd)
|
|
||||||
frostfsidListGroupSubjectsCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
frostfsidListGroupSubjectsCmd.Flags().String(namespaceFlag, "", "Namespace name")
|
|
||||||
frostfsidListGroupSubjectsCmd.Flags().Int64(groupIDFlag, 0, "Group id")
|
|
||||||
frostfsidListGroupSubjectsCmd.Flags().Bool(includeNamesFlag, false, "Whether include subject name (require additional requests)")
|
|
||||||
frostfsidListGroupSubjectsCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func frostfsidCreateNamespace(cmd *cobra.Command, _ []string) {
|
|
||||||
ns := getFrostfsIDNamespace(cmd)
|
|
||||||
|
|
||||||
ffsid, err := newFrostfsIDClient(cmd)
|
|
||||||
commonCmd.ExitOnErr(cmd, "init contract client: %w", err)
|
|
||||||
|
|
||||||
ffsid.addCall(ffsid.roCli.CreateNamespaceCall(ns))
|
|
||||||
|
|
||||||
err = ffsid.sendWait()
|
|
||||||
commonCmd.ExitOnErr(cmd, "create namespace error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func frostfsidListNamespaces(cmd *cobra.Command, _ []string) {
|
|
||||||
inv, _, hash := initInvoker(cmd)
|
|
||||||
reader := frostfsidrpclient.NewReader(inv, hash)
|
|
||||||
sessionID, it, err := reader.ListNamespaces()
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't get namespace: %w", err)
|
|
||||||
items, err := readIterator(inv, &it, iteratorBatchSize, sessionID)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't read iterator: %w", err)
|
|
||||||
|
|
||||||
namespaces, err := frostfsidclient.ParseNamespaces(items)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't parse namespace: %w", err)
|
|
||||||
sort.Slice(namespaces, func(i, j int) bool { return namespaces[i].Name < namespaces[j].Name })
|
|
||||||
|
|
||||||
for _, namespace := range namespaces {
|
|
||||||
if namespace.Name == "" {
|
|
||||||
namespace.Name = rootNamespacePlaceholder
|
|
||||||
}
|
|
||||||
cmd.Printf("%s\n", namespace.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func frostfsidCreateSubject(cmd *cobra.Command, _ []string) {
|
|
||||||
ns := getFrostfsIDNamespace(cmd)
|
|
||||||
subjName := getFrostfsIDSubjectName(cmd)
|
|
||||||
subjKey := getFrostfsIDSubjectKey(cmd)
|
|
||||||
|
|
||||||
ffsid, err := newFrostfsIDClient(cmd)
|
|
||||||
commonCmd.ExitOnErr(cmd, "init contract client: %w", err)
|
|
||||||
|
|
||||||
ffsid.addCall(ffsid.roCli.CreateSubjectCall(ns, subjKey))
|
|
||||||
if subjName != "" {
|
|
||||||
ffsid.addCall(ffsid.roCli.SetSubjectNameCall(subjKey.GetScriptHash(), subjName))
|
|
||||||
}
|
|
||||||
|
|
||||||
err = ffsid.sendWait()
|
|
||||||
commonCmd.ExitOnErr(cmd, "create subject: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func frostfsidDeleteSubject(cmd *cobra.Command, _ []string) {
|
|
||||||
subjectAddress := getFrostfsIDSubjectAddress(cmd)
|
|
||||||
|
|
||||||
ffsid, err := newFrostfsIDClient(cmd)
|
|
||||||
commonCmd.ExitOnErr(cmd, "init contract client: %w", err)
|
|
||||||
|
|
||||||
ffsid.addCall(ffsid.roCli.DeleteSubjectCall(subjectAddress))
|
|
||||||
|
|
||||||
err = ffsid.sendWait()
|
|
||||||
commonCmd.ExitOnErr(cmd, "delete subject error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func frostfsidListSubjects(cmd *cobra.Command, _ []string) {
|
|
||||||
includeNames, _ := cmd.Flags().GetBool(includeNamesFlag)
|
|
||||||
ns := getFrostfsIDNamespace(cmd)
|
|
||||||
inv, _, hash := initInvoker(cmd)
|
|
||||||
reader := frostfsidrpclient.NewReader(inv, hash)
|
|
||||||
sessionID, it, err := reader.ListNamespaceSubjects(ns)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't get namespace: %w", err)
|
|
||||||
|
|
||||||
subAddresses, err := frostfsidclient.UnwrapArrayOfUint160(readIterator(inv, &it, iteratorBatchSize, sessionID))
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't unwrap: %w", err)
|
|
||||||
|
|
||||||
sort.Slice(subAddresses, func(i, j int) bool { return subAddresses[i].Less(subAddresses[j]) })
|
|
||||||
|
|
||||||
for _, addr := range subAddresses {
|
|
||||||
if !includeNames {
|
|
||||||
cmd.Println(address.Uint160ToString(addr))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionID, it, err := reader.ListSubjects()
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't get subject: %w", err)
|
|
||||||
|
|
||||||
items, err := readIterator(inv, &it, iteratorBatchSize, sessionID)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't read iterator: %w", err)
|
|
||||||
|
|
||||||
subj, err := frostfsidclient.ParseSubject(items)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't parse subject: %w", err)
|
|
||||||
|
|
||||||
cmd.Printf("%s (%s)\n", address.Uint160ToString(addr), subj.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func frostfsidCreateGroup(cmd *cobra.Command, _ []string) {
|
|
||||||
ns := getFrostfsIDNamespace(cmd)
|
|
||||||
groupName := getFrostfsIDGroupName(cmd)
|
|
||||||
|
|
||||||
ffsid, err := newFrostfsIDClient(cmd)
|
|
||||||
commonCmd.ExitOnErr(cmd, "init contract client: %w", err)
|
|
||||||
|
|
||||||
ffsid.addCall(ffsid.roCli.CreateGroupCall(ns, groupName))
|
|
||||||
|
|
||||||
groupID, err := ffsid.roCli.ParseGroupID(ffsid.sendWaitRes())
|
|
||||||
commonCmd.ExitOnErr(cmd, "create group: %w", err)
|
|
||||||
|
|
||||||
cmd.Printf("group '%s' created with id: %d\n", groupName, groupID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func frostfsidDeleteGroup(cmd *cobra.Command, _ []string) {
|
|
||||||
ns := getFrostfsIDNamespace(cmd)
|
|
||||||
groupID := getFrostfsIDGroupID(cmd)
|
|
||||||
|
|
||||||
ffsid, err := newFrostfsIDClient(cmd)
|
|
||||||
commonCmd.ExitOnErr(cmd, "init contract client: %w", err)
|
|
||||||
|
|
||||||
ffsid.addCall(ffsid.roCli.DeleteGroupCall(ns, groupID))
|
|
||||||
|
|
||||||
err = ffsid.sendWait()
|
|
||||||
commonCmd.ExitOnErr(cmd, "delete group error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func frostfsidListGroups(cmd *cobra.Command, _ []string) {
|
|
||||||
inv, _, hash := initInvoker(cmd)
|
|
||||||
ns := getFrostfsIDNamespace(cmd)
|
|
||||||
|
|
||||||
reader := frostfsidrpclient.NewReader(inv, hash)
|
|
||||||
sessionID, it, err := reader.ListGroups(ns)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't get namespace: %w", err)
|
|
||||||
|
|
||||||
items, err := readIterator(inv, &it, iteratorBatchSize, sessionID)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't list groups: %w", err)
|
|
||||||
groups, err := frostfsidclient.ParseGroups(items)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't parse groups: %w", err)
|
|
||||||
|
|
||||||
sort.Slice(groups, func(i, j int) bool { return groups[i].Name < groups[j].Name })
|
|
||||||
|
|
||||||
for _, group := range groups {
|
|
||||||
cmd.Printf("%s (%d)\n", group.Name, group.ID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func frostfsidAddSubjectToGroup(cmd *cobra.Command, _ []string) {
|
|
||||||
subjectAddress := getFrostfsIDSubjectAddress(cmd)
|
|
||||||
groupID := getFrostfsIDGroupID(cmd)
|
|
||||||
|
|
||||||
ffsid, err := newFrostfsIDClient(cmd)
|
|
||||||
commonCmd.ExitOnErr(cmd, "init contract client: %w", err)
|
|
||||||
|
|
||||||
ffsid.addCall(ffsid.roCli.AddSubjectToGroupCall(subjectAddress, groupID))
|
|
||||||
|
|
||||||
err = ffsid.sendWait()
|
|
||||||
commonCmd.ExitOnErr(cmd, "add subject to group error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func frostfsidRemoveSubjectFromGroup(cmd *cobra.Command, _ []string) {
|
|
||||||
subjectAddress := getFrostfsIDSubjectAddress(cmd)
|
|
||||||
groupID := getFrostfsIDGroupID(cmd)
|
|
||||||
|
|
||||||
ffsid, err := newFrostfsIDClient(cmd)
|
|
||||||
commonCmd.ExitOnErr(cmd, "init contract client: %w", err)
|
|
||||||
|
|
||||||
ffsid.addCall(ffsid.roCli.RemoveSubjectFromGroupCall(subjectAddress, groupID))
|
|
||||||
|
|
||||||
err = ffsid.sendWait()
|
|
||||||
commonCmd.ExitOnErr(cmd, "remove subject from group error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func frostfsidListGroupSubjects(cmd *cobra.Command, _ []string) {
|
|
||||||
ns := getFrostfsIDNamespace(cmd)
|
|
||||||
groupID := getFrostfsIDGroupID(cmd)
|
|
||||||
includeNames, _ := cmd.Flags().GetBool(includeNamesFlag)
|
|
||||||
inv, cs, hash := initInvoker(cmd)
|
|
||||||
_, err := helper.NNSResolveHash(inv, cs.Hash, helper.DomainOf(constants.FrostfsIDContract))
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't get netmap contract hash: %w", err)
|
|
||||||
|
|
||||||
reader := frostfsidrpclient.NewReader(inv, hash)
|
|
||||||
sessionID, it, err := reader.ListGroupSubjects(ns, big.NewInt(groupID))
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't list groups: %w", err)
|
|
||||||
|
|
||||||
items, err := readIterator(inv, &it, iteratorBatchSize, sessionID)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't read iterator: %w", err)
|
|
||||||
|
|
||||||
subjects, err := frostfsidclient.UnwrapArrayOfUint160(items, err)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't unwrap: %w", err)
|
|
||||||
|
|
||||||
sort.Slice(subjects, func(i, j int) bool { return subjects[i].Less(subjects[j]) })
|
|
||||||
|
|
||||||
for _, subjAddr := range subjects {
|
|
||||||
if !includeNames {
|
|
||||||
cmd.Println(address.Uint160ToString(subjAddr))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
items, err := reader.GetSubject(subjAddr)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't get subject: %w", err)
|
|
||||||
subj, err := frostfsidclient.ParseSubject(items)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't parse subject: %w", err)
|
|
||||||
cmd.Printf("%s (%s)\n", address.Uint160ToString(subjAddr), subj.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type frostfsidClient struct {
|
|
||||||
bw *io.BufBinWriter
|
|
||||||
contractHash util.Uint160
|
|
||||||
roCli *frostfsidclient.Client // client can be used only for waiting tx, parsing and forming method params
|
|
||||||
wCtx *helper.InitializeContext
|
|
||||||
}
|
|
||||||
|
|
||||||
func newFrostfsIDClient(cmd *cobra.Command) (*frostfsidClient, error) {
|
|
||||||
wCtx, err := helper.NewInitializeContext(cmd, viper.GetViper())
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't initialize context: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r := management.NewReader(wCtx.ReadOnlyInvoker)
|
|
||||||
cs, err := helper.GetContractByID(r, 1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't get NNS contract info: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ffsidHash, err := helper.NNSResolveHash(wCtx.ReadOnlyInvoker, cs.Hash, helper.DomainOf(constants.FrostfsIDContract))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't get proxy contract hash: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &frostfsidClient{
|
|
||||||
bw: io.NewBufBinWriter(),
|
|
||||||
contractHash: ffsidHash,
|
|
||||||
roCli: frostfsidclient.NewSimple(wCtx.CommitteeAct, ffsidHash),
|
|
||||||
wCtx: wCtx,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *frostfsidClient) addCall(method string, args []any) {
|
|
||||||
emit.AppCall(f.bw.BinWriter, f.contractHash, method, callflag.All, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *frostfsidClient) sendWait() error {
|
|
||||||
if err := f.wCtx.SendConsensusTx(f.bw.Bytes()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
f.bw.Reset()
|
|
||||||
|
|
||||||
return f.wCtx.AwaitTx()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *frostfsidClient) sendWaitRes() (*state.AppExecResult, error) {
|
|
||||||
if err := f.wCtx.SendConsensusTx(f.bw.Bytes()); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
f.bw.Reset()
|
|
||||||
|
|
||||||
if len(f.wCtx.SentTxs) == 0 {
|
|
||||||
return nil, errors.New("no transactions to wait")
|
|
||||||
}
|
|
||||||
|
|
||||||
f.wCtx.Command.Println("Waiting for transactions to persist...")
|
|
||||||
return f.roCli.Wait(f.wCtx.SentTxs[0].Hash, f.wCtx.SentTxs[0].Vub, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func readIterator(inv *invoker.Invoker, iter *result.Iterator, batchSize int, sessionID uuid.UUID) ([]stackitem.Item, error) {
|
|
||||||
var shouldStop bool
|
|
||||||
res := make([]stackitem.Item, 0)
|
|
||||||
for !shouldStop {
|
|
||||||
items, err := inv.TraverseIterator(sessionID, iter, batchSize)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res = append(res, items...)
|
|
||||||
shouldStop = len(items) < batchSize
|
|
||||||
}
|
|
||||||
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func initInvoker(cmd *cobra.Command) (*invoker.Invoker, *state.Contract, util.Uint160) {
|
|
||||||
c, err := helper.GetN3Client(viper.GetViper())
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't create N3 client: %w", err)
|
|
||||||
|
|
||||||
inv := invoker.New(c, nil)
|
|
||||||
r := management.NewReader(inv)
|
|
||||||
|
|
||||||
cs, err := r.GetContractByID(1)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't get NNS contract info: %w", err)
|
|
||||||
|
|
||||||
nmHash, err := helper.NNSResolveHash(inv, cs.Hash, helper.DomainOf(constants.FrostfsIDContract))
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't get netmap contract hash: %w", err)
|
|
||||||
|
|
||||||
return inv, cs, nmHash
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
package frostfsid
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/internal/ape"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getFrostfsIDSubjectKey(cmd *cobra.Command) *keys.PublicKey {
|
|
||||||
subjKeyHex, _ := cmd.Flags().GetString(subjectKeyFlag)
|
|
||||||
subjKey, err := keys.NewPublicKeyFromString(subjKeyHex)
|
|
||||||
commonCmd.ExitOnErr(cmd, "invalid subject key: %w", err)
|
|
||||||
return subjKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFrostfsIDSubjectAddress(cmd *cobra.Command) util.Uint160 {
|
|
||||||
subjAddress, _ := cmd.Flags().GetString(subjectAddressFlag)
|
|
||||||
subjAddr, err := address.StringToUint160(subjAddress)
|
|
||||||
commonCmd.ExitOnErr(cmd, "invalid subject address: %w", err)
|
|
||||||
return subjAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFrostfsIDSubjectName(cmd *cobra.Command) string {
|
|
||||||
subjectName, _ := cmd.Flags().GetString(subjectNameFlag)
|
|
||||||
|
|
||||||
if subjectName == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ape.SubjectNameRegexp.MatchString(subjectName) {
|
|
||||||
commonCmd.ExitOnErr(cmd, "invalid subject name: %w",
|
|
||||||
fmt.Errorf("name must match regexp: %s", ape.SubjectNameRegexp.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return subjectName
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFrostfsIDGroupName(cmd *cobra.Command) string {
|
|
||||||
groupName, _ := cmd.Flags().GetString(groupNameFlag)
|
|
||||||
|
|
||||||
if !ape.GroupNameRegexp.MatchString(groupName) {
|
|
||||||
commonCmd.ExitOnErr(cmd, "invalid group name: %w",
|
|
||||||
fmt.Errorf("name must match regexp: %s", ape.GroupNameRegexp.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return groupName
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFrostfsIDGroupID(cmd *cobra.Command) int64 {
|
|
||||||
groupID, _ := cmd.Flags().GetInt64(groupIDFlag)
|
|
||||||
if groupID <= 0 {
|
|
||||||
commonCmd.ExitOnErr(cmd, "invalid group id: %w",
|
|
||||||
errors.New("group id must be positive integer"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return groupID
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFrostfsIDNamespace(cmd *cobra.Command) string {
|
|
||||||
ns, _ := cmd.Flags().GetString(namespaceFlag)
|
|
||||||
if ns == rootNamespacePlaceholder {
|
|
||||||
ns = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ape.NamespaceNameRegexp.MatchString(ns) {
|
|
||||||
commonCmd.ExitOnErr(cmd, "invalid namespace: %w",
|
|
||||||
fmt.Errorf("name must match regexp: %s", ape.NamespaceNameRegexp.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return ns
|
|
||||||
}
|
|
|
@ -1,174 +0,0 @@
|
||||||
package frostfsid
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/internal/ape"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFrostfsIDConfig(t *testing.T) {
|
|
||||||
pks := make([]*keys.PrivateKey, 4)
|
|
||||||
for i := range pks {
|
|
||||||
pk, err := keys.NewPrivateKey()
|
|
||||||
require.NoError(t, err)
|
|
||||||
pks[i] = pk
|
|
||||||
}
|
|
||||||
|
|
||||||
fmts := []string{
|
|
||||||
pks[0].GetScriptHash().StringLE(),
|
|
||||||
address.Uint160ToString(pks[1].GetScriptHash()),
|
|
||||||
hex.EncodeToString(pks[2].PublicKey().UncompressedBytes()),
|
|
||||||
hex.EncodeToString(pks[3].PublicKey().Bytes()),
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range fmts {
|
|
||||||
v := viper.New()
|
|
||||||
v.Set("frostfsid.admin", fmts[i])
|
|
||||||
|
|
||||||
actual, found, err := helper.GetFrostfsIDAdmin(v)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.True(t, found)
|
|
||||||
require.Equal(t, pks[i].GetScriptHash(), actual)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("bad key", func(t *testing.T) {
|
|
||||||
v := viper.New()
|
|
||||||
v.Set("frostfsid.admin", "abc")
|
|
||||||
|
|
||||||
_, found, err := helper.GetFrostfsIDAdmin(v)
|
|
||||||
require.Error(t, err)
|
|
||||||
require.True(t, found)
|
|
||||||
})
|
|
||||||
t.Run("missing key", func(t *testing.T) {
|
|
||||||
v := viper.New()
|
|
||||||
|
|
||||||
_, found, err := helper.GetFrostfsIDAdmin(v)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.False(t, found)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNamespaceRegexp(t *testing.T) {
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
namespace string
|
|
||||||
matched bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "root empty ns",
|
|
||||||
namespace: "",
|
|
||||||
matched: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simple valid ns",
|
|
||||||
namespace: "my-namespace-123",
|
|
||||||
matched: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "root placeholder",
|
|
||||||
namespace: "<root>",
|
|
||||||
matched: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "too long",
|
|
||||||
namespace: "abcdefghijklmnopkrstuvwxyzabcdefghijklmnopkrstuvwxyz",
|
|
||||||
matched: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "start with hyphen",
|
|
||||||
namespace: "-ns",
|
|
||||||
matched: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "end with hyphen",
|
|
||||||
namespace: "ns-",
|
|
||||||
matched: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "with spaces",
|
|
||||||
namespace: "ns ns",
|
|
||||||
matched: false,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
require.Equal(t, tc.matched, ape.NamespaceNameRegexp.MatchString(tc.namespace))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubjectNameRegexp(t *testing.T) {
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
subject string
|
|
||||||
matched bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty",
|
|
||||||
subject: "",
|
|
||||||
matched: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid",
|
|
||||||
subject: "invalid{name}",
|
|
||||||
matched: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "too long",
|
|
||||||
subject: "abcdefghijklmnopkrstuvwxyzabcdefghijklmnopkrstuvwxyzabcdefghijklmnopkrstuvwxyz",
|
|
||||||
matched: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid",
|
|
||||||
subject: "valid_name.012345@6789",
|
|
||||||
matched: true,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
require.Equal(t, tc.matched, ape.SubjectNameRegexp.MatchString(tc.subject))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSubjectGroupRegexp(t *testing.T) {
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
subject string
|
|
||||||
matched bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty",
|
|
||||||
subject: "",
|
|
||||||
matched: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid",
|
|
||||||
subject: "invalid{name}",
|
|
||||||
matched: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "too long",
|
|
||||||
subject: "abcdefghijklmnopkrstuvwxyzabcdefghijklmnopkrstuvwxyzabcdefghijklmnopkrstuvwxyzabcdefghijklmnopkrstuvwxyzabcdefghijklmnopkrstuvwxyz",
|
|
||||||
matched: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "long",
|
|
||||||
subject: "abcdefghijklmnopkrstuvwxyzabcdefghijklmnopkrstuvwxyzabcdefghijklmnopkrstuvwxyz",
|
|
||||||
matched: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid",
|
|
||||||
subject: "valid_name.012345@6789",
|
|
||||||
matched: true,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
require.Equal(t, tc.matched, ape.GroupNameRegexp.MatchString(tc.subject))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
package frostfsid
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
initFrostfsIDCreateNamespaceCmd()
|
|
||||||
initFrostfsIDListNamespacesCmd()
|
|
||||||
initFrostfsIDCreateSubjectCmd()
|
|
||||||
initFrostfsIDDeleteSubjectCmd()
|
|
||||||
initFrostfsIDListSubjectsCmd()
|
|
||||||
initFrostfsIDCreateGroupCmd()
|
|
||||||
initFrostfsIDDeleteGroupCmd()
|
|
||||||
initFrostfsIDListGroupsCmd()
|
|
||||||
initFrostfsIDAddSubjectToGroupCmd()
|
|
||||||
initFrostfsIDRemoveSubjectFromGroupCmd()
|
|
||||||
initFrostfsIDListGroupSubjectsCmd()
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
package generate
|
package morph
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -6,13 +6,11 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/config"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/config"
|
"github.com/TrueCloudLab/frostfs-node/pkg/innerring"
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/innerring"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/gas"
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/gas"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||||
|
@ -23,30 +21,32 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func AlphabetCreds(cmd *cobra.Command, _ []string) error {
|
const (
|
||||||
|
singleAccountName = "single"
|
||||||
|
committeeAccountName = "committee"
|
||||||
|
consensusAccountName = "consensus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateAlphabetCreds(cmd *cobra.Command, args []string) error {
|
||||||
// alphabet size is not part of the config
|
// alphabet size is not part of the config
|
||||||
size, err := cmd.Flags().GetUint(commonflags.AlphabetSizeFlag)
|
size, err := cmd.Flags().GetUint(alphabetSizeFlag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if size == 0 {
|
if size == 0 {
|
||||||
return errors.New("size must be > 0")
|
return errors.New("size must be > 0")
|
||||||
}
|
}
|
||||||
if size > constants.MaxAlphabetNodes {
|
|
||||||
return helper.ErrTooManyAlphabetNodes
|
|
||||||
}
|
|
||||||
|
|
||||||
v := viper.GetViper()
|
v := viper.GetViper()
|
||||||
walletDir := config.ResolveHomePath(viper.GetString(commonflags.AlphabetWalletsFlag))
|
walletDir := config.ResolveHomePath(viper.GetString(alphabetWalletsFlag))
|
||||||
pwds, err := initializeWallets(v, walletDir, int(size))
|
pwds, err := initializeWallets(v, walletDir, int(size))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = helper.InitializeContractWallet(v, walletDir)
|
_, err = initializeContractWallet(v, walletDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -65,64 +65,55 @@ func initializeWallets(v *viper.Viper, walletDir string, size int) ([]string, er
|
||||||
pubs := make(keys.PublicKeys, size)
|
pubs := make(keys.PublicKeys, size)
|
||||||
passwords := make([]string, size)
|
passwords := make([]string, size)
|
||||||
|
|
||||||
var errG errgroup.Group
|
|
||||||
|
|
||||||
for i := range wallets {
|
for i := range wallets {
|
||||||
password, err := config.GetPassword(v, innerring.GlagoliticLetter(i).String())
|
password, err := config.GetPassword(v, innerring.GlagoliticLetter(i).String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("can't fetch password: %w", err)
|
return nil, fmt.Errorf("can't fetch password: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
errG.Go(func() error {
|
p := filepath.Join(walletDir, innerring.GlagoliticLetter(i).String()+".json")
|
||||||
p := filepath.Join(walletDir, innerring.GlagoliticLetter(i).String()+".json")
|
f, err := os.OpenFile(p, os.O_CREATE, 0644)
|
||||||
f, err := os.OpenFile(p, os.O_CREATE, 0o644)
|
if err != nil {
|
||||||
if err != nil {
|
return nil, fmt.Errorf("can't create wallet file: %w", err)
|
||||||
return fmt.Errorf("can't create wallet file: %w", err)
|
}
|
||||||
}
|
if err := f.Close(); err != nil {
|
||||||
if err := f.Close(); err != nil {
|
return nil, fmt.Errorf("can't close wallet file: %w", err)
|
||||||
return fmt.Errorf("can't close wallet file: %w", err)
|
}
|
||||||
}
|
w, err := wallet.NewWallet(p)
|
||||||
w, err := wallet.NewWallet(p)
|
if err != nil {
|
||||||
if err != nil {
|
return nil, fmt.Errorf("can't create wallet: %w", err)
|
||||||
return fmt.Errorf("can't create wallet: %w", err)
|
}
|
||||||
}
|
if err := w.CreateAccount(singleAccountName, password); err != nil {
|
||||||
if err := w.CreateAccount(constants.SingleAccountName, password); err != nil {
|
return nil, fmt.Errorf("can't create account: %w", err)
|
||||||
return fmt.Errorf("can't create account: %w", err)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
passwords[i] = password
|
passwords[i] = password
|
||||||
wallets[i] = w
|
wallets[i] = w
|
||||||
pubs[i] = w.Accounts[0].PrivateKey().PublicKey()
|
pubs[i] = w.Accounts[0].PrivateKey().PublicKey()
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := errG.Wait(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create committee account with N/2+1 multi-signature.
|
// Create committee account with N/2+1 multi-signature.
|
||||||
majCount := smartcontract.GetMajorityHonestNodeCount(size)
|
majCount := smartcontract.GetMajorityHonestNodeCount(size)
|
||||||
|
for i, w := range wallets {
|
||||||
|
if err := addMultisigAccount(w, majCount, committeeAccountName, passwords[i], pubs); err != nil {
|
||||||
|
return nil, fmt.Errorf("can't create committee account: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create consensus account with 2*N/3+1 multi-signature.
|
// Create consensus account with 2*N/3+1 multi-signature.
|
||||||
bftCount := smartcontract.GetDefaultHonestNodeCount(size)
|
bftCount := smartcontract.GetDefaultHonestNodeCount(size)
|
||||||
for i := range wallets {
|
for i, w := range wallets {
|
||||||
ps := pubs.Copy()
|
if err := addMultisigAccount(w, bftCount, consensusAccountName, passwords[i], pubs); err != nil {
|
||||||
errG.Go(func() error {
|
return nil, fmt.Errorf("can't create consensus account: %w", err)
|
||||||
if err := addMultisigAccount(wallets[i], majCount, constants.CommitteeAccountName, passwords[i], ps); err != nil {
|
}
|
||||||
return fmt.Errorf("can't create committee account: %w", err)
|
|
||||||
}
|
|
||||||
if err := addMultisigAccount(wallets[i], bftCount, constants.ConsensusAccountName, passwords[i], ps); err != nil {
|
|
||||||
return fmt.Errorf("can't create consentus account: %w", err)
|
|
||||||
}
|
|
||||||
if err := wallets[i].SavePretty(); err != nil {
|
|
||||||
return fmt.Errorf("can't save wallet: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if err := errG.Wait(); err != nil {
|
|
||||||
return nil, err
|
for _, w := range wallets {
|
||||||
|
if err := w.SavePretty(); err != nil {
|
||||||
|
return nil, fmt.Errorf("can't save wallet: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return passwords, nil
|
return passwords, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,7 +137,7 @@ func generateStorageCreds(cmd *cobra.Command, _ []string) error {
|
||||||
|
|
||||||
func refillGas(cmd *cobra.Command, gasFlag string, createWallet bool) (err error) {
|
func refillGas(cmd *cobra.Command, gasFlag string, createWallet bool) (err error) {
|
||||||
// storage wallet path is not part of the config
|
// storage wallet path is not part of the config
|
||||||
storageWalletPath, _ := cmd.Flags().GetString(commonflags.StorageWalletFlag)
|
storageWalletPath, _ := cmd.Flags().GetString(storageWalletFlag)
|
||||||
// wallet address is not part of the config
|
// wallet address is not part of the config
|
||||||
walletAddress, _ := cmd.Flags().GetString(walletAddressFlag)
|
walletAddress, _ := cmd.Flags().GetString(walletAddressFlag)
|
||||||
|
|
||||||
|
@ -159,7 +150,7 @@ func refillGas(cmd *cobra.Command, gasFlag string, createWallet bool) (err error
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if storageWalletPath == "" {
|
if storageWalletPath == "" {
|
||||||
return fmt.Errorf("missing wallet path (use '--%s <out.json>')", commonflags.StorageWalletFlag)
|
return fmt.Errorf("missing wallet path (use '--%s <out.json>')", storageWalletFlag)
|
||||||
}
|
}
|
||||||
|
|
||||||
var w *wallet.Wallet
|
var w *wallet.Wallet
|
||||||
|
@ -184,7 +175,7 @@ func refillGas(cmd *cobra.Command, gasFlag string, createWallet bool) (err error
|
||||||
}
|
}
|
||||||
|
|
||||||
if label == "" {
|
if label == "" {
|
||||||
label = constants.SingleAccountName
|
label = singleAccountName
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := w.CreateAccount(label, password); err != nil {
|
if err := w.CreateAccount(label, password); err != nil {
|
||||||
|
@ -197,12 +188,12 @@ func refillGas(cmd *cobra.Command, gasFlag string, createWallet bool) (err error
|
||||||
|
|
||||||
gasStr := viper.GetString(gasFlag)
|
gasStr := viper.GetString(gasFlag)
|
||||||
|
|
||||||
gasAmount, err := helper.ParseGASAmount(gasStr)
|
gasAmount, err := parseGASAmount(gasStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
wCtx, err := helper.NewInitializeContext(cmd, viper.GetViper())
|
wCtx, err := newInitializeContext(cmd, viper.GetViper())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -215,9 +206,20 @@ func refillGas(cmd *cobra.Command, gasFlag string, createWallet bool) (err error
|
||||||
return fmt.Errorf("BUG: invalid transfer arguments: %w", bw.Err)
|
return fmt.Errorf("BUG: invalid transfer arguments: %w", bw.Err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := wCtx.SendCommitteeTx(bw.Bytes(), false); err != nil {
|
if err := wCtx.sendCommitteeTx(bw.Bytes(), false); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return wCtx.AwaitTx()
|
return wCtx.awaitTx()
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseGASAmount(s string) (fixedn.Fixed8, error) {
|
||||||
|
gasAmount, err := fixedn.Fixed8FromString(s)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid GAS amount %s: %w", s, err)
|
||||||
|
}
|
||||||
|
if gasAmount <= 0 {
|
||||||
|
return 0, fmt.Errorf("GAS amount must be positive (got %d)", gasAmount)
|
||||||
|
}
|
||||||
|
return gasAmount, nil
|
||||||
}
|
}
|
|
@ -1,119 +0,0 @@
|
||||||
package generate
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"math/rand"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/innerring"
|
|
||||||
"github.com/nspcc-dev/neo-go/cli/input"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"golang.org/x/term"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGenerateAlphabet(t *testing.T) {
|
|
||||||
walletDir := t.TempDir()
|
|
||||||
buf := setupTestTerminal(t)
|
|
||||||
|
|
||||||
cmd := GenerateAlphabetCmd
|
|
||||||
v := viper.GetViper()
|
|
||||||
|
|
||||||
t.Run("zero size", func(t *testing.T) {
|
|
||||||
buf.Reset()
|
|
||||||
v.Set(commonflags.AlphabetWalletsFlag, walletDir)
|
|
||||||
require.NoError(t, cmd.Flags().Set(commonflags.AlphabetSizeFlag, "0"))
|
|
||||||
buf.WriteString("pass\r")
|
|
||||||
require.Error(t, AlphabetCreds(cmd, nil))
|
|
||||||
})
|
|
||||||
t.Run("no password provided", func(t *testing.T) {
|
|
||||||
buf.Reset()
|
|
||||||
v.Set(commonflags.AlphabetWalletsFlag, walletDir)
|
|
||||||
require.NoError(t, cmd.Flags().Set(commonflags.AlphabetSizeFlag, "1"))
|
|
||||||
require.Error(t, AlphabetCreds(cmd, nil))
|
|
||||||
})
|
|
||||||
t.Run("missing directory", func(t *testing.T) {
|
|
||||||
buf.Reset()
|
|
||||||
dir := filepath.Join(os.TempDir(), "notexist."+strconv.FormatUint(rand.Uint64(), 10))
|
|
||||||
v.Set(commonflags.AlphabetWalletsFlag, dir)
|
|
||||||
require.NoError(t, cmd.Flags().Set(commonflags.AlphabetSizeFlag, "1"))
|
|
||||||
buf.WriteString("pass\r")
|
|
||||||
require.Error(t, AlphabetCreds(cmd, nil))
|
|
||||||
})
|
|
||||||
t.Run("no password for contract group wallet", func(t *testing.T) {
|
|
||||||
buf.Reset()
|
|
||||||
v.Set(commonflags.AlphabetWalletsFlag, walletDir)
|
|
||||||
require.NoError(t, cmd.Flags().Set(commonflags.AlphabetSizeFlag, "1"))
|
|
||||||
buf.WriteString("pass\r")
|
|
||||||
require.Error(t, AlphabetCreds(cmd, nil))
|
|
||||||
})
|
|
||||||
|
|
||||||
const size = 4
|
|
||||||
|
|
||||||
buf.Reset()
|
|
||||||
v.Set(commonflags.AlphabetWalletsFlag, walletDir)
|
|
||||||
require.NoError(t, GenerateAlphabetCmd.Flags().Set(commonflags.AlphabetSizeFlag, strconv.FormatUint(size, 10)))
|
|
||||||
for i := range uint64(size) {
|
|
||||||
buf.WriteString(strconv.FormatUint(i, 10) + "\r")
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.WriteString(constants.TestContractPassword + "\r")
|
|
||||||
require.NoError(t, AlphabetCreds(GenerateAlphabetCmd, nil))
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
for i := uint64(0); i < size; i++ {
|
|
||||||
i := i
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
p := filepath.Join(walletDir, innerring.GlagoliticLetter(i).String()+".json")
|
|
||||||
w, err := wallet.NewWalletFromFile(p)
|
|
||||||
require.NoError(t, err, "wallet doesn't exist")
|
|
||||||
require.Equal(t, 3, len(w.Accounts), "not all accounts were created")
|
|
||||||
|
|
||||||
for _, a := range w.Accounts {
|
|
||||||
err := a.Decrypt(strconv.FormatUint(i, 10), keys.NEP2ScryptParams())
|
|
||||||
require.NoError(t, err, "can't decrypt account")
|
|
||||||
switch a.Label {
|
|
||||||
case constants.ConsensusAccountName:
|
|
||||||
require.Equal(t, smartcontract.GetDefaultHonestNodeCount(size), len(a.Contract.Parameters))
|
|
||||||
case constants.CommitteeAccountName:
|
|
||||||
require.Equal(t, smartcontract.GetMajorityHonestNodeCount(size), len(a.Contract.Parameters))
|
|
||||||
default:
|
|
||||||
require.Equal(t, constants.SingleAccountName, a.Label)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
t.Run("check contract group wallet", func(t *testing.T) {
|
|
||||||
p := filepath.Join(walletDir, constants.ContractWalletFilename)
|
|
||||||
w, err := wallet.NewWalletFromFile(p)
|
|
||||||
require.NoError(t, err, "contract wallet doesn't exist")
|
|
||||||
require.Equal(t, 1, len(w.Accounts), "contract wallet must have 1 accout")
|
|
||||||
require.NoError(t, w.Accounts[0].Decrypt(constants.TestContractPassword, keys.NEP2ScryptParams()))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupTestTerminal(t *testing.T) *bytes.Buffer {
|
|
||||||
in := bytes.NewBuffer(nil)
|
|
||||||
input.Terminal = term.NewTerminal(input.ReadWriter{
|
|
||||||
Reader: in,
|
|
||||||
Writer: io.Discard,
|
|
||||||
}, "")
|
|
||||||
|
|
||||||
t.Cleanup(func() { input.Terminal = nil })
|
|
||||||
|
|
||||||
return in
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
package generate
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
storageWalletLabelFlag = "label"
|
|
||||||
storageGasCLIFlag = "initial-gas"
|
|
||||||
storageGasConfigFlag = "storage.initial_gas"
|
|
||||||
walletAddressFlag = "wallet-address"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
GenerateStorageCmd = &cobra.Command{
|
|
||||||
Use: "generate-storage-wallet",
|
|
||||||
Short: "Generate storage node wallet for the morph network",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
_ = viper.BindPFlag(storageGasConfigFlag, cmd.Flags().Lookup(storageGasCLIFlag))
|
|
||||||
},
|
|
||||||
RunE: generateStorageCreds,
|
|
||||||
}
|
|
||||||
RefillGasCmd = &cobra.Command{
|
|
||||||
Use: "refill-gas",
|
|
||||||
Short: "Refill GAS of storage node's wallet in the morph network",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.RefillGasAmountFlag, cmd.Flags().Lookup(commonflags.RefillGasAmountFlag))
|
|
||||||
},
|
|
||||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
|
||||||
return refillGas(cmd, commonflags.RefillGasAmountFlag, false)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
GenerateAlphabetCmd = &cobra.Command{
|
|
||||||
Use: "generate-alphabet",
|
|
||||||
Short: "Generate alphabet wallets for consensus nodes of the morph network",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
// PreRun fixes https://github.com/spf13/viper/issues/233
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
},
|
|
||||||
RunE: AlphabetCreds,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func initRefillGasCmd() {
|
|
||||||
RefillGasCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
RefillGasCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
RefillGasCmd.Flags().String(commonflags.StorageWalletFlag, "", "Path to storage node wallet")
|
|
||||||
RefillGasCmd.Flags().String(walletAddressFlag, "", "Address of wallet")
|
|
||||||
RefillGasCmd.Flags().String(commonflags.RefillGasAmountFlag, "", "Additional amount of GAS to transfer")
|
|
||||||
RefillGasCmd.MarkFlagsMutuallyExclusive(walletAddressFlag, commonflags.StorageWalletFlag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initGenerateStorageCmd() {
|
|
||||||
GenerateStorageCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
GenerateStorageCmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
GenerateStorageCmd.Flags().String(commonflags.StorageWalletFlag, "", "Path to new storage node wallet")
|
|
||||||
GenerateStorageCmd.Flags().String(storageGasCLIFlag, "", "Initial amount of GAS to transfer")
|
|
||||||
GenerateStorageCmd.Flags().StringP(storageWalletLabelFlag, "l", "", "Wallet label")
|
|
||||||
}
|
|
||||||
|
|
||||||
func initGenerateAlphabetCmd() {
|
|
||||||
GenerateAlphabetCmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
GenerateAlphabetCmd.Flags().Uint(commonflags.AlphabetSizeFlag, 7, "Amount of alphabet wallets to generate")
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
initRefillGasCmd()
|
|
||||||
initGenerateStorageCmd()
|
|
||||||
initGenerateAlphabetCmd()
|
|
||||||
}
|
|
112
cmd/frostfs-adm/internal/modules/morph/generate_test.go
Normal file
112
cmd/frostfs-adm/internal/modules/morph/generate_test.go
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/pkg/innerring"
|
||||||
|
"github.com/nspcc-dev/neo-go/cli/input"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
const testContractPassword = "grouppass"
|
||||||
|
|
||||||
|
func TestGenerateAlphabet(t *testing.T) {
|
||||||
|
const size = 4
|
||||||
|
|
||||||
|
walletDir := t.TempDir()
|
||||||
|
buf := setupTestTerminal(t)
|
||||||
|
|
||||||
|
cmd := generateAlphabetCmd
|
||||||
|
v := viper.GetViper()
|
||||||
|
|
||||||
|
t.Run("zero size", func(t *testing.T) {
|
||||||
|
buf.Reset()
|
||||||
|
v.Set(alphabetWalletsFlag, walletDir)
|
||||||
|
require.NoError(t, cmd.Flags().Set(alphabetSizeFlag, "0"))
|
||||||
|
buf.WriteString("pass\r")
|
||||||
|
require.Error(t, generateAlphabetCreds(cmd, nil))
|
||||||
|
})
|
||||||
|
t.Run("no password provided", func(t *testing.T) {
|
||||||
|
buf.Reset()
|
||||||
|
v.Set(alphabetWalletsFlag, walletDir)
|
||||||
|
require.NoError(t, cmd.Flags().Set(alphabetSizeFlag, "1"))
|
||||||
|
require.Error(t, generateAlphabetCreds(cmd, nil))
|
||||||
|
})
|
||||||
|
t.Run("missing directory", func(t *testing.T) {
|
||||||
|
buf.Reset()
|
||||||
|
dir := filepath.Join(os.TempDir(), "notexist."+strconv.FormatUint(rand.Uint64(), 10))
|
||||||
|
v.Set(alphabetWalletsFlag, dir)
|
||||||
|
require.NoError(t, cmd.Flags().Set(alphabetSizeFlag, "1"))
|
||||||
|
buf.WriteString("pass\r")
|
||||||
|
require.Error(t, generateAlphabetCreds(cmd, nil))
|
||||||
|
})
|
||||||
|
t.Run("no password for contract group wallet", func(t *testing.T) {
|
||||||
|
buf.Reset()
|
||||||
|
v.Set(alphabetWalletsFlag, walletDir)
|
||||||
|
require.NoError(t, cmd.Flags().Set(alphabetSizeFlag, strconv.FormatUint(size, 10)))
|
||||||
|
for i := uint64(0); i < size; i++ {
|
||||||
|
buf.WriteString(strconv.FormatUint(i, 10) + "\r")
|
||||||
|
}
|
||||||
|
require.Error(t, generateAlphabetCreds(cmd, nil))
|
||||||
|
})
|
||||||
|
|
||||||
|
buf.Reset()
|
||||||
|
v.Set(alphabetWalletsFlag, walletDir)
|
||||||
|
require.NoError(t, generateAlphabetCmd.Flags().Set(alphabetSizeFlag, strconv.FormatUint(size, 10)))
|
||||||
|
for i := uint64(0); i < size; i++ {
|
||||||
|
buf.WriteString(strconv.FormatUint(i, 10) + "\r")
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteString(testContractPassword + "\r")
|
||||||
|
require.NoError(t, generateAlphabetCreds(generateAlphabetCmd, nil))
|
||||||
|
|
||||||
|
for i := uint64(0); i < size; i++ {
|
||||||
|
p := filepath.Join(walletDir, innerring.GlagoliticLetter(i).String()+".json")
|
||||||
|
w, err := wallet.NewWalletFromFile(p)
|
||||||
|
require.NoError(t, err, "wallet doesn't exist")
|
||||||
|
require.Equal(t, 3, len(w.Accounts), "not all accounts were created")
|
||||||
|
for _, a := range w.Accounts {
|
||||||
|
err := a.Decrypt(strconv.FormatUint(i, 10), keys.NEP2ScryptParams())
|
||||||
|
require.NoError(t, err, "can't decrypt account")
|
||||||
|
switch a.Label {
|
||||||
|
case consensusAccountName:
|
||||||
|
require.Equal(t, smartcontract.GetDefaultHonestNodeCount(size), len(a.Contract.Parameters))
|
||||||
|
case committeeAccountName:
|
||||||
|
require.Equal(t, smartcontract.GetMajorityHonestNodeCount(size), len(a.Contract.Parameters))
|
||||||
|
default:
|
||||||
|
require.Equal(t, singleAccountName, a.Label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("check contract group wallet", func(t *testing.T) {
|
||||||
|
p := filepath.Join(walletDir, contractWalletFilename)
|
||||||
|
w, err := wallet.NewWalletFromFile(p)
|
||||||
|
require.NoError(t, err, "contract wallet doesn't exist")
|
||||||
|
require.Equal(t, 1, len(w.Accounts), "contract wallet must have 1 accout")
|
||||||
|
require.NoError(t, w.Accounts[0].Decrypt(testContractPassword, keys.NEP2ScryptParams()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupTestTerminal(t *testing.T) *bytes.Buffer {
|
||||||
|
in := bytes.NewBuffer(nil)
|
||||||
|
input.Terminal = term.NewTerminal(input.ReadWriter{
|
||||||
|
Reader: in,
|
||||||
|
Writer: io.Discard,
|
||||||
|
}, "")
|
||||||
|
|
||||||
|
t.Cleanup(func() { input.Terminal = nil })
|
||||||
|
|
||||||
|
return in
|
||||||
|
}
|
106
cmd/frostfs-adm/internal/modules/morph/group.go
Normal file
106
cmd/frostfs-adm/internal/modules/morph/group.go
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/config"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
contractWalletFilename = "contract.json"
|
||||||
|
contractWalletPasswordKey = "contract"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initializeContractWallet(v *viper.Viper, walletDir string) (*wallet.Wallet, error) {
|
||||||
|
password, err := config.GetPassword(v, contractWalletPasswordKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
w, err := wallet.NewWallet(filepath.Join(walletDir, contractWalletFilename))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
acc, err := wallet.NewAccount()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = acc.Encrypt(password, keys.NEP2ScryptParams())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.AddAccount(acc)
|
||||||
|
if err := w.SavePretty(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openContractWallet(v *viper.Viper, cmd *cobra.Command, walletDir string) (*wallet.Wallet, error) {
|
||||||
|
p := filepath.Join(walletDir, contractWalletFilename)
|
||||||
|
w, err := wallet.NewWalletFromFile(p)
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return nil, fmt.Errorf("can't open wallet: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Printf("Contract group wallet is missing, initialize at %s\n", p)
|
||||||
|
return initializeContractWallet(v, walletDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
password, err := config.GetPassword(v, contractWalletPasswordKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range w.Accounts {
|
||||||
|
if err := w.Accounts[i].Decrypt(password, keys.NEP2ScryptParams()); err != nil {
|
||||||
|
return nil, fmt.Errorf("can't unlock wallet: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) addManifestGroup(h util.Uint160, cs *contractState) error {
|
||||||
|
priv := c.ContractWallet.Accounts[0].PrivateKey()
|
||||||
|
pub := priv.PublicKey()
|
||||||
|
|
||||||
|
sig := priv.Sign(h.BytesBE())
|
||||||
|
found := false
|
||||||
|
|
||||||
|
for i := range cs.Manifest.Groups {
|
||||||
|
if cs.Manifest.Groups[i].PublicKey.Equal(pub) {
|
||||||
|
cs.Manifest.Groups[i].Signature = sig
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
cs.Manifest.Groups = append(cs.Manifest.Groups, manifest.Group{
|
||||||
|
PublicKey: pub,
|
||||||
|
Signature: sig,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(cs.Manifest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cs.RawManifest = data
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,178 +0,0 @@
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/config"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/context"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LocalActor is a kludge, do not use it outside of the morph commands.
|
|
||||||
type LocalActor struct {
|
|
||||||
neoActor *actor.Actor
|
|
||||||
accounts []*wallet.Account
|
|
||||||
Invoker *invoker.Invoker
|
|
||||||
rpcInvoker invoker.RPCInvoke
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewLocalActor create LocalActor with accounts form provided wallets.
|
|
||||||
// In case of empty wallets provided created actor with dummy account only for read operation.
|
|
||||||
//
|
|
||||||
// If wallets are provided, the contract client will use accounts with accName name from these wallets.
|
|
||||||
// To determine which account name should be used in a contract client, refer to how the contract
|
|
||||||
// verifies the transaction signature.
|
|
||||||
func NewLocalActor(cmd *cobra.Command, c actor.RPCActor, accName string) (*LocalActor, error) {
|
|
||||||
walletDir := config.ResolveHomePath(viper.GetString(commonflags.AlphabetWalletsFlag))
|
|
||||||
var act *actor.Actor
|
|
||||||
var accounts []*wallet.Account
|
|
||||||
if walletDir == "" {
|
|
||||||
account, err := wallet.NewAccount()
|
|
||||||
commonCmd.ExitOnErr(cmd, "unable to create dummy account: %w", err)
|
|
||||||
act, err = actor.New(c, []actor.SignerAccount{{
|
|
||||||
Signer: transaction.Signer{
|
|
||||||
Account: account.Contract.ScriptHash(),
|
|
||||||
Scopes: transaction.Global,
|
|
||||||
},
|
|
||||||
Account: account,
|
|
||||||
}})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
wallets, err := GetAlphabetWallets(viper.GetViper(), walletDir)
|
|
||||||
commonCmd.ExitOnErr(cmd, "unable to get alphabet wallets: %w", err)
|
|
||||||
|
|
||||||
for _, w := range wallets {
|
|
||||||
acc, err := GetWalletAccount(w, accName)
|
|
||||||
commonCmd.ExitOnErr(cmd, fmt.Sprintf("can't find %s account: %%w", accName), err)
|
|
||||||
accounts = append(accounts, acc)
|
|
||||||
}
|
|
||||||
act, err = actor.New(c, []actor.SignerAccount{{
|
|
||||||
Signer: transaction.Signer{
|
|
||||||
Account: accounts[0].Contract.ScriptHash(),
|
|
||||||
Scopes: transaction.Global,
|
|
||||||
},
|
|
||||||
Account: accounts[0],
|
|
||||||
}})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &LocalActor{
|
|
||||||
neoActor: act,
|
|
||||||
accounts: accounts,
|
|
||||||
Invoker: &act.Invoker,
|
|
||||||
rpcInvoker: c,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *LocalActor) SendCall(contract util.Uint160, method string, params ...any) (util.Uint256, uint32, error) {
|
|
||||||
tx, err := a.neoActor.MakeCall(contract, method, params...)
|
|
||||||
if err != nil {
|
|
||||||
return util.Uint256{}, 0, err
|
|
||||||
}
|
|
||||||
err = a.resign(tx)
|
|
||||||
if err != nil {
|
|
||||||
return util.Uint256{}, 0, err
|
|
||||||
}
|
|
||||||
return a.neoActor.Send(tx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *LocalActor) SendRun(script []byte) (util.Uint256, uint32, error) {
|
|
||||||
tx, err := a.neoActor.MakeRun(script)
|
|
||||||
if err != nil {
|
|
||||||
return util.Uint256{}, 0, err
|
|
||||||
}
|
|
||||||
err = a.resign(tx)
|
|
||||||
if err != nil {
|
|
||||||
return util.Uint256{}, 0, err
|
|
||||||
}
|
|
||||||
return a.neoActor.Send(tx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// resign is used to sign tx with committee accounts.
|
|
||||||
// Inside the methods `MakeCall` and `SendRun` of the NeoGO's actor transaction is signing by committee account,
|
|
||||||
// because actor uses committee wallet.
|
|
||||||
// But it is not enough, need to sign with another committee accounts.
|
|
||||||
func (a *LocalActor) resign(tx *transaction.Transaction) error {
|
|
||||||
if len(a.accounts[0].Contract.Parameters) > 1 {
|
|
||||||
// Use parameter context to avoid dealing with signature order.
|
|
||||||
network := a.neoActor.GetNetwork()
|
|
||||||
pc := context.NewParameterContext("", network, tx)
|
|
||||||
h := a.accounts[0].Contract.ScriptHash()
|
|
||||||
for _, acc := range a.accounts {
|
|
||||||
priv := acc.PrivateKey()
|
|
||||||
sign := priv.SignHashable(uint32(network), tx)
|
|
||||||
if err := pc.AddSignature(h, acc.Contract, priv.PublicKey(), sign); err != nil {
|
|
||||||
return fmt.Errorf("can't add signature: %w", err)
|
|
||||||
}
|
|
||||||
if len(pc.Items[h].Signatures) == len(acc.Contract.Parameters) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w, err := pc.GetWitness(h)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("incomplete signature: %w", err)
|
|
||||||
}
|
|
||||||
tx.Scripts[0] = *w
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *LocalActor) Wait(h util.Uint256, vub uint32, err error) (*state.AppExecResult, error) {
|
|
||||||
return a.neoActor.Wait(h, vub, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *LocalActor) Sender() util.Uint160 {
|
|
||||||
return a.neoActor.Sender()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *LocalActor) Call(contract util.Uint160, operation string, params ...any) (*result.Invoke, error) {
|
|
||||||
return a.neoActor.Call(contract, operation, params...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *LocalActor) CallAndExpandIterator(_ util.Uint160, _ string, _ int, _ ...any) (*result.Invoke, error) {
|
|
||||||
panic("unimplemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *LocalActor) TerminateSession(_ uuid.UUID) error {
|
|
||||||
panic("unimplemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *LocalActor) TraverseIterator(sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, error) {
|
|
||||||
return a.neoActor.TraverseIterator(sessionID, iterator, num)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *LocalActor) MakeRun(_ []byte) (*transaction.Transaction, error) {
|
|
||||||
panic("unimplemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *LocalActor) MakeUnsignedCall(_ util.Uint160, _ string, _ []transaction.Attribute, _ ...any) (*transaction.Transaction, error) {
|
|
||||||
panic("unimplemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *LocalActor) MakeUnsignedRun(_ []byte, _ []transaction.Attribute) (*transaction.Transaction, error) {
|
|
||||||
panic("unimplemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *LocalActor) MakeCall(_ util.Uint160, _ string, _ ...any) (*transaction.Transaction, error) {
|
|
||||||
panic("unimplemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *LocalActor) GetRPCInvoker() invoker.RPCInvoke {
|
|
||||||
return a.rpcInvoker
|
|
||||||
}
|
|
|
@ -1,171 +0,0 @@
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getFrostfsIDAdminFromContract(roInvoker *invoker.Invoker) (util.Uint160, bool, error) {
|
|
||||||
r := management.NewReader(roInvoker)
|
|
||||||
cs, err := GetContractByID(r, 1)
|
|
||||||
if err != nil {
|
|
||||||
return util.Uint160{}, false, fmt.Errorf("get nns contract: %w", err)
|
|
||||||
}
|
|
||||||
fidHash, err := NNSResolveHash(roInvoker, cs.Hash, DomainOf(constants.FrostfsIDContract))
|
|
||||||
if err != nil {
|
|
||||||
return util.Uint160{}, false, fmt.Errorf("resolve frostfsid contract hash: %w", err)
|
|
||||||
}
|
|
||||||
item, err := unwrap.Item(roInvoker.Call(fidHash, "getAdmin"))
|
|
||||||
if err != nil {
|
|
||||||
return util.Uint160{}, false, fmt.Errorf("getAdmin: %w", err)
|
|
||||||
}
|
|
||||||
if _, ok := item.(stackitem.Null); ok {
|
|
||||||
return util.Uint160{}, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
bs, err := item.TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
return util.Uint160{}, true, fmt.Errorf("getAdmin: decode result: %w", err)
|
|
||||||
}
|
|
||||||
h, err := util.Uint160DecodeBytesBE(bs)
|
|
||||||
if err != nil {
|
|
||||||
return util.Uint160{}, true, fmt.Errorf("getAdmin: decode result: %w", err)
|
|
||||||
}
|
|
||||||
return h, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetContractDeployData(c *InitializeContext, ctrName string, keysParam []any, method string) ([]any, error) {
|
|
||||||
items := make([]any, 0, 6)
|
|
||||||
|
|
||||||
switch ctrName {
|
|
||||||
case constants.FrostfsContract:
|
|
||||||
items = append(items,
|
|
||||||
c.Contracts[constants.ProcessingContract].Hash,
|
|
||||||
keysParam,
|
|
||||||
smartcontract.Parameter{})
|
|
||||||
case constants.ProcessingContract:
|
|
||||||
items = append(items, c.Contracts[constants.FrostfsContract].Hash)
|
|
||||||
return items[1:], nil // no notary info
|
|
||||||
case constants.BalanceContract:
|
|
||||||
items = append(items,
|
|
||||||
c.Contracts[constants.NetmapContract].Hash,
|
|
||||||
c.Contracts[constants.ContainerContract].Hash)
|
|
||||||
case constants.ContainerContract:
|
|
||||||
// In case if NNS is updated multiple times, we can't calculate
|
|
||||||
// it's actual hash based on local data, thus query chain.
|
|
||||||
r := management.NewReader(c.ReadOnlyInvoker)
|
|
||||||
nnsCs, err := GetContractByID(r, 1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("get nns contract: %w", err)
|
|
||||||
}
|
|
||||||
items = append(items,
|
|
||||||
c.Contracts[constants.NetmapContract].Hash,
|
|
||||||
c.Contracts[constants.BalanceContract].Hash,
|
|
||||||
c.Contracts[constants.FrostfsIDContract].Hash,
|
|
||||||
nnsCs.Hash,
|
|
||||||
"container")
|
|
||||||
case constants.FrostfsIDContract:
|
|
||||||
var (
|
|
||||||
h util.Uint160
|
|
||||||
found bool
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if method == constants.UpdateMethodName {
|
|
||||||
h, found, err = getFrostfsIDAdminFromContract(c.ReadOnlyInvoker)
|
|
||||||
}
|
|
||||||
if method != constants.UpdateMethodName || err == nil && !found {
|
|
||||||
h, found, err = GetFrostfsIDAdmin(viper.GetViper())
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if found {
|
|
||||||
items = append(items, h)
|
|
||||||
} else {
|
|
||||||
items = append(items, c.Contracts[constants.ProxyContract].Hash)
|
|
||||||
}
|
|
||||||
case constants.NetmapContract:
|
|
||||||
md := GetDefaultNetmapContractConfigMap()
|
|
||||||
if method == constants.UpdateMethodName {
|
|
||||||
if err := MergeNetmapConfig(c.ReadOnlyInvoker, md); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var configParam []any
|
|
||||||
for k, v := range md {
|
|
||||||
configParam = append(configParam, k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
items = append(items,
|
|
||||||
c.Contracts[constants.BalanceContract].Hash,
|
|
||||||
c.Contracts[constants.ContainerContract].Hash,
|
|
||||||
keysParam,
|
|
||||||
configParam)
|
|
||||||
case constants.ProxyContract:
|
|
||||||
items = nil
|
|
||||||
case constants.PolicyContract:
|
|
||||||
items = append(items, c.Contracts[constants.ProxyContract].Hash)
|
|
||||||
default:
|
|
||||||
panic("invalid contract name: " + ctrName)
|
|
||||||
}
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetContractDeployParameters(cs *ContractState, deployData []any) []any {
|
|
||||||
return []any{cs.RawNEF, cs.RawManifest, deployData}
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeployNNS(c *InitializeContext, method string) error {
|
|
||||||
cs := c.GetContract(constants.NNSContract)
|
|
||||||
h := cs.Hash
|
|
||||||
|
|
||||||
nnsCs, err := c.NNSContractState()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if nnsCs != nil {
|
|
||||||
if nnsCs.NEF.Checksum == cs.NEF.Checksum {
|
|
||||||
if method == constants.DeployMethodName {
|
|
||||||
c.Command.Println("NNS contract is already deployed.")
|
|
||||||
} else {
|
|
||||||
c.Command.Println("NNS contract is already updated.")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
h = nnsCs.Hash
|
|
||||||
}
|
|
||||||
|
|
||||||
err = AddManifestGroup(c.ContractWallet, h, cs)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't sign manifest group: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
params := GetContractDeployParameters(cs, nil)
|
|
||||||
|
|
||||||
invokeHash := management.Hash
|
|
||||||
if method == constants.UpdateMethodName {
|
|
||||||
invokeHash = nnsCs.Hash
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := c.CommitteeAct.MakeCall(invokeHash, method, params...)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create deploy tx for %s: %w", constants.NNSContract, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.MultiSignAndSend(tx, constants.CommitteeAccountName); err != nil {
|
|
||||||
return fmt.Errorf("can't send deploy transaction: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Command.Println("NNS hash:", invokeHash.StringLE())
|
|
||||||
return c.AwaitTx()
|
|
||||||
}
|
|
|
@ -1,83 +0,0 @@
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errNoReleasesFound = errors.New("attempt to fetch contracts archive from the offitial repository failed: no releases found")
|
|
||||||
|
|
||||||
func downloadContracts(cmd *cobra.Command, url string) (io.ReadCloser, error) {
|
|
||||||
cmd.Printf("Downloading contracts archive from '%s'\n", url)
|
|
||||||
|
|
||||||
// HTTP client with connect timeout
|
|
||||||
client := http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DialContext: (&net.Dialer{
|
|
||||||
Timeout: 10 * time.Second,
|
|
||||||
}).DialContext,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(cmd.Context(), 60*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't create request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't fetch contracts archive: %w", err)
|
|
||||||
}
|
|
||||||
return resp.Body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func downloadContractsFromRepository(cmd *cobra.Command) (io.ReadCloser, error) {
|
|
||||||
client, err := gitea.NewClient("https://git.frostfs.info")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't initialize repository client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
releases, _, err := client.ListReleases("TrueCloudLab", "frostfs-contract", gitea.ListReleasesOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't fetch release information: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var latestRelease *gitea.Release
|
|
||||||
for _, r := range releases {
|
|
||||||
if !r.IsDraft && !r.IsPrerelease {
|
|
||||||
latestRelease = r
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if latestRelease == nil {
|
|
||||||
return nil, errNoReleasesFound
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Printf("Found release %s (%s)\n", latestRelease.TagName, latestRelease.Title)
|
|
||||||
|
|
||||||
var url string
|
|
||||||
for _, a := range latestRelease.Attachments {
|
|
||||||
if strings.HasPrefix(a.Name, "frostfs-contract") {
|
|
||||||
url = a.DownloadURL
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if url == "" {
|
|
||||||
return nil, errors.New("can't find contracts archive in the latest release")
|
|
||||||
}
|
|
||||||
|
|
||||||
return downloadContracts(cmd, url)
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
const frostfsIDAdminConfigKey = "frostfsid.admin"
|
|
||||||
|
|
||||||
func GetFrostfsIDAdmin(v *viper.Viper) (util.Uint160, bool, error) {
|
|
||||||
admin := v.GetString(frostfsIDAdminConfigKey)
|
|
||||||
if admin == "" {
|
|
||||||
return util.Uint160{}, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
h, err := address.StringToUint160(admin)
|
|
||||||
if err == nil {
|
|
||||||
return h, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
h, err = util.Uint160DecodeStringLE(admin)
|
|
||||||
if err == nil {
|
|
||||||
return h, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
pk, err := keys.NewPublicKeyFromString(admin)
|
|
||||||
if err == nil {
|
|
||||||
return pk.GetScriptHash(), true, nil
|
|
||||||
}
|
|
||||||
return util.Uint160{}, true, fmt.Errorf("frostfsid: admin is invalid: '%s'", admin)
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
|
||||||
)
|
|
||||||
|
|
||||||
func AddManifestGroup(cw *wallet.Wallet, h util.Uint160, cs *ContractState) error {
|
|
||||||
priv := cw.Accounts[0].PrivateKey()
|
|
||||||
pub := priv.PublicKey()
|
|
||||||
|
|
||||||
sig := priv.Sign(h.BytesBE())
|
|
||||||
found := false
|
|
||||||
|
|
||||||
for i := range cs.Manifest.Groups {
|
|
||||||
if cs.Manifest.Groups[i].PublicKey.Equal(pub) {
|
|
||||||
cs.Manifest.Groups[i].Signature = sig
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
cs.Manifest.Groups = append(cs.Manifest.Groups, manifest.Group{
|
|
||||||
PublicKey: pub,
|
|
||||||
Signature: sig,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.Marshal(cs.Manifest)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cs.RawManifest = data
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,223 +0,0 @@
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/config"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
|
||||||
nns2 "github.com/nspcc-dev/neo-go/pkg/rpcclient/nns"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ErrTooManyAlphabetNodes = fmt.Errorf("too many alphabet nodes (maximum allowed is %d)", constants.MaxAlphabetNodes)
|
|
||||||
|
|
||||||
func AwaitTx(cmd *cobra.Command, c Client, txs []HashVUBPair) error {
|
|
||||||
cmd.Println("Waiting for transactions to persist...")
|
|
||||||
|
|
||||||
at := trigger.Application
|
|
||||||
|
|
||||||
var retErr error
|
|
||||||
|
|
||||||
loop:
|
|
||||||
for i := range txs {
|
|
||||||
var it int
|
|
||||||
var pollInterval time.Duration
|
|
||||||
var pollIntervalChanged bool
|
|
||||||
for {
|
|
||||||
// We must fetch current height before application log, to avoid race condition.
|
|
||||||
currBlock, err := c.GetBlockCount()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't fetch current block height: %w", err)
|
|
||||||
}
|
|
||||||
res, err := c.GetApplicationLog(txs[i].Hash, &at)
|
|
||||||
if err == nil {
|
|
||||||
if retErr == nil && len(res.Executions) > 0 && res.Executions[0].VMState != vmstate.Halt {
|
|
||||||
retErr = fmt.Errorf("tx %d persisted in %s state: %s",
|
|
||||||
i, res.Executions[0].VMState, res.Executions[0].FaultException)
|
|
||||||
}
|
|
||||||
continue loop
|
|
||||||
}
|
|
||||||
if txs[i].Vub < currBlock {
|
|
||||||
return fmt.Errorf("tx was not persisted: Vub=%d, height=%d", txs[i].Vub, currBlock)
|
|
||||||
}
|
|
||||||
|
|
||||||
pollInterval, pollIntervalChanged = NextPollInterval(it, pollInterval)
|
|
||||||
if pollIntervalChanged && viper.GetBool(commonflags.Verbose) {
|
|
||||||
cmd.Printf("Pool interval to check transaction persistence changed: %s\n", pollInterval.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
timer := time.NewTimer(pollInterval)
|
|
||||||
select {
|
|
||||||
case <-cmd.Context().Done():
|
|
||||||
return cmd.Context().Err()
|
|
||||||
case <-timer.C:
|
|
||||||
}
|
|
||||||
|
|
||||||
it++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return retErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func NextPollInterval(it int, previous time.Duration) (time.Duration, bool) {
|
|
||||||
const minPollInterval = 1 * time.Second
|
|
||||||
const maxPollInterval = 16 * time.Second
|
|
||||||
const changeAfter = 5
|
|
||||||
if it == 0 {
|
|
||||||
return minPollInterval, true
|
|
||||||
}
|
|
||||||
if it%changeAfter != 0 {
|
|
||||||
return previous, false
|
|
||||||
}
|
|
||||||
nextInterval := previous * 2
|
|
||||||
if nextInterval > maxPollInterval {
|
|
||||||
return maxPollInterval, previous != maxPollInterval
|
|
||||||
}
|
|
||||||
return nextInterval, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetWalletAccount(w *wallet.Wallet, typ string) (*wallet.Account, error) {
|
|
||||||
for i := range w.Accounts {
|
|
||||||
if w.Accounts[i].Label == typ {
|
|
||||||
return w.Accounts[i], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("account for '%s' not found", typ)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetComitteAcc(cmd *cobra.Command, v *viper.Viper) *wallet.Account {
|
|
||||||
walletDir := config.ResolveHomePath(viper.GetString(commonflags.AlphabetWalletsFlag))
|
|
||||||
wallets, err := GetAlphabetWallets(v, walletDir)
|
|
||||||
commonCmd.ExitOnErr(cmd, "unable to get alphabet wallets: %w", err)
|
|
||||||
|
|
||||||
committeeAcc, err := GetWalletAccount(wallets[0], constants.CommitteeAccountName)
|
|
||||||
commonCmd.ExitOnErr(cmd, "can't find committee account: %w", err)
|
|
||||||
return committeeAcc
|
|
||||||
}
|
|
||||||
|
|
||||||
func NNSResolve(inv *invoker.Invoker, nnsHash util.Uint160, domain string) (stackitem.Item, error) {
|
|
||||||
return unwrap.Item(inv.Call(nnsHash, "resolve", domain, int64(nns.TXT)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseNNSResolveResult parses the result of resolving NNS record.
|
|
||||||
// It works with multiple formats (corresponding to multiple NNS versions).
|
|
||||||
// If array of hashes is provided, it returns only the first one.
|
|
||||||
func ParseNNSResolveResult(res stackitem.Item) (util.Uint160, error) {
|
|
||||||
arr, ok := res.Value().([]stackitem.Item)
|
|
||||||
if !ok {
|
|
||||||
arr = []stackitem.Item{res}
|
|
||||||
}
|
|
||||||
if _, ok := res.Value().(stackitem.Null); ok || len(arr) == 0 {
|
|
||||||
return util.Uint160{}, errors.New("NNS record is missing")
|
|
||||||
}
|
|
||||||
for i := range arr {
|
|
||||||
bs, err := arr[i].TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// We support several formats for hash encoding, this logic should be maintained in sync
|
|
||||||
// with NNSResolve from pkg/morph/client/nns.go
|
|
||||||
h, err := util.Uint160DecodeStringLE(string(bs))
|
|
||||||
if err == nil {
|
|
||||||
return h, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
h, err = address.StringToUint160(string(bs))
|
|
||||||
if err == nil {
|
|
||||||
return h, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return util.Uint160{}, errors.New("no valid hashes are found")
|
|
||||||
}
|
|
||||||
|
|
||||||
// NNSResolveHash Returns errMissingNNSRecord if invocation fault exception contains "token not found".
|
|
||||||
func NNSResolveHash(inv *invoker.Invoker, nnsHash util.Uint160, domain string) (util.Uint160, error) {
|
|
||||||
item, err := NNSResolve(inv, nnsHash, domain)
|
|
||||||
if err != nil {
|
|
||||||
return util.Uint160{}, err
|
|
||||||
}
|
|
||||||
return ParseNNSResolveResult(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DomainOf(contract string) string {
|
|
||||||
return contract + ".frostfs"
|
|
||||||
}
|
|
||||||
|
|
||||||
func NNSResolveKey(inv *invoker.Invoker, nnsHash util.Uint160, domain string) (*keys.PublicKey, error) {
|
|
||||||
res, err := NNSResolve(inv, nnsHash, domain)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if _, ok := res.Value().(stackitem.Null); ok {
|
|
||||||
return nil, errors.New("NNS record is missing")
|
|
||||||
}
|
|
||||||
arr, ok := res.Value().([]stackitem.Item)
|
|
||||||
if !ok {
|
|
||||||
return nil, errors.New("API of the NNS contract method `resolve` has changed")
|
|
||||||
}
|
|
||||||
for i := range arr {
|
|
||||||
var bs []byte
|
|
||||||
bs, err = arr[i].TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return keys.NewPublicKeyFromString(string(bs))
|
|
||||||
}
|
|
||||||
return nil, errors.New("no valid keys are found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func NNSIsAvailable(c Client, nnsHash util.Uint160, name string) (bool, error) {
|
|
||||||
switch c.(type) {
|
|
||||||
case *rpcclient.Client:
|
|
||||||
inv := invoker.New(c, nil)
|
|
||||||
reader := nns2.NewReader(inv, nnsHash)
|
|
||||||
return reader.IsAvailable(name)
|
|
||||||
default:
|
|
||||||
b, err := unwrap.Bool(InvokeFunction(c, nnsHash, "isAvailable", []any{name}, nil))
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("`isAvailable`: invalid response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func CheckNotaryEnabled(c Client) error {
|
|
||||||
ns, err := c.GetNativeContracts()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get native contract hashes: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
notaryEnabled := false
|
|
||||||
nativeHashes := make(map[string]util.Uint160, len(ns))
|
|
||||||
for i := range ns {
|
|
||||||
if ns[i].Manifest.Name == nativenames.Notary {
|
|
||||||
notaryEnabled = true
|
|
||||||
}
|
|
||||||
nativeHashes[ns[i].Manifest.Name] = ns[i].Hash
|
|
||||||
}
|
|
||||||
if !notaryEnabled {
|
|
||||||
return errors.New("notary contract must be enabled")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,553 +0,0 @@
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
io2 "io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/config"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/innerring"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/context"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errNegativeDuration = errors.New("epoch duration must be positive")
|
|
||||||
errNegativeSize = errors.New("max object size must be positive")
|
|
||||||
)
|
|
||||||
|
|
||||||
type ContractState struct {
|
|
||||||
NEF *nef.File
|
|
||||||
RawNEF []byte
|
|
||||||
Manifest *manifest.Manifest
|
|
||||||
RawManifest []byte
|
|
||||||
Hash util.Uint160
|
|
||||||
}
|
|
||||||
|
|
||||||
type Cache struct {
|
|
||||||
NNSCs *state.Contract
|
|
||||||
GroupKey *keys.PublicKey
|
|
||||||
}
|
|
||||||
|
|
||||||
type InitializeContext struct {
|
|
||||||
ClientContext
|
|
||||||
Cache
|
|
||||||
// CommitteeAcc is used for retrieving the committee address and the verification script.
|
|
||||||
CommitteeAcc *wallet.Account
|
|
||||||
// ConsensusAcc is used for retrieving the committee address and the verification script.
|
|
||||||
ConsensusAcc *wallet.Account
|
|
||||||
Wallets []*wallet.Wallet
|
|
||||||
// ContractWallet is a wallet for providing the contract group signature.
|
|
||||||
ContractWallet *wallet.Wallet
|
|
||||||
// Accounts contains simple signature accounts in the same order as in Wallets.
|
|
||||||
Accounts []*wallet.Account
|
|
||||||
Contracts map[string]*ContractState
|
|
||||||
Command *cobra.Command
|
|
||||||
ContractPath string
|
|
||||||
ContractURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cs *ContractState) Parse() error {
|
|
||||||
nf, err := nef.FileFromBytes(cs.RawNEF)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't parse NEF file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
m := new(manifest.Manifest)
|
|
||||||
if err := json.Unmarshal(cs.RawManifest, m); err != nil {
|
|
||||||
return fmt.Errorf("can't parse manifest file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cs.NEF = &nf
|
|
||||||
cs.Manifest = m
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewInitializeContext(cmd *cobra.Command, v *viper.Viper) (*InitializeContext, error) {
|
|
||||||
walletDir := config.ResolveHomePath(viper.GetString(commonflags.AlphabetWalletsFlag))
|
|
||||||
wallets, err := GetAlphabetWallets(v, walletDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
needContracts := cmd.Name() == "update-contracts" || cmd.Name() == "init"
|
|
||||||
|
|
||||||
var w *wallet.Wallet
|
|
||||||
w, err = getWallet(cmd, v, needContracts, walletDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := createClient(cmd, v, wallets)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
committeeAcc, err := GetWalletAccount(wallets[0], constants.CommitteeAccountName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't find committee account: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
consensusAcc, err := GetWalletAccount(wallets[0], constants.ConsensusAccountName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't find consensus account: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validateInit(cmd); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrPath, err := getContractsPath(cmd, needContracts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var ctrURL string
|
|
||||||
if needContracts {
|
|
||||||
ctrURL, _ = cmd.Flags().GetString(commonflags.ContractsURLFlag)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := CheckNotaryEnabled(c); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
accounts, err := createWalletAccounts(wallets)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cliCtx, err := DefaultClientContext(c, committeeAcc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("client context: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
initCtx := &InitializeContext{
|
|
||||||
ClientContext: *cliCtx,
|
|
||||||
ConsensusAcc: consensusAcc,
|
|
||||||
CommitteeAcc: committeeAcc,
|
|
||||||
ContractWallet: w,
|
|
||||||
Wallets: wallets,
|
|
||||||
Accounts: accounts,
|
|
||||||
Command: cmd,
|
|
||||||
Contracts: make(map[string]*ContractState),
|
|
||||||
ContractPath: ctrPath,
|
|
||||||
ContractURL: ctrURL,
|
|
||||||
}
|
|
||||||
|
|
||||||
if needContracts {
|
|
||||||
err := readContracts(initCtx, constants.FullContractList)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return initCtx, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateInit(cmd *cobra.Command) error {
|
|
||||||
if cmd.Name() != "init" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if viper.GetInt64(commonflags.EpochDurationInitFlag) <= 0 {
|
|
||||||
return errNegativeDuration
|
|
||||||
}
|
|
||||||
|
|
||||||
if viper.GetInt64(commonflags.MaxObjectSizeInitFlag) <= 0 {
|
|
||||||
return errNegativeSize
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createClient(cmd *cobra.Command, v *viper.Viper, wallets []*wallet.Wallet) (Client, error) {
|
|
||||||
var c Client
|
|
||||||
var err error
|
|
||||||
if ldf := cmd.Flags().Lookup(commonflags.LocalDumpFlag); ldf != nil && ldf.Changed {
|
|
||||||
if cmd.Flags().Changed(commonflags.EndpointFlag) {
|
|
||||||
return nil, fmt.Errorf("`%s` and `%s` flags are mutually exclusive", commonflags.EndpointFlag, commonflags.LocalDumpFlag)
|
|
||||||
}
|
|
||||||
c, err = NewLocalClient(cmd, v, wallets, ldf.Value.String())
|
|
||||||
} else {
|
|
||||||
c, err = GetN3Client(v)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't create N3 client: %w", err)
|
|
||||||
}
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getContractsPath(cmd *cobra.Command, needContracts bool) (string, error) {
|
|
||||||
if !needContracts {
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrPath, err := cmd.Flags().GetString(commonflags.ContractsInitFlag)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("invalid contracts path: %w", err)
|
|
||||||
}
|
|
||||||
return ctrPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createWalletAccounts(wallets []*wallet.Wallet) ([]*wallet.Account, error) {
|
|
||||||
accounts := make([]*wallet.Account, len(wallets))
|
|
||||||
for i, w := range wallets {
|
|
||||||
acc, err := GetWalletAccount(w, constants.SingleAccountName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("wallet %s is invalid (no single account): %w", w.Path(), err)
|
|
||||||
}
|
|
||||||
accounts[i] = acc
|
|
||||||
}
|
|
||||||
return accounts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func readContracts(c *InitializeContext, names []string) error {
|
|
||||||
var (
|
|
||||||
fi os.FileInfo
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if c.ContractPath != "" {
|
|
||||||
fi, err = os.Stat(c.ContractPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid contracts path: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.ContractPath != "" && fi.IsDir() {
|
|
||||||
for _, ctrName := range names {
|
|
||||||
cs, err := ReadContract(filepath.Join(c.ContractPath, ctrName), ctrName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.Contracts[ctrName] = cs
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var r io2.ReadCloser
|
|
||||||
if c.ContractPath != "" {
|
|
||||||
r, err = os.Open(c.ContractPath)
|
|
||||||
} else if c.ContractURL != "" {
|
|
||||||
r, err = downloadContracts(c.Command, c.ContractURL)
|
|
||||||
} else {
|
|
||||||
r, err = downloadContractsFromRepository(c.Command)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't open contracts archive: %w", err)
|
|
||||||
}
|
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
m, err := readContractsFromArchive(r, names)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, name := range names {
|
|
||||||
if err := m[name].Parse(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.Contracts[name] = m[name]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ctrName := range names {
|
|
||||||
if ctrName != constants.AlphabetContract {
|
|
||||||
cs := c.Contracts[ctrName]
|
|
||||||
cs.Hash = state.CreateContractHash(c.CommitteeAcc.Contract.ScriptHash(),
|
|
||||||
cs.NEF.Checksum, cs.Manifest.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *InitializeContext) Close() {
|
|
||||||
if local, ok := c.Client.(*LocalClient); ok {
|
|
||||||
err := local.Dump()
|
|
||||||
if err != nil {
|
|
||||||
c.Command.PrintErrf("Can't write dump: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *InitializeContext) AwaitTx() error {
|
|
||||||
return c.ClientContext.AwaitTx(c.Command)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *InitializeContext) NNSContractState() (*state.Contract, error) {
|
|
||||||
if c.NNSCs != nil {
|
|
||||||
return c.NNSCs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
r := management.NewReader(c.ReadOnlyInvoker)
|
|
||||||
cs, err := r.GetContractByID(1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.NNSCs = cs
|
|
||||||
return cs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *InitializeContext) GetSigner(tryGroup bool, acc *wallet.Account) transaction.Signer {
|
|
||||||
if tryGroup && c.GroupKey != nil {
|
|
||||||
return transaction.Signer{
|
|
||||||
Account: acc.Contract.ScriptHash(),
|
|
||||||
Scopes: transaction.CustomGroups,
|
|
||||||
AllowedGroups: keys.PublicKeys{c.GroupKey},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
signer := transaction.Signer{
|
|
||||||
Account: acc.Contract.ScriptHash(),
|
|
||||||
Scopes: transaction.Global, // Scope is important, as we have nested call to container contract.
|
|
||||||
}
|
|
||||||
|
|
||||||
if !tryGroup {
|
|
||||||
return signer
|
|
||||||
}
|
|
||||||
|
|
||||||
nnsCs, err := c.NNSContractState()
|
|
||||||
if err != nil {
|
|
||||||
return signer
|
|
||||||
}
|
|
||||||
|
|
||||||
groupKey, err := NNSResolveKey(c.ReadOnlyInvoker, nnsCs.Hash, client.NNSGroupKeyName)
|
|
||||||
if err == nil {
|
|
||||||
c.GroupKey = groupKey
|
|
||||||
|
|
||||||
signer.Scopes = transaction.CustomGroups
|
|
||||||
signer.AllowedGroups = keys.PublicKeys{groupKey}
|
|
||||||
}
|
|
||||||
return signer
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendCommitteeTx creates transaction from script, signs it by committee nodes and sends it to RPC.
|
|
||||||
// If tryGroup is false, global scope is used for the signer (useful when
|
|
||||||
// working with native contracts).
|
|
||||||
func (c *InitializeContext) SendCommitteeTx(script []byte, tryGroup bool) error {
|
|
||||||
return c.sendMultiTx(script, tryGroup, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendConsensusTx creates transaction from script, signs it by alphabet nodes and sends it to RPC.
|
|
||||||
// Not that because this is used only after the contracts were initialized and deployed,
|
|
||||||
// we always try to have a group scope.
|
|
||||||
func (c *InitializeContext) SendConsensusTx(script []byte) error {
|
|
||||||
return c.sendMultiTx(script, true, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *InitializeContext) sendMultiTx(script []byte, tryGroup bool, withConsensus bool) error {
|
|
||||||
var act *actor.Actor
|
|
||||||
var err error
|
|
||||||
|
|
||||||
withConsensus = withConsensus && !c.ConsensusAcc.Contract.ScriptHash().Equals(c.CommitteeAcc.ScriptHash())
|
|
||||||
if tryGroup {
|
|
||||||
// Even for consensus signatures we need the committee to pay.
|
|
||||||
signers := make([]actor.SignerAccount, 1, 2)
|
|
||||||
signers[0] = actor.SignerAccount{
|
|
||||||
Signer: c.GetSigner(tryGroup, c.CommitteeAcc),
|
|
||||||
Account: c.CommitteeAcc,
|
|
||||||
}
|
|
||||||
if withConsensus {
|
|
||||||
signers = append(signers, actor.SignerAccount{
|
|
||||||
Signer: c.GetSigner(tryGroup, c.ConsensusAcc),
|
|
||||||
Account: c.ConsensusAcc,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
act, err = actor.New(c.Client, signers)
|
|
||||||
} else {
|
|
||||||
if withConsensus {
|
|
||||||
panic("BUG: should never happen")
|
|
||||||
}
|
|
||||||
act, err = c.CommitteeAct, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not create actor: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := act.MakeUnsignedRun(script, []transaction.Attribute{{Type: transaction.HighPriority}})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not perform test invocation: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.MultiSign(tx, constants.CommitteeAccountName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if withConsensus {
|
|
||||||
if err := c.MultiSign(tx, constants.ConsensusAccountName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.SendTx(tx, c.Command, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *InitializeContext) MultiSignAndSend(tx *transaction.Transaction, accType string) error {
|
|
||||||
if err := c.MultiSign(tx, accType); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.SendTx(tx, c.Command, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *InitializeContext) MultiSign(tx *transaction.Transaction, accType string) error {
|
|
||||||
version, err := c.Client.GetVersion()
|
|
||||||
if err != nil {
|
|
||||||
// error appears only if client
|
|
||||||
// has not been initialized
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
network := version.Protocol.Network
|
|
||||||
|
|
||||||
// Use parameter context to avoid dealing with signature order.
|
|
||||||
pc := context.NewParameterContext("", network, tx)
|
|
||||||
h := c.CommitteeAcc.Contract.ScriptHash()
|
|
||||||
if accType == constants.ConsensusAccountName {
|
|
||||||
h = c.ConsensusAcc.Contract.ScriptHash()
|
|
||||||
}
|
|
||||||
for _, w := range c.Wallets {
|
|
||||||
acc, err := GetWalletAccount(w, accType)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't find %s wallet account: %w", accType, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
priv := acc.PrivateKey()
|
|
||||||
sign := priv.SignHashable(uint32(network), tx)
|
|
||||||
if err := pc.AddSignature(h, acc.Contract, priv.PublicKey(), sign); err != nil {
|
|
||||||
return fmt.Errorf("can't add signature: %w", err)
|
|
||||||
}
|
|
||||||
if len(pc.Items[h].Signatures) == len(acc.Contract.Parameters) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w, err := pc.GetWitness(h)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("incomplete signature: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range tx.Signers {
|
|
||||||
if tx.Signers[i].Account == h {
|
|
||||||
if i < len(tx.Scripts) {
|
|
||||||
tx.Scripts[i] = *w
|
|
||||||
} else if i == len(tx.Scripts) {
|
|
||||||
tx.Scripts = append(tx.Scripts, *w)
|
|
||||||
} else {
|
|
||||||
panic("BUG: invalid signing order")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("%s account was not found among transaction signers", accType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EmitUpdateNNSGroupScript emits script for updating group key stored in NNS.
|
|
||||||
// First return value is true iff the key is already there and nothing should be done.
|
|
||||||
// Second return value is true iff a domain registration code was emitted.
|
|
||||||
func (c *InitializeContext) EmitUpdateNNSGroupScript(bw *io.BufBinWriter, nnsHash util.Uint160, pub *keys.PublicKey) (bool, bool, error) {
|
|
||||||
isAvail, err := NNSIsAvailable(c.Client, nnsHash, client.NNSGroupKeyName)
|
|
||||||
if err != nil {
|
|
||||||
return false, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isAvail {
|
|
||||||
currentPub, err := NNSResolveKey(c.ReadOnlyInvoker, nnsHash, client.NNSGroupKeyName)
|
|
||||||
if err != nil {
|
|
||||||
return false, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if pub.Equal(currentPub) {
|
|
||||||
return true, false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if isAvail {
|
|
||||||
emit.AppCall(bw.BinWriter, nnsHash, "register", callflag.All,
|
|
||||||
client.NNSGroupKeyName, c.CommitteeAcc.Contract.ScriptHash(),
|
|
||||||
constants.FrostfsOpsEmail, constants.NNSRefreshDefVal, constants.NNSRetryDefVal,
|
|
||||||
int64(constants.DefaultExpirationTime), constants.NNSTtlDefVal)
|
|
||||||
emit.Opcodes(bw.BinWriter, opcode.ASSERT)
|
|
||||||
}
|
|
||||||
|
|
||||||
emit.AppCall(bw.BinWriter, nnsHash, "deleteRecords", callflag.All, "group.frostfs", int64(nns.TXT))
|
|
||||||
emit.AppCall(bw.BinWriter, nnsHash, "addRecord", callflag.All,
|
|
||||||
"group.frostfs", int64(nns.TXT), hex.EncodeToString(pub.Bytes()))
|
|
||||||
|
|
||||||
return false, isAvail, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *InitializeContext) NNSRegisterDomainScript(nnsHash, expectedHash util.Uint160, domain string) ([]byte, bool, error) {
|
|
||||||
ok, err := NNSIsAvailable(c.Client, nnsHash, domain)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok {
|
|
||||||
bw := io.NewBufBinWriter()
|
|
||||||
emit.AppCall(bw.BinWriter, nnsHash, "register", callflag.All,
|
|
||||||
domain, c.CommitteeAcc.Contract.ScriptHash(),
|
|
||||||
constants.FrostfsOpsEmail, constants.NNSRefreshDefVal, constants.NNSRetryDefVal,
|
|
||||||
int64(constants.DefaultExpirationTime), constants.NNSTtlDefVal)
|
|
||||||
emit.Opcodes(bw.BinWriter, opcode.ASSERT)
|
|
||||||
|
|
||||||
if bw.Err != nil {
|
|
||||||
panic(bw.Err)
|
|
||||||
}
|
|
||||||
return bw.Bytes(), false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := NNSResolveHash(c.ReadOnlyInvoker, nnsHash, domain)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
return nil, s == expectedHash, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *InitializeContext) NNSRootRegistered(nnsHash util.Uint160, zone string) (bool, error) {
|
|
||||||
res, err := c.CommitteeAct.Call(nnsHash, "isAvailable", "name."+zone)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.State == vmstate.Halt.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *InitializeContext) IsUpdated(ctrHash util.Uint160, cs *ContractState) bool {
|
|
||||||
r := management.NewReader(c.ReadOnlyInvoker)
|
|
||||||
realCs, err := r.GetContract(ctrHash)
|
|
||||||
return err == nil && realCs != nil && realCs.NEF.Checksum == cs.NEF.Checksum
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *InitializeContext) GetContract(ctrName string) *ContractState {
|
|
||||||
return c.Contracts[ctrName]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *InitializeContext) GetAlphabetDeployItems(i, n int) []any {
|
|
||||||
items := make([]any, 5)
|
|
||||||
items[0] = c.Contracts[constants.NetmapContract].Hash
|
|
||||||
items[1] = c.Contracts[constants.ProxyContract].Hash
|
|
||||||
items[2] = innerring.GlagoliticLetter(i).String()
|
|
||||||
items[3] = int64(i)
|
|
||||||
items[4] = int64(n)
|
|
||||||
return items
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNextPollInterval(t *testing.T) {
|
|
||||||
var pollInterval time.Duration
|
|
||||||
var iteration int
|
|
||||||
|
|
||||||
pollInterval, hasChanged := NextPollInterval(iteration, pollInterval)
|
|
||||||
require.True(t, hasChanged)
|
|
||||||
require.Equal(t, time.Second, pollInterval)
|
|
||||||
|
|
||||||
iteration = 4
|
|
||||||
pollInterval, hasChanged = NextPollInterval(iteration, pollInterval)
|
|
||||||
require.False(t, hasChanged)
|
|
||||||
require.Equal(t, time.Second, pollInterval)
|
|
||||||
|
|
||||||
iteration = 5
|
|
||||||
pollInterval, hasChanged = NextPollInterval(iteration, pollInterval)
|
|
||||||
require.True(t, hasChanged)
|
|
||||||
require.Equal(t, 2*time.Second, pollInterval)
|
|
||||||
|
|
||||||
iteration = 10
|
|
||||||
pollInterval, hasChanged = NextPollInterval(iteration, pollInterval)
|
|
||||||
require.True(t, hasChanged)
|
|
||||||
require.Equal(t, 4*time.Second, pollInterval)
|
|
||||||
|
|
||||||
iteration = 20
|
|
||||||
pollInterval = 32 * time.Second
|
|
||||||
pollInterval, hasChanged = NextPollInterval(iteration, pollInterval)
|
|
||||||
require.True(t, hasChanged) // from 32s to 16s
|
|
||||||
require.Equal(t, 16*time.Second, pollInterval)
|
|
||||||
|
|
||||||
pollInterval = 16 * time.Second
|
|
||||||
pollInterval, hasChanged = NextPollInterval(iteration, pollInterval)
|
|
||||||
require.False(t, hasChanged)
|
|
||||||
require.Equal(t, 16*time.Second, pollInterval)
|
|
||||||
}
|
|
|
@ -1,129 +0,0 @@
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/netmap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
var NetmapConfigKeys = []string{
|
|
||||||
netmap.EpochDurationConfig,
|
|
||||||
netmap.MaxObjectSizeConfig,
|
|
||||||
netmap.ContainerFeeConfig,
|
|
||||||
netmap.ContainerAliasFeeConfig,
|
|
||||||
netmap.IrCandidateFeeConfig,
|
|
||||||
netmap.WithdrawFeeConfig,
|
|
||||||
netmap.HomomorphicHashingDisabledKey,
|
|
||||||
netmap.MaintenanceModeAllowedConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
var errFailedToFetchListOfNetworkKeys = errors.New("can't fetch list of network config keys from the netmap contract")
|
|
||||||
|
|
||||||
func GetDefaultNetmapContractConfigMap() map[string]any {
|
|
||||||
m := make(map[string]any)
|
|
||||||
m[netmap.EpochDurationConfig] = viper.GetInt64(commonflags.EpochDurationInitFlag)
|
|
||||||
m[netmap.MaxObjectSizeConfig] = viper.GetInt64(commonflags.MaxObjectSizeInitFlag)
|
|
||||||
m[netmap.MaxECDataCountConfig] = viper.GetInt64(commonflags.MaxECDataCountFlag)
|
|
||||||
m[netmap.MaxECParityCountConfig] = viper.GetInt64(commonflags.MaxECParityCounFlag)
|
|
||||||
m[netmap.ContainerFeeConfig] = viper.GetInt64(commonflags.ContainerFeeInitFlag)
|
|
||||||
m[netmap.ContainerAliasFeeConfig] = viper.GetInt64(commonflags.ContainerAliasFeeInitFlag)
|
|
||||||
m[netmap.IrCandidateFeeConfig] = viper.GetInt64(commonflags.CandidateFeeInitFlag)
|
|
||||||
m[netmap.WithdrawFeeConfig] = viper.GetInt64(commonflags.WithdrawFeeInitFlag)
|
|
||||||
m[netmap.HomomorphicHashingDisabledKey] = viper.GetBool(commonflags.HomomorphicHashDisabledInitFlag)
|
|
||||||
m[netmap.MaintenanceModeAllowedConfig] = viper.GetBool(commonflags.MaintenanceModeAllowedInitFlag)
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseConfigFromNetmapContract(arr []stackitem.Item) (map[string][]byte, error) {
|
|
||||||
m := make(map[string][]byte, len(arr))
|
|
||||||
for _, param := range arr {
|
|
||||||
tuple, ok := param.Value().([]stackitem.Item)
|
|
||||||
if !ok || len(tuple) != 2 {
|
|
||||||
return nil, errors.New("invalid ListConfig response from netmap contract")
|
|
||||||
}
|
|
||||||
|
|
||||||
k, err := tuple[0].TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.New("invalid config key from netmap contract")
|
|
||||||
}
|
|
||||||
|
|
||||||
v, err := tuple[1].TryBytes()
|
|
||||||
if err != nil {
|
|
||||||
return nil, InvalidConfigValueErr(string(k))
|
|
||||||
}
|
|
||||||
m[string(k)] = v
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func InvalidConfigValueErr(key string) error {
|
|
||||||
return fmt.Errorf("invalid %s config value from netmap contract", key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func EmitNewEpochCall(bw *io.BufBinWriter, wCtx *InitializeContext, nmHash util.Uint160, countEpoch int64) error {
|
|
||||||
if countEpoch <= 0 {
|
|
||||||
return errors.New("number of epochs cannot be less than 1")
|
|
||||||
}
|
|
||||||
|
|
||||||
curr, err := unwrap.Int64(wCtx.ReadOnlyInvoker.Call(nmHash, "epoch"))
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("can't fetch current epoch from the netmap contract")
|
|
||||||
}
|
|
||||||
|
|
||||||
newEpoch := curr + countEpoch
|
|
||||||
wCtx.Command.Printf("Current epoch: %d, increase to %d.\n", curr, newEpoch)
|
|
||||||
|
|
||||||
// In NeoFS this is done via Notary contract. Here, however, we can form the
|
|
||||||
// transaction locally.
|
|
||||||
emit.AppCall(bw.BinWriter, nmHash, "newEpoch", callflag.All, newEpoch)
|
|
||||||
return bw.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetNetConfigFromNetmapContract(roInvoker *invoker.Invoker) ([]stackitem.Item, error) {
|
|
||||||
r := management.NewReader(roInvoker)
|
|
||||||
cs, err := GetContractByID(r, 1)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("get nns contract: %w", err)
|
|
||||||
}
|
|
||||||
nmHash, err := NNSResolveHash(roInvoker, cs.Hash, DomainOf(constants.NetmapContract))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't get netmap contract hash: %w", err)
|
|
||||||
}
|
|
||||||
arr, err := unwrap.Array(roInvoker.Call(nmHash, "listConfig"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errFailedToFetchListOfNetworkKeys
|
|
||||||
}
|
|
||||||
return arr, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func MergeNetmapConfig(roInvoker *invoker.Invoker, md map[string]any) error {
|
|
||||||
arr, err := GetNetConfigFromNetmapContract(roInvoker)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m, err := ParseConfigFromNetmapContract(arr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for k, v := range m {
|
|
||||||
for _, key := range NetmapConfigKeys {
|
|
||||||
if k == key {
|
|
||||||
md[k] = v
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,200 +0,0 @@
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"archive/tar"
|
|
||||||
"compress/gzip"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/config"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/innerring"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetAlphabetWallets(v *viper.Viper, walletDir string) ([]*wallet.Wallet, error) {
|
|
||||||
wallets, err := openAlphabetWallets(v, walletDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(wallets) > constants.MaxAlphabetNodes {
|
|
||||||
return nil, ErrTooManyAlphabetNodes
|
|
||||||
}
|
|
||||||
return wallets, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func openAlphabetWallets(v *viper.Viper, walletDir string) ([]*wallet.Wallet, error) {
|
|
||||||
walletFiles, err := os.ReadDir(walletDir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't read alphabet wallets dir: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var wallets []*wallet.Wallet
|
|
||||||
var letter string
|
|
||||||
for i := range constants.MaxAlphabetNodes {
|
|
||||||
letter = innerring.GlagoliticLetter(i).String()
|
|
||||||
p := filepath.Join(walletDir, letter+".json")
|
|
||||||
var w *wallet.Wallet
|
|
||||||
w, err = wallet.NewWalletFromFile(p)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
err = nil
|
|
||||||
} else {
|
|
||||||
err = fmt.Errorf("can't open wallet: %w", err)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
var password string
|
|
||||||
password, err = config.GetPassword(v, letter)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("can't fetch password: %w", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range w.Accounts {
|
|
||||||
if err = w.Accounts[i].Decrypt(password, keys.NEP2ScryptParams()); err != nil {
|
|
||||||
err = fmt.Errorf("can't unlock wallet: %w", err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wallets = append(wallets, w)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't read wallet for letter '%s': %w", letter, err)
|
|
||||||
}
|
|
||||||
if len(wallets) == 0 {
|
|
||||||
err = errors.New("there are no alphabet wallets in dir (run `generate-alphabet` command first)")
|
|
||||||
if len(walletFiles) > 0 {
|
|
||||||
err = fmt.Errorf("use glagolitic names for wallets(run `print-alphabet`): %w", err)
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return wallets, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewActor(c actor.RPCActor, committeeAcc *wallet.Account) (*actor.Actor, error) {
|
|
||||||
return actor.New(c, []actor.SignerAccount{{
|
|
||||||
Signer: transaction.Signer{
|
|
||||||
Account: committeeAcc.Contract.ScriptHash(),
|
|
||||||
Scopes: transaction.Global,
|
|
||||||
},
|
|
||||||
Account: committeeAcc,
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadContract(ctrPath, ctrName string) (*ContractState, error) {
|
|
||||||
rawNef, err := os.ReadFile(filepath.Join(ctrPath, ctrName+"_contract.nef"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't read NEF file for %s contract: %w", ctrName, err)
|
|
||||||
}
|
|
||||||
rawManif, err := os.ReadFile(filepath.Join(ctrPath, "config.json"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't read manifest file for %s contract: %w", ctrName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cs := &ContractState{
|
|
||||||
RawNEF: rawNef,
|
|
||||||
RawManifest: rawManif,
|
|
||||||
}
|
|
||||||
|
|
||||||
return cs, cs.Parse()
|
|
||||||
}
|
|
||||||
|
|
||||||
func readContractsFromArchive(file io.Reader, names []string) (map[string]*ContractState, error) {
|
|
||||||
m := make(map[string]*ContractState, len(names))
|
|
||||||
for i := range names {
|
|
||||||
m[names[i]] = new(ContractState)
|
|
||||||
}
|
|
||||||
|
|
||||||
gr, err := gzip.NewReader(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("contracts file must be tar.gz archive: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r := tar.NewReader(gr)
|
|
||||||
var h *tar.Header
|
|
||||||
for h, err = r.Next(); err == nil && h != nil; h, err = r.Next() {
|
|
||||||
if h.Typeflag != tar.TypeReg {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dir, _ := filepath.Split(h.Name)
|
|
||||||
ctrName := filepath.Base(dir)
|
|
||||||
|
|
||||||
cs, ok := m[ctrName]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case strings.HasSuffix(h.Name, filepath.Join(ctrName, ctrName+"_contract.nef")):
|
|
||||||
cs.RawNEF, err = io.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't read NEF file for %s contract: %w", ctrName, err)
|
|
||||||
}
|
|
||||||
case strings.HasSuffix(h.Name, "config.json"):
|
|
||||||
cs.RawManifest, err = io.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't read manifest file for %s contract: %w", ctrName, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m[ctrName] = cs
|
|
||||||
}
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
return nil, fmt.Errorf("can't read contracts from archive: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for ctrName, cs := range m {
|
|
||||||
if cs.RawNEF == nil {
|
|
||||||
return nil, fmt.Errorf("NEF for %s contract wasn't found", ctrName)
|
|
||||||
}
|
|
||||||
if cs.RawManifest == nil {
|
|
||||||
return nil, fmt.Errorf("manifest for %s contract wasn't found", ctrName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetAlphabetNNSDomain(i int) string {
|
|
||||||
return constants.AlphabetContract + strconv.FormatUint(uint64(i), 10) + ".frostfs"
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseGASAmount(s string) (fixedn.Fixed8, error) {
|
|
||||||
gasAmount, err := fixedn.Fixed8FromString(s)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("invalid GAS amount %s: %w", s, err)
|
|
||||||
}
|
|
||||||
if gasAmount <= 0 {
|
|
||||||
return 0, fmt.Errorf("GAS amount must be positive (got %d)", gasAmount)
|
|
||||||
}
|
|
||||||
return gasAmount, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetContractByID retrieves a contract by its ID using the standard GetContractByID method.
|
|
||||||
// However, if the returned state.Contract is nil, it returns an error indicating that the contract was not found.
|
|
||||||
// See https://git.frostfs.info/TrueCloudLab/frostfs-node/issues/1210
|
|
||||||
func GetContractByID(r *management.ContractReader, id int32) (*state.Contract, error) {
|
|
||||||
cs, err := r.GetContractByID(id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if cs == nil {
|
|
||||||
return nil, errors.New("contract not found")
|
|
||||||
}
|
|
||||||
return cs, nil
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/config"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
func InitializeContractWallet(v *viper.Viper, walletDir string) (*wallet.Wallet, error) {
|
|
||||||
password, err := config.GetPassword(v, constants.ContractWalletPasswordKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
w, err := wallet.NewWallet(filepath.Join(walletDir, constants.ContractWalletFilename))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
acc, err := wallet.NewAccount()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = acc.Encrypt(password, keys.NEP2ScryptParams())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
w.AddAccount(acc)
|
|
||||||
if err := w.SavePretty(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return w, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func openContractWallet(v *viper.Viper, cmd *cobra.Command, walletDir string) (*wallet.Wallet, error) {
|
|
||||||
p := filepath.Join(walletDir, constants.ContractWalletFilename)
|
|
||||||
w, err := wallet.NewWalletFromFile(p)
|
|
||||||
if err != nil {
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
return nil, fmt.Errorf("can't open wallet: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Printf("Contract group wallet is missing, initialize at %s\n", p)
|
|
||||||
return InitializeContractWallet(v, walletDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
password, err := config.GetPassword(v, constants.ContractWalletPasswordKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range w.Accounts {
|
|
||||||
if err := w.Accounts[i].Decrypt(password, keys.NEP2ScryptParams()); err != nil {
|
|
||||||
return nil, fmt.Errorf("can't unlock wallet: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return w, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getWallet(cmd *cobra.Command, v *viper.Viper, needContracts bool, walletDir string) (*wallet.Wallet, error) {
|
|
||||||
if !needContracts {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return openContractWallet(v, cmd, walletDir)
|
|
||||||
}
|
|
464
cmd/frostfs-adm/internal/modules/morph/initialize.go
Normal file
464
cmd/frostfs-adm/internal/modules/morph/initialize.go
Normal file
|
@ -0,0 +1,464 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/config"
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/pkg/innerring"
|
||||||
|
morphClient "github.com/TrueCloudLab/frostfs-node/pkg/morph/client"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type cache struct {
|
||||||
|
nnsCs *state.Contract
|
||||||
|
groupKey *keys.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
type initializeContext struct {
|
||||||
|
clientContext
|
||||||
|
cache
|
||||||
|
// CommitteeAcc is used for retrieving the committee address and the verification script.
|
||||||
|
CommitteeAcc *wallet.Account
|
||||||
|
// ConsensusAcc is used for retrieving the committee address and the verification script.
|
||||||
|
ConsensusAcc *wallet.Account
|
||||||
|
Wallets []*wallet.Wallet
|
||||||
|
// ContractWallet is a wallet for providing the contract group signature.
|
||||||
|
ContractWallet *wallet.Wallet
|
||||||
|
// Accounts contains simple signature accounts in the same order as in Wallets.
|
||||||
|
Accounts []*wallet.Account
|
||||||
|
Contracts map[string]*contractState
|
||||||
|
Command *cobra.Command
|
||||||
|
ContractPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func initializeSideChainCmd(cmd *cobra.Command, args []string) error {
|
||||||
|
initCtx, err := newInitializeContext(cmd, viper.GetViper())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("initialization error: %w", err)
|
||||||
|
}
|
||||||
|
defer initCtx.close()
|
||||||
|
|
||||||
|
// 1. Transfer funds to committee accounts.
|
||||||
|
cmd.Println("Stage 1: transfer GAS to alphabet nodes.")
|
||||||
|
if err := initCtx.transferFunds(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println("Stage 2: set notary and alphabet nodes in designate contract.")
|
||||||
|
if err := initCtx.setNotaryAndAlphabetNodes(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Deploy NNS contract.
|
||||||
|
cmd.Println("Stage 3: deploy NNS contract.")
|
||||||
|
if err := initCtx.deployNNS(deployMethodName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Deploy NeoFS contracts.
|
||||||
|
cmd.Println("Stage 4: deploy NeoFS contracts.")
|
||||||
|
if err := initCtx.deployContracts(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println("Stage 4.1: Transfer GAS to proxy contract.")
|
||||||
|
if err := initCtx.transferGASToProxy(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println("Stage 5: register candidates.")
|
||||||
|
if err := initCtx.registerCandidates(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println("Stage 6: transfer NEO to alphabet contracts.")
|
||||||
|
if err := initCtx.transferNEOToAlphabetContracts(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println("Stage 7: set addresses in NNS.")
|
||||||
|
if err := initCtx.setNNS(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) close() {
|
||||||
|
if local, ok := c.Client.(*localClient); ok {
|
||||||
|
err := local.dump()
|
||||||
|
if err != nil {
|
||||||
|
c.Command.PrintErrf("Can't write dump: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInitializeContext(cmd *cobra.Command, v *viper.Viper) (*initializeContext, error) {
|
||||||
|
walletDir := config.ResolveHomePath(viper.GetString(alphabetWalletsFlag))
|
||||||
|
wallets, err := openAlphabetWallets(v, walletDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var w *wallet.Wallet
|
||||||
|
if cmd.Name() != "deploy" {
|
||||||
|
w, err = openContractWallet(v, cmd, walletDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var c Client
|
||||||
|
if v.GetString(localDumpFlag) != "" {
|
||||||
|
if v.GetString(endpointFlag) != "" {
|
||||||
|
return nil, fmt.Errorf("`%s` and `%s` flags are mutually exclusive", endpointFlag, localDumpFlag)
|
||||||
|
}
|
||||||
|
c, err = newLocalClient(cmd, v, wallets)
|
||||||
|
} else {
|
||||||
|
c, err = getN3Client(v)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't create N3 client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
committeeAcc, err := getWalletAccount(wallets[0], committeeAccountName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't find committee account: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
consensusAcc, err := getWalletAccount(wallets[0], consensusAccountName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't find consensus account: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ctrPath string
|
||||||
|
if cmd.Name() == "init" {
|
||||||
|
if viper.GetInt64(epochDurationInitFlag) <= 0 {
|
||||||
|
return nil, fmt.Errorf("epoch duration must be positive")
|
||||||
|
}
|
||||||
|
|
||||||
|
if viper.GetInt64(maxObjectSizeInitFlag) <= 0 {
|
||||||
|
return nil, fmt.Errorf("max object size must be positive")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
needContracts := cmd.Name() == "update-contracts" || cmd.Name() == "init"
|
||||||
|
if needContracts {
|
||||||
|
ctrPath, err = cmd.Flags().GetString(contractsInitFlag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid contracts path: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := checkNotaryEnabled(c); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accounts := make([]*wallet.Account, len(wallets))
|
||||||
|
for i, w := range wallets {
|
||||||
|
acc, err := getWalletAccount(w, singleAccountName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("wallet %s is invalid (no single account): %w", w.Path(), err)
|
||||||
|
}
|
||||||
|
accounts[i] = acc
|
||||||
|
}
|
||||||
|
|
||||||
|
cliCtx, err := defaultClientContext(c, committeeAcc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("client context: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
initCtx := &initializeContext{
|
||||||
|
clientContext: *cliCtx,
|
||||||
|
ConsensusAcc: consensusAcc,
|
||||||
|
CommitteeAcc: committeeAcc,
|
||||||
|
ContractWallet: w,
|
||||||
|
Wallets: wallets,
|
||||||
|
Accounts: accounts,
|
||||||
|
Command: cmd,
|
||||||
|
Contracts: make(map[string]*contractState),
|
||||||
|
ContractPath: ctrPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
if needContracts {
|
||||||
|
err := initCtx.readContracts(fullContractList)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return initCtx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func openAlphabetWallets(v *viper.Viper, walletDir string) ([]*wallet.Wallet, error) {
|
||||||
|
walletFiles, err := os.ReadDir(walletDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't read alphabet wallets dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var size int
|
||||||
|
loop:
|
||||||
|
for i := 0; i < len(walletFiles); i++ {
|
||||||
|
name := innerring.GlagoliticLetter(i).String() + ".json"
|
||||||
|
for j := range walletFiles {
|
||||||
|
if walletFiles[j].Name() == name {
|
||||||
|
size++
|
||||||
|
continue loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if size == 0 {
|
||||||
|
return nil, errors.New("alphabet wallets dir is empty (run `generate-alphabet` command first)")
|
||||||
|
}
|
||||||
|
|
||||||
|
wallets := make([]*wallet.Wallet, size)
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
letter := innerring.GlagoliticLetter(i).String()
|
||||||
|
p := filepath.Join(walletDir, letter+".json")
|
||||||
|
w, err := wallet.NewWalletFromFile(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't open wallet: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
password, err := config.GetPassword(v, letter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't fetch password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range w.Accounts {
|
||||||
|
if err := w.Accounts[i].Decrypt(password, keys.NEP2ScryptParams()); err != nil {
|
||||||
|
return nil, fmt.Errorf("can't unlock wallet: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wallets[i] = w
|
||||||
|
}
|
||||||
|
|
||||||
|
return wallets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) awaitTx() error {
|
||||||
|
return c.clientContext.awaitTx(c.Command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) nnsContractState() (*state.Contract, error) {
|
||||||
|
if c.nnsCs != nil {
|
||||||
|
return c.nnsCs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cs, err := c.Client.GetContractStateByID(1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.nnsCs = cs
|
||||||
|
return cs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) getSigner(tryGroup bool, acc *wallet.Account) transaction.Signer {
|
||||||
|
if tryGroup && c.groupKey != nil {
|
||||||
|
return transaction.Signer{
|
||||||
|
Account: acc.Contract.ScriptHash(),
|
||||||
|
Scopes: transaction.CustomGroups,
|
||||||
|
AllowedGroups: keys.PublicKeys{c.groupKey},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signer := transaction.Signer{
|
||||||
|
Account: acc.Contract.ScriptHash(),
|
||||||
|
Scopes: transaction.Global, // Scope is important, as we have nested call to container contract.
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tryGroup {
|
||||||
|
return signer
|
||||||
|
}
|
||||||
|
|
||||||
|
nnsCs, err := c.nnsContractState()
|
||||||
|
if err != nil {
|
||||||
|
return signer
|
||||||
|
}
|
||||||
|
|
||||||
|
groupKey, err := nnsResolveKey(c.ReadOnlyInvoker, nnsCs.Hash, morphClient.NNSGroupKeyName)
|
||||||
|
if err == nil {
|
||||||
|
c.groupKey = groupKey
|
||||||
|
|
||||||
|
signer.Scopes = transaction.CustomGroups
|
||||||
|
signer.AllowedGroups = keys.PublicKeys{groupKey}
|
||||||
|
}
|
||||||
|
return signer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientContext) awaitTx(cmd *cobra.Command) error {
|
||||||
|
if len(c.SentTxs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if local, ok := c.Client.(*localClient); ok {
|
||||||
|
if err := local.putTransactions(); err != nil {
|
||||||
|
return fmt.Errorf("can't persist transactions: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := awaitTx(cmd, c.Client, c.SentTxs)
|
||||||
|
c.SentTxs = c.SentTxs[:0]
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func awaitTx(cmd *cobra.Command, c Client, txs []hashVUBPair) error {
|
||||||
|
cmd.Println("Waiting for transactions to persist...")
|
||||||
|
|
||||||
|
const pollInterval = time.Second
|
||||||
|
|
||||||
|
tick := time.NewTicker(pollInterval)
|
||||||
|
defer tick.Stop()
|
||||||
|
|
||||||
|
at := trigger.Application
|
||||||
|
|
||||||
|
var retErr error
|
||||||
|
|
||||||
|
currBlock, err := c.GetBlockCount()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't fetch current block height: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
loop:
|
||||||
|
for i := range txs {
|
||||||
|
res, err := c.GetApplicationLog(txs[i].hash, &at)
|
||||||
|
if err == nil {
|
||||||
|
if retErr == nil && len(res.Executions) > 0 && res.Executions[0].VMState != vmstate.Halt {
|
||||||
|
retErr = fmt.Errorf("tx %d persisted in %s state: %s",
|
||||||
|
i, res.Executions[0].VMState, res.Executions[0].FaultException)
|
||||||
|
}
|
||||||
|
continue loop
|
||||||
|
}
|
||||||
|
if txs[i].vub < currBlock {
|
||||||
|
return fmt.Errorf("tx was not persisted: vub=%d, height=%d", txs[i].vub, currBlock)
|
||||||
|
}
|
||||||
|
for range tick.C {
|
||||||
|
// We must fetch current height before application log, to avoid race condition.
|
||||||
|
currBlock, err = c.GetBlockCount()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't fetch current block height: %w", err)
|
||||||
|
}
|
||||||
|
res, err := c.GetApplicationLog(txs[i].hash, &at)
|
||||||
|
if err == nil {
|
||||||
|
if retErr == nil && len(res.Executions) > 0 && res.Executions[0].VMState != vmstate.Halt {
|
||||||
|
retErr = fmt.Errorf("tx %d persisted in %s state: %s",
|
||||||
|
i, res.Executions[0].VMState, res.Executions[0].FaultException)
|
||||||
|
}
|
||||||
|
continue loop
|
||||||
|
}
|
||||||
|
if txs[i].vub < currBlock {
|
||||||
|
return fmt.Errorf("tx was not persisted: vub=%d, height=%d", txs[i].vub, currBlock)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return retErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendCommitteeTx creates transaction from script, signs it by committee nodes and sends it to RPC.
|
||||||
|
// If tryGroup is false, global scope is used for the signer (useful when
|
||||||
|
// working with native contracts).
|
||||||
|
func (c *initializeContext) sendCommitteeTx(script []byte, tryGroup bool) error {
|
||||||
|
return c.sendMultiTx(script, tryGroup, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendConsensusTx creates transaction from script, signs it by alphabet nodes and sends it to RPC.
|
||||||
|
// Not that because this is used only after the contracts were initialized and deployed,
|
||||||
|
// we always try to have a group scope.
|
||||||
|
func (c *initializeContext) sendConsensusTx(script []byte) error {
|
||||||
|
return c.sendMultiTx(script, true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) sendMultiTx(script []byte, tryGroup bool, withConsensus bool) error {
|
||||||
|
var act *actor.Actor
|
||||||
|
var err error
|
||||||
|
|
||||||
|
withConsensus = withConsensus && !c.ConsensusAcc.Contract.ScriptHash().Equals(c.CommitteeAcc.ScriptHash())
|
||||||
|
if tryGroup {
|
||||||
|
// Even for consensus signatures we need the committee to pay.
|
||||||
|
signers := make([]actor.SignerAccount, 1, 2)
|
||||||
|
signers[0] = actor.SignerAccount{
|
||||||
|
Signer: c.getSigner(tryGroup, c.CommitteeAcc),
|
||||||
|
Account: c.CommitteeAcc,
|
||||||
|
}
|
||||||
|
if withConsensus {
|
||||||
|
signers = append(signers, actor.SignerAccount{
|
||||||
|
Signer: c.getSigner(tryGroup, c.ConsensusAcc),
|
||||||
|
Account: c.ConsensusAcc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
act, err = actor.New(c.Client, signers)
|
||||||
|
} else {
|
||||||
|
if withConsensus {
|
||||||
|
panic("BUG: should never happen")
|
||||||
|
}
|
||||||
|
act, err = c.CommitteeAct, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create actor: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := act.MakeUnsignedRun(script, []transaction.Attribute{{Type: transaction.HighPriority}})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not perform test invocation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.multiSign(tx, committeeAccountName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if withConsensus {
|
||||||
|
if err := c.multiSign(tx, consensusAccountName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.sendTx(tx, c.Command, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWalletAccount(w *wallet.Wallet, typ string) (*wallet.Account, error) {
|
||||||
|
for i := range w.Accounts {
|
||||||
|
if w.Accounts[i].Label == typ {
|
||||||
|
return w.Accounts[i], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("account for '%s' not found", typ)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkNotaryEnabled(c Client) error {
|
||||||
|
ns, err := c.GetNativeContracts()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't get native contract hashes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
notaryEnabled := false
|
||||||
|
nativeHashes := make(map[string]util.Uint160, len(ns))
|
||||||
|
for i := range ns {
|
||||||
|
if ns[i].Manifest.Name == nativenames.Notary {
|
||||||
|
notaryEnabled = len(ns[i].UpdateHistory) > 0
|
||||||
|
}
|
||||||
|
nativeHashes[ns[i].Manifest.Name] = ns[i].Hash
|
||||||
|
}
|
||||||
|
if !notaryEnabled {
|
||||||
|
return errors.New("notary contract must be enabled")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,59 +0,0 @@
|
||||||
package initialize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
func initializeSideChainCmd(cmd *cobra.Command, _ []string) error {
|
|
||||||
initCtx, err := helper.NewInitializeContext(cmd, viper.GetViper())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("initialization error: %w", err)
|
|
||||||
}
|
|
||||||
defer initCtx.Close()
|
|
||||||
|
|
||||||
// 1. Transfer funds to committee accounts.
|
|
||||||
cmd.Println("Stage 1: transfer GAS to alphabet nodes.")
|
|
||||||
if err := transferFunds(initCtx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Println("Stage 2: set notary and alphabet nodes in designate contract.")
|
|
||||||
if err := setNotaryAndAlphabetNodes(initCtx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Deploy NNS contract.
|
|
||||||
cmd.Println("Stage 3: deploy NNS contract.")
|
|
||||||
if err := helper.DeployNNS(initCtx, constants.DeployMethodName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Deploy NeoFS contracts.
|
|
||||||
cmd.Println("Stage 4: deploy NeoFS contracts.")
|
|
||||||
if err := deployContracts(initCtx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Println("Stage 4.1: Transfer GAS to proxy contract.")
|
|
||||||
if err := transferGASToProxy(initCtx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Println("Stage 5: register candidates.")
|
|
||||||
if err := registerCandidates(initCtx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Println("Stage 6: transfer NEO to alphabet contracts.")
|
|
||||||
if err := transferNEOToAlphabetContracts(initCtx); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Println("Stage 7: set addresses in NNS.")
|
|
||||||
return setNNS(initCtx)
|
|
||||||
}
|
|
|
@ -1,80 +0,0 @@
|
||||||
package initialize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
|
|
||||||
)
|
|
||||||
|
|
||||||
func deployContracts(c *helper.InitializeContext) error {
|
|
||||||
alphaCs := c.GetContract(constants.AlphabetContract)
|
|
||||||
|
|
||||||
var keysParam []any
|
|
||||||
|
|
||||||
baseGroups := alphaCs.Manifest.Groups
|
|
||||||
|
|
||||||
// alphabet contracts should be deployed by individual nodes to get different hashes.
|
|
||||||
for i, acc := range c.Accounts {
|
|
||||||
ctrHash := state.CreateContractHash(acc.Contract.ScriptHash(), alphaCs.NEF.Checksum, alphaCs.Manifest.Name)
|
|
||||||
if c.IsUpdated(ctrHash, alphaCs) {
|
|
||||||
c.Command.Printf("Alphabet contract #%d is already deployed.\n", i)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
alphaCs.Manifest.Groups = baseGroups
|
|
||||||
err := helper.AddManifestGroup(c.ContractWallet, ctrHash, alphaCs)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't sign manifest group: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
keysParam = append(keysParam, acc.PrivateKey().PublicKey().Bytes())
|
|
||||||
params := helper.GetContractDeployParameters(alphaCs, c.GetAlphabetDeployItems(i, len(c.Wallets)))
|
|
||||||
|
|
||||||
act, err := actor.NewSimple(c.Client, acc)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("could not create actor: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
txHash, vub, err := act.SendCall(management.Hash, constants.DeployMethodName, params...)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't deploy alphabet #%d contract: %w", i, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.SentTxs = append(c.SentTxs, helper.HashVUBPair{Hash: txHash, Vub: vub})
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ctrName := range constants.ContractList {
|
|
||||||
cs := c.GetContract(ctrName)
|
|
||||||
|
|
||||||
ctrHash := cs.Hash
|
|
||||||
if c.IsUpdated(ctrHash, cs) {
|
|
||||||
c.Command.Printf("%s contract is already deployed.\n", ctrName)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
err := helper.AddManifestGroup(c.ContractWallet, ctrHash, cs)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't sign manifest group: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
args, err := helper.GetContractDeployData(c, ctrName, keysParam, constants.DeployMethodName)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: getting deploy params: %v", ctrName, err)
|
|
||||||
}
|
|
||||||
params := helper.GetContractDeployParameters(cs, args)
|
|
||||||
res, err := c.CommitteeAct.MakeCall(management.Hash, constants.DeployMethodName, params...)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't deploy %s contract: %w", ctrName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.SendCommitteeTx(res.Script, false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.AwaitTx()
|
|
||||||
}
|
|
|
@ -1,135 +0,0 @@
|
||||||
package initialize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-contract/nns"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper"
|
|
||||||
morphClient "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
|
||||||
)
|
|
||||||
|
|
||||||
func setNNS(c *helper.InitializeContext) error {
|
|
||||||
r := management.NewReader(c.ReadOnlyInvoker)
|
|
||||||
nnsCs, err := helper.GetContractByID(r, 1)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ok, err := c.NNSRootRegistered(nnsCs.Hash, "frostfs")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if !ok {
|
|
||||||
bw := io.NewBufBinWriter()
|
|
||||||
emit.AppCall(bw.BinWriter, nnsCs.Hash, "register", callflag.All,
|
|
||||||
"frostfs", c.CommitteeAcc.Contract.ScriptHash(),
|
|
||||||
constants.FrostfsOpsEmail, constants.NNSRefreshDefVal, constants.NNSRetryDefVal,
|
|
||||||
int64(constants.DefaultExpirationTime), constants.NNSTtlDefVal)
|
|
||||||
emit.Opcodes(bw.BinWriter, opcode.ASSERT)
|
|
||||||
if err := c.SendCommitteeTx(bw.Bytes(), true); err != nil {
|
|
||||||
return fmt.Errorf("can't add domain root to NNS: %w", err)
|
|
||||||
}
|
|
||||||
if err := c.AwaitTx(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
alphaCs := c.GetContract(constants.AlphabetContract)
|
|
||||||
for i, acc := range c.Accounts {
|
|
||||||
alphaCs.Hash = state.CreateContractHash(acc.Contract.ScriptHash(), alphaCs.NEF.Checksum, alphaCs.Manifest.Name)
|
|
||||||
|
|
||||||
domain := helper.GetAlphabetNNSDomain(i)
|
|
||||||
if err := nnsRegisterDomain(c, nnsCs.Hash, alphaCs.Hash, domain); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.Command.Printf("NNS: Set %s -> %s\n", domain, alphaCs.Hash.StringLE())
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ctrName := range constants.ContractList {
|
|
||||||
cs := c.GetContract(ctrName)
|
|
||||||
|
|
||||||
domain := ctrName + ".frostfs"
|
|
||||||
if err := nnsRegisterDomain(c, nnsCs.Hash, cs.Hash, domain); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.Command.Printf("NNS: Set %s -> %s\n", domain, cs.Hash.StringLE())
|
|
||||||
}
|
|
||||||
|
|
||||||
groupKey := c.ContractWallet.Accounts[0].PrivateKey().PublicKey()
|
|
||||||
err = updateNNSGroup(c, nnsCs.Hash, groupKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.Command.Printf("NNS: Set %s -> %s\n", morphClient.NNSGroupKeyName, hex.EncodeToString(groupKey.Bytes()))
|
|
||||||
|
|
||||||
return c.AwaitTx()
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateNNSGroup(c *helper.InitializeContext, nnsHash util.Uint160, pub *keys.PublicKey) error {
|
|
||||||
bw := io.NewBufBinWriter()
|
|
||||||
keyAlreadyAdded, domainRegCodeEmitted, err := c.EmitUpdateNNSGroupScript(bw, nnsHash, pub)
|
|
||||||
if keyAlreadyAdded || err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
script := bw.Bytes()
|
|
||||||
if domainRegCodeEmitted {
|
|
||||||
w := io.NewBufBinWriter()
|
|
||||||
emit.Instruction(w.BinWriter, opcode.INITSSLOT, []byte{1})
|
|
||||||
wrapRegisterScriptWithPrice(w, nnsHash, script)
|
|
||||||
script = w.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.SendCommitteeTx(script, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// wrapRegisterScriptWithPrice wraps a given script with `getPrice`/`setPrice` calls for NNS.
|
|
||||||
// It is intended to be used for a single transaction, and not as a part of other scripts.
|
|
||||||
// It is assumed that script already contains static slot initialization code, the first one
|
|
||||||
// (with index 0) is used to store the price.
|
|
||||||
func wrapRegisterScriptWithPrice(w *io.BufBinWriter, nnsHash util.Uint160, s []byte) {
|
|
||||||
if len(s) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "getPrice", callflag.All)
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.STSFLD0)
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "setPrice", callflag.All, 1)
|
|
||||||
|
|
||||||
w.WriteBytes(s)
|
|
||||||
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.LDSFLD0, opcode.PUSH1, opcode.PACK)
|
|
||||||
emit.AppCallNoArgs(w.BinWriter, nnsHash, "setPrice", callflag.All)
|
|
||||||
|
|
||||||
if w.Err != nil {
|
|
||||||
panic(fmt.Errorf("BUG: can't wrap register script: %w", w.Err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func nnsRegisterDomain(c *helper.InitializeContext, nnsHash, expectedHash util.Uint160, domain string) error {
|
|
||||||
script, ok, err := c.NNSRegisterDomainScript(nnsHash, expectedHash, domain)
|
|
||||||
if ok || err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
w := io.NewBufBinWriter()
|
|
||||||
emit.Instruction(w.BinWriter, opcode.INITSSLOT, []byte{1})
|
|
||||||
wrapRegisterScriptWithPrice(w, nnsHash, script)
|
|
||||||
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "deleteRecords", callflag.All, domain, int64(nns.TXT))
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "addRecord", callflag.All,
|
|
||||||
domain, int64(nns.TXT), expectedHash.StringLE())
|
|
||||||
emit.AppCall(w.BinWriter, nnsHash, "addRecord", callflag.All,
|
|
||||||
domain, int64(nns.TXT), address.Uint160ToString(expectedHash))
|
|
||||||
return c.SendCommitteeTx(w.Bytes(), true)
|
|
||||||
}
|
|
|
@ -1,171 +0,0 @@
|
||||||
package initialize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/neo"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
|
||||||
)
|
|
||||||
|
|
||||||
// initialAlphabetNEOAmount represents the total amount of GAS distributed between alphabet nodes.
|
|
||||||
const (
|
|
||||||
initialAlphabetNEOAmount = native.NEOTotalSupply
|
|
||||||
registerBatchSize = transaction.MaxAttributes - 1
|
|
||||||
)
|
|
||||||
|
|
||||||
func registerCandidateRange(c *helper.InitializeContext, start, end int) error {
|
|
||||||
regPrice, err := getCandidateRegisterPrice(c)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't fetch registration price: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
w := io.NewBufBinWriter()
|
|
||||||
emit.AppCall(w.BinWriter, neo.Hash, "setRegisterPrice", callflag.States, 1)
|
|
||||||
for _, acc := range c.Accounts[start:end] {
|
|
||||||
emit.AppCall(w.BinWriter, neo.Hash, "registerCandidate", callflag.States, acc.PrivateKey().PublicKey().Bytes())
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.ASSERT)
|
|
||||||
}
|
|
||||||
emit.AppCall(w.BinWriter, neo.Hash, "setRegisterPrice", callflag.States, regPrice)
|
|
||||||
if w.Err != nil {
|
|
||||||
panic(fmt.Sprintf("BUG: %v", w.Err))
|
|
||||||
}
|
|
||||||
|
|
||||||
signers := []actor.SignerAccount{{
|
|
||||||
Signer: c.GetSigner(false, c.CommitteeAcc),
|
|
||||||
Account: c.CommitteeAcc,
|
|
||||||
}}
|
|
||||||
for _, acc := range c.Accounts[start:end] {
|
|
||||||
signers = append(signers, actor.SignerAccount{
|
|
||||||
Signer: transaction.Signer{
|
|
||||||
Account: acc.Contract.ScriptHash(),
|
|
||||||
Scopes: transaction.CustomContracts,
|
|
||||||
AllowedContracts: []util.Uint160{neo.Hash},
|
|
||||||
},
|
|
||||||
Account: acc,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
act, err := actor.New(c.Client, signers)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't create actor: %w", err)
|
|
||||||
}
|
|
||||||
tx, err := act.MakeRun(w.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't create tx: %w", err)
|
|
||||||
}
|
|
||||||
if err := c.MultiSign(tx, constants.CommitteeAccountName); err != nil {
|
|
||||||
return fmt.Errorf("can't sign a transaction: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
network := c.CommitteeAct.GetNetwork()
|
|
||||||
for _, acc := range c.Accounts[start:end] {
|
|
||||||
if err := acc.SignTx(network, tx); err != nil {
|
|
||||||
return fmt.Errorf("can't sign a transaction: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.SendTx(tx, c.Command, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerCandidates(c *helper.InitializeContext) error {
|
|
||||||
cc, err := unwrap.Array(c.ReadOnlyInvoker.Call(neo.Hash, "getCandidates"))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("`getCandidates`: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
need := len(c.Accounts)
|
|
||||||
have := len(cc)
|
|
||||||
|
|
||||||
if need == have {
|
|
||||||
c.Command.Println("Candidates are already registered.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register candidates in batches in order to overcome the signers amount limit.
|
|
||||||
// See: https://github.com/nspcc-dev/neo-go/blob/master/pkg/core/transaction/transaction.go#L27
|
|
||||||
for i := 0; i < need; i += registerBatchSize {
|
|
||||||
start, end := i, min(i+registerBatchSize, need)
|
|
||||||
// This check is sound because transactions are accepted/rejected atomically.
|
|
||||||
if have >= end {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := registerCandidateRange(c, start, end); err != nil {
|
|
||||||
return fmt.Errorf("registering candidates %d..%d: %q", start, end-1, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func transferNEOToAlphabetContracts(c *helper.InitializeContext) error {
|
|
||||||
neoHash := neo.Hash
|
|
||||||
|
|
||||||
ok, err := transferNEOFinished(c, neoHash)
|
|
||||||
if ok || err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cs := c.GetContract(constants.AlphabetContract)
|
|
||||||
amount := initialAlphabetNEOAmount / len(c.Wallets)
|
|
||||||
|
|
||||||
bw := io.NewBufBinWriter()
|
|
||||||
for _, acc := range c.Accounts {
|
|
||||||
h := state.CreateContractHash(acc.Contract.ScriptHash(), cs.NEF.Checksum, cs.Manifest.Name)
|
|
||||||
emit.AppCall(bw.BinWriter, neoHash, "transfer", callflag.All,
|
|
||||||
c.CommitteeAcc.Contract.ScriptHash(), h, int64(amount), nil)
|
|
||||||
emit.Opcodes(bw.BinWriter, opcode.ASSERT)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.SendCommitteeTx(bw.Bytes(), false); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.AwaitTx()
|
|
||||||
}
|
|
||||||
|
|
||||||
func transferNEOFinished(c *helper.InitializeContext, neoHash util.Uint160) (bool, error) {
|
|
||||||
r := nep17.NewReader(c.ReadOnlyInvoker, neoHash)
|
|
||||||
bal, err := r.BalanceOf(c.CommitteeAcc.Contract.ScriptHash())
|
|
||||||
return bal.Cmp(big.NewInt(native.NEOTotalSupply)) == -1, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var errGetPriceInvalid = errors.New("`getRegisterPrice`: invalid response")
|
|
||||||
|
|
||||||
func getCandidateRegisterPrice(c *helper.InitializeContext) (int64, error) {
|
|
||||||
switch c.Client.(type) {
|
|
||||||
case *rpcclient.Client:
|
|
||||||
inv := invoker.New(c.Client, nil)
|
|
||||||
reader := neo.NewReader(inv)
|
|
||||||
return reader.GetRegisterPrice()
|
|
||||||
default:
|
|
||||||
neoHash := neo.Hash
|
|
||||||
res, err := helper.InvokeFunction(c.Client, neoHash, "getRegisterPrice", nil, nil)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if len(res.Stack) == 0 {
|
|
||||||
return 0, errGetPriceInvalid
|
|
||||||
}
|
|
||||||
bi, err := res.Stack[0].TryInteger()
|
|
||||||
if err != nil || !bi.IsInt64() {
|
|
||||||
return 0, errGetPriceInvalid
|
|
||||||
}
|
|
||||||
return bi.Int64(), nil
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,155 +0,0 @@
|
||||||
package initialize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
|
||||||
cmdConfig "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/config"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/generate"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/netmap"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/node"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/policy"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/innerring"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/config"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
contractsPath = "../../../../../../contract/frostfs-contract-v0.18.0.tar.gz"
|
|
||||||
protoFileName = "proto.yml"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestInitialize(t *testing.T) {
|
|
||||||
// This test needs frostfs-contract tarball, so it is skipped by default.
|
|
||||||
// It is here for performing local testing after the changes.
|
|
||||||
t.Skip()
|
|
||||||
|
|
||||||
t.Run("1 nodes", func(t *testing.T) {
|
|
||||||
testInitialize(t, 1)
|
|
||||||
})
|
|
||||||
t.Run("4 nodes", func(t *testing.T) {
|
|
||||||
testInitialize(t, 4)
|
|
||||||
})
|
|
||||||
t.Run("7 nodes", func(t *testing.T) {
|
|
||||||
testInitialize(t, 7)
|
|
||||||
})
|
|
||||||
t.Run("16 nodes", func(t *testing.T) {
|
|
||||||
testInitialize(t, 16)
|
|
||||||
})
|
|
||||||
t.Run("max nodes", func(t *testing.T) {
|
|
||||||
testInitialize(t, constants.MaxAlphabetNodes)
|
|
||||||
})
|
|
||||||
t.Run("too many nodes", func(t *testing.T) {
|
|
||||||
require.ErrorIs(t, generateTestData(t.TempDir(), constants.MaxAlphabetNodes+1), helper.ErrTooManyAlphabetNodes)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func testInitialize(t *testing.T, committeeSize int) {
|
|
||||||
testdataDir := t.TempDir()
|
|
||||||
v := viper.GetViper()
|
|
||||||
|
|
||||||
require.NoError(t, generateTestData(testdataDir, committeeSize))
|
|
||||||
v.Set(constants.ProtoConfigPath, filepath.Join(testdataDir, protoFileName))
|
|
||||||
|
|
||||||
// Set to the path or remove the next statement to download from the network.
|
|
||||||
require.NoError(t, Cmd.Flags().Set(commonflags.ContractsInitFlag, contractsPath))
|
|
||||||
|
|
||||||
dumpPath := filepath.Join(testdataDir, "out")
|
|
||||||
require.NoError(t, Cmd.Flags().Set(commonflags.LocalDumpFlag, dumpPath))
|
|
||||||
v.Set(commonflags.AlphabetWalletsFlag, testdataDir)
|
|
||||||
v.Set(commonflags.EpochDurationInitFlag, 1)
|
|
||||||
v.Set(commonflags.MaxObjectSizeInitFlag, 1024)
|
|
||||||
|
|
||||||
setTestCredentials(v, committeeSize)
|
|
||||||
require.NoError(t, initializeSideChainCmd(Cmd, nil))
|
|
||||||
|
|
||||||
t.Run("force-new-epoch", func(t *testing.T) {
|
|
||||||
require.NoError(t, netmap.ForceNewEpoch.Flags().Set(commonflags.LocalDumpFlag, dumpPath))
|
|
||||||
require.NoError(t, netmap.ForceNewEpochCmd(netmap.ForceNewEpoch, nil))
|
|
||||||
})
|
|
||||||
t.Run("set-config", func(t *testing.T) {
|
|
||||||
require.NoError(t, cmdConfig.SetCmd.Flags().Set(commonflags.LocalDumpFlag, dumpPath))
|
|
||||||
require.NoError(t, cmdConfig.SetConfigCmd(cmdConfig.SetCmd, []string{"MaintenanceModeAllowed=true"}))
|
|
||||||
})
|
|
||||||
t.Run("set-policy", func(t *testing.T) {
|
|
||||||
require.NoError(t, policy.Set.Flags().Set(commonflags.LocalDumpFlag, dumpPath))
|
|
||||||
require.NoError(t, policy.SetPolicyCmd(policy.Set, []string{"ExecFeeFactor=1"}))
|
|
||||||
})
|
|
||||||
t.Run("remove-node", func(t *testing.T) {
|
|
||||||
pk, err := keys.NewPrivateKey()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
pub := hex.EncodeToString(pk.PublicKey().Bytes())
|
|
||||||
require.NoError(t, node.RemoveCmd.Flags().Set(commonflags.LocalDumpFlag, dumpPath))
|
|
||||||
require.NoError(t, node.RemoveNodesCmd(node.RemoveCmd, []string{pub}))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateTestData(dir string, size int) error {
|
|
||||||
v := viper.GetViper()
|
|
||||||
v.Set(commonflags.AlphabetWalletsFlag, dir)
|
|
||||||
|
|
||||||
sizeStr := strconv.FormatUint(uint64(size), 10)
|
|
||||||
if err := generate.GenerateAlphabetCmd.Flags().Set(commonflags.AlphabetSizeFlag, sizeStr); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
setTestCredentials(v, size)
|
|
||||||
if err := generate.AlphabetCreds(generate.GenerateAlphabetCmd, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var pubs []string
|
|
||||||
for i := range size {
|
|
||||||
p := filepath.Join(dir, innerring.GlagoliticLetter(i).String()+".json")
|
|
||||||
w, err := wallet.NewWalletFromFile(p)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("wallet doesn't exist: %w", err)
|
|
||||||
}
|
|
||||||
for _, acc := range w.Accounts {
|
|
||||||
if acc.Label == constants.SingleAccountName {
|
|
||||||
pub, ok := vm.ParseSignatureContract(acc.Contract.Script)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("could not parse signature script for %s", acc.Address)
|
|
||||||
}
|
|
||||||
pubs = append(pubs, hex.EncodeToString(pub))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := config.Config{}
|
|
||||||
cfg.ProtocolConfiguration.Magic = 12345
|
|
||||||
cfg.ProtocolConfiguration.ValidatorsCount = uint32(size)
|
|
||||||
cfg.ProtocolConfiguration.TimePerBlock = time.Second
|
|
||||||
cfg.ProtocolConfiguration.StandbyCommittee = pubs // sorted by glagolic letters
|
|
||||||
cfg.ProtocolConfiguration.P2PSigExtensions = true
|
|
||||||
cfg.ProtocolConfiguration.VerifyTransactions = true
|
|
||||||
data, err := yaml.Marshal(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
protoPath := filepath.Join(dir, protoFileName)
|
|
||||||
return os.WriteFile(protoPath, data, os.ModePerm)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setTestCredentials(v *viper.Viper, size int) {
|
|
||||||
for i := range size {
|
|
||||||
v.Set("credentials."+innerring.GlagoliticLetter(i).String(), strconv.FormatUint(uint64(i), 10))
|
|
||||||
}
|
|
||||||
v.Set("credentials.contract", constants.TestContractPassword)
|
|
||||||
}
|
|
|
@ -1,168 +0,0 @@
|
||||||
package initialize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/gas"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/neo"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
gasInitialTotalSupply = 30000000 * native.GASFactor
|
|
||||||
// initialAlphabetGASAmount represents the amount of GAS given to each alphabet node.
|
|
||||||
initialAlphabetGASAmount = 10_000 * native.GASFactor
|
|
||||||
// initialProxyGASAmount represents the amount of GAS given to a proxy contract.
|
|
||||||
initialProxyGASAmount = 50_000 * native.GASFactor
|
|
||||||
)
|
|
||||||
|
|
||||||
func initialCommitteeGASAmount(c *helper.InitializeContext) int64 {
|
|
||||||
return (gasInitialTotalSupply - initialAlphabetGASAmount*int64(len(c.Wallets))) / 2
|
|
||||||
}
|
|
||||||
|
|
||||||
func transferFunds(c *helper.InitializeContext) error {
|
|
||||||
ok, err := transferFundsFinished(c)
|
|
||||||
if ok || err != nil {
|
|
||||||
if err == nil {
|
|
||||||
c.Command.Println("Stage 1: already performed.")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var transfers []transferTarget
|
|
||||||
for _, acc := range c.Accounts {
|
|
||||||
to := acc.Contract.ScriptHash()
|
|
||||||
transfers = append(transfers,
|
|
||||||
transferTarget{
|
|
||||||
Token: gas.Hash,
|
|
||||||
Address: to,
|
|
||||||
Amount: initialAlphabetGASAmount,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// It is convenient to have all funds at the committee account.
|
|
||||||
transfers = append(transfers,
|
|
||||||
transferTarget{
|
|
||||||
Token: gas.Hash,
|
|
||||||
Address: c.CommitteeAcc.Contract.ScriptHash(),
|
|
||||||
Amount: initialCommitteeGASAmount(c),
|
|
||||||
},
|
|
||||||
transferTarget{
|
|
||||||
Token: neo.Hash,
|
|
||||||
Address: c.CommitteeAcc.Contract.ScriptHash(),
|
|
||||||
Amount: native.NEOTotalSupply,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
tx, err := createNEP17MultiTransferTx(c.Client, c.ConsensusAcc, transfers)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't create transfer transaction: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.MultiSignAndSend(tx, constants.ConsensusAccountName); err != nil {
|
|
||||||
return fmt.Errorf("can't send transfer transaction: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.AwaitTx()
|
|
||||||
}
|
|
||||||
|
|
||||||
// transferFundsFinished checks balances of accounts we transfer GAS to.
|
|
||||||
// The stage is considered finished if the balance is greater than the half of what we need to transfer.
|
|
||||||
func transferFundsFinished(c *helper.InitializeContext) (bool, error) {
|
|
||||||
acc := c.Accounts[0]
|
|
||||||
|
|
||||||
r := nep17.NewReader(c.ReadOnlyInvoker, gas.Hash)
|
|
||||||
res, err := r.BalanceOf(acc.Contract.ScriptHash())
|
|
||||||
if err != nil || res.Cmp(big.NewInt(initialAlphabetGASAmount/2)) != 1 {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err = r.BalanceOf(c.CommitteeAcc.ScriptHash())
|
|
||||||
return res != nil && res.Cmp(big.NewInt(initialCommitteeGASAmount(c)/2)) == 1, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func transferGASToProxy(c *helper.InitializeContext) error {
|
|
||||||
proxyCs := c.GetContract(constants.ProxyContract)
|
|
||||||
|
|
||||||
r := nep17.NewReader(c.ReadOnlyInvoker, gas.Hash)
|
|
||||||
bal, err := r.BalanceOf(proxyCs.Hash)
|
|
||||||
if err != nil || bal.Sign() > 0 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tx, err := createNEP17MultiTransferTx(c.Client, c.CommitteeAcc, []transferTarget{{
|
|
||||||
Token: gas.Hash,
|
|
||||||
Address: proxyCs.Hash,
|
|
||||||
Amount: initialProxyGASAmount,
|
|
||||||
}})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.MultiSignAndSend(tx, constants.CommitteeAccountName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.AwaitTx()
|
|
||||||
}
|
|
||||||
|
|
||||||
type transferTarget struct {
|
|
||||||
Token util.Uint160
|
|
||||||
Address util.Uint160
|
|
||||||
Amount int64
|
|
||||||
Data any
|
|
||||||
}
|
|
||||||
|
|
||||||
func createNEP17MultiTransferTx(c helper.Client, acc *wallet.Account, recipients []transferTarget) (*transaction.Transaction, error) {
|
|
||||||
from := acc.Contract.ScriptHash()
|
|
||||||
|
|
||||||
w := io.NewBufBinWriter()
|
|
||||||
for i := range recipients {
|
|
||||||
emit.AppCall(w.BinWriter, recipients[i].Token, "transfer", callflag.All,
|
|
||||||
from, recipients[i].Address, recipients[i].Amount, recipients[i].Data)
|
|
||||||
emit.Opcodes(w.BinWriter, opcode.ASSERT)
|
|
||||||
}
|
|
||||||
if w.Err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create transfer script: %w", w.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
signers := []actor.SignerAccount{{
|
|
||||||
Signer: transaction.Signer{
|
|
||||||
Account: acc.Contract.ScriptHash(),
|
|
||||||
Scopes: transaction.CalledByEntry,
|
|
||||||
},
|
|
||||||
Account: acc,
|
|
||||||
}}
|
|
||||||
|
|
||||||
act, err := actor.New(c, signers)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't create actor: %w", err)
|
|
||||||
}
|
|
||||||
tx, err := act.MakeRun(w.Bytes())
|
|
||||||
if err != nil {
|
|
||||||
sum := make(map[util.Uint160]int64)
|
|
||||||
for _, recipient := range recipients {
|
|
||||||
sum[recipient.Token] += recipient.Amount
|
|
||||||
}
|
|
||||||
detail := make([]string, 0, len(sum))
|
|
||||||
for _, value := range sum {
|
|
||||||
detail = append(detail, fmt.Sprintf("amount=%v", value))
|
|
||||||
}
|
|
||||||
err = fmt.Errorf("transfer failed: from=%s(%s) %s: %w", acc.Label, acc.Address, strings.Join(detail, " "), err)
|
|
||||||
}
|
|
||||||
return tx, err
|
|
||||||
}
|
|
|
@ -1,58 +0,0 @@
|
||||||
package initialize
|
|
||||||
|
|
||||||
import (
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
maxObjectSizeCLIFlag = "max-object-size"
|
|
||||||
epochDurationCLIFlag = "epoch-duration"
|
|
||||||
containerFeeCLIFlag = "container-fee"
|
|
||||||
containerAliasFeeCLIFlag = "container-alias-fee"
|
|
||||||
candidateFeeCLIFlag = "candidate-fee"
|
|
||||||
homomorphicHashDisabledCLIFlag = "homomorphic-disabled"
|
|
||||||
withdrawFeeCLIFlag = "withdraw-fee"
|
|
||||||
)
|
|
||||||
|
|
||||||
var Cmd = &cobra.Command{
|
|
||||||
Use: "init",
|
|
||||||
Short: "Initialize side chain network with smart-contracts and network settings",
|
|
||||||
PreRun: func(cmd *cobra.Command, _ []string) {
|
|
||||||
_ = viper.BindPFlag(commonflags.AlphabetWalletsFlag, cmd.Flags().Lookup(commonflags.AlphabetWalletsFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.EndpointFlag, cmd.Flags().Lookup(commonflags.EndpointFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.EpochDurationInitFlag, cmd.Flags().Lookup(epochDurationCLIFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.MaxObjectSizeInitFlag, cmd.Flags().Lookup(maxObjectSizeCLIFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.MaxECDataCountFlag, cmd.Flags().Lookup(commonflags.MaxECDataCountFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.MaxECParityCounFlag, cmd.Flags().Lookup(commonflags.MaxECParityCounFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.HomomorphicHashDisabledInitFlag, cmd.Flags().Lookup(homomorphicHashDisabledCLIFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.CandidateFeeInitFlag, cmd.Flags().Lookup(candidateFeeCLIFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.ContainerFeeInitFlag, cmd.Flags().Lookup(containerFeeCLIFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.ContainerAliasFeeInitFlag, cmd.Flags().Lookup(containerAliasFeeCLIFlag))
|
|
||||||
_ = viper.BindPFlag(commonflags.WithdrawFeeInitFlag, cmd.Flags().Lookup(withdrawFeeCLIFlag))
|
|
||||||
_ = viper.BindPFlag(constants.ProtoConfigPath, cmd.Flags().Lookup(constants.ProtoConfigPath))
|
|
||||||
},
|
|
||||||
RunE: initializeSideChainCmd,
|
|
||||||
}
|
|
||||||
|
|
||||||
func initInitCmd() {
|
|
||||||
Cmd.Flags().String(commonflags.AlphabetWalletsFlag, "", commonflags.AlphabetWalletsFlagDesc)
|
|
||||||
Cmd.Flags().StringP(commonflags.EndpointFlag, commonflags.EndpointFlagShort, "", commonflags.EndpointFlagDesc)
|
|
||||||
Cmd.Flags().String(commonflags.ContractsInitFlag, "", commonflags.ContractsInitFlagDesc)
|
|
||||||
Cmd.Flags().String(commonflags.ContractsURLFlag, "", commonflags.ContractsURLFlagDesc)
|
|
||||||
Cmd.Flags().Uint(epochDurationCLIFlag, 240, "Amount of side chain blocks in one FrostFS epoch")
|
|
||||||
Cmd.Flags().Uint(maxObjectSizeCLIFlag, 67108864, "Max single object size in bytes")
|
|
||||||
Cmd.Flags().Bool(homomorphicHashDisabledCLIFlag, false, "Disable object homomorphic hashing")
|
|
||||||
// Defaults are taken from neo-preodolenie.
|
|
||||||
Cmd.Flags().Uint64(containerFeeCLIFlag, 1000, "Container registration fee")
|
|
||||||
Cmd.Flags().Uint64(containerAliasFeeCLIFlag, 500, "Container alias fee")
|
|
||||||
Cmd.Flags().String(constants.ProtoConfigPath, "", "Path to the consensus node configuration")
|
|
||||||
Cmd.Flags().String(commonflags.LocalDumpFlag, "", "Path to the blocks dump file")
|
|
||||||
Cmd.MarkFlagsMutuallyExclusive(commonflags.ContractsInitFlag, commonflags.ContractsURLFlag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
initInitCmd()
|
|
||||||
}
|
|
592
cmd/frostfs-adm/internal/modules/morph/initialize_deploy.go
Normal file
592
cmd/frostfs-adm/internal/modules/morph/initialize_deploy.go
Normal file
|
@ -0,0 +1,592 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-contract/common"
|
||||||
|
"github.com/TrueCloudLab/frostfs-contract/nns"
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/pkg/innerring"
|
||||||
|
morphClient "github.com/TrueCloudLab/frostfs-node/pkg/morph/client"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
||||||
|
io2 "github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/nef"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
nnsContract = "nns"
|
||||||
|
frostfsContract = "frostfs" // not deployed in side-chain.
|
||||||
|
processingContract = "processing" // not deployed in side-chain.
|
||||||
|
alphabetContract = "alphabet"
|
||||||
|
auditContract = "audit"
|
||||||
|
balanceContract = "balance"
|
||||||
|
containerContract = "container"
|
||||||
|
frostfsIDContract = "frostfsid"
|
||||||
|
netmapContract = "netmap"
|
||||||
|
proxyContract = "proxy"
|
||||||
|
reputationContract = "reputation"
|
||||||
|
subnetContract = "subnet"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
netmapEpochKey = "EpochDuration"
|
||||||
|
netmapMaxObjectSizeKey = "MaxObjectSize"
|
||||||
|
netmapAuditFeeKey = "AuditFee"
|
||||||
|
netmapContainerFeeKey = "ContainerFee"
|
||||||
|
netmapContainerAliasFeeKey = "ContainerAliasFee"
|
||||||
|
netmapEigenTrustIterationsKey = "EigenTrustIterations"
|
||||||
|
netmapEigenTrustAlphaKey = "EigenTrustAlpha"
|
||||||
|
netmapBasicIncomeRateKey = "BasicIncomeRate"
|
||||||
|
netmapInnerRingCandidateFeeKey = "InnerRingCandidateFee"
|
||||||
|
netmapWithdrawFeeKey = "WithdrawFee"
|
||||||
|
netmapHomomorphicHashDisabledKey = "HomomorphicHashingDisabled"
|
||||||
|
netmapMaintenanceAllowedKey = "MaintenanceModeAllowed"
|
||||||
|
|
||||||
|
defaultEigenTrustIterations = 4
|
||||||
|
defaultEigenTrustAlpha = "0.1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
contractList = []string{
|
||||||
|
auditContract,
|
||||||
|
balanceContract,
|
||||||
|
containerContract,
|
||||||
|
frostfsIDContract,
|
||||||
|
netmapContract,
|
||||||
|
proxyContract,
|
||||||
|
reputationContract,
|
||||||
|
subnetContract,
|
||||||
|
}
|
||||||
|
|
||||||
|
fullContractList = append([]string{
|
||||||
|
frostfsContract,
|
||||||
|
processingContract,
|
||||||
|
nnsContract,
|
||||||
|
alphabetContract,
|
||||||
|
}, contractList...)
|
||||||
|
)
|
||||||
|
|
||||||
|
type contractState struct {
|
||||||
|
NEF *nef.File
|
||||||
|
RawNEF []byte
|
||||||
|
Manifest *manifest.Manifest
|
||||||
|
RawManifest []byte
|
||||||
|
Hash util.Uint160
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
updateMethodName = "update"
|
||||||
|
deployMethodName = "deploy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *initializeContext) deployNNS(method string) error {
|
||||||
|
cs := c.getContract(nnsContract)
|
||||||
|
h := cs.Hash
|
||||||
|
|
||||||
|
nnsCs, err := c.nnsContractState()
|
||||||
|
if err == nil {
|
||||||
|
if nnsCs.NEF.Checksum == cs.NEF.Checksum {
|
||||||
|
if method == deployMethodName {
|
||||||
|
c.Command.Println("NNS contract is already deployed.")
|
||||||
|
} else {
|
||||||
|
c.Command.Println("NNS contract is already updated.")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
h = nnsCs.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.addManifestGroup(h, cs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't sign manifest group: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
params := getContractDeployParameters(cs, nil)
|
||||||
|
signer := transaction.Signer{
|
||||||
|
Account: c.CommitteeAcc.Contract.ScriptHash(),
|
||||||
|
Scopes: transaction.CalledByEntry,
|
||||||
|
}
|
||||||
|
|
||||||
|
invokeHash := management.Hash
|
||||||
|
if method == updateMethodName {
|
||||||
|
invokeHash = nnsCs.Hash
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := invokeFunction(c.Client, invokeHash, method, params, []transaction.Signer{signer})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't deploy NNS contract: %w", err)
|
||||||
|
}
|
||||||
|
if res.State != vmstate.Halt.String() {
|
||||||
|
return fmt.Errorf("can't deploy NNS contract: %s", res.FaultException)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := c.Client.CreateTxFromScript(res.Script, c.CommitteeAcc, res.GasConsumed, 0, []rpcclient.SignerAccount{{
|
||||||
|
Signer: signer,
|
||||||
|
Account: c.CommitteeAcc,
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create deploy tx for %s: %w", nnsContract, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.multiSignAndSend(tx, committeeAccountName); err != nil {
|
||||||
|
return fmt.Errorf("can't send deploy transaction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.awaitTx()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) updateContracts() error {
|
||||||
|
alphaCs := c.getContract(alphabetContract)
|
||||||
|
|
||||||
|
nnsCs, err := c.nnsContractState()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
nnsHash := nnsCs.Hash
|
||||||
|
|
||||||
|
w := io2.NewBufBinWriter()
|
||||||
|
|
||||||
|
var keysParam []interface{}
|
||||||
|
|
||||||
|
// Update script size for a single-node committee is close to the maximum allowed size of 65535.
|
||||||
|
// Because of this we want to reuse alphabet contract NEF and manifest for different updates.
|
||||||
|
// The generated script is as following.
|
||||||
|
// 1. Initialize static slot for alphabet NEF.
|
||||||
|
// 2. Store NEF into the static slot.
|
||||||
|
// 3. Push parameters for each alphabet contract on stack.
|
||||||
|
// 4. Add contract group to the manifest.
|
||||||
|
// 5. For each alphabet contract, invoke `update` using parameters on stack and
|
||||||
|
// NEF from step 2 and manifest from step 4.
|
||||||
|
emit.Instruction(w.BinWriter, opcode.INITSSLOT, []byte{1})
|
||||||
|
emit.Bytes(w.BinWriter, alphaCs.RawNEF)
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.STSFLD0)
|
||||||
|
|
||||||
|
baseGroups := alphaCs.Manifest.Groups
|
||||||
|
|
||||||
|
// alphabet contracts should be deployed by individual nodes to get different hashes.
|
||||||
|
for i, acc := range c.Accounts {
|
||||||
|
ctrHash, err := nnsResolveHash(c.ReadOnlyInvoker, nnsHash, getAlphabetNNSDomain(i))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't resolve hash for contract update: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keysParam = append(keysParam, acc.PrivateKey().PublicKey().Bytes())
|
||||||
|
|
||||||
|
params := c.getAlphabetDeployItems(i, len(c.Wallets))
|
||||||
|
emit.Array(w.BinWriter, params...)
|
||||||
|
|
||||||
|
alphaCs.Manifest.Groups = baseGroups
|
||||||
|
err = c.addManifestGroup(ctrHash, alphaCs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't sign manifest group: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
emit.Bytes(w.BinWriter, alphaCs.RawManifest)
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.LDSFLD0)
|
||||||
|
emit.Int(w.BinWriter, 3)
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.PACK)
|
||||||
|
emit.AppCallNoArgs(w.BinWriter, ctrHash, updateMethodName, callflag.All)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.sendCommitteeTx(w.Bytes(), false); err != nil {
|
||||||
|
if !strings.Contains(err.Error(), common.ErrAlreadyUpdated) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Command.Println("Alphabet contracts are already updated.")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Reset()
|
||||||
|
|
||||||
|
emit.Instruction(w.BinWriter, opcode.INITSSLOT, []byte{1})
|
||||||
|
emit.AppCall(w.BinWriter, nnsHash, "getPrice", callflag.All)
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.STSFLD0)
|
||||||
|
emit.AppCall(w.BinWriter, nnsHash, "setPrice", callflag.All, 1)
|
||||||
|
|
||||||
|
for _, ctrName := range contractList {
|
||||||
|
cs := c.getContract(ctrName)
|
||||||
|
|
||||||
|
method := updateMethodName
|
||||||
|
ctrHash, err := nnsResolveHash(c.ReadOnlyInvoker, nnsHash, ctrName+".frostfs")
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errMissingNNSRecord) {
|
||||||
|
// if contract not found we deploy it instead of update
|
||||||
|
method = deployMethodName
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("can't resolve hash for contract update: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.addManifestGroup(ctrHash, cs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't sign manifest group: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
invokeHash := management.Hash
|
||||||
|
if method == updateMethodName {
|
||||||
|
invokeHash = ctrHash
|
||||||
|
}
|
||||||
|
|
||||||
|
params := getContractDeployParameters(cs, c.getContractDeployData(ctrName, keysParam))
|
||||||
|
res, err := c.CommitteeAct.MakeCall(invokeHash, method, params...)
|
||||||
|
if err != nil {
|
||||||
|
if method != updateMethodName || !strings.Contains(err.Error(), common.ErrAlreadyUpdated) {
|
||||||
|
return fmt.Errorf("deploy contract: %w", err)
|
||||||
|
}
|
||||||
|
c.Command.Printf("%s contract is already updated.\n", ctrName)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteBytes(res.Script)
|
||||||
|
|
||||||
|
if method == deployMethodName {
|
||||||
|
// same actions are done in initializeContext.setNNS, can be unified
|
||||||
|
domain := ctrName + ".frostfs"
|
||||||
|
script, ok, err := c.nnsRegisterDomainScript(nnsHash, cs.Hash, domain)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
w.WriteBytes(script)
|
||||||
|
emit.AppCall(w.BinWriter, nnsHash, "deleteRecords", callflag.All, domain, int64(nns.TXT))
|
||||||
|
emit.AppCall(w.BinWriter, nnsHash, "addRecord", callflag.All,
|
||||||
|
domain, int64(nns.TXT), cs.Hash.StringLE())
|
||||||
|
emit.AppCall(w.BinWriter, nnsHash, "addRecord", callflag.All,
|
||||||
|
domain, int64(nns.TXT), address.Uint160ToString(cs.Hash))
|
||||||
|
}
|
||||||
|
c.Command.Printf("NNS: Set %s -> %s\n", domain, cs.Hash.StringLE())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
groupKey := c.ContractWallet.Accounts[0].PrivateKey().PublicKey()
|
||||||
|
_, _, err = c.emitUpdateNNSGroupScript(w, nnsHash, groupKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Command.Printf("NNS: Set %s -> %s\n", morphClient.NNSGroupKeyName, hex.EncodeToString(groupKey.Bytes()))
|
||||||
|
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.LDSFLD0)
|
||||||
|
emit.Int(w.BinWriter, 1)
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.PACK)
|
||||||
|
emit.AppCallNoArgs(w.BinWriter, nnsHash, "setPrice", callflag.All)
|
||||||
|
|
||||||
|
if err := c.sendCommitteeTx(w.Bytes(), false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.awaitTx()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) deployContracts() error {
|
||||||
|
alphaCs := c.getContract(alphabetContract)
|
||||||
|
|
||||||
|
var keysParam []interface{}
|
||||||
|
|
||||||
|
baseGroups := alphaCs.Manifest.Groups
|
||||||
|
|
||||||
|
// alphabet contracts should be deployed by individual nodes to get different hashes.
|
||||||
|
for i, acc := range c.Accounts {
|
||||||
|
ctrHash := state.CreateContractHash(acc.Contract.ScriptHash(), alphaCs.NEF.Checksum, alphaCs.Manifest.Name)
|
||||||
|
if c.isUpdated(ctrHash, alphaCs) {
|
||||||
|
c.Command.Printf("Alphabet contract #%d is already deployed.\n", i)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
alphaCs.Manifest.Groups = baseGroups
|
||||||
|
err := c.addManifestGroup(ctrHash, alphaCs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't sign manifest group: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keysParam = append(keysParam, acc.PrivateKey().PublicKey().Bytes())
|
||||||
|
params := getContractDeployParameters(alphaCs, c.getAlphabetDeployItems(i, len(c.Wallets)))
|
||||||
|
|
||||||
|
act, err := actor.NewSimple(c.Client, acc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create actor: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
txHash, vub, err := act.SendCall(management.Hash, deployMethodName, params...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't deploy alphabet #%d contract: %w", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.SentTxs = append(c.SentTxs, hashVUBPair{hash: txHash, vub: vub})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ctrName := range contractList {
|
||||||
|
cs := c.getContract(ctrName)
|
||||||
|
|
||||||
|
ctrHash := cs.Hash
|
||||||
|
if c.isUpdated(ctrHash, cs) {
|
||||||
|
c.Command.Printf("%s contract is already deployed.\n", ctrName)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.addManifestGroup(ctrHash, cs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't sign manifest group: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
params := getContractDeployParameters(cs, c.getContractDeployData(ctrName, keysParam))
|
||||||
|
res, err := c.CommitteeAct.MakeCall(management.Hash, deployMethodName, params...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't deploy %s contract: %w", ctrName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.sendCommitteeTx(res.Script, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.awaitTx()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) isUpdated(ctrHash util.Uint160, cs *contractState) bool {
|
||||||
|
realCs, err := c.Client.GetContractStateByHash(ctrHash)
|
||||||
|
return err == nil && realCs.NEF.Checksum == cs.NEF.Checksum
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) getContract(ctrName string) *contractState {
|
||||||
|
return c.Contracts[ctrName]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) readContracts(names []string) error {
|
||||||
|
var (
|
||||||
|
fi os.FileInfo
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if c.ContractPath != "" {
|
||||||
|
fi, err = os.Stat(c.ContractPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid contracts path: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ContractPath != "" && fi.IsDir() {
|
||||||
|
for _, ctrName := range names {
|
||||||
|
cs, err := readContract(filepath.Join(c.ContractPath, ctrName), ctrName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Contracts[ctrName] = cs
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var r io.ReadCloser
|
||||||
|
if c.ContractPath == "" {
|
||||||
|
c.Command.Println("Contracts flag is missing, latest release will be fetched from Github.")
|
||||||
|
r, err = downloadContractsFromGithub(c.Command)
|
||||||
|
} else {
|
||||||
|
r, err = os.Open(c.ContractPath)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't open contracts archive: %w", err)
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
|
||||||
|
m, err := readContractsFromArchive(r, names)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, name := range names {
|
||||||
|
if err := m[name].parse(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Contracts[name] = m[name]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ctrName := range names {
|
||||||
|
if ctrName != alphabetContract {
|
||||||
|
cs := c.Contracts[ctrName]
|
||||||
|
cs.Hash = state.CreateContractHash(c.CommitteeAcc.Contract.ScriptHash(),
|
||||||
|
cs.NEF.Checksum, cs.Manifest.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readContract(ctrPath, ctrName string) (*contractState, error) {
|
||||||
|
rawNef, err := os.ReadFile(filepath.Join(ctrPath, ctrName+"_contract.nef"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't read NEF file for %s contract: %w", ctrName, err)
|
||||||
|
}
|
||||||
|
rawManif, err := os.ReadFile(filepath.Join(ctrPath, "config.json"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't read manifest file for %s contract: %w", ctrName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cs := &contractState{
|
||||||
|
RawNEF: rawNef,
|
||||||
|
RawManifest: rawManif,
|
||||||
|
}
|
||||||
|
|
||||||
|
return cs, cs.parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *contractState) parse() error {
|
||||||
|
nf, err := nef.FileFromBytes(cs.RawNEF)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't parse NEF file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := new(manifest.Manifest)
|
||||||
|
if err := json.Unmarshal(cs.RawManifest, m); err != nil {
|
||||||
|
return fmt.Errorf("can't parse manifest file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cs.NEF = &nf
|
||||||
|
cs.Manifest = m
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readContractsFromArchive(file io.Reader, names []string) (map[string]*contractState, error) {
|
||||||
|
m := make(map[string]*contractState, len(names))
|
||||||
|
for i := range names {
|
||||||
|
m[names[i]] = new(contractState)
|
||||||
|
}
|
||||||
|
|
||||||
|
gr, err := gzip.NewReader(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("contracts file must be tar.gz archive: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := tar.NewReader(gr)
|
||||||
|
for h, err := r.Next(); ; h, err = r.Next() {
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
dir, _ := filepath.Split(h.Name)
|
||||||
|
ctrName := filepath.Base(dir)
|
||||||
|
|
||||||
|
cs, ok := m[ctrName]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(h.Name, filepath.Join(ctrName, ctrName+"_contract.nef")):
|
||||||
|
cs.RawNEF, err = io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't read NEF file for %s contract: %w", ctrName, err)
|
||||||
|
}
|
||||||
|
case strings.HasSuffix(h.Name, "config.json"):
|
||||||
|
cs.RawManifest, err = io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't read manifest file for %s contract: %w", ctrName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m[ctrName] = cs
|
||||||
|
}
|
||||||
|
|
||||||
|
for ctrName, cs := range m {
|
||||||
|
if cs.RawNEF == nil {
|
||||||
|
return nil, fmt.Errorf("NEF for %s contract wasn't found", ctrName)
|
||||||
|
}
|
||||||
|
if cs.RawManifest == nil {
|
||||||
|
return nil, fmt.Errorf("manifest for %s contract wasn't found", ctrName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getContractDeployParameters(cs *contractState, deployData []interface{}) []interface{} {
|
||||||
|
return []interface{}{cs.RawNEF, cs.RawManifest, deployData}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) getContractDeployData(ctrName string, keysParam []interface{}) []interface{} {
|
||||||
|
items := make([]interface{}, 1, 6)
|
||||||
|
items[0] = false // notaryDisabled is false
|
||||||
|
|
||||||
|
switch ctrName {
|
||||||
|
case frostfsContract:
|
||||||
|
items = append(items,
|
||||||
|
c.Contracts[processingContract].Hash,
|
||||||
|
keysParam,
|
||||||
|
smartcontract.Parameter{})
|
||||||
|
case processingContract:
|
||||||
|
items = append(items, c.Contracts[frostfsContract].Hash)
|
||||||
|
return items[1:] // no notary info
|
||||||
|
case auditContract:
|
||||||
|
items = append(items, c.Contracts[netmapContract].Hash)
|
||||||
|
case balanceContract:
|
||||||
|
items = append(items,
|
||||||
|
c.Contracts[netmapContract].Hash,
|
||||||
|
c.Contracts[containerContract].Hash)
|
||||||
|
case containerContract:
|
||||||
|
// In case if NNS is updated multiple times, we can't calculate
|
||||||
|
// it's actual hash based on local data, thus query chain.
|
||||||
|
nnsCs, err := c.Client.GetContractStateByID(1)
|
||||||
|
if err != nil {
|
||||||
|
panic("NNS is not yet deployed")
|
||||||
|
}
|
||||||
|
items = append(items,
|
||||||
|
c.Contracts[netmapContract].Hash,
|
||||||
|
c.Contracts[balanceContract].Hash,
|
||||||
|
c.Contracts[frostfsIDContract].Hash,
|
||||||
|
nnsCs.Hash,
|
||||||
|
"container")
|
||||||
|
case frostfsIDContract:
|
||||||
|
items = append(items,
|
||||||
|
c.Contracts[netmapContract].Hash,
|
||||||
|
c.Contracts[containerContract].Hash)
|
||||||
|
case netmapContract:
|
||||||
|
configParam := []interface{}{
|
||||||
|
netmapEpochKey, viper.GetInt64(epochDurationInitFlag),
|
||||||
|
netmapMaxObjectSizeKey, viper.GetInt64(maxObjectSizeInitFlag),
|
||||||
|
netmapAuditFeeKey, viper.GetInt64(auditFeeInitFlag),
|
||||||
|
netmapContainerFeeKey, viper.GetInt64(containerFeeInitFlag),
|
||||||
|
netmapContainerAliasFeeKey, viper.GetInt64(containerAliasFeeInitFlag),
|
||||||
|
netmapEigenTrustIterationsKey, int64(defaultEigenTrustIterations),
|
||||||
|
netmapEigenTrustAlphaKey, defaultEigenTrustAlpha,
|
||||||
|
netmapBasicIncomeRateKey, viper.GetInt64(incomeRateInitFlag),
|
||||||
|
netmapInnerRingCandidateFeeKey, viper.GetInt64(candidateFeeInitFlag),
|
||||||
|
netmapWithdrawFeeKey, viper.GetInt64(withdrawFeeInitFlag),
|
||||||
|
netmapHomomorphicHashDisabledKey, viper.GetBool(homomorphicHashDisabledInitFlag),
|
||||||
|
netmapMaintenanceAllowedKey, viper.GetBool(maintenanceModeAllowedInitFlag),
|
||||||
|
}
|
||||||
|
items = append(items,
|
||||||
|
c.Contracts[balanceContract].Hash,
|
||||||
|
c.Contracts[containerContract].Hash,
|
||||||
|
keysParam,
|
||||||
|
configParam)
|
||||||
|
case proxyContract:
|
||||||
|
items = nil
|
||||||
|
case reputationContract:
|
||||||
|
case subnetContract:
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("invalid contract name: %s", ctrName))
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) getAlphabetDeployItems(i, n int) []interface{} {
|
||||||
|
items := make([]interface{}, 6)
|
||||||
|
items[0] = false
|
||||||
|
items[1] = c.Contracts[netmapContract].Hash
|
||||||
|
items[2] = c.Contracts[proxyContract].Hash
|
||||||
|
items[3] = innerring.GlagoliticLetter(i).String()
|
||||||
|
items[4] = int64(i)
|
||||||
|
items[5] = int64(n)
|
||||||
|
return items
|
||||||
|
}
|
291
cmd/frostfs-adm/internal/modules/morph/initialize_nns.go
Normal file
291
cmd/frostfs-adm/internal/modules/morph/initialize_nns.go
Normal file
|
@ -0,0 +1,291 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-contract/nns"
|
||||||
|
morphClient "github.com/TrueCloudLab/frostfs-node/pkg/morph/client"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultExpirationTime = 10 * 365 * 24 * time.Hour / time.Second
|
||||||
|
|
||||||
|
func (c *initializeContext) setNNS() error {
|
||||||
|
nnsCs, err := c.Client.GetContractStateByID(1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, err := c.nnsRootRegistered(nnsCs.Hash, "frostfs")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !ok {
|
||||||
|
bw := io.NewBufBinWriter()
|
||||||
|
emit.AppCall(bw.BinWriter, nnsCs.Hash, "register", callflag.All,
|
||||||
|
"frostfs", c.CommitteeAcc.Contract.ScriptHash(),
|
||||||
|
"ops@nspcc.ru", int64(3600), int64(600), int64(defaultExpirationTime), int64(3600))
|
||||||
|
emit.Opcodes(bw.BinWriter, opcode.ASSERT)
|
||||||
|
if err := c.sendCommitteeTx(bw.Bytes(), true); err != nil {
|
||||||
|
return fmt.Errorf("can't add domain root to NNS: %w", err)
|
||||||
|
}
|
||||||
|
if err := c.awaitTx(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alphaCs := c.getContract(alphabetContract)
|
||||||
|
for i, acc := range c.Accounts {
|
||||||
|
alphaCs.Hash = state.CreateContractHash(acc.Contract.ScriptHash(), alphaCs.NEF.Checksum, alphaCs.Manifest.Name)
|
||||||
|
|
||||||
|
domain := getAlphabetNNSDomain(i)
|
||||||
|
if err := c.nnsRegisterDomain(nnsCs.Hash, alphaCs.Hash, domain); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Command.Printf("NNS: Set %s -> %s\n", domain, alphaCs.Hash.StringLE())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ctrName := range contractList {
|
||||||
|
cs := c.getContract(ctrName)
|
||||||
|
|
||||||
|
domain := ctrName + ".frostfs"
|
||||||
|
if err := c.nnsRegisterDomain(nnsCs.Hash, cs.Hash, domain); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Command.Printf("NNS: Set %s -> %s\n", domain, cs.Hash.StringLE())
|
||||||
|
}
|
||||||
|
|
||||||
|
groupKey := c.ContractWallet.Accounts[0].PrivateKey().PublicKey()
|
||||||
|
err = c.updateNNSGroup(nnsCs.Hash, groupKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.Command.Printf("NNS: Set %s -> %s\n", morphClient.NNSGroupKeyName, hex.EncodeToString(groupKey.Bytes()))
|
||||||
|
|
||||||
|
return c.awaitTx()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) updateNNSGroup(nnsHash util.Uint160, pub *keys.PublicKey) error {
|
||||||
|
bw := io.NewBufBinWriter()
|
||||||
|
needUpdate, needRegister, err := c.emitUpdateNNSGroupScript(bw, nnsHash, pub)
|
||||||
|
if !needUpdate || err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
script := bw.Bytes()
|
||||||
|
if needRegister {
|
||||||
|
w := io.NewBufBinWriter()
|
||||||
|
emit.Instruction(w.BinWriter, opcode.INITSSLOT, []byte{1})
|
||||||
|
wrapRegisterScriptWithPrice(w, nnsHash, script)
|
||||||
|
script = w.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.sendCommitteeTx(script, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// emitUpdateNNSGroupScript emits script for updating group key stored in NNS.
|
||||||
|
// First return value is true iff the key is already there and nothing should be done.
|
||||||
|
// Second return value is true iff a domain registration code was emitted.
|
||||||
|
func (c *initializeContext) emitUpdateNNSGroupScript(bw *io.BufBinWriter, nnsHash util.Uint160, pub *keys.PublicKey) (bool, bool, error) {
|
||||||
|
isAvail, err := nnsIsAvailable(c.Client, nnsHash, morphClient.NNSGroupKeyName)
|
||||||
|
if err != nil {
|
||||||
|
return false, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isAvail {
|
||||||
|
currentPub, err := nnsResolveKey(c.ReadOnlyInvoker, nnsHash, morphClient.NNSGroupKeyName)
|
||||||
|
if err != nil {
|
||||||
|
return false, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if pub.Equal(currentPub) {
|
||||||
|
return true, false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isAvail {
|
||||||
|
emit.AppCall(bw.BinWriter, nnsHash, "register", callflag.All,
|
||||||
|
morphClient.NNSGroupKeyName, c.CommitteeAcc.Contract.ScriptHash(),
|
||||||
|
"ops@nspcc.ru", int64(3600), int64(600), int64(defaultExpirationTime), int64(3600))
|
||||||
|
emit.Opcodes(bw.BinWriter, opcode.ASSERT)
|
||||||
|
}
|
||||||
|
|
||||||
|
emit.AppCall(bw.BinWriter, nnsHash, "deleteRecords", callflag.All, "group.frostfs", int64(nns.TXT))
|
||||||
|
emit.AppCall(bw.BinWriter, nnsHash, "addRecord", callflag.All,
|
||||||
|
"group.frostfs", int64(nns.TXT), hex.EncodeToString(pub.Bytes()))
|
||||||
|
|
||||||
|
return false, isAvail, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAlphabetNNSDomain(i int) string {
|
||||||
|
return alphabetContract + strconv.FormatUint(uint64(i), 10) + ".frostfs"
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapRegisterScriptWithPrice wraps a given script with `getPrice`/`setPrice` calls for NNS.
|
||||||
|
// It is intended to be used for a single transaction, and not as a part of other scripts.
|
||||||
|
// It is assumed that script already contains static slot initialization code, the first one
|
||||||
|
// (with index 0) is used to store the price.
|
||||||
|
func wrapRegisterScriptWithPrice(w *io.BufBinWriter, nnsHash util.Uint160, s []byte) {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit.AppCall(w.BinWriter, nnsHash, "getPrice", callflag.All)
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.STSFLD0)
|
||||||
|
emit.AppCall(w.BinWriter, nnsHash, "setPrice", callflag.All, 1)
|
||||||
|
|
||||||
|
w.WriteBytes(s)
|
||||||
|
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.LDSFLD0, opcode.PUSH1, opcode.PACK)
|
||||||
|
emit.AppCallNoArgs(w.BinWriter, nnsHash, "setPrice", callflag.All)
|
||||||
|
|
||||||
|
if w.Err != nil {
|
||||||
|
panic(fmt.Errorf("BUG: can't wrap register script: %w", w.Err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) nnsRegisterDomainScript(nnsHash, expectedHash util.Uint160, domain string) ([]byte, bool, error) {
|
||||||
|
ok, err := nnsIsAvailable(c.Client, nnsHash, domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
bw := io.NewBufBinWriter()
|
||||||
|
emit.AppCall(bw.BinWriter, nnsHash, "register", callflag.All,
|
||||||
|
domain, c.CommitteeAcc.Contract.ScriptHash(),
|
||||||
|
"ops@nspcc.ru", int64(3600), int64(600), int64(defaultExpirationTime), int64(3600))
|
||||||
|
emit.Opcodes(bw.BinWriter, opcode.ASSERT)
|
||||||
|
|
||||||
|
if bw.Err != nil {
|
||||||
|
panic(bw.Err)
|
||||||
|
}
|
||||||
|
return bw.Bytes(), false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := nnsResolveHash(c.ReadOnlyInvoker, nnsHash, domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
return nil, s == expectedHash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) nnsRegisterDomain(nnsHash, expectedHash util.Uint160, domain string) error {
|
||||||
|
script, ok, err := c.nnsRegisterDomainScript(nnsHash, expectedHash, domain)
|
||||||
|
if ok || err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w := io.NewBufBinWriter()
|
||||||
|
emit.Instruction(w.BinWriter, opcode.INITSSLOT, []byte{1})
|
||||||
|
wrapRegisterScriptWithPrice(w, nnsHash, script)
|
||||||
|
|
||||||
|
emit.AppCall(w.BinWriter, nnsHash, "deleteRecords", callflag.All, domain, int64(nns.TXT))
|
||||||
|
emit.AppCall(w.BinWriter, nnsHash, "addRecord", callflag.All,
|
||||||
|
domain, int64(nns.TXT), expectedHash.StringLE())
|
||||||
|
emit.AppCall(w.BinWriter, nnsHash, "addRecord", callflag.All,
|
||||||
|
domain, int64(nns.TXT), address.Uint160ToString(expectedHash))
|
||||||
|
return c.sendCommitteeTx(w.Bytes(), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) nnsRootRegistered(nnsHash util.Uint160, zone string) (bool, error) {
|
||||||
|
res, err := c.CommitteeAct.Call(nnsHash, "isAvailable", "name."+zone)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.State == vmstate.Halt.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var errMissingNNSRecord = errors.New("missing NNS record")
|
||||||
|
|
||||||
|
// Returns errMissingNNSRecord if invocation fault exception contains "token not found".
|
||||||
|
func nnsResolveHash(inv *invoker.Invoker, nnsHash util.Uint160, domain string) (util.Uint160, error) {
|
||||||
|
item, err := nnsResolve(inv, nnsHash, domain)
|
||||||
|
if err != nil {
|
||||||
|
return util.Uint160{}, err
|
||||||
|
}
|
||||||
|
return parseNNSResolveResult(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func nnsResolve(inv *invoker.Invoker, nnsHash util.Uint160, domain string) (stackitem.Item, error) {
|
||||||
|
return unwrap.Item(inv.Call(nnsHash, "resolve", domain, int64(nns.TXT)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func nnsResolveKey(inv *invoker.Invoker, nnsHash util.Uint160, domain string) (*keys.PublicKey, error) {
|
||||||
|
item, err := nnsResolve(inv, nnsHash, domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
v, ok := item.Value().(stackitem.Null)
|
||||||
|
if ok {
|
||||||
|
return nil, errors.New("NNS record is missing")
|
||||||
|
}
|
||||||
|
bs, err := v.TryBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("malformed response")
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys.NewPublicKeyFromString(string(bs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseNNSResolveResult parses the result of resolving NNS record.
|
||||||
|
// It works with multiple formats (corresponding to multiple NNS versions).
|
||||||
|
// If array of hashes is provided, it returns only the first one.
|
||||||
|
func parseNNSResolveResult(res stackitem.Item) (util.Uint160, error) {
|
||||||
|
arr, ok := res.Value().([]stackitem.Item)
|
||||||
|
if !ok {
|
||||||
|
arr = []stackitem.Item{res}
|
||||||
|
}
|
||||||
|
if _, ok := res.Value().(stackitem.Null); ok || len(arr) == 0 {
|
||||||
|
return util.Uint160{}, errors.New("NNS record is missing")
|
||||||
|
}
|
||||||
|
for i := range arr {
|
||||||
|
bs, err := arr[i].TryBytes()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// We support several formats for hash encoding, this logic should be maintained in sync
|
||||||
|
// with nnsResolve from pkg/morph/client/nns.go
|
||||||
|
h, err := util.Uint160DecodeStringLE(string(bs))
|
||||||
|
if err == nil {
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
h, err = address.StringToUint160(string(bs))
|
||||||
|
if err == nil {
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.Uint160{}, errors.New("no valid hashes are found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func nnsIsAvailable(c Client, nnsHash util.Uint160, name string) (bool, error) {
|
||||||
|
switch ct := c.(type) {
|
||||||
|
case *rpcclient.Client:
|
||||||
|
return ct.NNSIsAvailable(nnsHash, name)
|
||||||
|
default:
|
||||||
|
b, err := unwrap.Bool(invokeFunction(c, nnsHash, "isAvailable", []interface{}{name}, nil))
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("`isAvailable`: invalid response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
}
|
137
cmd/frostfs-adm/internal/modules/morph/initialize_register.go
Normal file
137
cmd/frostfs-adm/internal/modules/morph/initialize_register.go
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/neo"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// initialAlphabetNEOAmount represents the total amount of GAS distributed between alphabet nodes.
|
||||||
|
const initialAlphabetNEOAmount = native.NEOTotalSupply
|
||||||
|
|
||||||
|
func (c *initializeContext) registerCandidates() error {
|
||||||
|
neoHash := neo.Hash
|
||||||
|
|
||||||
|
cc, err := unwrap.Array(c.ReadOnlyInvoker.Call(neoHash, "getCandidates"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("`getCandidates`: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cc) > 0 {
|
||||||
|
c.Command.Println("Candidates are already registered.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
regPrice, err := c.getCandidateRegisterPrice()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't fetch registration price: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w := io.NewBufBinWriter()
|
||||||
|
emit.AppCall(w.BinWriter, neoHash, "setRegisterPrice", callflag.States, 1)
|
||||||
|
for _, acc := range c.Accounts {
|
||||||
|
emit.AppCall(w.BinWriter, neoHash, "registerCandidate", callflag.States, acc.PrivateKey().PublicKey().Bytes())
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.ASSERT)
|
||||||
|
}
|
||||||
|
emit.AppCall(w.BinWriter, neoHash, "setRegisterPrice", callflag.States, regPrice)
|
||||||
|
if w.Err != nil {
|
||||||
|
panic(fmt.Sprintf("BUG: %v", w.Err))
|
||||||
|
}
|
||||||
|
|
||||||
|
signers := []rpcclient.SignerAccount{{
|
||||||
|
Signer: c.getSigner(false, c.CommitteeAcc),
|
||||||
|
Account: c.CommitteeAcc,
|
||||||
|
}}
|
||||||
|
for i := range c.Accounts {
|
||||||
|
signers = append(signers, rpcclient.SignerAccount{
|
||||||
|
Signer: transaction.Signer{
|
||||||
|
Account: c.Accounts[i].Contract.ScriptHash(),
|
||||||
|
Scopes: transaction.CustomContracts,
|
||||||
|
AllowedContracts: []util.Uint160{neoHash},
|
||||||
|
},
|
||||||
|
Account: c.Accounts[i],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := c.Client.CreateTxFromScript(w.Bytes(), c.CommitteeAcc, -1, 0, signers)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't create tx: %w", err)
|
||||||
|
}
|
||||||
|
if err := c.multiSign(tx, committeeAccountName); err != nil {
|
||||||
|
return fmt.Errorf("can't sign a transaction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
network := c.CommitteeAct.GetNetwork()
|
||||||
|
for i := range c.Accounts {
|
||||||
|
if err := c.Accounts[i].SignTx(network, tx); err != nil {
|
||||||
|
return fmt.Errorf("can't sign a transaction: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.sendTx(tx, c.Command, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) transferNEOToAlphabetContracts() error {
|
||||||
|
neoHash := neo.Hash
|
||||||
|
|
||||||
|
ok, err := c.transferNEOFinished(neoHash)
|
||||||
|
if ok || err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cs := c.getContract(alphabetContract)
|
||||||
|
amount := initialAlphabetNEOAmount / len(c.Wallets)
|
||||||
|
|
||||||
|
bw := io.NewBufBinWriter()
|
||||||
|
for _, acc := range c.Accounts {
|
||||||
|
h := state.CreateContractHash(acc.Contract.ScriptHash(), cs.NEF.Checksum, cs.Manifest.Name)
|
||||||
|
emit.AppCall(bw.BinWriter, neoHash, "transfer", callflag.All,
|
||||||
|
c.CommitteeAcc.Contract.ScriptHash(), h, int64(amount), nil)
|
||||||
|
emit.Opcodes(bw.BinWriter, opcode.ASSERT)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.sendCommitteeTx(bw.Bytes(), false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.awaitTx()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) transferNEOFinished(neoHash util.Uint160) (bool, error) {
|
||||||
|
bal, err := c.Client.NEP17BalanceOf(neoHash, c.CommitteeAcc.Contract.ScriptHash())
|
||||||
|
return bal < native.NEOTotalSupply, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var errGetPriceInvalid = errors.New("`getRegisterPrice`: invalid response")
|
||||||
|
|
||||||
|
func (c *initializeContext) getCandidateRegisterPrice() (int64, error) {
|
||||||
|
switch ct := c.Client.(type) {
|
||||||
|
case *rpcclient.Client:
|
||||||
|
return ct.GetCandidateRegisterPrice()
|
||||||
|
default:
|
||||||
|
neoHash := neo.Hash
|
||||||
|
res, err := invokeFunction(c.Client, neoHash, "getRegisterPrice", nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if len(res.Stack) == 0 {
|
||||||
|
return 0, errGetPriceInvalid
|
||||||
|
}
|
||||||
|
bi, err := res.Stack[0].TryInteger()
|
||||||
|
if err != nil || !bi.IsInt64() {
|
||||||
|
return 0, errGetPriceInvalid
|
||||||
|
}
|
||||||
|
return bi.Int64(), nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,6 @@
|
||||||
package initialize
|
package morph
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
|
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/rolemgmt"
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/rolemgmt"
|
||||||
|
@ -11,15 +8,15 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setNotaryAndAlphabetNodes(c *helper.InitializeContext) error {
|
func (c *initializeContext) setNotaryAndAlphabetNodes() error {
|
||||||
if ok, err := setRolesFinished(c); ok || err != nil {
|
if ok, err := c.setRolesFinished(); ok || err != nil {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.Command.Println("Stage 2: already performed.")
|
c.Command.Println("Stage 2: already performed.")
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var pubs []any
|
var pubs []interface{}
|
||||||
for _, acc := range c.Accounts {
|
for _, acc := range c.Accounts {
|
||||||
pubs = append(pubs, acc.PrivateKey().PublicKey().Bytes())
|
pubs = append(pubs, acc.PrivateKey().PublicKey().Bytes())
|
||||||
}
|
}
|
||||||
|
@ -30,23 +27,19 @@ func setNotaryAndAlphabetNodes(c *helper.InitializeContext) error {
|
||||||
emit.AppCall(w.BinWriter, rolemgmt.Hash, "designateAsRole",
|
emit.AppCall(w.BinWriter, rolemgmt.Hash, "designateAsRole",
|
||||||
callflag.States|callflag.AllowNotify, int64(noderoles.NeoFSAlphabet), pubs)
|
callflag.States|callflag.AllowNotify, int64(noderoles.NeoFSAlphabet), pubs)
|
||||||
|
|
||||||
if err := c.SendCommitteeTx(w.Bytes(), false); err != nil {
|
if err := c.sendCommitteeTx(w.Bytes(), false); err != nil {
|
||||||
return fmt.Errorf("send committee transaction: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err := c.AwaitTx()
|
return c.awaitTx()
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("await committee transaction: %w", err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setRolesFinished(c *helper.InitializeContext) (bool, error) {
|
func (c *initializeContext) setRolesFinished() (bool, error) {
|
||||||
height, err := c.Client.GetBlockCount()
|
height, err := c.Client.GetBlockCount()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pubs, err := helper.GetDesignatedByRole(c.ReadOnlyInvoker, rolemgmt.Hash, noderoles.NeoFSAlphabet, height)
|
pubs, err := getDesignatedByRole(c.ReadOnlyInvoker, rolemgmt.Hash, noderoles.NeoFSAlphabet, height)
|
||||||
return len(pubs) == len(c.Wallets), err
|
return len(pubs) == len(c.Wallets), err
|
||||||
}
|
}
|
117
cmd/frostfs-adm/internal/modules/morph/initialize_test.go
Normal file
117
cmd/frostfs-adm/internal/modules/morph/initialize_test.go
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/pkg/innerring"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/config"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
contractsPath = "../../../../../../frostfs-contract/frostfs-contract-v0.16.0.tar.gz"
|
||||||
|
protoFileName = "proto.yml"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInitialize(t *testing.T) {
|
||||||
|
// This test needs frostfs-contract tarball, so it is skipped by default.
|
||||||
|
// It is here for performing local testing after the changes.
|
||||||
|
t.Skip()
|
||||||
|
|
||||||
|
t.Run("4 nodes", func(t *testing.T) {
|
||||||
|
testInitialize(t, 4)
|
||||||
|
})
|
||||||
|
t.Run("7 nodes", func(t *testing.T) {
|
||||||
|
testInitialize(t, 7)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInitialize(t *testing.T, committeeSize int) {
|
||||||
|
testdataDir := t.TempDir()
|
||||||
|
v := viper.GetViper()
|
||||||
|
|
||||||
|
generateTestData(t, testdataDir, committeeSize)
|
||||||
|
v.Set(protoConfigPath, filepath.Join(testdataDir, protoFileName))
|
||||||
|
|
||||||
|
// Set to the path or remove the next statement to download from the network.
|
||||||
|
require.NoError(t, initCmd.Flags().Set(contractsInitFlag, contractsPath))
|
||||||
|
v.Set(localDumpFlag, filepath.Join(testdataDir, "out"))
|
||||||
|
v.Set(alphabetWalletsFlag, testdataDir)
|
||||||
|
v.Set(epochDurationInitFlag, 1)
|
||||||
|
v.Set(maxObjectSizeInitFlag, 1024)
|
||||||
|
|
||||||
|
setTestCredentials(v, committeeSize)
|
||||||
|
require.NoError(t, initializeSideChainCmd(initCmd, nil))
|
||||||
|
|
||||||
|
t.Run("force-new-epoch", func(t *testing.T) {
|
||||||
|
require.NoError(t, forceNewEpochCmd(forceNewEpoch, nil))
|
||||||
|
})
|
||||||
|
t.Run("set-config", func(t *testing.T) {
|
||||||
|
require.NoError(t, setConfigCmd(setConfig, []string{"MaintenanceModeAllowed=true"}))
|
||||||
|
})
|
||||||
|
t.Run("set-policy", func(t *testing.T) {
|
||||||
|
require.NoError(t, setPolicyCmd(setPolicy, []string{"ExecFeeFactor=1"}))
|
||||||
|
})
|
||||||
|
t.Run("remove-node", func(t *testing.T) {
|
||||||
|
pk, err := keys.NewPrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
pub := hex.EncodeToString(pk.PublicKey().Bytes())
|
||||||
|
require.NoError(t, removeNodesCmd(removeNodes, []string{pub}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateTestData(t *testing.T, dir string, size int) {
|
||||||
|
v := viper.GetViper()
|
||||||
|
v.Set(alphabetWalletsFlag, dir)
|
||||||
|
|
||||||
|
sizeStr := strconv.FormatUint(uint64(size), 10)
|
||||||
|
require.NoError(t, generateAlphabetCmd.Flags().Set(alphabetSizeFlag, sizeStr))
|
||||||
|
|
||||||
|
setTestCredentials(v, size)
|
||||||
|
require.NoError(t, generateAlphabetCreds(generateAlphabetCmd, nil))
|
||||||
|
|
||||||
|
var pubs []string
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
p := filepath.Join(dir, innerring.GlagoliticLetter(i).String()+".json")
|
||||||
|
w, err := wallet.NewWalletFromFile(p)
|
||||||
|
require.NoError(t, err, "wallet doesn't exist")
|
||||||
|
for _, acc := range w.Accounts {
|
||||||
|
if acc.Label == singleAccountName {
|
||||||
|
pub, ok := vm.ParseSignatureContract(acc.Contract.Script)
|
||||||
|
require.True(t, ok)
|
||||||
|
pubs = append(pubs, hex.EncodeToString(pub))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := config.Config{}
|
||||||
|
cfg.ProtocolConfiguration.ValidatorsCount = size
|
||||||
|
cfg.ProtocolConfiguration.SecondsPerBlock = 1
|
||||||
|
cfg.ProtocolConfiguration.StandbyCommittee = pubs // sorted by glagolic letters
|
||||||
|
cfg.ProtocolConfiguration.P2PSigExtensions = true
|
||||||
|
cfg.ProtocolConfiguration.VerifyTransactions = true
|
||||||
|
cfg.ProtocolConfiguration.VerifyBlocks = true
|
||||||
|
data, err := yaml.Marshal(cfg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
protoPath := filepath.Join(dir, protoFileName)
|
||||||
|
require.NoError(t, os.WriteFile(protoPath, data, os.ModePerm))
|
||||||
|
}
|
||||||
|
|
||||||
|
func setTestCredentials(v *viper.Viper, size int) {
|
||||||
|
for i := 0; i < size; i++ {
|
||||||
|
v.Set("credentials."+innerring.GlagoliticLetter(i).String(), strconv.FormatUint(uint64(i), 10))
|
||||||
|
}
|
||||||
|
v.Set("credentials.contract", testContractPassword)
|
||||||
|
}
|
190
cmd/frostfs-adm/internal/modules/morph/initialize_transfer.go
Normal file
190
cmd/frostfs-adm/internal/modules/morph/initialize_transfer.go
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/gas"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/neo"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||||
|
scContext "github.com/nspcc-dev/neo-go/pkg/smartcontract/context"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
gasInitialTotalSupply = 30000000 * native.GASFactor
|
||||||
|
// initialAlphabetGASAmount represents the amount of GAS given to each alphabet node.
|
||||||
|
initialAlphabetGASAmount = 10_000 * native.GASFactor
|
||||||
|
// initialProxyGASAmount represents the amount of GAS given to a proxy contract.
|
||||||
|
initialProxyGASAmount = 50_000 * native.GASFactor
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *initializeContext) transferFunds() error {
|
||||||
|
ok, err := c.transferFundsFinished()
|
||||||
|
if ok || err != nil {
|
||||||
|
if err == nil {
|
||||||
|
c.Command.Println("Stage 1: already performed.")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var transfers []rpcclient.TransferTarget
|
||||||
|
for _, acc := range c.Accounts {
|
||||||
|
to := acc.Contract.ScriptHash()
|
||||||
|
transfers = append(transfers,
|
||||||
|
rpcclient.TransferTarget{
|
||||||
|
Token: gas.Hash,
|
||||||
|
Address: to,
|
||||||
|
Amount: initialAlphabetGASAmount,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// It is convenient to have all funds at the committee account.
|
||||||
|
transfers = append(transfers,
|
||||||
|
rpcclient.TransferTarget{
|
||||||
|
Token: gas.Hash,
|
||||||
|
Address: c.CommitteeAcc.Contract.ScriptHash(),
|
||||||
|
Amount: (gasInitialTotalSupply - initialAlphabetGASAmount*int64(len(c.Wallets))) / 2,
|
||||||
|
},
|
||||||
|
rpcclient.TransferTarget{
|
||||||
|
Token: neo.Hash,
|
||||||
|
Address: c.CommitteeAcc.Contract.ScriptHash(),
|
||||||
|
Amount: native.NEOTotalSupply,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
tx, err := createNEP17MultiTransferTx(c.Client, c.ConsensusAcc, 0, transfers, []rpcclient.SignerAccount{{
|
||||||
|
Signer: transaction.Signer{
|
||||||
|
Account: c.ConsensusAcc.Contract.ScriptHash(),
|
||||||
|
Scopes: transaction.CalledByEntry,
|
||||||
|
},
|
||||||
|
Account: c.ConsensusAcc,
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't create transfer transaction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.multiSignAndSend(tx, consensusAccountName); err != nil {
|
||||||
|
return fmt.Errorf("can't send transfer transaction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.awaitTx()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) transferFundsFinished() (bool, error) {
|
||||||
|
acc := c.Accounts[0]
|
||||||
|
|
||||||
|
res, err := c.Client.NEP17BalanceOf(gas.Hash, acc.Contract.ScriptHash())
|
||||||
|
return res > initialAlphabetGASAmount/2, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) multiSignAndSend(tx *transaction.Transaction, accType string) error {
|
||||||
|
if err := c.multiSign(tx, accType); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.sendTx(tx, c.Command, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) multiSign(tx *transaction.Transaction, accType string) error {
|
||||||
|
network, err := c.Client.GetNetwork()
|
||||||
|
if err != nil {
|
||||||
|
// error appears only if client
|
||||||
|
// has not been initialized
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use parameter context to avoid dealing with signature order.
|
||||||
|
pc := scContext.NewParameterContext("", network, tx)
|
||||||
|
h := c.CommitteeAcc.Contract.ScriptHash()
|
||||||
|
if accType == consensusAccountName {
|
||||||
|
h = c.ConsensusAcc.Contract.ScriptHash()
|
||||||
|
}
|
||||||
|
for _, w := range c.Wallets {
|
||||||
|
acc, err := getWalletAccount(w, accType)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't find %s wallet account: %w", accType, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
priv := acc.PrivateKey()
|
||||||
|
sign := priv.SignHashable(uint32(network), tx)
|
||||||
|
if err := pc.AddSignature(h, acc.Contract, priv.PublicKey(), sign); err != nil {
|
||||||
|
return fmt.Errorf("can't add signature: %w", err)
|
||||||
|
}
|
||||||
|
if len(pc.Items[h].Signatures) == len(acc.Contract.Parameters) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w, err := pc.GetWitness(h)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("incomplete signature: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range tx.Signers {
|
||||||
|
if tx.Signers[i].Account == h {
|
||||||
|
if i < len(tx.Scripts) {
|
||||||
|
tx.Scripts[i] = *w
|
||||||
|
} else if i == len(tx.Scripts) {
|
||||||
|
tx.Scripts = append(tx.Scripts, *w)
|
||||||
|
} else {
|
||||||
|
panic("BUG: invalid signing order")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("%s account was not found among transaction signers", accType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) transferGASToProxy() error {
|
||||||
|
proxyCs := c.getContract(proxyContract)
|
||||||
|
|
||||||
|
bal, err := c.Client.NEP17BalanceOf(gas.Hash, proxyCs.Hash)
|
||||||
|
if err != nil || bal > 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := createNEP17MultiTransferTx(c.Client, c.CommitteeAcc, 0, []rpcclient.TransferTarget{{
|
||||||
|
Token: gas.Hash,
|
||||||
|
Address: proxyCs.Hash,
|
||||||
|
Amount: initialProxyGASAmount,
|
||||||
|
}}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.multiSignAndSend(tx, committeeAccountName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.awaitTx()
|
||||||
|
}
|
||||||
|
|
||||||
|
func createNEP17MultiTransferTx(c Client, acc *wallet.Account, netFee int64,
|
||||||
|
recipients []rpcclient.TransferTarget, cosigners []rpcclient.SignerAccount) (*transaction.Transaction, error) {
|
||||||
|
from := acc.Contract.ScriptHash()
|
||||||
|
|
||||||
|
w := io.NewBufBinWriter()
|
||||||
|
for i := range recipients {
|
||||||
|
emit.AppCall(w.BinWriter, recipients[i].Token, "transfer", callflag.All,
|
||||||
|
from, recipients[i].Address, recipients[i].Amount, recipients[i].Data)
|
||||||
|
emit.Opcodes(w.BinWriter, opcode.ASSERT)
|
||||||
|
}
|
||||||
|
if w.Err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create transfer script: %w", w.Err)
|
||||||
|
}
|
||||||
|
return c.CreateTxFromScript(w.Bytes(), acc, -1, netFee, append([]rpcclient.SignerAccount{{
|
||||||
|
Signer: transaction.Signer{
|
||||||
|
Account: from,
|
||||||
|
Scopes: transaction.CalledByEntry,
|
||||||
|
},
|
||||||
|
Account: acc,
|
||||||
|
}}, cosigners...))
|
||||||
|
}
|
65
cmd/frostfs-adm/internal/modules/morph/internal/types.go
Normal file
65
cmd/frostfs-adm/internal/modules/morph/internal/types.go
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StringifySubnetClientGroupID returns string representation of SubnetClientGroupID using MarshalText.
|
||||||
|
// Returns a string with a message on error.
|
||||||
|
func StringifySubnetClientGroupID(id *SubnetClientGroupID) string {
|
||||||
|
text, err := id.MarshalText()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("<invalid> %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText encodes SubnetClientGroupID into text format according to FrostFS API V2 protocol:
|
||||||
|
// value in base-10 integer string format.
|
||||||
|
//
|
||||||
|
// It implements encoding.TextMarshaler.
|
||||||
|
func (x *SubnetClientGroupID) MarshalText() ([]byte, error) {
|
||||||
|
num := x.GetValue() // NPE safe, returns zero on nil
|
||||||
|
|
||||||
|
return []byte(strconv.FormatUint(uint64(num), 10)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText decodes the SubnetID from the text according to FrostFS API V2 protocol:
|
||||||
|
// should be base-10 integer string format with bitsize = 32.
|
||||||
|
//
|
||||||
|
// Returns strconv.ErrRange if integer overflows uint32.
|
||||||
|
//
|
||||||
|
// Must not be called on nil.
|
||||||
|
//
|
||||||
|
// Implements encoding.TextUnmarshaler.
|
||||||
|
func (x *SubnetClientGroupID) UnmarshalText(txt []byte) error {
|
||||||
|
num, err := strconv.ParseUint(string(txt), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid numeric value: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
x.SetNumber(uint32(num))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal encodes the SubnetClientGroupID into a binary format of FrostFS API V2 protocol
|
||||||
|
// (Protocol Buffers with direct field order).
|
||||||
|
func (x *SubnetClientGroupID) Marshal() ([]byte, error) {
|
||||||
|
return proto.Marshal(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal decodes the SubnetClientGroupID from FrostFS API V2 binary format (see Marshal). Must not be called on nil.
|
||||||
|
func (x *SubnetClientGroupID) Unmarshal(data []byte) error {
|
||||||
|
return proto.Unmarshal(data, x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNumber sets SubnetClientGroupID value in uint32 format. Must not be called on nil.
|
||||||
|
// By default, number is 0.
|
||||||
|
func (x *SubnetClientGroupID) SetNumber(num uint32) {
|
||||||
|
x.Value = num
|
||||||
|
}
|
BIN
cmd/frostfs-adm/internal/modules/morph/internal/types.pb.go
generated
Normal file
BIN
cmd/frostfs-adm/internal/modules/morph/internal/types.pb.go
generated
Normal file
Binary file not shown.
15
cmd/frostfs-adm/internal/modules/morph/internal/types.proto
Normal file
15
cmd/frostfs-adm/internal/modules/morph/internal/types.proto
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package neo.fs.v2.refs;
|
||||||
|
|
||||||
|
option go_package = "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/internal";
|
||||||
|
|
||||||
|
// Client group identifier in the FrostFS subnet.
|
||||||
|
//
|
||||||
|
// String representation of a value is base-10 integer.
|
||||||
|
//
|
||||||
|
// JSON representation is an object containing single `value` number field.
|
||||||
|
message SubnetClientGroupID {
|
||||||
|
// 4-byte integer identifier of the subnet client group.
|
||||||
|
fixed32 value = 1 [json_name = "value"];
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package helper
|
package morph
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
|
@ -6,48 +6,51 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/config"
|
"github.com/nspcc-dev/neo-go/pkg/config"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core"
|
"github.com/nspcc-dev/neo-go/pkg/core"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/chaindump"
|
"github.com/nspcc-dev/neo-go/pkg/core/chaindump"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/core/fee"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
|
"github.com/nspcc-dev/neo-go/pkg/core/native/noderoles"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/storage"
|
"github.com/nspcc-dev/neo-go/pkg/core/storage"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
|
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
|
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/network/payload"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LocalClient struct {
|
type localClient struct {
|
||||||
bc *core.Blockchain
|
bc *core.Blockchain
|
||||||
transactions []*transaction.Transaction
|
transactions []*transaction.Transaction
|
||||||
dumpPath string
|
dumpPath string
|
||||||
accounts []*wallet.Account
|
accounts []*wallet.Account
|
||||||
|
maxGasInvoke int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLocalClient(cmd *cobra.Command, v *viper.Viper, wallets []*wallet.Wallet, dumpPath string) (*LocalClient, error) {
|
func newLocalClient(cmd *cobra.Command, v *viper.Viper, wallets []*wallet.Wallet) (*localClient, error) {
|
||||||
cfg, err := config.LoadFile(v.GetString(constants.ProtoConfigPath))
|
cfg, err := config.LoadFile(v.GetString(protoConfigPath))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -57,10 +60,10 @@ func NewLocalClient(cmd *cobra.Command, v *viper.Viper, wallets []*wallet.Wallet
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
m := smartcontract.GetDefaultHonestNodeCount(int(cfg.ProtocolConfiguration.ValidatorsCount))
|
m := smartcontract.GetDefaultHonestNodeCount(cfg.ProtocolConfiguration.ValidatorsCount)
|
||||||
accounts := make([]*wallet.Account, len(wallets))
|
accounts := make([]*wallet.Account, len(wallets))
|
||||||
for i := range accounts {
|
for i := range accounts {
|
||||||
accounts[i], err = GetWalletAccount(wallets[i], constants.ConsensusAccountName)
|
accounts[i], err = getWalletAccount(wallets[i], consensusAccountName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -82,8 +85,9 @@ func NewLocalClient(cmd *cobra.Command, v *viper.Viper, wallets []*wallet.Wallet
|
||||||
|
|
||||||
go bc.Run()
|
go bc.Run()
|
||||||
|
|
||||||
|
dumpPath := v.GetString(localDumpFlag)
|
||||||
if cmd.Name() != "init" {
|
if cmd.Name() != "init" {
|
||||||
f, err := os.OpenFile(dumpPath, os.O_RDONLY, 0o600)
|
f, err := os.OpenFile(dumpPath, os.O_RDONLY, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("can't open local dump: %w", err)
|
return nil, fmt.Errorf("can't open local dump: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -102,22 +106,42 @@ func NewLocalClient(cmd *cobra.Command, v *viper.Viper, wallets []*wallet.Wallet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &LocalClient{
|
return &localClient{
|
||||||
bc: bc,
|
bc: bc,
|
||||||
dumpPath: dumpPath,
|
dumpPath: dumpPath,
|
||||||
accounts: accounts[:m],
|
accounts: accounts[:m],
|
||||||
|
maxGasInvoke: 15_0000_0000,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LocalClient) GetBlockCount() (uint32, error) {
|
func (l *localClient) GetBlockCount() (uint32, error) {
|
||||||
return l.bc.BlockHeight(), nil
|
return l.bc.BlockHeight(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LocalClient) GetNativeContracts() ([]state.Contract, error) {
|
func (l *localClient) GetContractStateByID(id int32) (*state.Contract, error) {
|
||||||
|
h, err := l.bc.GetContractScriptHash(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return l.GetContractStateByHash(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *localClient) GetContractStateByHash(h util.Uint160) (*state.Contract, error) {
|
||||||
|
if cs := l.bc.GetContractState(h); cs != nil {
|
||||||
|
return cs, nil
|
||||||
|
}
|
||||||
|
return nil, storage.ErrKeyNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *localClient) GetNativeContracts() ([]state.NativeContract, error) {
|
||||||
return l.bc.GetNatives(), nil
|
return l.bc.GetNatives(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LocalClient) GetApplicationLog(h util.Uint256, t *trigger.Type) (*result.ApplicationLog, error) {
|
func (l *localClient) GetNetwork() (netmode.Magic, error) {
|
||||||
|
return l.bc.GetConfig().Magic, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *localClient) GetApplicationLog(h util.Uint256, t *trigger.Type) (*result.ApplicationLog, error) {
|
||||||
aer, err := l.bc.GetAppExecResults(h, *t)
|
aer, err := l.bc.GetAppExecResults(h, *t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -127,16 +151,44 @@ func (l *LocalClient) GetApplicationLog(h util.Uint256, t *trigger.Type) (*resul
|
||||||
return &a, nil
|
return &a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LocalClient) GetCommittee() (keys.PublicKeys, error) {
|
func (l *localClient) CreateTxFromScript(script []byte, acc *wallet.Account, sysFee int64, netFee int64, cosigners []rpcclient.SignerAccount) (*transaction.Transaction, error) {
|
||||||
|
signers, accounts, err := getSigners(acc, cosigners)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to construct tx signers: %w", err)
|
||||||
|
}
|
||||||
|
if sysFee < 0 {
|
||||||
|
res, err := l.InvokeScript(script, signers)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't add system fee to transaction: %w", err)
|
||||||
|
}
|
||||||
|
if res.State != "HALT" {
|
||||||
|
return nil, fmt.Errorf("can't add system fee to transaction: bad vm state: %s due to an error: %s", res.State, res.FaultException)
|
||||||
|
}
|
||||||
|
sysFee = res.GasConsumed
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := transaction.New(script, sysFee)
|
||||||
|
tx.Signers = signers
|
||||||
|
tx.ValidUntilBlock = l.bc.BlockHeight() + 2
|
||||||
|
|
||||||
|
err = l.AddNetworkFee(tx, netFee, accounts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to add network fee: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *localClient) GetCommittee() (keys.PublicKeys, error) {
|
||||||
// not used by `morph init` command
|
// not used by `morph init` command
|
||||||
panic("unexpected call")
|
panic("unexpected call")
|
||||||
}
|
}
|
||||||
|
|
||||||
// InvokeFunction is implemented via `InvokeScript`.
|
// InvokeFunction is implemented via `InvokeScript`.
|
||||||
func (l *LocalClient) InvokeFunction(h util.Uint160, method string, sPrm []smartcontract.Parameter, ss []transaction.Signer) (*result.Invoke, error) {
|
func (l *localClient) InvokeFunction(h util.Uint160, method string, sPrm []smartcontract.Parameter, ss []transaction.Signer) (*result.Invoke, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
pp := make([]any, len(sPrm))
|
pp := make([]interface{}, len(sPrm))
|
||||||
for i, p := range sPrm {
|
for i, p := range sPrm {
|
||||||
pp[i], err = smartcontract.ExpandParameterToEmitable(p)
|
pp[i], err = smartcontract.ExpandParameterToEmitable(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -144,121 +196,153 @@ func (l *LocalClient) InvokeFunction(h util.Uint160, method string, sPrm []smart
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return InvokeFunction(l, h, method, pp, ss)
|
return invokeFunction(l, h, method, pp, ss)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LocalClient) TerminateSession(_ uuid.UUID) (bool, error) {
|
func (l *localClient) CalculateNotaryFee(_ uint8) (int64, error) {
|
||||||
// not used by `morph init` command
|
// not used by `morph init` command
|
||||||
panic("unexpected call")
|
panic("unexpected call")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LocalClient) TraverseIterator(_, _ uuid.UUID, _ int) ([]stackitem.Item, error) {
|
func (l *localClient) SignAndPushP2PNotaryRequest(_ *transaction.Transaction, _ []byte, _ int64, _ int64, _ uint32, _ *wallet.Account) (*payload.P2PNotaryRequest, error) {
|
||||||
|
// not used by `morph init` command
|
||||||
|
panic("unexpected call")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *localClient) SignAndPushInvocationTx(_ []byte, _ *wallet.Account, _ int64, _ fixedn.Fixed8, _ []rpcclient.SignerAccount) (util.Uint256, error) {
|
||||||
|
// not used by `morph init` command
|
||||||
|
panic("unexpected call")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *localClient) TerminateSession(_ uuid.UUID) (bool, error) {
|
||||||
|
// not used by `morph init` command
|
||||||
|
panic("unexpected call")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *localClient) TraverseIterator(_, _ uuid.UUID, _ int) ([]stackitem.Item, error) {
|
||||||
// not used by `morph init` command
|
// not used by `morph init` command
|
||||||
panic("unexpected call")
|
panic("unexpected call")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetVersion return default version.
|
// GetVersion return default version.
|
||||||
func (l *LocalClient) GetVersion() (*result.Version, error) {
|
func (l *localClient) GetVersion() (*result.Version, error) {
|
||||||
c := l.bc.GetConfig()
|
return &result.Version{}, nil
|
||||||
return &result.Version{
|
|
||||||
Protocol: result.Protocol{
|
|
||||||
AddressVersion: address.NEO3Prefix,
|
|
||||||
Network: c.Magic,
|
|
||||||
MillisecondsPerBlock: int(c.TimePerBlock / time.Millisecond),
|
|
||||||
MaxTraceableBlocks: c.MaxTraceableBlocks,
|
|
||||||
MaxValidUntilBlockIncrement: c.MaxValidUntilBlockIncrement,
|
|
||||||
MaxTransactionsPerBlock: c.MaxTransactionsPerBlock,
|
|
||||||
MemoryPoolMaxTransactions: c.MemPoolSize,
|
|
||||||
ValidatorsCount: byte(c.ValidatorsCount),
|
|
||||||
InitialGasDistribution: c.InitialGASSupply,
|
|
||||||
CommitteeHistory: c.CommitteeHistory,
|
|
||||||
P2PSigExtensions: c.P2PSigExtensions,
|
|
||||||
StateRootInHeader: c.StateRootInHeader,
|
|
||||||
ValidatorsHistory: c.ValidatorsHistory,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LocalClient) InvokeContractVerify(util.Uint160, []smartcontract.Parameter, []transaction.Signer, ...transaction.Witness) (*result.Invoke, error) {
|
func (l *localClient) InvokeContractVerify(contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) {
|
||||||
// not used by `morph init` command
|
// not used by `morph init` command
|
||||||
panic("unexpected call")
|
panic("unexpected call")
|
||||||
}
|
}
|
||||||
|
|
||||||
// CalculateNetworkFee calculates network fee for the given transaction.
|
// CalculateNetworkFee calculates network fee for the given transaction.
|
||||||
// Copied from neo-go with minor corrections (no need to support non-notary mode):
|
// Copied from neo-go with minor corrections (no need to support non-notary mode):
|
||||||
// https://github.com/nspcc-dev/neo-go/blob/v0.103.0/pkg/services/rpcsrv/server.go#L911
|
// https://github.com/nspcc-dev/neo-go/blob/v0.99.2/pkg/services/rpcsrv/server.go#L744
|
||||||
func (l *LocalClient) CalculateNetworkFee(tx *transaction.Transaction) (int64, error) {
|
func (l *localClient) CalculateNetworkFee(tx *transaction.Transaction) (int64, error) {
|
||||||
// Avoid setting hash for this tx: server code doesn't touch client transaction.
|
|
||||||
data := tx.Bytes()
|
|
||||||
tx, err := transaction.NewTransactionFromBytes(data)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
hashablePart, err := tx.EncodeHashableFields()
|
hashablePart, err := tx.EncodeHashableFields()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, fmt.Errorf("failed to compute tx size: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
size := len(hashablePart) + io.GetVarSize(len(tx.Signers))
|
size := len(hashablePart) + io.GetVarSize(len(tx.Signers))
|
||||||
var (
|
ef := l.bc.GetBaseExecFee()
|
||||||
netFee int64
|
|
||||||
// Verification GAS cost can't exceed this policy.
|
var netFee int64
|
||||||
gasLimit = l.bc.GetMaxVerificationGAS()
|
|
||||||
)
|
|
||||||
for i, signer := range tx.Signers {
|
for i, signer := range tx.Signers {
|
||||||
w := tx.Scripts[i]
|
var verificationScript []byte
|
||||||
if len(w.InvocationScript) == 0 { // No invocation provided, try to infer one.
|
for _, w := range tx.Scripts {
|
||||||
var paramz []manifest.Parameter
|
if w.VerificationScript != nil && hash.Hash160(w.VerificationScript).Equals(signer.Account) {
|
||||||
if len(w.VerificationScript) == 0 { // Contract-based verification
|
verificationScript = w.VerificationScript
|
||||||
cs := l.bc.GetContractState(signer.Account)
|
break
|
||||||
if cs == nil {
|
|
||||||
return 0, fmt.Errorf("signer %d has no verification script and no deployed contract", i)
|
|
||||||
}
|
|
||||||
md := cs.Manifest.ABI.GetMethod(manifest.MethodVerify, -1)
|
|
||||||
if md == nil || md.ReturnType != smartcontract.BoolType {
|
|
||||||
return 0, fmt.Errorf("signer %d has no verify method in deployed contract", i)
|
|
||||||
}
|
|
||||||
paramz = md.Parameters // Might as well have none params and it's OK.
|
|
||||||
} else { // Regular signature verification.
|
|
||||||
if vm.IsSignatureContract(w.VerificationScript) {
|
|
||||||
paramz = []manifest.Parameter{{Type: smartcontract.SignatureType}}
|
|
||||||
} else if nSigs, _, ok := vm.ParseMultiSigContract(w.VerificationScript); ok {
|
|
||||||
paramz = make([]manifest.Parameter, nSigs)
|
|
||||||
for j := range nSigs {
|
|
||||||
paramz[j] = manifest.Parameter{Type: smartcontract.SignatureType}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
inv := io.NewBufBinWriter()
|
|
||||||
for _, p := range paramz {
|
|
||||||
p.Type.EncodeDefaultValue(inv.BinWriter)
|
|
||||||
}
|
|
||||||
if inv.Err != nil {
|
|
||||||
return 0, fmt.Errorf("failed to create dummy invocation script (signer %d): %s", i, inv.Err.Error())
|
|
||||||
}
|
|
||||||
w.InvocationScript = inv.Bytes()
|
|
||||||
}
|
}
|
||||||
gasConsumed, err := l.bc.VerifyWitness(signer.Account, tx, &w, gasLimit)
|
if verificationScript == nil {
|
||||||
if err != nil && !errors.Is(err, core.ErrInvalidSignature) {
|
gasConsumed, err := l.bc.VerifyWitness(signer.Account, tx, &tx.Scripts[i], l.maxGasInvoke)
|
||||||
return 0, err
|
if err != nil {
|
||||||
}
|
return 0, fmt.Errorf("invalid signature: %w", err)
|
||||||
gasLimit -= gasConsumed
|
}
|
||||||
netFee += gasConsumed
|
netFee += gasConsumed
|
||||||
size += io.GetVarSize(w.VerificationScript) + io.GetVarSize(w.InvocationScript)
|
size += io.GetVarSize([]byte{}) + io.GetVarSize(tx.Scripts[i].InvocationScript)
|
||||||
}
|
continue
|
||||||
if l.bc.P2PSigExtensionsEnabled() {
|
|
||||||
attrs := tx.GetAttributes(transaction.NotaryAssistedT)
|
|
||||||
if len(attrs) != 0 {
|
|
||||||
na := attrs[0].Value.(*transaction.NotaryAssisted)
|
|
||||||
netFee += (int64(na.NKeys) + 1) * l.bc.GetNotaryServiceFeePerKey()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fee, sizeDelta := fee.Calculate(ef, verificationScript)
|
||||||
|
netFee += fee
|
||||||
|
size += sizeDelta
|
||||||
}
|
}
|
||||||
|
|
||||||
fee := l.bc.FeePerByte()
|
fee := l.bc.FeePerByte()
|
||||||
netFee += int64(size) * fee
|
netFee += int64(size) * fee
|
||||||
|
|
||||||
return netFee, nil
|
return netFee, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LocalClient) InvokeScript(script []byte, signers []transaction.Signer) (*result.Invoke, error) {
|
// AddNetworkFee adds network fee for each witness script and optional extra
|
||||||
|
// network fee to transaction. `accs` is an array signer's accounts.
|
||||||
|
// Copied from neo-go with minor corrections (no need to support contract signers):
|
||||||
|
// https://github.com/nspcc-dev/neo-go/blob/6ff11baa1b9e4c71ef0d1de43b92a8c541ca732c/pkg/rpc/client/rpc.go#L960
|
||||||
|
func (l *localClient) AddNetworkFee(tx *transaction.Transaction, extraFee int64, accs ...*wallet.Account) error {
|
||||||
|
if len(tx.Signers) != len(accs) {
|
||||||
|
return errors.New("number of signers must match number of scripts")
|
||||||
|
}
|
||||||
|
|
||||||
|
size := io.GetVarSize(tx)
|
||||||
|
ef := l.bc.GetBaseExecFee()
|
||||||
|
for i := range tx.Signers {
|
||||||
|
netFee, sizeDelta := fee.Calculate(ef, accs[i].Contract.Script)
|
||||||
|
tx.NetworkFee += netFee
|
||||||
|
size += sizeDelta
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.NetworkFee += int64(size)*l.bc.FeePerByte() + extraFee
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSigners returns an array of transaction signers and corresponding accounts from
|
||||||
|
// given sender and cosigners. If cosigners list already contains sender, the sender
|
||||||
|
// will be placed at the start of the list.
|
||||||
|
// Copied from neo-go with minor corrections:
|
||||||
|
// https://github.com/nspcc-dev/neo-go/blob/6ff11baa1b9e4c71ef0d1de43b92a8c541ca732c/pkg/rpc/client/rpc.go#L735
|
||||||
|
func getSigners(sender *wallet.Account, cosigners []rpcclient.SignerAccount) ([]transaction.Signer, []*wallet.Account, error) {
|
||||||
|
var (
|
||||||
|
signers []transaction.Signer
|
||||||
|
accounts []*wallet.Account
|
||||||
|
)
|
||||||
|
|
||||||
|
from := sender.Contract.ScriptHash()
|
||||||
|
s := transaction.Signer{
|
||||||
|
Account: from,
|
||||||
|
Scopes: transaction.None,
|
||||||
|
}
|
||||||
|
for _, c := range cosigners {
|
||||||
|
if c.Signer.Account == from {
|
||||||
|
s = c.Signer
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
signers = append(signers, c.Signer)
|
||||||
|
accounts = append(accounts, c.Account)
|
||||||
|
}
|
||||||
|
signers = append([]transaction.Signer{s}, signers...)
|
||||||
|
accounts = append([]*wallet.Account{sender}, accounts...)
|
||||||
|
return signers, accounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *localClient) NEP17BalanceOf(h util.Uint160, acc util.Uint160) (int64, error) {
|
||||||
|
res, err := invokeFunction(l, h, "balanceOf", []interface{}{acc}, nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if res.State != vmstate.Halt.String() || len(res.Stack) == 0 {
|
||||||
|
return 0, fmt.Errorf("`balance`: invalid response (empty: %t): %s",
|
||||||
|
len(res.Stack) == 0, res.FaultException)
|
||||||
|
}
|
||||||
|
bi, err := res.Stack[0].TryInteger()
|
||||||
|
if err != nil || !bi.IsInt64() {
|
||||||
|
return 0, fmt.Errorf("`balance`: invalid response")
|
||||||
|
}
|
||||||
|
return bi.Int64(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *localClient) InvokeScript(script []byte, signers []transaction.Signer) (*result.Invoke, error) {
|
||||||
lastBlock, err := l.bc.GetBlock(l.bc.CurrentBlockHash())
|
lastBlock, err := l.bc.GetBlock(l.bc.CurrentBlockHash())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -294,7 +378,7 @@ func (l *LocalClient) InvokeScript(script []byte, signers []transaction.Signer)
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LocalClient) SendRawTransaction(tx *transaction.Transaction) (util.Uint256, error) {
|
func (l *localClient) SendRawTransaction(tx *transaction.Transaction) (util.Uint256, error) {
|
||||||
// We need to test that transaction was formed correctly to catch as many errors as we can.
|
// We need to test that transaction was formed correctly to catch as many errors as we can.
|
||||||
bs := tx.Bytes()
|
bs := tx.Bytes()
|
||||||
_, err := transaction.NewTransactionFromBytes(bs)
|
_, err := transaction.NewTransactionFromBytes(bs)
|
||||||
|
@ -306,7 +390,7 @@ func (l *LocalClient) SendRawTransaction(tx *transaction.Transaction) (util.Uint
|
||||||
return tx.Hash(), nil
|
return tx.Hash(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LocalClient) putTransactions() error {
|
func (l *localClient) putTransactions() error {
|
||||||
// 1. Prepare new block.
|
// 1. Prepare new block.
|
||||||
lastBlock, err := l.bc.GetBlock(l.bc.CurrentBlockHash())
|
lastBlock, err := l.bc.GetBlock(l.bc.CurrentBlockHash())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -348,7 +432,7 @@ func (l *LocalClient) putTransactions() error {
|
||||||
return l.bc.AddBlock(b)
|
return l.bc.AddBlock(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func InvokeFunction(c Client, h util.Uint160, method string, parameters []any, signers []transaction.Signer) (*result.Invoke, error) {
|
func invokeFunction(c Client, h util.Uint160, method string, parameters []interface{}, signers []transaction.Signer) (*result.Invoke, error) {
|
||||||
w := io.NewBufBinWriter()
|
w := io.NewBufBinWriter()
|
||||||
emit.Array(w.BinWriter, parameters...)
|
emit.Array(w.BinWriter, parameters...)
|
||||||
emit.AppCallNoArgs(w.BinWriter, h, method, callflag.All)
|
emit.AppCallNoArgs(w.BinWriter, h, method, callflag.All)
|
||||||
|
@ -360,7 +444,7 @@ func InvokeFunction(c Client, h util.Uint160, method string, parameters []any, s
|
||||||
|
|
||||||
var errGetDesignatedByRoleResponse = errors.New("`getDesignatedByRole`: invalid response")
|
var errGetDesignatedByRoleResponse = errors.New("`getDesignatedByRole`: invalid response")
|
||||||
|
|
||||||
func GetDesignatedByRole(inv *invoker.Invoker, h util.Uint160, role noderoles.Role, u uint32) (keys.PublicKeys, error) {
|
func getDesignatedByRole(inv *invoker.Invoker, h util.Uint160, role noderoles.Role, u uint32) (keys.PublicKeys, error) {
|
||||||
arr, err := unwrap.Array(inv.Call(h, "getDesignatedByRole", int64(role), int64(u)))
|
arr, err := unwrap.Array(inv.Call(h, "getDesignatedByRole", int64(role), int64(u)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errGetDesignatedByRoleResponse
|
return nil, errGetDesignatedByRoleResponse
|
||||||
|
@ -381,7 +465,7 @@ func GetDesignatedByRole(inv *invoker.Invoker, h util.Uint160, role noderoles.Ro
|
||||||
return pubs, nil
|
return pubs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LocalClient) Dump() (err error) {
|
func (l *localClient) dump() (err error) {
|
||||||
defer l.bc.Close()
|
defer l.bc.Close()
|
||||||
|
|
||||||
f, err := os.Create(l.dumpPath)
|
f, err := os.Create(l.dumpPath)
|
|
@ -1,17 +1,18 @@
|
||||||
package helper
|
package morph
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/commonflags"
|
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
|
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/network/payload"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
||||||
|
@ -28,27 +29,36 @@ type Client interface {
|
||||||
invoker.RPCInvoke
|
invoker.RPCInvoke
|
||||||
|
|
||||||
GetBlockCount() (uint32, error)
|
GetBlockCount() (uint32, error)
|
||||||
GetNativeContracts() ([]state.Contract, error)
|
GetContractStateByID(int32) (*state.Contract, error)
|
||||||
|
GetContractStateByHash(util.Uint160) (*state.Contract, error)
|
||||||
|
GetNativeContracts() ([]state.NativeContract, error)
|
||||||
|
GetNetwork() (netmode.Magic, error)
|
||||||
GetApplicationLog(util.Uint256, *trigger.Type) (*result.ApplicationLog, error)
|
GetApplicationLog(util.Uint256, *trigger.Type) (*result.ApplicationLog, error)
|
||||||
GetVersion() (*result.Version, error)
|
GetVersion() (*result.Version, error)
|
||||||
|
CreateTxFromScript([]byte, *wallet.Account, int64, int64, []rpcclient.SignerAccount) (*transaction.Transaction, error)
|
||||||
|
NEP17BalanceOf(util.Uint160, util.Uint160) (int64, error)
|
||||||
SendRawTransaction(*transaction.Transaction) (util.Uint256, error)
|
SendRawTransaction(*transaction.Transaction) (util.Uint256, error)
|
||||||
GetCommittee() (keys.PublicKeys, error)
|
GetCommittee() (keys.PublicKeys, error)
|
||||||
|
CalculateNotaryFee(uint8) (int64, error)
|
||||||
CalculateNetworkFee(tx *transaction.Transaction) (int64, error)
|
CalculateNetworkFee(tx *transaction.Transaction) (int64, error)
|
||||||
|
AddNetworkFee(*transaction.Transaction, int64, ...*wallet.Account) error
|
||||||
|
SignAndPushInvocationTx([]byte, *wallet.Account, int64, fixedn.Fixed8, []rpcclient.SignerAccount) (util.Uint256, error)
|
||||||
|
SignAndPushP2PNotaryRequest(*transaction.Transaction, []byte, int64, int64, uint32, *wallet.Account) (*payload.P2PNotaryRequest, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type HashVUBPair struct {
|
type hashVUBPair struct {
|
||||||
Hash util.Uint256
|
hash util.Uint256
|
||||||
Vub uint32
|
vub uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientContext struct {
|
type clientContext struct {
|
||||||
Client Client // a raw neo-go client OR a local chain implementation
|
Client Client // a raw neo-go client OR a local chain implementation
|
||||||
CommitteeAct *actor.Actor // committee actor with the Global witness scope
|
CommitteeAct *actor.Actor // committee actor with the Global witness scope
|
||||||
ReadOnlyInvoker *invoker.Invoker // R/O contract invoker, does not contain any signer
|
ReadOnlyInvoker *invoker.Invoker // R/O contract invoker, does not contain any signer
|
||||||
SentTxs []HashVUBPair
|
SentTxs []hashVUBPair
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetN3Client(v *viper.Viper) (Client, error) {
|
func getN3Client(v *viper.Viper) (Client, error) {
|
||||||
// number of opened connections
|
// number of opened connections
|
||||||
// by neo-go client per one host
|
// by neo-go client per one host
|
||||||
const (
|
const (
|
||||||
|
@ -57,27 +67,13 @@ func GetN3Client(v *viper.Viper) (Client, error) {
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
endpoint := v.GetString(commonflags.EndpointFlag)
|
endpoint := v.GetString(endpointFlag)
|
||||||
if endpoint == "" {
|
if endpoint == "" {
|
||||||
return nil, errors.New("missing endpoint")
|
return nil, errors.New("missing endpoint")
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg *tls.Config
|
|
||||||
if rootCAs := v.GetStringSlice("tls.trusted_ca_list"); len(rootCAs) != 0 {
|
|
||||||
certFile := v.GetString("tls.certificate")
|
|
||||||
keyFile := v.GetString("tls.key")
|
|
||||||
|
|
||||||
tlsConfig, err := rpcclient.TLSClientConfig(rootCAs, certFile, keyFile)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg = tlsConfig
|
|
||||||
}
|
|
||||||
c, err := rpcclient.New(ctx, endpoint, rpcclient.Options{
|
c, err := rpcclient.New(ctx, endpoint, rpcclient.Options{
|
||||||
MaxConnsPerHost: maxConnsPerHost,
|
MaxConnsPerHost: maxConnsPerHost,
|
||||||
RequestTimeout: requestTimeout,
|
RequestTimeout: requestTimeout,
|
||||||
TLSClientConfig: cfg,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -88,20 +84,26 @@ func GetN3Client(v *viper.Viper) (Client, error) {
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultClientContext(c Client, committeeAcc *wallet.Account) (*ClientContext, error) {
|
func defaultClientContext(c Client, committeeAcc *wallet.Account) (*clientContext, error) {
|
||||||
commAct, err := NewActor(c, committeeAcc)
|
commAct, err := actor.New(c, []actor.SignerAccount{{
|
||||||
|
Signer: transaction.Signer{
|
||||||
|
Account: committeeAcc.Contract.ScriptHash(),
|
||||||
|
Scopes: transaction.Global,
|
||||||
|
},
|
||||||
|
Account: committeeAcc,
|
||||||
|
}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ClientContext{
|
return &clientContext{
|
||||||
Client: c,
|
Client: c,
|
||||||
CommitteeAct: commAct,
|
CommitteeAct: commAct,
|
||||||
ReadOnlyInvoker: invoker.New(c, nil),
|
ReadOnlyInvoker: invoker.New(c, nil),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClientContext) SendTx(tx *transaction.Transaction, cmd *cobra.Command, await bool) error {
|
func (c *clientContext) sendTx(tx *transaction.Transaction, cmd *cobra.Command, await bool) error {
|
||||||
h, err := c.Client.SendRawTransaction(tx)
|
h, err := c.Client.SendRawTransaction(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -111,27 +113,10 @@ func (c *ClientContext) SendTx(tx *transaction.Transaction, cmd *cobra.Command,
|
||||||
return fmt.Errorf("sent and actual tx hashes mismatch:\n\tsent: %v\n\tactual: %v", tx.Hash().StringLE(), h.StringLE())
|
return fmt.Errorf("sent and actual tx hashes mismatch:\n\tsent: %v\n\tactual: %v", tx.Hash().StringLE(), h.StringLE())
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SentTxs = append(c.SentTxs, HashVUBPair{Hash: h, Vub: tx.ValidUntilBlock})
|
c.SentTxs = append(c.SentTxs, hashVUBPair{hash: h, vub: tx.ValidUntilBlock})
|
||||||
|
|
||||||
if await {
|
if await {
|
||||||
return c.AwaitTx(cmd)
|
return c.awaitTx(cmd)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClientContext) AwaitTx(cmd *cobra.Command) error {
|
|
||||||
if len(c.SentTxs) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if local, ok := c.Client.(*LocalClient); ok {
|
|
||||||
if err := local.putTransactions(); err != nil {
|
|
||||||
return fmt.Errorf("can't persist transactions: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := AwaitTx(cmd, c.Client, c.SentTxs)
|
|
||||||
c.SentTxs = c.SentTxs[:0]
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
package netmap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/constants"
|
|
||||||
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph/helper"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
||||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
|
||||||
)
|
|
||||||
|
|
||||||
const deltaFlag = "delta"
|
|
||||||
|
|
||||||
func ForceNewEpochCmd(cmd *cobra.Command, _ []string) error {
|
|
||||||
wCtx, err := helper.NewInitializeContext(cmd, viper.GetViper())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't initialize context: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r := management.NewReader(wCtx.ReadOnlyInvoker)
|
|
||||||
cs, err := helper.GetContractByID(r, 1)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get NNS contract info: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
nmHash, err := helper.NNSResolveHash(wCtx.ReadOnlyInvoker, cs.Hash, helper.DomainOf(constants.NetmapContract))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't get netmap contract hash: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bw := io.NewBufBinWriter()
|
|
||||||
delta, _ := cmd.Flags().GetInt64(deltaFlag)
|
|
||||||
if err := helper.EmitNewEpochCall(bw, wCtx, nmHash, delta); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = wCtx.SendConsensusTx(bw.Bytes()); err == nil {
|
|
||||||
err = wCtx.AwaitTx()
|
|
||||||
}
|
|
||||||
if err != nil && strings.Contains(err.Error(), "invalid epoch") {
|
|
||||||
cmd.Println("Epoch has already ticked.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue