Newdependencies #4
1247 changed files with 146249 additions and 14311 deletions
102
.devenv.hosting.yaml
Normal file
102
.devenv.hosting.yaml
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
hosts:
|
||||||
|
- address: localhost
|
||||||
|
plugin_name: docker
|
||||||
|
services:
|
||||||
|
- name: s01
|
||||||
|
attributes:
|
||||||
|
container_name: s01
|
||||||
|
config_path: ../frostfs-dev-env/services/storage/.storage.env
|
||||||
|
wallet_path: ../frostfs-dev-env/services/storage/wallet01.json
|
||||||
|
local_config_path: ./TemporaryDir/empty-password.yml
|
||||||
|
local_wallet_path: ../frostfs-dev-env/services/storage/wallet01.json
|
||||||
|
wallet_password: ""
|
||||||
|
volume_name: storage_storage_s01
|
||||||
|
endpoint_data0: s01.frostfs.devenv:8080
|
||||||
|
control_endpoint: s01.frostfs.devenv:8081
|
||||||
|
un_locode: "RU MOW"
|
||||||
|
- name: s02
|
||||||
|
attributes:
|
||||||
|
container_name: s02
|
||||||
|
config_path: ../frostfs-dev-env/services/storage/.storage.env
|
||||||
|
wallet_path: ../frostfs-dev-env/services/storage/wallet02.json
|
||||||
|
local_config_path: ./TemporaryDir/empty-password.yml
|
||||||
|
local_wallet_path: ../frostfs-dev-env/services/storage/wallet02.json
|
||||||
|
wallet_password: ""
|
||||||
|
volume_name: storage_storage_s02
|
||||||
|
endpoint_data0: s02.frostfs.devenv:8080
|
||||||
|
control_endpoint: s02.frostfs.devenv:8081
|
||||||
|
un_locode: "RU LED"
|
||||||
|
- name: s03
|
||||||
|
attributes:
|
||||||
|
container_name: s03
|
||||||
|
config_path: ../frostfs-dev-env/services/storage/.storage.env
|
||||||
|
wallet_path: ../frostfs-dev-env/services/storage/wallet03.json
|
||||||
|
local_config_path: ./TemporaryDir/empty-password.yml
|
||||||
|
local_wallet_path: ../frostfs-dev-env/services/storage/wallet03.json
|
||||||
|
wallet_password: ""
|
||||||
|
volume_name: storage_storage_s03
|
||||||
|
endpoint_data0: s03.frostfs.devenv:8080
|
||||||
|
control_endpoint: s03.frostfs.devenv:8081
|
||||||
|
un_locode: "SE STO"
|
||||||
|
- name: s04
|
||||||
|
attributes:
|
||||||
|
container_name: s04
|
||||||
|
config_path: ../frostfs-dev-env/services/storage/.storage.env
|
||||||
|
wallet_path: ../frostfs-dev-env/services/storage/wallet04.json
|
||||||
|
local_config_path: ./TemporaryDir/empty-password.yml
|
||||||
|
local_wallet_path: ../frostfs-dev-env/services/storage/wallet04.json
|
||||||
|
wallet_password: ""
|
||||||
|
volume_name: storage_storage_s04
|
||||||
|
endpoint_data0: s04.frostfs.devenv:8080
|
||||||
|
control_endpoint: s04.frostfs.devenv:8081
|
||||||
|
un_locode: "FI HEL"
|
||||||
|
- name: s3-gate01
|
||||||
|
attributes:
|
||||||
|
container_name: s3_gate
|
||||||
|
config_path: ../frostfs-dev-env/services/s3_gate/.s3.env
|
||||||
|
wallet_path: ../frostfs-dev-env/services/s3_gate/wallet.json
|
||||||
|
local_config_path: ./TemporaryDir/password-s3.yml
|
||||||
|
local_wallet_path: ../frostfs-dev-env/services/s3_gate/wallet.json
|
||||||
|
wallet_password: "s3"
|
||||||
|
endpoint_data0: https://s3.frostfs.devenv:8080
|
||||||
|
- name: http-gate01
|
||||||
|
attributes:
|
||||||
|
container_name: http_gate
|
||||||
|
config_path: ../frostfs-dev-env/services/http_gate/.http.env
|
||||||
|
wallet_path: ../frostfs-dev-env/services/http_gate/wallet.json
|
||||||
|
local_config_path: ./TemporaryDir/password-other.yml
|
||||||
|
local_wallet_path: ../frostfs-dev-env/services/http_gate/wallet.json
|
||||||
|
wallet_password: "one"
|
||||||
|
endpoint_data0: http://http.frostfs.devenv
|
||||||
|
- name: ir01
|
||||||
|
attributes:
|
||||||
|
container_name: ir01
|
||||||
|
config_path: ../frostfs-dev-env/services/ir/.ir.env
|
||||||
|
wallet_path: ../frostfs-dev-env/services/ir/az.json
|
||||||
|
local_config_path: ./TemporaryDir/password-other.yml
|
||||||
|
local_wallet_path: ../frostfs-dev-env/services/ir/az.json
|
||||||
|
wallet_password: "one"
|
||||||
|
- name: morph-chain01
|
||||||
|
attributes:
|
||||||
|
container_name: morph_chain
|
||||||
|
config_path: ../frostfs-dev-env/services/morph_chain/protocol.privnet.yml
|
||||||
|
wallet_path: ../frostfs-dev-env/services/morph_chain/node-wallet.json
|
||||||
|
local_config_path: ./TemporaryDir/password-other.yml
|
||||||
|
local_wallet_path: ../frostfs-dev-env/services/morph_chain/node-wallet.json
|
||||||
|
wallet_password: "one"
|
||||||
|
endpoint_internal0: http://morph-chain.frostfs.devenv:30333
|
||||||
|
- name: main-chain01
|
||||||
|
attributes:
|
||||||
|
container_name: main_chain
|
||||||
|
config_path: ../frostfs-dev-env/services/chain/protocol.privnet.yml
|
||||||
|
wallet_path: ../frostfs-dev-env/services/chain/node-wallet.json
|
||||||
|
local_config_path: ./TemporaryDir/password-other.yml
|
||||||
|
local_wallet_path: ../frostfs-dev-env/services/chain/node-wallet.json
|
||||||
|
wallet_password: "one"
|
||||||
|
endpoint_internal0: http://main-chain.frostfs.devenv:30333
|
||||||
|
- name: coredns01
|
||||||
|
attributes:
|
||||||
|
container_name: coredns
|
||||||
|
clis:
|
||||||
|
- name: frostfs-cli
|
||||||
|
exec_path: frostfs-cli
|
19
.docker/Dockerfile.adm
Normal file
19
.docker/Dockerfile.adm
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-adm
|
||||||
|
|
||||||
|
# Executable image
|
||||||
|
FROM alpine AS frostfs-adm
|
||||||
|
RUN apk add --no-cache bash
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
COPY --from=builder /src/bin/frostfs-adm /bin/frostfs-adm
|
||||||
|
|
||||||
|
CMD ["frostfs-adm"]
|
19
.docker/Dockerfile.cli
Normal file
19
.docker/Dockerfile.cli
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-cli
|
||||||
|
|
||||||
|
# Executable image
|
||||||
|
FROM alpine AS frostfs-cli
|
||||||
|
RUN apk add --no-cache bash
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
COPY --from=builder /src/bin/frostfs-cli /bin/frostfs-cli
|
||||||
|
|
||||||
|
CMD ["frostfs-cli"]
|
8
.docker/Dockerfile.dirty-adm
Normal file
8
.docker/Dockerfile.dirty-adm
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
FROM alpine
|
||||||
|
RUN apk add --no-cache bash ca-certificates
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
COPY bin/frostfs-adm /bin/frostfs-adm
|
||||||
|
|
||||||
|
CMD ["frostfs-adm"]
|
8
.docker/Dockerfile.dirty-cli
Normal file
8
.docker/Dockerfile.dirty-cli
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
FROM alpine
|
||||||
|
RUN apk add --no-cache bash ca-certificates
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
COPY bin/frostfs-cli /bin/frostfs-cli
|
||||||
|
|
||||||
|
CMD ["frostfs-cli"]
|
8
.docker/Dockerfile.dirty-ir
Normal file
8
.docker/Dockerfile.dirty-ir
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
FROM alpine
|
||||||
|
RUN apk add --no-cache bash ca-certificates
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
COPY bin/frostfs-ir /bin/frostfs-ir
|
||||||
|
|
||||||
|
CMD ["frostfs-ir"]
|
8
.docker/Dockerfile.dirty-storage
Normal file
8
.docker/Dockerfile.dirty-storage
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
FROM alpine
|
||||||
|
RUN apk add --no-cache bash ca-certificates
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
COPY bin/frostfs-node /bin/frostfs-node
|
||||||
|
|
||||||
|
CMD ["frostfs-node"]
|
18
.docker/Dockerfile.ir
Normal file
18
.docker/Dockerfile.ir
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
FROM golang:1.18 as builder
|
||||||
|
ARG BUILD=now
|
||||||
|
ARG VERSION=dev
|
||||||
|
ARG REPO=repository
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . /src
|
||||||
|
|
||||||
|
RUN make bin/frostfs-ir
|
||||||
|
|
||||||
|
# Executable image
|
||||||
|
FROM alpine AS frostfs-ir
|
||||||
|
RUN apk add --no-cache bash
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
COPY --from=builder /src/bin/frostfs-ir /bin/frostfs-ir
|
||||||
|
|
||||||
|
CMD ["frostfs-ir"]
|
18
.docker/Dockerfile.storage
Normal file
18
.docker/Dockerfile.storage
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
CMD ["frostfs-node"]
|
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"]
|
8
.dockerignore
Normal file
8
.dockerignore
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
.git
|
||||||
|
docker-compose.yml
|
||||||
|
Dockerfile
|
||||||
|
temp
|
||||||
|
.dockerignore
|
||||||
|
docker
|
|
@ -1,26 +0,0 @@
|
||||||
# yamllint disable rule:truthy
|
|
||||||
|
|
||||||
name: DCO check
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
dco:
|
|
||||||
name: DCO
|
|
||||||
runs-on: docker
|
|
||||||
container:
|
|
||||||
image: node:22-bookworm
|
|
||||||
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 }}'
|
|
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/**/*.pb.go -diff -merge
|
||||||
|
/**/*.pb.go linguist-generated=true
|
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
* @vdomnich-yadro @dansingjulia @yadro-vavdeev
|
50
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
50
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: community, triage
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Provide a general summary of the issue in the Title above -->
|
||||||
|
|
||||||
|
## Expected Behavior
|
||||||
|
<!-- If you're describing a bug, tell us what should happen
|
||||||
|
If you're suggesting a change/improvement, tell us how it should work -->
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
<!-- If describing a bug, tell us what happens instead of the expected behavior
|
||||||
|
If suggesting a change/improvement, explain the difference from current behavior -->
|
||||||
|
|
||||||
|
## Possible Solution
|
||||||
|
<!-- Not obligatory, but suggest a fix/reason for the bug,
|
||||||
|
or ideas how to implement the addition or change -->
|
||||||
|
|
||||||
|
## Steps to Reproduce (for bugs)
|
||||||
|
<!-- Provide a link to a live example, or an unambiguous set of steps
|
||||||
|
to reproduce this bug. -->
|
||||||
|
|
||||||
|
1.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
<!-- How has this issue affected you? What are you trying to accomplish?
|
||||||
|
Providing context helps us come up with a solution that is most useful in the real world -->
|
||||||
|
|
||||||
|
## Regression
|
||||||
|
<!-- Is this issue a regression? (Yes / No)
|
||||||
|
If Yes, optionally please include version or commit id or PR# that caused this regression,
|
||||||
|
if you have these details -->
|
||||||
|
|
||||||
|
## Your Environment
|
||||||
|
<!-- Include as many relevant details about the environment you experienced the bug in -->
|
||||||
|
* Version used:
|
||||||
|
* Server setup and configuration:
|
||||||
|
* 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
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
blank_issues_enabled: false
|
28
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
28
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: community, triage
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Is your feature request related to a problem? Please describe.
|
||||||
|
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when ... -->
|
||||||
|
|
||||||
|
## Describe the solution you'd like
|
||||||
|
<!-- A clear and concise description of what you want to happen. -->
|
||||||
|
|
||||||
|
## Describe alternatives you've considered
|
||||||
|
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||||
|
|
||||||
|
## Additional context
|
||||||
|
<!-- Add any other context or screenshots about the feature request here. -->
|
||||||
|
|
||||||
|
## Don't forget to add labels!
|
||||||
|
- component label (`neofs-adm`, `neofs-storage`, ...)
|
||||||
|
- issue type (`enhancement`, `refactor`, ...)
|
||||||
|
- `goodfirstissue`, `helpwanted` if needed
|
||||||
|
- does this issue belong to an epic?
|
||||||
|
- priority (`P0`-`P4`) if already triaged
|
||||||
|
- quarter label (`202XQY`) if possible
|
70
.github/logo.svg
vendored
Normal file
70
.github/logo.svg
vendored
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 25.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Слой_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 184.2 51.8" style="enable-background:new 0 0 184.2 51.8;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{display:none;}
|
||||||
|
.st1{display:inline;}
|
||||||
|
.st2{fill:#01E397;}
|
||||||
|
.st3{display:inline;fill:#010032;}
|
||||||
|
.st4{display:inline;fill:#00E599;}
|
||||||
|
.st5{display:inline;fill:#00AF92;}
|
||||||
|
.st6{fill:#00C3E5;}
|
||||||
|
</style>
|
||||||
|
<g id="Layer_2">
|
||||||
|
<g id="Layer_1-2" class="st0">
|
||||||
|
<g class="st1">
|
||||||
|
<path class="st2" d="M146.6,18.3v7.2h10.9V29h-10.9v10.7h-4V14.8h18v3.5H146.6z"/>
|
||||||
|
<path class="st2" d="M180,15.7c1.7,0.9,3,2.2,4,3.8l-3,2.7c-0.6-1.3-1.5-2.4-2.6-3.3c-1.3-0.7-2.8-1-4.3-1
|
||||||
|
c-1.4-0.1-2.8,0.3-4,1.1c-0.9,0.5-1.5,1.5-1.4,2.6c0,1,0.5,1.9,1.4,2.4c1.5,0.8,3.2,1.3,4.9,1.5c1.9,0.3,3.7,0.8,5.4,1.6
|
||||||
|
c1.2,0.5,2.2,1.3,2.9,2.3c0.6,1,1,2.2,0.9,3.4c0,1.4-0.5,2.7-1.3,3.8c-0.9,1.2-2.1,2.1-3.5,2.6c-1.7,0.6-3.4,0.9-5.2,0.8
|
||||||
|
c-5,0-8.6-1.6-10.7-5l2.9-2.8c0.7,1.4,1.8,2.5,3.1,3.3c1.5,0.7,3.1,1.1,4.7,1c1.5,0.1,2.9-0.2,4.2-0.9c0.9-0.5,1.5-1.5,1.5-2.6
|
||||||
|
c0-0.9-0.5-1.8-1.3-2.2c-1.5-0.7-3.1-1.2-4.8-1.5c-1.9-0.3-3.7-0.8-5.5-1.5c-1.2-0.5-2.2-1.4-3-2.4c-0.6-1-1-2.2-0.9-3.4
|
||||||
|
c0-1.4,0.4-2.7,1.2-3.8c0.8-1.2,2-2.2,3.3-2.8c1.6-0.7,3.4-1.1,5.2-1C176.1,14.3,178.2,14.8,180,15.7z"/>
|
||||||
|
</g>
|
||||||
|
<path class="st3" d="M73.3,16.3c1.9,1.9,2.9,4.5,2.7,7.1v15.9h-4V24.8c0-2.6-0.5-4.5-1.6-5.7c-1.2-1.2-2.8-1.8-4.5-1.7
|
||||||
|
c-1.3,0-2.5,0.3-3.7,0.8c-1.2,0.7-2.2,1.7-2.9,2.9c-0.8,1.5-1.1,3.2-1.1,4.9v13.3h-4V15.1l3.6,1.5v1.7c0.8-1.5,2.1-2.6,3.6-3.3
|
||||||
|
c1.5-0.8,3.2-1.2,4.9-1.1C68.9,13.8,71.3,14.7,73.3,16.3z"/>
|
||||||
|
<path class="st3" d="M104.4,28.3H85.6c0.1,2.2,1,4.3,2.5,5.9c1.5,1.4,3.5,2.2,5.6,2.1c1.6,0.1,3.2-0.2,4.6-0.9
|
||||||
|
c1.1-0.6,2-1.6,2.5-2.8l3.3,1.8c-0.9,1.7-2.3,3.1-4,4c-2,1-4.2,1.5-6.4,1.4c-3.7,0-6.7-1.1-8.8-3.4s-3.2-5.5-3.2-9.6s1-7.2,3-9.5
|
||||||
|
s5-3.4,8.7-3.4c2.1-0.1,4.2,0.5,6.1,1.5c1.6,1,3,2.5,3.8,4.2c0.9,1.8,1.3,3.9,1.3,5.9C104.6,26.4,104.6,27.4,104.4,28.3z
|
||||||
|
M88.1,19.3c-1.4,1.5-2.2,3.4-2.4,5.5h15.1c-0.2-2-1-3.9-2.3-5.5c-1.4-1.3-3.2-2-5.1-1.9C91.5,17.3,89.6,18,88.1,19.3z"/>
|
||||||
|
<path class="st3" d="M131,17.3c2.2,2.3,3.2,5.5,3.2,9.5s-1,7.3-3.2,9.6s-5.1,3.4-8.8,3.4s-6.7-1.1-8.9-3.4s-3.2-5.5-3.2-9.6
|
||||||
|
s1.1-7.2,3.2-9.5s5.1-3.4,8.9-3.4S128.9,15,131,17.3z M116.2,19.9c-1.5,2-2.2,4.4-2.1,6.9c-0.2,2.5,0.6,5,2.1,7
|
||||||
|
c1.5,1.7,3.7,2.7,6,2.6c2.3,0.1,4.4-0.9,5.9-2.6c1.5-2,2.3-4.5,2.1-7c0.1-2.5-0.6-4.9-2.1-6.9c-1.5-1.7-3.6-2.7-5.9-2.6
|
||||||
|
C119.9,17.2,117.7,18.2,116.2,19.9z"/>
|
||||||
|
<polygon class="st4" points="0,9.1 0,43.7 22.5,51.8 22.5,16.9 46.8,7.9 24.8,0 "/>
|
||||||
|
<polygon class="st5" points="24.3,17.9 24.3,36.8 46.8,44.9 46.8,9.6 "/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<path class="st6" d="M41.6,17.5H28.2v6.9h10.4v3.3H28.2v10.2h-3.9V14.2h17.2V17.5z"/>
|
||||||
|
<path class="st6" d="M45.8,37.9v-18h3.3l0.4,3.2c0.5-1.2,1.2-2.1,2.1-2.7c0.9-0.6,2.1-0.9,3.5-0.9c0.4,0,0.7,0,1.1,0.1
|
||||||
|
c0.4,0.1,0.7,0.2,0.9,0.3l-0.5,3.4c-0.3-0.1-0.6-0.2-0.9-0.2C55.4,23,54.9,23,54.4,23c-0.7,0-1.5,0.2-2.2,0.6
|
||||||
|
c-0.7,0.4-1.3,1-1.8,1.8s-0.7,1.8-0.7,3v9.5H45.8z"/>
|
||||||
|
<path class="st6" d="M68.6,19.6c1.8,0,3.3,0.4,4.6,1.1c1.3,0.7,2.4,1.8,3.1,3.2s1.1,3.1,1.1,5c0,1.9-0.4,3.6-1.1,5
|
||||||
|
c-0.8,1.4-1.8,2.5-3.1,3.2c-1.3,0.7-2.9,1.1-4.6,1.1s-3.3-0.4-4.6-1.1c-1.3-0.7-2.4-1.8-3.2-3.2c-0.8-1.4-1.2-3.1-1.2-5
|
||||||
|
c0-1.9,0.4-3.6,1.2-5s1.8-2.5,3.2-3.2C65.3,19.9,66.8,19.6,68.6,19.6z M68.6,22.6c-1.1,0-2,0.2-2.8,0.7c-0.8,0.5-1.3,1.2-1.7,2.1
|
||||||
|
s-0.6,2.1-0.6,3.5c0,1.3,0.2,2.5,0.6,3.4s1,1.7,1.7,2.2s1.7,0.7,2.8,0.7c1.1,0,2-0.2,2.7-0.7c0.7-0.5,1.3-1.2,1.7-2.2
|
||||||
|
s0.6-2.1,0.6-3.4c0-1.4-0.2-2.5-0.6-3.5s-1-1.6-1.7-2.1C70.6,22.8,69.6,22.6,68.6,22.6z"/>
|
||||||
|
<path class="st6" d="M89.2,38.3c-1.8,0-3.4-0.3-4.9-1c-1.5-0.7-2.7-1.7-3.5-3l2.7-2.3c0.5,1,1.3,1.8,2.3,2.4
|
||||||
|
c1,0.6,2.2,0.9,3.6,0.9c1.1,0,2-0.2,2.6-0.6c0.6-0.4,1-0.9,1-1.6c0-0.5-0.2-0.9-0.5-1.2s-0.9-0.6-1.7-0.8l-3.8-0.8
|
||||||
|
c-1.9-0.4-3.3-1-4.1-1.9c-0.8-0.9-1.2-1.9-1.2-3.3c0-1,0.3-1.9,0.9-2.7c0.6-0.8,1.4-1.5,2.5-2s2.5-0.8,4-0.8c1.8,0,3.3,0.3,4.6,1
|
||||||
|
c1.3,0.6,2.2,1.5,2.9,2.7l-2.7,2.2c-0.5-1-1.1-1.7-2-2.1c-0.9-0.5-1.8-0.7-2.8-0.7c-0.8,0-1.4,0.1-2,0.3c-0.6,0.2-1,0.5-1.3,0.8
|
||||||
|
c-0.3,0.3-0.4,0.7-0.4,1.2c0,0.5,0.2,0.9,0.5,1.3s1,0.6,1.9,0.8l4.1,0.9c1.7,0.3,2.9,0.9,3.7,1.7c0.7,0.8,1.1,1.8,1.1,2.9
|
||||||
|
c0,1.2-0.3,2.2-0.9,3c-0.6,0.9-1.5,1.6-2.6,2C92.1,38.1,90.7,38.3,89.2,38.3z"/>
|
||||||
|
<path class="st6" d="M112.8,19.9v3H99.3v-3H112.8z M106.6,14.6v17.9c0,0.9,0.2,1.5,0.7,1.9c0.5,0.4,1.1,0.6,1.9,0.6
|
||||||
|
c0.6,0,1.2-0.1,1.7-0.3c0.5-0.2,0.9-0.5,1.3-0.8l0.9,2.8c-0.6,0.5-1.2,0.9-2,1.1c-0.8,0.3-1.7,0.4-2.7,0.4c-1,0-2-0.2-2.8-0.5
|
||||||
|
s-1.5-0.9-2-1.6c-0.5-0.8-0.7-1.7-0.8-3V15.7L106.6,14.6z"/>
|
||||||
|
<path d="M137.9,17.5h-13.3v6.9h10.4v3.3h-10.4v10.2h-3.9V14.2h17.2V17.5z"/>
|
||||||
|
<path d="M150.9,13.8c2.1,0,4,0.4,5.5,1.2c1.6,0.8,2.9,2,4,3.5l-2.6,2.5c-0.9-1.4-1.9-2.4-3.1-3c-1.1-0.6-2.5-0.9-4-0.9
|
||||||
|
c-1.2,0-2.1,0.2-2.8,0.5c-0.7,0.3-1.3,0.7-1.6,1.2c-0.3,0.5-0.5,1.1-0.5,1.7c0,0.7,0.3,1.4,0.8,1.9c0.5,0.6,1.5,1,2.9,1.3
|
||||||
|
l4.8,1.1c2.3,0.5,3.9,1.3,4.9,2.3c1,1,1.4,2.3,1.4,3.9c0,1.5-0.4,2.7-1.2,3.8c-0.8,1.1-1.9,1.9-3.3,2.5s-3.1,0.9-5,0.9
|
||||||
|
c-1.7,0-3.2-0.2-4.5-0.6c-1.3-0.4-2.5-1-3.5-1.8c-1-0.7-1.8-1.6-2.5-2.6l2.7-2.7c0.5,0.8,1.1,1.6,1.9,2.2
|
||||||
|
c0.8,0.7,1.7,1.2,2.7,1.5c1,0.4,2.2,0.5,3.4,0.5c1.1,0,2.1-0.1,2.9-0.4c0.8-0.3,1.4-0.7,1.8-1.2c0.4-0.5,0.6-1.1,0.6-1.9
|
||||||
|
c0-0.7-0.2-1.3-0.7-1.8c-0.5-0.5-1.3-0.9-2.6-1.2l-5.2-1.2c-1.4-0.3-2.6-0.8-3.6-1.3c-0.9-0.6-1.6-1.3-2.1-2.1s-0.7-1.8-0.7-2.8
|
||||||
|
c0-1.3,0.4-2.6,1.1-3.7c0.7-1.1,1.8-2,3.2-2.6C147.3,14.1,148.9,13.8,150.9,13.8z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
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
|
||||||
|
- develop
|
||||||
|
|
||||||
|
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
|
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -1,11 +1,5 @@
|
||||||
# ignore IDE files
|
# ignore IDE files
|
||||||
.vscode
|
.vscode
|
||||||
.idea
|
|
||||||
|
|
||||||
.DS_Store
|
|
||||||
venv.*
|
|
||||||
venv_macos
|
|
||||||
|
|
||||||
|
|
||||||
# ignore test results
|
# ignore test results
|
||||||
**/log.html
|
**/log.html
|
||||||
|
@ -18,7 +12,6 @@ xunit_results.xml
|
||||||
# ignore caches under any path
|
# ignore caches under any path
|
||||||
**/__pycache__
|
**/__pycache__
|
||||||
**/.pytest_cache
|
**/.pytest_cache
|
||||||
*.egg-info
|
|
||||||
|
|
||||||
# ignore work directories and setup files
|
# ignore work directories and setup files
|
||||||
.setup
|
.setup
|
||||||
|
@ -27,4 +20,4 @@ TemporaryDir/*
|
||||||
artifacts/*
|
artifacts/*
|
||||||
docs/*
|
docs/*
|
||||||
venv.*/*
|
venv.*/*
|
||||||
wallet_config.yml
|
/*wallet_config.yml
|
||||||
|
|
56
.golangci.yml
Normal file
56
.golangci.yml
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
# This file contains all available configuration options
|
||||||
|
# with their default values.
|
||||||
|
|
||||||
|
# options for analysis running
|
||||||
|
run:
|
||||||
|
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
||||||
|
timeout: 5m
|
||||||
|
|
||||||
|
# include test files or not, default is true
|
||||||
|
tests: false
|
||||||
|
|
||||||
|
# output configuration options
|
||||||
|
output:
|
||||||
|
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
|
||||||
|
format: tab
|
||||||
|
|
||||||
|
# all available settings of specific linters
|
||||||
|
linters-settings:
|
||||||
|
exhaustive:
|
||||||
|
# indicates that switch statements are to be considered exhaustive if a
|
||||||
|
# 'default' case is present, even if all enum members aren't listed in the
|
||||||
|
# switch
|
||||||
|
default-signifies-exhaustive: true
|
||||||
|
govet:
|
||||||
|
# report about shadowed variables
|
||||||
|
check-shadowing: false
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
# mandatory linters
|
||||||
|
- govet
|
||||||
|
- revive
|
||||||
|
|
||||||
|
# some default golangci-lint linters
|
||||||
|
- errcheck
|
||||||
|
- gosimple
|
||||||
|
- godot
|
||||||
|
- ineffassign
|
||||||
|
- staticcheck
|
||||||
|
- typecheck
|
||||||
|
- unused
|
||||||
|
|
||||||
|
# extra linters
|
||||||
|
- bidichk
|
||||||
|
- durationcheck
|
||||||
|
- exhaustive
|
||||||
|
- exportloopref
|
||||||
|
- gofmt
|
||||||
|
- goimports
|
||||||
|
- misspell
|
||||||
|
- predeclared
|
||||||
|
- reassign
|
||||||
|
- whitespace
|
||||||
|
disable-all: true
|
||||||
|
fast: false
|
||||||
|
|
|
@ -3,22 +3,12 @@ repos:
|
||||||
rev: 22.8.0
|
rev: 22.8.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
language_version: python3.10
|
language_version: python3.9
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
rev: 5.12.0
|
rev: 5.10.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
name: isort (python)
|
name: isort (python)
|
||||||
- repo: https://git.frostfs.info/TrueCloudLab/allure-validator
|
|
||||||
rev: 1.1.1
|
|
||||||
hooks:
|
|
||||||
- id: allure-validator
|
|
||||||
args: [
|
|
||||||
"pytest_tests/",
|
|
||||||
"--plugins",
|
|
||||||
"frostfs[-_]testlib*",
|
|
||||||
]
|
|
||||||
pass_filenames: false
|
|
||||||
|
|
||||||
ci:
|
ci:
|
||||||
autofix_prs: false
|
autofix_prs: false
|
||||||
|
|
1593
CHANGELOG.md
Normal file
1593
CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,3 +0,0 @@
|
||||||
.* @TrueCloudLab/qa-committers
|
|
||||||
.forgejo/.* @potyarkin
|
|
||||||
Makefile @potyarkin
|
|
115
CONTRIBUTING.md
115
CONTRIBUTING.md
|
@ -3,127 +3,97 @@
|
||||||
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-testcases/issues) and
|
- Check the open [issues](https://github.com/TrueCloudLab/frostfs-node/issues) and
|
||||||
[pull requests](https://git.frostfs.info/TrueCloudLab/frostfs-testcases/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.
|
||||||
|
|
||||||
- Write tests, and make sure the test suite passes locally.
|
- Write tests, and make sure the test suite passes locally and on CI.
|
||||||
|
|
||||||
- Open a pull request, and reference the relevant issue(s).
|
- Open a pull request, and reference the relevant issue(s).
|
||||||
|
|
||||||
- Make sure your commits are logically separated and have good comments
|
- Make sure your commits are logically separated and have good comments
|
||||||
explaining the details of your change.
|
explaining the details of your change.
|
||||||
|
|
||||||
- After receiving feedback, amend your commits or add new ones as appropriate.
|
- After receiving feedback, amend your commits or add new ones as
|
||||||
|
appropriate.
|
||||||
|
|
||||||
- **Have fun!**
|
- **Have fun!**
|
||||||
|
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
|
|
||||||
Start by forking the `frostfs-testcases` repository, make changes in a branch and then
|
Start by forking the `frostfs-node` repository, make changes in a branch and then
|
||||||
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 Git Repository
|
### Set up your GitHub Repository
|
||||||
|
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
|
||||||
|
need it for the `git clone` command below).
|
||||||
|
|
||||||
Fork [FrosfFS testcases upstream](https://git.frostfs.info/TrueCloudLab/frostfs-testcases/forks) source
|
```sh
|
||||||
repository to your own personal repository. Copy the URL of your fork and clone it:
|
$ git clone https://github.com/TrueCloudLab/frostfs-node
|
||||||
|
|
||||||
```shell
|
|
||||||
$ git clone <url of your fork>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Set up git remote as ``upstream``
|
### Set up git remote as ``upstream``
|
||||||
```sh
|
```sh
|
||||||
$ cd frostfs-testcases
|
$ cd frostfs-node
|
||||||
$ git remote add upstream https://git.frostfs.info/TrueCloudLab/frostfs-testcases
|
$ git remote add upstream https://github.com/TrueCloudLab/frostfs-node
|
||||||
$ git fetch upstream
|
$ git fetch upstream
|
||||||
|
$ git merge upstream/master
|
||||||
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
### Set up development environment
|
|
||||||
|
|
||||||
To setup development environment for `frosfs-testcases`, please, take the following steps:
|
|
||||||
1. Prepare virtualenv
|
|
||||||
|
|
||||||
```shell
|
|
||||||
$ make venv
|
|
||||||
$ source frostfs-testcases-3.10/bin/activate
|
|
||||||
```
|
|
||||||
|
|
||||||
Optionally you might want to integrate code formatters with your code editor to apply formatters to code files as you go:
|
|
||||||
* isort is supported by [PyCharm](https://plugins.jetbrains.com/plugin/15434-isortconnect), [VS Code](https://cereblanco.medium.com/setup-black-and-isort-in-vscode-514804590bf9). Plugins exist for other IDEs/editors as well.
|
|
||||||
* black can be integrated with multiple editors, please, instructions are available [here](https://black.readthedocs.io/en/stable/integrations/editors.html).
|
|
||||||
|
|
||||||
### Create your feature branch
|
### Create your feature branch
|
||||||
|
|
||||||
Before making code changes, make sure you create a separate branch for these
|
Before making code changes, make sure you create a separate branch for these
|
||||||
changes. Maybe you will find it convenient to name branch in
|
changes. Maybe you will find it convenient to name branch in
|
||||||
`<type>/<issue>-<changes_topic>` format.
|
`<type>/<Issue>-<changes_topic>` format.
|
||||||
|
|
||||||
```shell
|
```
|
||||||
$ git checkout -b feature/123-something_awesome
|
$ git checkout -b feature/123-something_awesome
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Test your changes
|
||||||
|
After your code changes, make sure
|
||||||
|
|
||||||
|
- To add test cases for the new code.
|
||||||
|
- To run `make lint`
|
||||||
|
- 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.
|
||||||
|
- To run `make test` and `make all` completes.
|
||||||
|
|
||||||
### Commit changes
|
### Commit changes
|
||||||
|
After verification, commit your changes. This is a [great
|
||||||
After verification, commit your changes. There is a [great
|
|
||||||
post](https://chris.beams.io/posts/git-commit/) on how to write useful commit
|
post](https://chris.beams.io/posts/git-commit/) on how to write useful commit
|
||||||
messages. Try following this template:
|
messages. Try following this template:
|
||||||
|
|
||||||
```
|
```
|
||||||
[#Issue] Summary
|
[#Issue] <component> Summary
|
||||||
|
|
||||||
Description
|
Description
|
||||||
|
|
||||||
<Macros>
|
<Macros>
|
||||||
|
|
||||||
<Sign-Off>
|
<Sign-Off>
|
||||||
```
|
```
|
||||||
|
|
||||||
```shell
|
```
|
||||||
$ git commit -am '[#123] Add some feature'
|
$ git commit -sam '[#123] Add some feature'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Push to the branch
|
### Push to the branch
|
||||||
|
Push your locally committed changes to the remote origin (your fork)
|
||||||
Push your locally committed changes to the remote origin (your fork):
|
```
|
||||||
```shell
|
|
||||||
$ git push origin feature/123-something_awesome
|
$ git push origin feature/123-something_awesome
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create a Pull Request
|
### Create a Pull Request
|
||||||
|
Pull requests can be created via GitHub. Refer to [this
|
||||||
Pull requests can be created via Git. Refer to [this
|
document](https://help.github.com/articles/creating-a-pull-request/) for
|
||||||
document](https://docs.codeberg.org/collaborating/pull-requests-and-git-flow/) 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.
|
||||||
|
|
||||||
## Code Style
|
|
||||||
|
|
||||||
The names of Python variables, functions and classes must comply with [PEP8](https://peps.python.org/pep-0008) rules, in particular:
|
|
||||||
* Name of a variable/function must be in snake_case (lowercase, with words separated by underscores as necessary to improve readability).
|
|
||||||
* Name of a global variable must be in UPPER_SNAKE_CASE, the underscore (`_`) symbol must be used as a separator between words.
|
|
||||||
* Name of a class must be in PascalCase (the first letter of each compound word in a variable name is capitalized).
|
|
||||||
* Names of other variables should not be ended with the underscore symbol.
|
|
||||||
|
|
||||||
Line length limit is set as 100 characters.
|
|
||||||
|
|
||||||
Imports should be ordered in accordance with [isort default rules](https://pycqa.github.io/isort/).
|
|
||||||
|
|
||||||
We use `black` and `isort` for code formatting. Please, refer to [Black code style](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html) for details.
|
|
||||||
|
|
||||||
Type hints are mandatory for library's code:
|
|
||||||
- class attributes;
|
|
||||||
- function or method's parameters;
|
|
||||||
- function or method's return type.
|
|
||||||
|
|
||||||
The only exception is return type of test functions or methods - there's no much use in specifying `None` as return type for each test function.
|
|
||||||
|
|
||||||
Do not use relative imports. Even if the module is in the same package, use the full package name.
|
|
||||||
|
|
||||||
To format docstrings, please, use [Google Style Docstrings](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html). Type annotations should be specified in the code and not in docstrings (please, refer to [this sample](https://sphinxcontrib-napoleon.readthedocs.io/en/latest/index.html#type-annotations)).
|
|
||||||
|
|
||||||
## DCO Sign off
|
## DCO Sign off
|
||||||
|
|
||||||
All authors to the project retain copyright to their work. However, to ensure
|
All authors to the project retain copyright to their work. However, to ensure
|
||||||
|
@ -136,7 +106,8 @@ contributors".
|
||||||
To sign your work, just add a line like this at the end of your commit message:
|
To sign your work, just add a line like this at the end of your commit message:
|
||||||
|
|
||||||
```
|
```
|
||||||
Signed-off-by: Samii Sakisaka <samii@nspcc.ru>
|
Signed-off-by: Samii Sakisaka <samii@ivunojikan.co.jp>
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This can easily be done with the `--signoff` option to `git commit`.
|
This can easily be done with the `--signoff` option to `git commit`.
|
||||||
|
@ -147,17 +118,24 @@ Certificate of Origin](https://developercertificate.org/)):
|
||||||
```
|
```
|
||||||
Developer Certificate of Origin
|
Developer Certificate of Origin
|
||||||
Version 1.1
|
Version 1.1
|
||||||
|
|
||||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||||
1 Letterman Drive
|
1 Letterman Drive
|
||||||
Suite D4700
|
Suite D4700
|
||||||
San Francisco, CA, 94129
|
San Francisco, CA, 94129
|
||||||
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies of this
|
Everyone is permitted to copy and distribute verbatim copies of this
|
||||||
license document, but changing it is not allowed.
|
license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
|
||||||
Developer's Certificate of Origin 1.1
|
Developer's Certificate of Origin 1.1
|
||||||
|
|
||||||
By making a contribution to this project, I certify that:
|
By making a contribution to this project, I certify that:
|
||||||
|
|
||||||
(a) The contribution was created in whole or in part by me and I
|
(a) The contribution was created in whole or in part by me and I
|
||||||
have the right to submit it under the open source license
|
have the right to submit it under the open source license
|
||||||
indicated in the file; or
|
indicated in the file; or
|
||||||
|
|
||||||
(b) The contribution is based upon previous work that, to the best
|
(b) The contribution is based upon previous work that, to the best
|
||||||
of my knowledge, is covered under an appropriate open source
|
of my knowledge, is covered under an appropriate open source
|
||||||
license and I have the right under that license to submit that
|
license and I have the right under that license to submit that
|
||||||
|
@ -165,13 +143,14 @@ By making a contribution to this project, I certify that:
|
||||||
by me, under the same open source license (unless I am
|
by me, under the same open source license (unless I am
|
||||||
permitted to submit under a different license), as indicated
|
permitted to submit under a different license), as indicated
|
||||||
in the file; or
|
in the file; or
|
||||||
|
|
||||||
(c) The contribution was provided directly to me by some other
|
(c) The contribution was provided directly to me by some other
|
||||||
person who certified (a), (b) or (c) and I have not modified
|
person who certified (a), (b) or (c) and I have not modified
|
||||||
it.
|
it.
|
||||||
|
|
||||||
(d) I understand and agree that this project and the contribution
|
(d) I understand and agree that this project and the contribution
|
||||||
are public and that a record of the contribution (including all
|
are public and that a record of the contribution (including all
|
||||||
personal information I submit with it, including my sign-off) is
|
personal information I submit with it, including my sign-off) is
|
||||||
maintained indefinitely and may be redistributed consistent with
|
maintained indefinitely and may be redistributed consistent with
|
||||||
this project or the open source license(s) involved.
|
this project or the open source license(s) involved.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
19
CREDITS.md
Normal file
19
CREDITS.md
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# Credits
|
||||||
|
|
||||||
|
FrostFS continues the development of FrostFS.
|
||||||
|
|
||||||
|
Initial FrostFS research and development (2022-2023) was done by
|
||||||
|
[TrueCloudLab](https://github.com/TrueCloudLab) team.
|
||||||
|
|
||||||
|
In alphabetical order:
|
||||||
|
|
||||||
|
- Alexey Vanin
|
||||||
|
- Anatoly Bogatyrev
|
||||||
|
- Evgeny Stratonikov
|
||||||
|
- Stanislav Bogatyrev
|
||||||
|
|
||||||
|
# Contributors
|
||||||
|
|
||||||
|
In chronological order:
|
||||||
|
- Pavel Karpy
|
||||||
|
|
50
Makefile
50
Makefile
|
@ -1,42 +1,28 @@
|
||||||
SHELL := /bin/bash
|
#!/usr/bin/make -f
|
||||||
PYTHON_VERSION := 3.10
|
|
||||||
VENV_NAME = frostfs-testcases-${PYTHON_VERSION}
|
|
||||||
VENV_DIR := venv.${VENV_NAME}
|
|
||||||
|
|
||||||
current_dir := $(shell pwd)
|
.DEFAULT_GOAL := help
|
||||||
FROM_VENV := . ${VENV_DIR}/bin/activate &&
|
|
||||||
|
|
||||||
venv: create requirements paths precommit
|
SHELL ?= bash
|
||||||
@echo Ready
|
|
||||||
|
|
||||||
precommit:
|
VENVS = $(shell ls -1d venv/*/ | sort -u | xargs basename -a)
|
||||||
@echo Isntalling pre-commit hooks
|
|
||||||
${FROM_VENV} pre-commit install
|
|
||||||
|
|
||||||
paths:
|
.PHONY: all
|
||||||
@echo Append paths for project
|
all: venvs
|
||||||
@echo Virtual environment: ${VENV_DIR}
|
|
||||||
@rm -rf ${VENV_DIR}/lib/python${PYTHON_VERSION}/site-packages/_paths.pth
|
|
||||||
@touch ${VENV_DIR}/lib/python${PYTHON_VERSION}/site-packages/_paths.pth
|
|
||||||
@echo ${current_dir} | tee ${VENV_DIR}/lib/python${PYTHON_VERSION}/site-packages/_paths.pth
|
|
||||||
|
|
||||||
create: ${VENV_DIR}
|
include venv_template.mk
|
||||||
|
|
||||||
${VENV_DIR}:
|
.PHONY: venvs
|
||||||
@echo Create virtual environment ${VENV_DIR}
|
venvs:
|
||||||
virtualenv --python=python${PYTHON_VERSION} --prompt=${VENV_NAME} ${VENV_DIR}
|
$(foreach venv,$(VENVS),venv.$(venv))
|
||||||
|
|
||||||
requirements:
|
$(foreach venv,$(VENVS),$(eval $(call VENV_template,$(venv))))
|
||||||
@echo Isntalling pip requirements
|
|
||||||
${FROM_VENV} pip install -e ../frostfs-testlib
|
|
||||||
${FROM_VENV} pip install -Ur requirements.txt
|
|
||||||
${FROM_VENV} pip install -Ur requirements_dev.txt
|
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf venv.*
|
||||||
|
|
||||||
#### VALIDATION SECTION ####
|
pytest-local:
|
||||||
lint: create requirements
|
@echo "⇒ Run Pytest"
|
||||||
${FROM_VENV} pip install -e ../frostfs-testlib;
|
python -m pytest pytest_tests/testsuites/
|
||||||
${FROM_VENV} pylint --disable R,C,W pytest_tests
|
|
||||||
|
|
||||||
validation: lint
|
help:
|
||||||
${FROM_VENV} pytest --collect-only
|
@echo "⇒ run Run testcases ${R}"
|
||||||
|
|
44
README.md
44
README.md
|
@ -2,58 +2,70 @@
|
||||||
|
|
||||||
Tests written with PyTest Framework are located under `pytest_tests/testsuites` directory.
|
Tests written with PyTest Framework are located under `pytest_tests/testsuites` directory.
|
||||||
|
|
||||||
These tests rely on resources and utility modules that have been originally developed for Pytest Framework.
|
These tests rely on resources and utility modules that have been originally developed for Robot Framework:
|
||||||
|
|
||||||
|
`robot/resources/files` - static files that are used in tests' commands.
|
||||||
|
|
||||||
|
`robot/resources/lib/` - common Python libraries that provide utility functions used as building blocks in tests.
|
||||||
|
|
||||||
|
`robot/variables/` - constants and configuration variables for tests.
|
||||||
|
|
||||||
## Testcases execution
|
## Testcases execution
|
||||||
|
|
||||||
### Initial preparation
|
### Initial preparation
|
||||||
|
|
||||||
1. Install frostfs-cli
|
1. Install frostfs-cli
|
||||||
- `git clone git@git.frostfs.info:TrueCloudLab/frostfs-node.git`
|
- `git clone git@github.com:nspcc-dev/frostfs-node.git`
|
||||||
- `cd frostfs-node`
|
- `cd frostfs-node`
|
||||||
- `make`
|
- `make`
|
||||||
- `sudo cp bin/frostfs-cli /usr/local/bin/frostfs-cli`
|
- `sudo cp bin/frostfs-cli /usr/local/bin/frostfs-cli`
|
||||||
|
|
||||||
2. Install frostfs-s3-authmate
|
2. Install frostfs-authmate
|
||||||
- `git clone git@git.frostfs.info:TrueCloudLab/frostfs-s3-gw.git`
|
- `git clone git@github.com:nspcc-dev/frostfs-s3-gw.git`
|
||||||
- `cd frostfs-s3-gw`
|
- `cd frostfs-s3-gw`
|
||||||
- `make`
|
- `make`
|
||||||
- `sudo cp bin/frostfs-s3-authmate /usr/local/bin/frostfs-s3-authmate`
|
- `sudo cp bin/frostfs-authmate /usr/local/bin/frostfs-authmate`
|
||||||
|
|
||||||
3. Install neo-go
|
3. Install neo-go
|
||||||
- `git clone git@git.frostfs.info:TrueCloudLab/neo-go.git`
|
- `git clone git@github.com:nspcc-dev/neo-go.git`
|
||||||
- `cd neo-go`
|
- `cd neo-go`
|
||||||
- `git checkout v0.101.0` (or the current version in the frostfs-dev-env)
|
- `git checkout v0.92.0` (or the current version in the frostfs-dev-env)
|
||||||
- `make`
|
- `make`
|
||||||
- `sudo cp bin/neo-go /usr/local/bin/neo-go`
|
- `sudo cp bin/neo-go /usr/local/bin/neo-go`
|
||||||
or download binary from releases: https://git.frostfs.info/TrueCloudLab/neo-go/releases
|
or download binary from releases: https://github.com/nspcc-dev/neo-go/releases
|
||||||
|
|
||||||
4. Clone frostfs-dev-env
|
4. Clone frostfs-dev-env
|
||||||
`git clone git@git.frostfs.info:TrueCloudLab/frostfs-dev-env.git`
|
`git clone git@github.com:nspcc-dev/frostfs-dev-env.git`
|
||||||
|
|
||||||
Note that we expect frostfs-dev-env to be located under
|
Note that we expect frostfs-dev-env to be located under
|
||||||
the `<testcases_root_dir>/../frostfs-dev-env` directory. If you put this repo in any other place,
|
the `<testcases_root_dir>/../frostfs-dev-env` directory. If you put this repo in any other place,
|
||||||
manually set the full path to frostfs-dev-env in the environment variable `DEVENV_PATH` at this step.
|
manually set the full path to frostfs-dev-env in the environment variable `DEVENV_PATH` at this step.
|
||||||
|
|
||||||
5. Make sure you have installed all the following prerequisites on your machine
|
5. Make sure you have installed all of the following prerequisites on your machine
|
||||||
|
|
||||||
```
|
```
|
||||||
make
|
make
|
||||||
python3.10
|
python3.9
|
||||||
python3.10-dev
|
python3.9-dev
|
||||||
libssl-dev
|
libssl-dev
|
||||||
```
|
```
|
||||||
As we use frostfs-dev-env, you'll also need to install
|
As we use frostfs-dev-env, you'll also need to install
|
||||||
[prerequisites](https://git.frostfs.info/TrueCloudLab/frostfs-dev-env#prerequisites) of this repository.
|
[prerequisites](https://github.com/nspcc-dev/frostfs-dev-env#prerequisites) of this repository.
|
||||||
|
|
||||||
6. Prepare virtualenv
|
6. Prepare virtualenv
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ make venv
|
$ make venv.local-pytest
|
||||||
$ source venv.frostfs-testcases-3.10/bin/activate
|
$ . venv.local-pytest/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
7. Optionally you might want to integrate code formatters with your code editor to apply formatters to code files as you go:
|
7. Setup pre-commit hooks to run code formatters on staged files before you run a `git commit` command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ pre-commit install
|
||||||
|
```
|
||||||
|
|
||||||
|
Optionally you might want to integrate code formatters with your code editor to apply formatters to code files as you go:
|
||||||
* isort is supported by [PyCharm](https://plugins.jetbrains.com/plugin/15434-isortconnect), [VS Code](https://cereblanco.medium.com/setup-black-and-isort-in-vscode-514804590bf9). Plugins exist for other IDEs/editors as well.
|
* isort is supported by [PyCharm](https://plugins.jetbrains.com/plugin/15434-isortconnect), [VS Code](https://cereblanco.medium.com/setup-black-and-isort-in-vscode-514804590bf9). Plugins exist for other IDEs/editors as well.
|
||||||
* black can be integrated with multiple editors, please, instructions are available [here](https://black.readthedocs.io/en/stable/integrations/editors.html).
|
* black can be integrated with multiple editors, please, instructions are available [here](https://black.readthedocs.io/en/stable/integrations/editors.html).
|
||||||
|
|
||||||
|
|
1
VERSION
Normal file
1
VERSION
Normal file
|
@ -0,0 +1 @@
|
||||||
|
v0.35.0
|
31
build_assets/activate.patch
Normal file
31
build_assets/activate.patch
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
diff -urN bin.orig/activate bin/activate
|
||||||
|
--- bin.orig/activate 2018-12-27 14:55:13.916461020 +0900
|
||||||
|
+++ bin/activate 2018-12-27 20:38:35.223248728 +0900
|
||||||
|
@@ -30,6 +30,15 @@
|
||||||
|
unset _OLD_VIRTUAL_PS1
|
||||||
|
fi
|
||||||
|
|
||||||
|
+ # Unset exported dev-env variables
|
||||||
|
+ pushd ${DEVENV_PATH} > /dev/null
|
||||||
|
+ unset `make env | awk -F= '{print $1}'`
|
||||||
|
+ popd > /dev/null
|
||||||
|
+
|
||||||
|
+ # Unset external env variables
|
||||||
|
+ declare -f env_deactivate > /dev/null && env_deactivate
|
||||||
|
+ declare -f venv_deactivate > /dev/null && venv_deactivate
|
||||||
|
+
|
||||||
|
unset VIRTUAL_ENV
|
||||||
|
if [ ! "${1-}" = "nondestructive" ] ; then
|
||||||
|
# Self destruct!
|
||||||
|
@@ -47,6 +56,11 @@
|
||||||
|
PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
export PATH
|
||||||
|
|
||||||
|
+# Set external variables
|
||||||
|
+if [ -f ${VIRTUAL_ENV}/bin/environment.sh ] ; then
|
||||||
|
+ . ${VIRTUAL_ENV}/bin/environment.sh
|
||||||
|
+fi
|
||||||
|
+
|
||||||
|
# unset PYTHONHOME if set
|
||||||
|
if ! [ -z "${PYTHONHOME+_}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PYTHONHOME="$PYTHONHOME"
|
104
cmd/frostfs-adm/README.md
Normal file
104
cmd/frostfs-adm/README.md
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
# FrostFS Admin Tool
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Admin tool provides an easier way to deploy and maintain private installation
|
||||||
|
of FrostFS. Private installation provides a set of N3 consensus nodes, FrostFS
|
||||||
|
Alphabet, and Storage nodes. Admin tool generates consensus keys, initializes
|
||||||
|
the sidechain, and provides functions to update the network and register new
|
||||||
|
Storage nodes.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
To build binary locally, use `make bin/frostfs-adm` command.
|
||||||
|
|
||||||
|
For clean build inside a docker container, use `make docker/bin/frostfs-adm`.
|
||||||
|
|
||||||
|
Build docker image with `make image-adm`.
|
||||||
|
|
||||||
|
At FrostFS private install deployment, frostfs-adm requires compiled FrostFS
|
||||||
|
contracts. Find them in the latest release of
|
||||||
|
[frostfs-contract repository](https://github.com/TrueCloudLab/frostfs-contract/releases).
|
||||||
|
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Config
|
||||||
|
|
||||||
|
Config section provides `init` command that creates a configuration file for
|
||||||
|
private installation deployment and updates. Config file is optional, all
|
||||||
|
parameters can be passed by arguments or read from standard input (wallet
|
||||||
|
passwords).
|
||||||
|
|
||||||
|
Config example:
|
||||||
|
```yaml
|
||||||
|
rpc-endpoint: https://address:port # sidechain RPC node endpoint
|
||||||
|
alphabet-wallets: /path # path to consensus node / alphabet wallets storage
|
||||||
|
network:
|
||||||
|
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
|
||||||
|
basic_income_rate: 0 # basic income rate, for private consider 0
|
||||||
|
fee:
|
||||||
|
audit: 0 # network audit 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_alias: 0 # container nice-name registration fee, for private installation consider 0
|
||||||
|
withdraw: 0 # withdraw fee, for private installation consider 0
|
||||||
|
credentials: # passwords for consensus node / alphabet wallets
|
||||||
|
az: password1
|
||||||
|
buky: password2
|
||||||
|
vedi: password3
|
||||||
|
glagoli: password4
|
||||||
|
dobro: password5
|
||||||
|
yest: password6
|
||||||
|
zhivete: password7
|
||||||
|
```
|
||||||
|
|
||||||
|
### Morph
|
||||||
|
|
||||||
|
#### Network deployment
|
||||||
|
|
||||||
|
- `generate-alphabet` generates a set of wallets for consensus and
|
||||||
|
Alphabet nodes.
|
||||||
|
|
||||||
|
- `init` initializes the sidechain by deploying smart contracts and
|
||||||
|
setting provided FrostFS network configuration.
|
||||||
|
|
||||||
|
- `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
|
||||||
|
wallet can be used for FrostFS bootstrap.
|
||||||
|
|
||||||
|
#### Network maintenance
|
||||||
|
|
||||||
|
- `set-config` add/update configuration values in the Netmap contract.
|
||||||
|
|
||||||
|
- `force-new-epoch` increments FrostFS epoch number and executes new epoch
|
||||||
|
handlers in FrostFS nodes.
|
||||||
|
|
||||||
|
- `refill-gas` transfers sidechain GAS to the specified wallet.
|
||||||
|
|
||||||
|
- `update-contracts` updates contracts to a new version.
|
||||||
|
|
||||||
|
#### Container migration
|
||||||
|
|
||||||
|
If a network has to be redeployed, these commands will migrate all container meta
|
||||||
|
info. These commands **do not migrate actual objects**.
|
||||||
|
|
||||||
|
- `dump-containers` saves all containers and metadata registered in the container
|
||||||
|
contract to a file.
|
||||||
|
|
||||||
|
- `restore-containers` restores previously saved containers by their repeated registration in
|
||||||
|
the container contract.
|
||||||
|
|
||||||
|
- `list-containers` output all containers ids.
|
||||||
|
|
||||||
|
#### Network info
|
||||||
|
|
||||||
|
- `dump-config` prints FrostFS network configuration.
|
||||||
|
|
||||||
|
- `dump-hashes` prints FrostFS contract addresses stored in NNS.
|
||||||
|
|
||||||
|
|
||||||
|
## Private network deployment
|
||||||
|
|
||||||
|
Read step-by-step guide of private storage deployment [in docs](./docs/deploy.md).
|
211
cmd/frostfs-adm/docs/deploy.md
Normal file
211
cmd/frostfs-adm/docs/deploy.md
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
# Step-by-step private FrostFS deployment
|
||||||
|
|
||||||
|
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
|
||||||
|
consensus, Alphabet, or Storage nodes. This guide covers only `frostfs-adm`
|
||||||
|
related configuration details.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
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 [frostfs-adm](https://github.com/TrueCloudLab/frostfs-node/releases) utility (v0.25.1 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
|
||||||
|
|
||||||
|
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
|
||||||
|
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.
|
||||||
|
|
||||||
|
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
|
||||||
|
consensus / Alphabet node in the network.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ frostfs-adm config init --path foo.network.yml
|
||||||
|
Initial config file saved to foo.network.yml
|
||||||
|
|
||||||
|
$ cat foo.network.yml
|
||||||
|
rpc-endpoint: https://neo.rpc.node:30333
|
||||||
|
alphabet-wallets: /home/user/deploy/alphabet-wallets
|
||||||
|
network:
|
||||||
|
max_object_size: 67108864
|
||||||
|
epoch_duration: 240
|
||||||
|
basic_income_rate: 0
|
||||||
|
fee:
|
||||||
|
audit: 0
|
||||||
|
candidate: 0
|
||||||
|
container: 0
|
||||||
|
withdraw: 0
|
||||||
|
credentials:
|
||||||
|
az: hunter2
|
||||||
|
```
|
||||||
|
|
||||||
|
For private installation, it is recommended to set all **fees** and **basic
|
||||||
|
income rate** to 0.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
For **max object size**, 67108864 (64 MiB) or 134217728 (128 MiB) should provide
|
||||||
|
good chunk distribution in most cases.
|
||||||
|
|
||||||
|
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 already exists.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ frostfs-adm -c foo.network.yml morph generate-alphabet --size 1
|
||||||
|
size: 1
|
||||||
|
alphabet-wallets: /home/user/deploy/alphabet-wallets
|
||||||
|
wallet[0]: hunter2
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not lose wallet files and network config. Store it in an encrypted backed up
|
||||||
|
storage.
|
||||||
|
|
||||||
|
## Step 2: Launch consensus nodes
|
||||||
|
|
||||||
|
Configure blockchain nodes with the generated wallets from the previous step.
|
||||||
|
Config examples can be found in
|
||||||
|
[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
|
||||||
|
`simple signature contract` public key.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ neo-go wallet dump-keys -w alphabet-wallets/az.json
|
||||||
|
NitdS4k4f1Hh5mbLJhAswBK3WC2gQgPN1o (simple signature contract):
|
||||||
|
02c1cc85f9c856dbe2d02017349bcb7b4e5defa78b8056a09b3240ba2a8c078869
|
||||||
|
|
||||||
|
NiMKabp3ddi3xShmLAXhTfbnuWb4cSJT6E (1 out of 1 multisig contract):
|
||||||
|
02c1cc85f9c856dbe2d02017349bcb7b4e5defa78b8056a09b3240ba2a8c078869
|
||||||
|
|
||||||
|
NiMKabp3ddi3xShmLAXhTfbnuWb4cSJT6E (1 out of 1 multisig contract):
|
||||||
|
02c1cc85f9c856dbe2d02017349bcb7b4e5defa78b8056a09b3240ba2a8c078869
|
||||||
|
```
|
||||||
|
|
||||||
|
Put the list of public keys into `ProtocolConfiguration.StandbyCommittee`
|
||||||
|
section. Specify the wallet path and the password in `ApplicationConfiguration.P2PNotary`
|
||||||
|
and `ApplicationConfiguration.UnlockWallet` sections. If config includes
|
||||||
|
`ProtocolConfiguration.NativeActivations` section, add notary
|
||||||
|
contract `Notary: [0]`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ProtocolConfiguration:
|
||||||
|
StandbyCommittee:
|
||||||
|
- 02c1cc85f9c856dbe2d02017349bcb7b4e5defa78b8056a09b3240ba2a8c078869
|
||||||
|
NativeActivations:
|
||||||
|
Notary: [0]
|
||||||
|
ApplicationConfiguration:
|
||||||
|
P2PNotary:
|
||||||
|
Enabled: true
|
||||||
|
UnlockWallet:
|
||||||
|
Path: "/home/user/deploy/alphabet-wallets/az.json"
|
||||||
|
Password: "hunter2"
|
||||||
|
UnlockWallet:
|
||||||
|
Path: "/home/user/deploy/alphabet-wallets/az.json"
|
||||||
|
Password: "hunter2"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, launch consensus nodes. They should connect to each other and start
|
||||||
|
producing blocks in consensus. You might want to deploy additional RPC
|
||||||
|
nodes at this stage because Storage nodes should be connected to the chain too.
|
||||||
|
It is not recommended to use a consensus node as an RPC node due to security policies
|
||||||
|
and possible overload issues.
|
||||||
|
|
||||||
|
## Step 3: Initialize sidechain
|
||||||
|
|
||||||
|
Use archive with compiled FrostFS contracts to initialize the sidechain.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ tar -xzvf frostfs-contract-v0.11.0.tar.gz
|
||||||
|
|
||||||
|
$ ./frostfs-adm -c foo.network.yml morph init --contracts ./frostfs-contract-v0.11.0
|
||||||
|
Stage 1: transfer GAS to alphabet nodes.
|
||||||
|
Waiting for transactions to persist...
|
||||||
|
Stage 2: set notary and alphabet nodes in designate contract.
|
||||||
|
Waiting for transactions to persist...
|
||||||
|
Stage 3: deploy NNS contract.
|
||||||
|
Waiting for transactions to persist...
|
||||||
|
Stage 4: deploy FrostFS contracts.
|
||||||
|
Waiting for transactions to persist...
|
||||||
|
Stage 4.1: Transfer GAS to proxy contract.
|
||||||
|
Waiting for transactions to persist...
|
||||||
|
Stage 5: register candidates.
|
||||||
|
Waiting for transactions to persist...
|
||||||
|
Stage 6: transfer NEO to alphabet contracts.
|
||||||
|
Waiting for transactions to persist...
|
||||||
|
Stage 7: set addresses in NNS.
|
||||||
|
Waiting for transactions to persist...
|
||||||
|
NNS: Set alphabet0.frostfs -> f692dfb4d43a15b464eb51a7041160fb29c44b6a
|
||||||
|
NNS: Set audit.frostfs -> 7df847b993affb3852074345a7c2bd622171ee0d
|
||||||
|
NNS: Set balance.frostfs -> 103519b3067a66307080a66570c0491ee8f68879
|
||||||
|
NNS: Set container.frostfs -> cae60bdd689d185901e495352d0247752ce50846
|
||||||
|
NNS: Set frostfsid.frostfs -> c421fb60a3895865a8f24d197d6a80ef686041d2
|
||||||
|
NNS: Set netmap.frostfs -> 894eb854632f50fb124412ce7951ebc00763525e
|
||||||
|
NNS: Set proxy.frostfs -> ac6e6fe4b373d0ca0ca4969d1e58fa0988724e7d
|
||||||
|
NNS: Set reputation.frostfs -> 6eda57c9d93d990573646762d1fea327ce41191f
|
||||||
|
Waiting for transactions to persist...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: Launch Alphabet nodes
|
||||||
|
|
||||||
|
Configure Alphabet nodes with the wallets generated in step 1. For
|
||||||
|
`morph.validators` use a list of public keys from
|
||||||
|
`ProtocolConfiguration.StandbyCommittee`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
wallet:
|
||||||
|
path: "/home/user/deploy/alphabet-wallets/az.json"
|
||||||
|
password: "hunter2"
|
||||||
|
account: "NitdS4k4f1Hh5mbLJhAswBK3WC2gQgPN1o"
|
||||||
|
|
||||||
|
morph:
|
||||||
|
validators:
|
||||||
|
- 02c1cc85f9c856dbe2d02017349bcb7b4e5defa78b8056a09b3240ba2a8c078869
|
||||||
|
|
||||||
|
contracts:
|
||||||
|
alphabet:
|
||||||
|
amount: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Step 4: Launch Storage node
|
||||||
|
|
||||||
|
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
|
||||||
|
New password >
|
||||||
|
Waiting for transactions to persist...
|
||||||
|
|
||||||
|
$ neo-go wallet dump-keys -w sn01.json
|
||||||
|
Ngr7p8Z9S22XDH6VkUG9oXobv8zZRAWwwv (simple signature contract):
|
||||||
|
0355eccb72cd46f09a3e5237eaa0f4949cceb5ecfa5a225bd3bb9fd021c4d75b85
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure the Storage node to use this wallet.
|
||||||
|
|
||||||
|
```
|
||||||
|
node:
|
||||||
|
wallet:
|
||||||
|
path: "/home/user/deploy/sn01.json"
|
||||||
|
address: "Ngr7p8Z9S22XDH6VkUG9oXobv8zZRAWwwv"
|
||||||
|
password: "foobar"
|
||||||
|
```
|
||||||
|
|
||||||
|
The storage node will be included in the network map in the next FrostFS epoch. To
|
||||||
|
speed up this process, you can increment epoch counter immediately.
|
||||||
|
|
||||||
|
```
|
||||||
|
$ frostfs-adm -c foo.network.yml morph force-new-epoch
|
||||||
|
Current epoch: 8, increase to 9.
|
||||||
|
Waiting for transactions to persist...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
After that, FrostFS Storage is ready to work. You can access it directly or
|
||||||
|
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>
|
||||||
|
```
|
166
cmd/frostfs-adm/internal/modules/config/config.go
Normal file
166
cmd/frostfs-adm/internal/modules/config/config.go
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/pkg/innerring"
|
||||||
|
"github.com/nspcc-dev/neo-go/cli/input"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type configTemplate struct {
|
||||||
|
Endpoint string
|
||||||
|
AlphabetDir string
|
||||||
|
MaxObjectSize int
|
||||||
|
EpochDuration int
|
||||||
|
BasicIncomeRate int
|
||||||
|
AuditFee int
|
||||||
|
CandidateFee int
|
||||||
|
ContainerFee int
|
||||||
|
ContainerAliasFee int
|
||||||
|
WithdrawFee int
|
||||||
|
Glagolitics []string
|
||||||
|
HomomorphicHashDisabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
const configTxtTemplate = `rpc-endpoint: {{ .Endpoint}}
|
||||||
|
alphabet-wallets: {{ .AlphabetDir}}
|
||||||
|
network:
|
||||||
|
max_object_size: {{ .MaxObjectSize}}
|
||||||
|
epoch_duration: {{ .EpochDuration}}
|
||||||
|
basic_income_rate: {{ .BasicIncomeRate}}
|
||||||
|
homomorphic_hash_disabled: {{ .HomomorphicHashDisabled}}
|
||||||
|
fee:
|
||||||
|
audit: {{ .AuditFee}}
|
||||||
|
candidate: {{ .CandidateFee}}
|
||||||
|
container: {{ .ContainerFee}}
|
||||||
|
container_alias: {{ .ContainerAliasFee }}
|
||||||
|
withdraw: {{ .WithdrawFee}}
|
||||||
|
# if credentials section is omitted, then frostfs-adm will require manual password input
|
||||||
|
credentials:
|
||||||
|
contract: password # wallet for contract group signature{{ range.Glagolitics}}
|
||||||
|
{{.}}: password{{end}}
|
||||||
|
`
|
||||||
|
|
||||||
|
func initConfig(cmd *cobra.Command, args []string) error {
|
||||||
|
configPath, err := readConfigPathFromArgs(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pathDir := filepath.Dir(configPath)
|
||||||
|
err = os.MkdirAll(pathDir, 0700)
|
||||||
|
if err != nil {
|
||||||
|
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, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open %s: %w", configPath, err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
configText, err := generateConfigExample(pathDir, 7)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = f.WriteString(configText)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("writing to %s: %w", configPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Printf("Initial config file saved to %s\n", configPath)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readConfigPathFromArgs(cmd *cobra.Command) (string, error) {
|
||||||
|
configPath, err := cmd.Flags().GetString(configPathFlag)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if configPath != "" {
|
||||||
|
return configPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultConfigPath()
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultConfigPath() (string, error) {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("getting home dir path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(home, ".frostfs", "adm", "config.yml"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateConfigExample builds .yml representation of the config file. It is
|
||||||
|
// easier to build it manually with template instead of using viper, because we
|
||||||
|
// want to order records in specific order in file and, probably, provide
|
||||||
|
// some comments as well.
|
||||||
|
func generateConfigExample(appDir string, credSize int) (string, error) {
|
||||||
|
tmpl := configTemplate{
|
||||||
|
Endpoint: "https://neo.rpc.node:30333",
|
||||||
|
MaxObjectSize: 67108864, // 64 MiB
|
||||||
|
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
|
||||||
|
AuditFee: 1_0000, // 0.00000001 GAS per audit (Fixed12)
|
||||||
|
CandidateFee: 100_0000_0000, // 100.0 GAS (Fixed8)
|
||||||
|
ContainerFee: 1000, // 0.000000001 * 7 GAS per container (Fixed12)
|
||||||
|
ContainerAliasFee: 500, // ContainerFee / 2
|
||||||
|
WithdrawFee: 1_0000_0000, // 1.0 GAS (Fixed8)
|
||||||
|
Glagolitics: make([]string, 0, credSize),
|
||||||
|
}
|
||||||
|
|
||||||
|
appDir, err := filepath.Abs(appDir)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("making absolute path for %s: %w", appDir, err)
|
||||||
|
}
|
||||||
|
tmpl.AlphabetDir = filepath.Join(appDir, "alphabet-wallets")
|
||||||
|
|
||||||
|
var i innerring.GlagoliticLetter
|
||||||
|
for i = 0; i < innerring.GlagoliticLetter(credSize); i++ {
|
||||||
|
tmpl.Glagolitics = append(tmpl.Glagolitics, i.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := template.New("config.yml").Parse(configTxtTemplate)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parsing config template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
|
||||||
|
err = t.Execute(buf, tmpl)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("generating config from template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPassword(v *viper.Viper, name string) (string, error) {
|
||||||
|
key := "credentials." + name
|
||||||
|
if v.IsSet(key) {
|
||||||
|
return v.GetString(key), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt := "Password for " + name + " wallet > "
|
||||||
|
return input.ReadPassword(prompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetStoragePassword(v *viper.Viper, name string) (string, error) {
|
||||||
|
key := "storage." + name
|
||||||
|
if name != "" && v.IsSet(key) {
|
||||||
|
return v.GetString(key), nil
|
||||||
|
}
|
||||||
|
return input.ReadPassword("New password > ")
|
||||||
|
}
|
46
cmd/frostfs-adm/internal/modules/config/config_test.go
Normal file
46
cmd/frostfs-adm/internal/modules/config/config_test.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/pkg/innerring"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateConfigExample(t *testing.T) {
|
||||||
|
const (
|
||||||
|
n = 10
|
||||||
|
appDir = "/home/example/.frostfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
configText, err := generateConfigExample(appDir, n)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
v := viper.New()
|
||||||
|
v.SetConfigType("yml")
|
||||||
|
|
||||||
|
require.NoError(t, v.ReadConfig(bytes.NewBufferString(configText)))
|
||||||
|
|
||||||
|
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, 67108864, v.GetInt("network.max_object_size"))
|
||||||
|
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, 1000, v.GetInt("network.fee.container"))
|
||||||
|
require.Equal(t, 100000000, v.GetInt("network.fee.withdraw"))
|
||||||
|
|
||||||
|
var i innerring.GlagoliticLetter
|
||||||
|
for i = 0; i < innerring.GlagoliticLetter(n); i++ {
|
||||||
|
key := "credentials." + i.String()
|
||||||
|
require.Equal(t, "password", v.GetString(key))
|
||||||
|
}
|
||||||
|
require.Equal(t, "password", v.GetString("credentials.contract"))
|
||||||
|
|
||||||
|
key := "credentials." + i.String()
|
||||||
|
require.Equal(t, "", v.GetString(key))
|
||||||
|
}
|
29
cmd/frostfs-adm/internal/modules/config/root.go
Normal file
29
cmd/frostfs-adm/internal/modules/config/root.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const configPathFlag = "path"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// RootCmd is a root command of config section.
|
||||||
|
RootCmd = &cobra.Command{
|
||||||
|
Use: "config",
|
||||||
|
Short: "Section for frostfs-adm config related commands",
|
||||||
|
}
|
||||||
|
|
||||||
|
initCmd = &cobra.Command{
|
||||||
|
Use: "init",
|
||||||
|
Short: "Initialize basic frostfs-adm configuration file",
|
||||||
|
Example: `frostfs-adm config init
|
||||||
|
frostfs-adm config init --path .config/frostfs-adm.yml`,
|
||||||
|
RunE: initConfig,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.AddCommand(initCmd)
|
||||||
|
|
||||||
|
initCmd.Flags().String(configPathFlag, "", "Path to config (default ~/.frostfs/adm/config.yml)")
|
||||||
|
}
|
24
cmd/frostfs-adm/internal/modules/config/util.go
Normal file
24
cmd/frostfs-adm/internal/modules/config/util.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResolveHomePath replaces leading `~`
|
||||||
|
// with home directory.
|
||||||
|
//
|
||||||
|
// Does nothing if path does not start
|
||||||
|
// with contain `~`.
|
||||||
|
func ResolveHomePath(path string) string {
|
||||||
|
homeDir, _ := os.UserHomeDir()
|
||||||
|
|
||||||
|
if path == "~" {
|
||||||
|
path = homeDir
|
||||||
|
} else if strings.HasPrefix(path, "~/") {
|
||||||
|
path = filepath.Join(homeDir, path[2:])
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
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
|
||||||
|
}
|
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)
|
||||||
|
}
|
373
cmd/frostfs-adm/internal/modules/morph/container.go
Normal file
373
cmd/frostfs-adm/internal/modules/morph/container.go
Normal file
|
@ -0,0 +1,373 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
|
||||||
|
"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/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"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errInvalidContainerResponse = errors.New("invalid response from container contract")
|
||||||
|
|
||||||
|
func getContainerContractHash(cmd *cobra.Command, inv *invoker.Invoker, c Client) (util.Uint160, error) {
|
||||||
|
s, err := cmd.Flags().GetString(containerContractFlag)
|
||||||
|
var ch util.Uint160
|
||||||
|
if err == nil {
|
||||||
|
ch, err = util.Uint160DecodeStringLE(s)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
nnsCs, err := c.GetContractStateByID(1)
|
||||||
|
if err != nil {
|
||||||
|
return util.Uint160{}, fmt.Errorf("can't get NNS contract state: %w", err)
|
||||||
|
}
|
||||||
|
ch, err = nnsResolveHash(inv, nnsCs.Hash, containerContract+".frostfs")
|
||||||
|
if err != nil {
|
||||||
|
return util.Uint160{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getContainersList(inv *invoker.Invoker, ch util.Uint160) ([][]byte, error) {
|
||||||
|
res, err := inv.Call(ch, "list", "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", errInvalidContainerResponse, err)
|
||||||
|
}
|
||||||
|
itm, err := unwrap.Item(res, err)
|
||||||
|
if _, ok := itm.(stackitem.Null); !ok {
|
||||||
|
return unwrap.ArrayOfBytes(res, err)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dumpContainers(cmd *cobra.Command, _ []string) error {
|
||||||
|
filename, err := cmd.Flags().GetString(containerDumpFlag)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid filename: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := getN3Client(viper.GetViper())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't create N3 client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inv := invoker.New(c, nil)
|
||||||
|
|
||||||
|
ch, err := getContainerContractHash(cmd, inv, c)
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var containers []*Container
|
||||||
|
bw := io.NewBufBinWriter()
|
||||||
|
for _, id := range cids {
|
||||||
|
if !isOK(id) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bw.Reset()
|
||||||
|
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 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
cnt := new(Container)
|
||||||
|
err = cnt.FromStackItem(res.Stack[0])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", errInvalidContainerResponse, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ea := new(EACL)
|
||||||
|
err = ea.FromStackItem(res.Stack[1])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %v", errInvalidContainerResponse, err)
|
||||||
|
}
|
||||||
|
if len(ea.Value) != 0 {
|
||||||
|
cnt.EACL = ea
|
||||||
|
}
|
||||||
|
|
||||||
|
containers = append(containers, cnt)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := json.Marshal(containers)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(filename, out, 0o660)
|
||||||
|
}
|
||||||
|
|
||||||
|
func listContainers(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)
|
||||||
|
|
||||||
|
ch, err := getContainerContractHash(cmd, inv, c)
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range cids {
|
||||||
|
var idCnr cid.ID
|
||||||
|
err = idCnr.Decode(id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to decode container id: %w", err)
|
||||||
|
}
|
||||||
|
cmd.Println(idCnr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreContainers(cmd *cobra.Command, _ []string) error {
|
||||||
|
filename, err := cmd.Flags().GetString(containerDumpFlag)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid filename: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wCtx, err := newInitializeContext(cmd, viper.GetViper())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer wCtx.close()
|
||||||
|
|
||||||
|
nnsCs, err := wCtx.Client.GetContractStateByID(1)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't get NNS contract state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ch, err := nnsResolveHash(wCtx.ReadOnlyInvoker, nnsCs.Hash, containerContract+".frostfs")
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
bw := io.NewBufBinWriter()
|
||||||
|
for _, cnt := range containers {
|
||||||
|
hv := hash.Sha256(cnt.Value)
|
||||||
|
if !isOK(hv[:]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bw.Reset()
|
||||||
|
emit.AppCall(bw.BinWriter, ch, "get", callflag.All, hv.BytesBE())
|
||||||
|
res, err := wCtx.Client.InvokeScript(bw.Bytes(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't check if container is already restored: %w", err)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
bw.Reset()
|
||||||
|
emit.AppCall(bw.BinWriter, ch, "put", callflag.All,
|
||||||
|
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 {
|
||||||
|
panic(bw.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wCtx.sendConsensusTx(bw.Bytes()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return wCtx.awaitTx()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Container represents container struct in contract storage.
|
||||||
|
type Container struct {
|
||||||
|
Value []byte `json:"value"`
|
||||||
|
Signature []byte `json:"signature"`
|
||||||
|
PublicKey []byte `json:"public_key"`
|
||||||
|
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.
|
||||||
|
func (c *Container) 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 *Container) 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 container value")
|
||||||
|
}
|
||||||
|
|
||||||
|
sig, err := arr[1].TryBytes()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("invalid container signature")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub, err := arr[2].TryBytes()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("invalid container public key")
|
||||||
|
}
|
||||||
|
|
||||||
|
tok, err := arr[3].TryBytes()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("invalid container token")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Value = value
|
||||||
|
c.Signature = sig
|
||||||
|
c.PublicKey = pub
|
||||||
|
c.Token = tok
|
||||||
|
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.
|
||||||
|
// Raw byte slices are used because it works with structures returned
|
||||||
|
// from contract.
|
||||||
|
func getCIDFilterFunc(cmd *cobra.Command) (func([]byte) bool, error) {
|
||||||
|
rawIDs, err := cmd.Flags().GetStringSlice(containerIDsFlag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(rawIDs) == 0 {
|
||||||
|
return func([]byte) bool { return true }, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range rawIDs {
|
||||||
|
err := new(cid.ID).DecodeString(rawIDs[i])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't parse CID %s: %w", rawIDs[i], err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(rawIDs)
|
||||||
|
return func(rawID []byte) bool {
|
||||||
|
var v [32]byte
|
||||||
|
copy(v[:], rawID)
|
||||||
|
|
||||||
|
var id cid.ID
|
||||||
|
id.SetSHA256(v)
|
||||||
|
idStr := id.EncodeToString()
|
||||||
|
n := sort.Search(len(rawIDs), func(i int) bool { return rawIDs[i] >= idStr })
|
||||||
|
return n < len(rawIDs) && rawIDs[n] == idStr
|
||||||
|
}, 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
|
||||||
|
}
|
217
cmd/frostfs-adm/internal/modules/morph/dump_hashes.go
Normal file
217
cmd/frostfs-adm/internal/modules/morph/dump_hashes.go
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-contract/nns"
|
||||||
|
"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/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"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
const lastGlagoliticLetter = 41
|
||||||
|
|
||||||
|
type contractDumpInfo struct {
|
||||||
|
hash util.Uint160
|
||||||
|
name string
|
||||||
|
version string
|
||||||
|
}
|
||||||
|
|
||||||
|
func dumpContractHashes(cmd *cobra.Command, _ []string) error {
|
||||||
|
c, err := getN3Client(viper.GetViper())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't create N3 client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cs, err := c.GetContractStateByID(1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
zone, _ := cmd.Flags().GetString(customZoneFlag)
|
||||||
|
if zone != "" {
|
||||||
|
return dumpCustomZoneHashes(cmd, cs.Hash, zone, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
infos := []contractDumpInfo{{name: nnsContract, hash: cs.Hash}}
|
||||||
|
|
||||||
|
irSize := 0
|
||||||
|
for ; irSize < lastGlagoliticLetter; irSize++ {
|
||||||
|
ok, err := nnsIsAvailable(c, cs.Hash, getAlphabetNNSDomain(irSize))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bw := io.NewBufBinWriter()
|
||||||
|
|
||||||
|
if irSize != 0 {
|
||||||
|
bw.Reset()
|
||||||
|
for i := 0; i < irSize; i++ {
|
||||||
|
emit.AppCall(bw.BinWriter, cs.Hash, "resolve", callflag.ReadOnly,
|
||||||
|
getAlphabetNNSDomain(i),
|
||||||
|
int64(nns.TXT))
|
||||||
|
}
|
||||||
|
|
||||||
|
alphaRes, err := c.InvokeScript(bw.Bytes(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't fetch info from NNS: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < irSize; i++ {
|
||||||
|
info := contractDumpInfo{name: fmt.Sprintf("alphabet %d", i)}
|
||||||
|
if h, err := parseNNSResolveResult(alphaRes.Stack[i]); err == nil {
|
||||||
|
info.hash = h
|
||||||
|
}
|
||||||
|
infos = append(infos, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ctrName := range contractList {
|
||||||
|
bw.Reset()
|
||||||
|
emit.AppCall(bw.BinWriter, cs.Hash, "resolve", callflag.ReadOnly,
|
||||||
|
ctrName+".frostfs", int64(nns.TXT))
|
||||||
|
|
||||||
|
res, err := c.InvokeScript(bw.Bytes(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't fetch info from NNS: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
info := contractDumpInfo{name: ctrName}
|
||||||
|
if len(res.Stack) != 0 {
|
||||||
|
if h, err := parseNNSResolveResult(res.Stack[0]); err == nil {
|
||||||
|
info.hash = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
infos = append(infos, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
fillContractVersion(cmd, c, infos)
|
||||||
|
printContractInfo(cmd, infos)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dumpCustomZoneHashes(cmd *cobra.Command, nnsHash util.Uint160, zone string, c Client) error {
|
||||||
|
const nnsMaxTokens = 100
|
||||||
|
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, ".") {
|
||||||
|
zone = "." + zone
|
||||||
|
}
|
||||||
|
|
||||||
|
var infos []contractDumpInfo
|
||||||
|
for i := range arr {
|
||||||
|
bs, err := arr[i].TryBytes()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.HasSuffix(bs, []byte(zone)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
h, err := nnsResolveHash(inv, nnsHash, string(bs))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
infos = append(infos, contractDumpInfo{
|
||||||
|
hash: h,
|
||||||
|
name: strings.TrimSuffix(string(bs), zone),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fillContractVersion(cmd, c, infos)
|
||||||
|
printContractInfo(cmd, infos)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseContractVersion(item stackitem.Item) string {
|
||||||
|
bi, err := item.TryInteger()
|
||||||
|
if err != nil || bi.Sign() == 0 || !bi.IsInt64() {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
v := bi.Int64()
|
||||||
|
major := v / 1_000_000
|
||||||
|
minor := (v % 1_000_000) / 1000
|
||||||
|
patch := v % 1_000
|
||||||
|
return fmt.Sprintf("v%d.%d.%d", major, minor, patch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printContractInfo(cmd *cobra.Command, infos []contractDumpInfo) {
|
||||||
|
if len(infos) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
tw := tabwriter.NewWriter(buf, 0, 2, 2, ' ', 0)
|
||||||
|
for _, info := range infos {
|
||||||
|
if info.version == "" {
|
||||||
|
info.version = "unknown"
|
||||||
|
}
|
||||||
|
_, _ = tw.Write([]byte(fmt.Sprintf("%s\t(%s):\t%s\n",
|
||||||
|
info.name, info.version, info.hash.StringLE())))
|
||||||
|
}
|
||||||
|
_ = tw.Flush()
|
||||||
|
|
||||||
|
cmd.Print(buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func fillContractVersion(cmd *cobra.Command, c Client, infos []contractDumpInfo) {
|
||||||
|
bw := io.NewBufBinWriter()
|
||||||
|
sub := io.NewBufBinWriter()
|
||||||
|
for i := range infos {
|
||||||
|
if infos[i].hash.Equals(util.Uint160{}) {
|
||||||
|
emit.Int(bw.BinWriter, 0)
|
||||||
|
} else {
|
||||||
|
sub.Reset()
|
||||||
|
emit.AppCall(sub.BinWriter, infos[i].hash, "version", callflag.NoneFlag)
|
||||||
|
if sub.Err != nil {
|
||||||
|
panic(fmt.Errorf("BUG: can't create version script: %w", bw.Err))
|
||||||
|
}
|
||||||
|
|
||||||
|
script := sub.Bytes()
|
||||||
|
emit.Instruction(bw.BinWriter, opcode.TRY, []byte{byte(3 + len(script) + 2), 0})
|
||||||
|
bw.BinWriter.WriteBytes(script)
|
||||||
|
emit.Instruction(bw.BinWriter, opcode.ENDTRY, []byte{2 + 1})
|
||||||
|
emit.Opcodes(bw.BinWriter, opcode.PUSH0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emit.Opcodes(bw.BinWriter, opcode.NOP) // for the last ENDTRY target
|
||||||
|
if bw.Err != nil {
|
||||||
|
panic(fmt.Errorf("BUG: can't create version script: %w", bw.Err))
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := c.InvokeScript(bw.Bytes(), nil)
|
||||||
|
if err != nil {
|
||||||
|
cmd.Printf("Can't fetch version from NNS: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.State == vmstate.Halt.String() {
|
||||||
|
for i := range res.Stack {
|
||||||
|
infos[i].version = parseContractVersion(res.Stack[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
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
|
||||||
|
}
|
225
cmd/frostfs-adm/internal/modules/morph/generate.go
Normal file
225
cmd/frostfs-adm/internal/modules/morph/generate.go
Normal file
|
@ -0,0 +1,225 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/config"
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/pkg/innerring"
|
||||||
|
"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/smartcontract"
|
||||||
|
"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"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
singleAccountName = "single"
|
||||||
|
committeeAccountName = "committee"
|
||||||
|
consensusAccountName = "consensus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func generateAlphabetCreds(cmd *cobra.Command, args []string) error {
|
||||||
|
// alphabet size is not part of the config
|
||||||
|
size, err := cmd.Flags().GetUint(alphabetSizeFlag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if size == 0 {
|
||||||
|
return errors.New("size must be > 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
v := viper.GetViper()
|
||||||
|
walletDir := config.ResolveHomePath(viper.GetString(alphabetWalletsFlag))
|
||||||
|
pwds, err := initializeWallets(v, walletDir, int(size))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = initializeContractWallet(v, walletDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Println("size:", size)
|
||||||
|
cmd.Println("alphabet-wallets:", walletDir)
|
||||||
|
for i := range pwds {
|
||||||
|
cmd.Printf("wallet[%d]: %s\n", i, pwds[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func initializeWallets(v *viper.Viper, walletDir string, size int) ([]string, error) {
|
||||||
|
wallets := make([]*wallet.Wallet, size)
|
||||||
|
pubs := make(keys.PublicKeys, size)
|
||||||
|
passwords := make([]string, size)
|
||||||
|
|
||||||
|
for i := range wallets {
|
||||||
|
password, err := config.GetPassword(v, innerring.GlagoliticLetter(i).String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't fetch password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := filepath.Join(walletDir, innerring.GlagoliticLetter(i).String()+".json")
|
||||||
|
f, err := os.OpenFile(p, os.O_CREATE, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't create wallet file: %w", err)
|
||||||
|
}
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
return nil, fmt.Errorf("can't close wallet file: %w", err)
|
||||||
|
}
|
||||||
|
w, err := wallet.NewWallet(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't create wallet: %w", err)
|
||||||
|
}
|
||||||
|
if err := w.CreateAccount(singleAccountName, password); err != nil {
|
||||||
|
return nil, fmt.Errorf("can't create account: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
passwords[i] = password
|
||||||
|
wallets[i] = w
|
||||||
|
pubs[i] = w.Accounts[0].PrivateKey().PublicKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create committee account with N/2+1 multi-signature.
|
||||||
|
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.
|
||||||
|
bftCount := smartcontract.GetDefaultHonestNodeCount(size)
|
||||||
|
for i, w := range wallets {
|
||||||
|
if err := addMultisigAccount(w, bftCount, consensusAccountName, passwords[i], pubs); err != nil {
|
||||||
|
return nil, fmt.Errorf("can't create consensus account: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, w := range wallets {
|
||||||
|
if err := w.SavePretty(); err != nil {
|
||||||
|
return nil, fmt.Errorf("can't save wallet: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return passwords, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addMultisigAccount(w *wallet.Wallet, m int, name, password string, pubs keys.PublicKeys) error {
|
||||||
|
acc := wallet.NewAccountFromPrivateKey(w.Accounts[0].PrivateKey())
|
||||||
|
acc.Label = name
|
||||||
|
|
||||||
|
if err := acc.ConvertMultisig(m, pubs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := acc.Encrypt(password, keys.NEP2ScryptParams()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.AddAccount(acc)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateStorageCreds(cmd *cobra.Command, _ []string) error {
|
||||||
|
return refillGas(cmd, storageGasConfigFlag, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func refillGas(cmd *cobra.Command, gasFlag string, createWallet bool) (err error) {
|
||||||
|
// storage wallet path is not part of the config
|
||||||
|
storageWalletPath, _ := cmd.Flags().GetString(storageWalletFlag)
|
||||||
|
// wallet address is not part of the config
|
||||||
|
walletAddress, _ := cmd.Flags().GetString(walletAddressFlag)
|
||||||
|
|
||||||
|
var gasReceiver util.Uint160
|
||||||
|
|
||||||
|
if len(walletAddress) != 0 {
|
||||||
|
gasReceiver, err = address.StringToUint160(walletAddress)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid wallet address %s: %w", walletAddress, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if storageWalletPath == "" {
|
||||||
|
return fmt.Errorf("missing wallet path (use '--%s <out.json>')", storageWalletFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
var w *wallet.Wallet
|
||||||
|
|
||||||
|
if createWallet {
|
||||||
|
w, err = wallet.NewWallet(storageWalletPath)
|
||||||
|
} else {
|
||||||
|
w, err = wallet.NewWalletFromFile(storageWalletPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't create wallet: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if createWallet {
|
||||||
|
var password string
|
||||||
|
|
||||||
|
label, _ := cmd.Flags().GetString(storageWalletLabelFlag)
|
||||||
|
password, err := config.GetStoragePassword(viper.GetViper(), label)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't fetch password: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if label == "" {
|
||||||
|
label = singleAccountName
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.CreateAccount(label, password); err != nil {
|
||||||
|
return fmt.Errorf("can't create account: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gasReceiver = w.Accounts[0].Contract.ScriptHash()
|
||||||
|
}
|
||||||
|
|
||||||
|
gasStr := viper.GetString(gasFlag)
|
||||||
|
|
||||||
|
gasAmount, err := parseGASAmount(gasStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
wCtx, err := newInitializeContext(cmd, viper.GetViper())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
bw := io.NewBufBinWriter()
|
||||||
|
emit.AppCall(bw.BinWriter, gas.Hash, "transfer", callflag.All,
|
||||||
|
wCtx.CommitteeAcc.Contract.ScriptHash(), gasReceiver, int64(gasAmount), nil)
|
||||||
|
emit.Opcodes(bw.BinWriter, opcode.ASSERT)
|
||||||
|
if bw.Err != nil {
|
||||||
|
return fmt.Errorf("BUG: invalid transfer arguments: %w", bw.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wCtx.sendCommitteeTx(bw.Bytes(), false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
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
|
||||||
|
}
|
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
|
||||||
|
}
|
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
|
||||||
|
}
|
||||||
|
}
|
45
cmd/frostfs-adm/internal/modules/morph/initialize_roles.go
Normal file
45
cmd/frostfs-adm/internal/modules/morph/initialize_roles.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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/rpcclient/rolemgmt"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/emit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *initializeContext) setNotaryAndAlphabetNodes() error {
|
||||||
|
if ok, err := c.setRolesFinished(); ok || err != nil {
|
||||||
|
if err == nil {
|
||||||
|
c.Command.Println("Stage 2: already performed.")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var pubs []interface{}
|
||||||
|
for _, acc := range c.Accounts {
|
||||||
|
pubs = append(pubs, acc.PrivateKey().PublicKey().Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
w := io.NewBufBinWriter()
|
||||||
|
emit.AppCall(w.BinWriter, rolemgmt.Hash, "designateAsRole",
|
||||||
|
callflag.States|callflag.AllowNotify, int64(noderoles.P2PNotary), pubs)
|
||||||
|
emit.AppCall(w.BinWriter, rolemgmt.Hash, "designateAsRole",
|
||||||
|
callflag.States|callflag.AllowNotify, int64(noderoles.NeoFSAlphabet), pubs)
|
||||||
|
|
||||||
|
if err := c.sendCommitteeTx(w.Bytes(), false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.awaitTx()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *initializeContext) setRolesFinished() (bool, error) {
|
||||||
|
height, err := c.Client.GetBlockCount()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pubs, err := getDesignatedByRole(c.ReadOnlyInvoker, rolemgmt.Hash, noderoles.NeoFSAlphabet, height)
|
||||||
|
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
|
||||||
|
}
|
156
cmd/frostfs-adm/internal/modules/morph/internal/types.pb.go
generated
Normal file
156
cmd/frostfs-adm/internal/modules/morph/internal/types.pb.go
generated
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.26.0
|
||||||
|
// protoc v3.21.12
|
||||||
|
// source: cmd/frostfs-adm/internal/modules/morph/internal/types.proto
|
||||||
|
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Verify that this generated code is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||||
|
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
type SubnetClientGroupID struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// 4-byte integer identifier of the subnet client group.
|
||||||
|
Value uint32 `protobuf:"fixed32,1,opt,name=value,proto3" json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SubnetClientGroupID) Reset() {
|
||||||
|
*x = SubnetClientGroupID{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SubnetClientGroupID) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SubnetClientGroupID) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *SubnetClientGroupID) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_msgTypes[0]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use SubnetClientGroupID.ProtoReflect.Descriptor instead.
|
||||||
|
func (*SubnetClientGroupID) Descriptor() ([]byte, []int) {
|
||||||
|
return file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SubnetClientGroupID) GetValue() uint32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Value
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var File_cmd_frostfs_adm_internal_modules_morph_internal_types_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
|
var file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_rawDesc = []byte{
|
||||||
|
0x0a, 0x3b, 0x63, 0x6d, 0x64, 0x2f, 0x66, 0x72, 0x6f, 0x73, 0x74, 0x66, 0x73, 0x2d, 0x61, 0x64,
|
||||||
|
0x6d, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x6f, 0x64, 0x75, 0x6c,
|
||||||
|
0x65, 0x73, 0x2f, 0x6d, 0x6f, 0x72, 0x70, 0x68, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61,
|
||||||
|
0x6c, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x6e,
|
||||||
|
0x65, 0x6f, 0x2e, 0x66, 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x72, 0x65, 0x66, 0x73, 0x22, 0x2b, 0x0a,
|
||||||
|
0x13, 0x53, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x47, 0x72, 0x6f,
|
||||||
|
0x75, 0x70, 0x49, 0x44, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20,
|
||||||
|
0x01, 0x28, 0x07, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x56, 0x5a, 0x54, 0x67, 0x69,
|
||||||
|
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x54, 0x72, 0x75, 0x65, 0x43, 0x6c, 0x6f,
|
||||||
|
0x75, 0x64, 0x4c, 0x61, 0x62, 0x2f, 0x66, 0x72, 0x6f, 0x73, 0x74, 0x66, 0x73, 0x2d, 0x6e, 0x6f,
|
||||||
|
0x64, 0x65, 0x2f, 0x63, 0x6d, 0x64, 0x2f, 0x66, 0x72, 0x6f, 0x73, 0x74, 0x66, 0x73, 0x2d, 0x61,
|
||||||
|
0x64, 0x6d, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x6d, 0x6f, 0x64, 0x75,
|
||||||
|
0x6c, 0x65, 0x73, 0x2f, 0x6d, 0x6f, 0x72, 0x70, 0x68, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e,
|
||||||
|
0x61, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_rawDescOnce sync.Once
|
||||||
|
file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_rawDescData = file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_rawDesc
|
||||||
|
)
|
||||||
|
|
||||||
|
func file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_rawDescGZIP() []byte {
|
||||||
|
file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_rawDescOnce.Do(func() {
|
||||||
|
file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_rawDescData = protoimpl.X.CompressGZIP(file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_rawDescData)
|
||||||
|
})
|
||||||
|
return file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_rawDescData
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
|
||||||
|
var file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_goTypes = []interface{}{
|
||||||
|
(*SubnetClientGroupID)(nil), // 0: neo.fs.v2.refs.SubnetClientGroupID
|
||||||
|
}
|
||||||
|
var file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_depIdxs = []int32{
|
||||||
|
0, // [0:0] is the sub-list for method output_type
|
||||||
|
0, // [0:0] is the sub-list for method input_type
|
||||||
|
0, // [0:0] is the sub-list for extension type_name
|
||||||
|
0, // [0:0] is the sub-list for extension extendee
|
||||||
|
0, // [0:0] is the sub-list for field type_name
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_init() }
|
||||||
|
func file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_init() {
|
||||||
|
if File_cmd_frostfs_adm_internal_modules_morph_internal_types_proto != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !protoimpl.UnsafeEnabled {
|
||||||
|
file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*SubnetClientGroupID); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type x struct{}
|
||||||
|
out := protoimpl.TypeBuilder{
|
||||||
|
File: protoimpl.DescBuilder{
|
||||||
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
|
RawDescriptor: file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_rawDesc,
|
||||||
|
NumEnums: 0,
|
||||||
|
NumMessages: 1,
|
||||||
|
NumExtensions: 0,
|
||||||
|
NumServices: 0,
|
||||||
|
},
|
||||||
|
GoTypes: file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_goTypes,
|
||||||
|
DependencyIndexes: file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_depIdxs,
|
||||||
|
MessageInfos: file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_msgTypes,
|
||||||
|
}.Build()
|
||||||
|
File_cmd_frostfs_adm_internal_modules_morph_internal_types_proto = out.File
|
||||||
|
file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_rawDesc = nil
|
||||||
|
file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_goTypes = nil
|
||||||
|
file_cmd_frostfs_adm_internal_modules_morph_internal_types_proto_depIdxs = nil
|
||||||
|
}
|
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"];
|
||||||
|
}
|
486
cmd/frostfs-adm/internal/modules/morph/local_client.go
Normal file
486
cmd/frostfs-adm/internal/modules/morph/local_client.go
Normal file
|
@ -0,0 +1,486 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/elliptic"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"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/block"
|
||||||
|
"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/state"
|
||||||
|
"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/crypto/hash"
|
||||||
|
"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/io"
|
||||||
|
"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/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/trigger"
|
||||||
|
"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"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type localClient struct {
|
||||||
|
bc *core.Blockchain
|
||||||
|
transactions []*transaction.Transaction
|
||||||
|
dumpPath string
|
||||||
|
accounts []*wallet.Account
|
||||||
|
maxGasInvoke int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLocalClient(cmd *cobra.Command, v *viper.Viper, wallets []*wallet.Wallet) (*localClient, error) {
|
||||||
|
cfg, err := config.LoadFile(v.GetString(protoConfigPath))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bc, err := core.NewBlockchain(storage.NewMemoryStore(), cfg.Blockchain(), zap.NewNop())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m := smartcontract.GetDefaultHonestNodeCount(cfg.ProtocolConfiguration.ValidatorsCount)
|
||||||
|
accounts := make([]*wallet.Account, len(wallets))
|
||||||
|
for i := range accounts {
|
||||||
|
accounts[i], err = getWalletAccount(wallets[i], consensusAccountName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
indexMap := make(map[string]int)
|
||||||
|
for i, pub := range cfg.ProtocolConfiguration.StandbyCommittee {
|
||||||
|
indexMap[pub] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(accounts, func(i, j int) bool {
|
||||||
|
pi := accounts[i].PrivateKey().PublicKey().Bytes()
|
||||||
|
pj := accounts[j].PrivateKey().PublicKey().Bytes()
|
||||||
|
return indexMap[string(pi)] < indexMap[string(pj)]
|
||||||
|
})
|
||||||
|
sort.Slice(accounts[:cfg.ProtocolConfiguration.ValidatorsCount], func(i, j int) bool {
|
||||||
|
return accounts[i].PublicKey().Cmp(accounts[j].PublicKey()) == -1
|
||||||
|
})
|
||||||
|
|
||||||
|
go bc.Run()
|
||||||
|
|
||||||
|
dumpPath := v.GetString(localDumpFlag)
|
||||||
|
if cmd.Name() != "init" {
|
||||||
|
f, err := os.OpenFile(dumpPath, os.O_RDONLY, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("can't open local dump: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
r := io.NewBinReaderFromIO(f)
|
||||||
|
|
||||||
|
var skip uint32
|
||||||
|
if bc.BlockHeight() != 0 {
|
||||||
|
skip = bc.BlockHeight() + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
count := r.ReadU32LE() - skip
|
||||||
|
if err := chaindump.Restore(bc, r, skip, count, nil); err != nil {
|
||||||
|
return nil, fmt.Errorf("can't restore local dump: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &localClient{
|
||||||
|
bc: bc,
|
||||||
|
dumpPath: dumpPath,
|
||||||
|
accounts: accounts[:m],
|
||||||
|
maxGasInvoke: 15_0000_0000,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *localClient) GetBlockCount() (uint32, error) {
|
||||||
|
return l.bc.BlockHeight(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
a := result.NewApplicationLog(h, aer, *t)
|
||||||
|
return &a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
panic("unexpected call")
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvokeFunction is implemented via `InvokeScript`.
|
||||||
|
func (l *localClient) InvokeFunction(h util.Uint160, method string, sPrm []smartcontract.Parameter, ss []transaction.Signer) (*result.Invoke, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
pp := make([]interface{}, len(sPrm))
|
||||||
|
for i, p := range sPrm {
|
||||||
|
pp[i], err = smartcontract.ExpandParameterToEmitable(p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("incorrect parameter type %s: %w", p.Type, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return invokeFunction(l, h, method, pp, ss)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *localClient) CalculateNotaryFee(_ uint8) (int64, error) {
|
||||||
|
// not used by `morph init` command
|
||||||
|
panic("unexpected call")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
panic("unexpected call")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVersion return default version.
|
||||||
|
func (l *localClient) GetVersion() (*result.Version, error) {
|
||||||
|
return &result.Version{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
panic("unexpected call")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculateNetworkFee calculates network fee for the given transaction.
|
||||||
|
// Copied from neo-go with minor corrections (no need to support non-notary mode):
|
||||||
|
// 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) {
|
||||||
|
hashablePart, err := tx.EncodeHashableFields()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to compute tx size: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
size := len(hashablePart) + io.GetVarSize(len(tx.Signers))
|
||||||
|
ef := l.bc.GetBaseExecFee()
|
||||||
|
|
||||||
|
var netFee int64
|
||||||
|
for i, signer := range tx.Signers {
|
||||||
|
var verificationScript []byte
|
||||||
|
for _, w := range tx.Scripts {
|
||||||
|
if w.VerificationScript != nil && hash.Hash160(w.VerificationScript).Equals(signer.Account) {
|
||||||
|
verificationScript = w.VerificationScript
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if verificationScript == nil {
|
||||||
|
gasConsumed, err := l.bc.VerifyWitness(signer.Account, tx, &tx.Scripts[i], l.maxGasInvoke)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("invalid signature: %w", err)
|
||||||
|
}
|
||||||
|
netFee += gasConsumed
|
||||||
|
size += io.GetVarSize([]byte{}) + io.GetVarSize(tx.Scripts[i].InvocationScript)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fee, sizeDelta := fee.Calculate(ef, verificationScript)
|
||||||
|
netFee += fee
|
||||||
|
size += sizeDelta
|
||||||
|
}
|
||||||
|
|
||||||
|
fee := l.bc.FeePerByte()
|
||||||
|
netFee += int64(size) * fee
|
||||||
|
|
||||||
|
return netFee, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := transaction.New(script, 0)
|
||||||
|
tx.Signers = signers
|
||||||
|
tx.ValidUntilBlock = l.bc.BlockHeight() + 2
|
||||||
|
|
||||||
|
ic, err := l.bc.GetTestVM(trigger.Application, tx, &block.Block{
|
||||||
|
Header: block.Header{
|
||||||
|
Index: lastBlock.Index + 1,
|
||||||
|
Timestamp: lastBlock.Timestamp + 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get test VM: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ic.VM.GasLimit = 100_0000_0000
|
||||||
|
ic.VM.LoadScriptWithFlags(script, callflag.All)
|
||||||
|
|
||||||
|
var errStr string
|
||||||
|
if err := ic.VM.Run(); err != nil {
|
||||||
|
errStr = err.Error()
|
||||||
|
}
|
||||||
|
return &result.Invoke{
|
||||||
|
State: ic.VM.State().String(),
|
||||||
|
GasConsumed: ic.VM.GasConsumed(),
|
||||||
|
Script: script,
|
||||||
|
Stack: ic.VM.Estack().ToArray(),
|
||||||
|
FaultException: errStr,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
bs := tx.Bytes()
|
||||||
|
_, err := transaction.NewTransactionFromBytes(bs)
|
||||||
|
if err != nil {
|
||||||
|
return tx.Hash(), fmt.Errorf("invalid transaction: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.transactions = append(l.transactions, tx)
|
||||||
|
return tx.Hash(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *localClient) putTransactions() error {
|
||||||
|
// 1. Prepare new block.
|
||||||
|
lastBlock, err := l.bc.GetBlock(l.bc.CurrentBlockHash())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer func() { l.transactions = l.transactions[:0] }()
|
||||||
|
|
||||||
|
b := &block.Block{
|
||||||
|
Header: block.Header{
|
||||||
|
NextConsensus: l.accounts[0].Contract.ScriptHash(),
|
||||||
|
Script: transaction.Witness{
|
||||||
|
VerificationScript: l.accounts[0].Contract.Script,
|
||||||
|
},
|
||||||
|
Timestamp: lastBlock.Timestamp + 1,
|
||||||
|
},
|
||||||
|
Transactions: l.transactions,
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.bc.GetConfig().StateRootInHeader {
|
||||||
|
b.StateRootEnabled = true
|
||||||
|
b.PrevStateRoot = l.bc.GetStateModule().CurrentLocalStateRoot()
|
||||||
|
}
|
||||||
|
b.PrevHash = lastBlock.Hash()
|
||||||
|
b.Index = lastBlock.Index + 1
|
||||||
|
b.RebuildMerkleRoot()
|
||||||
|
|
||||||
|
// 2. Sign prepared block.
|
||||||
|
var invocationScript []byte
|
||||||
|
|
||||||
|
magic := l.bc.GetConfig().Magic
|
||||||
|
for _, acc := range l.accounts {
|
||||||
|
sign := acc.PrivateKey().SignHashable(uint32(magic), b)
|
||||||
|
invocationScript = append(invocationScript, byte(opcode.PUSHDATA1), 64)
|
||||||
|
invocationScript = append(invocationScript, sign...)
|
||||||
|
}
|
||||||
|
b.Script.InvocationScript = invocationScript
|
||||||
|
|
||||||
|
// 3. Persist block.
|
||||||
|
return l.bc.AddBlock(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func invokeFunction(c Client, h util.Uint160, method string, parameters []interface{}, signers []transaction.Signer) (*result.Invoke, error) {
|
||||||
|
w := io.NewBufBinWriter()
|
||||||
|
emit.Array(w.BinWriter, parameters...)
|
||||||
|
emit.AppCallNoArgs(w.BinWriter, h, method, callflag.All)
|
||||||
|
if w.Err != nil {
|
||||||
|
panic(fmt.Sprintf("BUG: invalid parameters for '%s': %v", method, w.Err))
|
||||||
|
}
|
||||||
|
return c.InvokeScript(w.Bytes(), signers)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errGetDesignatedByRoleResponse = errors.New("`getDesignatedByRole`: invalid response")
|
||||||
|
|
||||||
|
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)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errGetDesignatedByRoleResponse
|
||||||
|
}
|
||||||
|
|
||||||
|
pubs := make(keys.PublicKeys, len(arr))
|
||||||
|
for i := range arr {
|
||||||
|
bs, err := arr[i].TryBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errGetDesignatedByRoleResponse
|
||||||
|
}
|
||||||
|
pubs[i], err = keys.NewPublicKeyFromBytes(bs, elliptic.P256())
|
||||||
|
if err != nil {
|
||||||
|
return nil, errGetDesignatedByRoleResponse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pubs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *localClient) dump() (err error) {
|
||||||
|
defer l.bc.Close()
|
||||||
|
|
||||||
|
f, err := os.Create(l.dumpPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
closeErr := f.Close()
|
||||||
|
if err == nil && closeErr != nil {
|
||||||
|
err = closeErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
w := io.NewBinWriterFromIO(f)
|
||||||
|
w.WriteU32LE(l.bc.BlockHeight() + 1)
|
||||||
|
err = chaindump.Dump(l.bc, w, 0, l.bc.BlockHeight()+1)
|
||||||
|
return
|
||||||
|
}
|
122
cmd/frostfs-adm/internal/modules/morph/n3client.go
Normal file
122
cmd/frostfs-adm/internal/modules/morph/n3client.go
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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/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/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/actor"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client represents N3 client interface capable of test-invoking scripts
|
||||||
|
// and sending signed transactions to chain.
|
||||||
|
type Client interface {
|
||||||
|
invoker.RPCInvoke
|
||||||
|
|
||||||
|
GetBlockCount() (uint32, 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)
|
||||||
|
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)
|
||||||
|
GetCommittee() (keys.PublicKeys, error)
|
||||||
|
CalculateNotaryFee(uint8) (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 {
|
||||||
|
hash util.Uint256
|
||||||
|
vub uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientContext struct {
|
||||||
|
Client Client // a raw neo-go client OR a local chain implementation
|
||||||
|
CommitteeAct *actor.Actor // committee actor with the Global witness scope
|
||||||
|
ReadOnlyInvoker *invoker.Invoker // R/O contract invoker, does not contain any signer
|
||||||
|
SentTxs []hashVUBPair
|
||||||
|
}
|
||||||
|
|
||||||
|
func getN3Client(v *viper.Viper) (Client, error) {
|
||||||
|
// number of opened connections
|
||||||
|
// by neo-go client per one host
|
||||||
|
const (
|
||||||
|
maxConnsPerHost = 10
|
||||||
|
requestTimeout = time.Second * 10
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
endpoint := v.GetString(endpointFlag)
|
||||||
|
if endpoint == "" {
|
||||||
|
return nil, errors.New("missing endpoint")
|
||||||
|
}
|
||||||
|
c, err := rpcclient.New(ctx, endpoint, rpcclient.Options{
|
||||||
|
MaxConnsPerHost: maxConnsPerHost,
|
||||||
|
RequestTimeout: requestTimeout,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := c.Init(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultClientContext(c Client, committeeAcc *wallet.Account) (*clientContext, error) {
|
||||||
|
commAct, err := actor.New(c, []actor.SignerAccount{{
|
||||||
|
Signer: transaction.Signer{
|
||||||
|
Account: committeeAcc.Contract.ScriptHash(),
|
||||||
|
Scopes: transaction.Global,
|
||||||
|
},
|
||||||
|
Account: committeeAcc,
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &clientContext{
|
||||||
|
Client: c,
|
||||||
|
CommitteeAct: commAct,
|
||||||
|
ReadOnlyInvoker: invoker.New(c, nil),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clientContext) sendTx(tx *transaction.Transaction, cmd *cobra.Command, await bool) error {
|
||||||
|
h, err := c.Client.SendRawTransaction(tx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if h != tx.Hash() {
|
||||||
|
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})
|
||||||
|
|
||||||
|
if await {
|
||||||
|
return c.awaitTx(cmd)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
121
cmd/frostfs-adm/internal/modules/morph/notary.go
Normal file
121
cmd/frostfs-adm/internal/modules/morph/notary.go
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/cli/input"
|
||||||
|
"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/address"
|
||||||
|
"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/nep17"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/notary"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// defaultNotaryDepositLifetime is an amount of blocks notary deposit stays valid.
|
||||||
|
// https://github.com/nspcc-dev/neo-go/blob/master/pkg/core/native/notary.go#L48
|
||||||
|
const defaultNotaryDepositLifetime = 5760
|
||||||
|
|
||||||
|
func depositNotary(cmd *cobra.Command, _ []string) error {
|
||||||
|
p, err := cmd.Flags().GetString(storageWalletFlag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if p == "" {
|
||||||
|
return fmt.Errorf("missing wallet path (use '--%s <out.json>')", storageWalletFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
w, err := wallet.NewWalletFromFile(p)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't open wallet: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
accHash := w.GetChangeAddress()
|
||||||
|
if addr, err := cmd.Flags().GetString(walletAccountFlag); err == nil {
|
||||||
|
accHash, err = address.StringToUint160(addr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid address: %s", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
acc := w.GetAccount(accHash)
|
||||||
|
if acc == nil {
|
||||||
|
return fmt.Errorf("can't find account for %s", accHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt := fmt.Sprintf("Enter password for %s >", address.Uint160ToString(accHash))
|
||||||
|
pass, err := input.ReadPassword(prompt)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't get password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = acc.Decrypt(pass, keys.NEP2ScryptParams())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't unlock account: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gasStr, err := cmd.Flags().GetString(refillGasAmountFlag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
gasAmount, err := parseGASAmount(gasStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
till := int64(defaultNotaryDepositLifetime)
|
||||||
|
tillStr, err := cmd.Flags().GetString(notaryDepositTillFlag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tillStr != "" {
|
||||||
|
till, err = strconv.ParseInt(tillStr, 10, 64)
|
||||||
|
if err != nil || till <= 0 {
|
||||||
|
return fmt.Errorf("notary deposit lifetime must be a positive integer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := getN3Client(viper.GetViper())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := checkNotaryEnabled(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
height, err := c.GetBlockCount()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't get current height: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
act, err := actor.New(c, []actor.SignerAccount{{
|
||||||
|
Signer: transaction.Signer{
|
||||||
|
Account: acc.Contract.ScriptHash(),
|
||||||
|
Scopes: transaction.Global,
|
||||||
|
},
|
||||||
|
Account: acc,
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create actor: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gasActor := nep17.New(act, gas.Hash)
|
||||||
|
|
||||||
|
txHash, vub, err := gasActor.Transfer(
|
||||||
|
accHash,
|
||||||
|
notary.Hash,
|
||||||
|
big.NewInt(int64(gasAmount)),
|
||||||
|
[]interface{}{nil, int64(height) + till},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not send tx: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return awaitTx(cmd, c, []hashVUBPair{{hash: txHash, vub: vub}})
|
||||||
|
}
|
54
cmd/frostfs-adm/internal/modules/morph/policy.go
Normal file
54
cmd/frostfs-adm/internal/modules/morph/policy.go
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/rpcclient/policy"
|
||||||
|
"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 (
|
||||||
|
execFeeParam = "ExecFeeFactor"
|
||||||
|
storagePriceParam = "StoragePrice"
|
||||||
|
setFeeParam = "FeePerByte"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setPolicyCmd(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
bw := io.NewBufBinWriter()
|
||||||
|
for i := range args {
|
||||||
|
kv := strings.SplitN(args[i], "=", 2)
|
||||||
|
if len(kv) != 2 {
|
||||||
|
return fmt.Errorf("invalid parameter format, must be Parameter=Value")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch kv[0] {
|
||||||
|
case execFeeParam, storagePriceParam, setFeeParam:
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("parameter must be one of %s, %s and %s", execFeeParam, storagePriceParam, setFeeParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := strconv.ParseUint(kv[1], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't parse parameter value '%s': %w", args[1], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
emit.AppCall(bw.BinWriter, policy.Hash, "set"+kv[0], callflag.All, int64(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wCtx.sendCommitteeTx(bw.Bytes(), false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return wCtx.awaitTx()
|
||||||
|
}
|
61
cmd/frostfs-adm/internal/modules/morph/remove_node.go
Normal file
61
cmd/frostfs-adm/internal/modules/morph/remove_node.go
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
netmapcontract "github.com/TrueCloudLab/frostfs-contract/netmap"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
func removeNodesCmd(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return errors.New("at least one node key must be provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeKeys := make(keys.PublicKeys, len(args))
|
||||||
|
for i := range args {
|
||||||
|
var err error
|
||||||
|
nodeKeys[i], err = keys.NewPublicKeyFromString(args[i])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't parse node public key: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wCtx, err := newInitializeContext(cmd, viper.GetViper())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("can't initialize context: %w", err)
|
||||||
|
}
|
||||||
|
defer wCtx.close()
|
||||||
|
|
||||||
|
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()
|
||||||
|
for i := range nodeKeys {
|
||||||
|
emit.AppCall(bw.BinWriter, nmHash, "updateStateIR", callflag.All,
|
||||||
|
int64(netmapcontract.NodeStateOffline), nodeKeys[i].Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := emitNewEpochCall(bw, wCtx, nmHash); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wCtx.sendConsensusTx(bw.Bytes()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return wCtx.awaitTx()
|
||||||
|
}
|
326
cmd/frostfs-adm/internal/modules/morph/root.go
Normal file
326
cmd/frostfs-adm/internal/modules/morph/root.go
Normal file
|
@ -0,0 +1,326 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
alphabetWalletsFlag = "alphabet-wallets"
|
||||||
|
alphabetSizeFlag = "size"
|
||||||
|
endpointFlag = "rpc-endpoint"
|
||||||
|
storageWalletFlag = "storage-wallet"
|
||||||
|
storageWalletLabelFlag = "label"
|
||||||
|
storageGasCLIFlag = "initial-gas"
|
||||||
|
storageGasConfigFlag = "storage.initial_gas"
|
||||||
|
contractsInitFlag = "contracts"
|
||||||
|
maxObjectSizeInitFlag = "network.max_object_size"
|
||||||
|
maxObjectSizeCLIFlag = "max-object-size"
|
||||||
|
epochDurationInitFlag = "network.epoch_duration"
|
||||||
|
epochDurationCLIFlag = "epoch-duration"
|
||||||
|
incomeRateInitFlag = "network.basic_income_rate"
|
||||||
|
incomeRateCLIFlag = "basic-income-rate"
|
||||||
|
auditFeeInitFlag = "network.fee.audit"
|
||||||
|
auditFeeCLIFlag = "audit-fee"
|
||||||
|
containerFeeInitFlag = "network.fee.container"
|
||||||
|
containerAliasFeeInitFlag = "network.fee.container_alias"
|
||||||
|
containerFeeCLIFlag = "container-fee"
|
||||||
|
containerAliasFeeCLIFlag = "container-alias-fee"
|
||||||
|
candidateFeeInitFlag = "network.fee.candidate"
|
||||||
|
candidateFeeCLIFlag = "candidate-fee"
|
||||||
|
homomorphicHashDisabledInitFlag = "network.homomorphic_hash_disabled"
|
||||||
|
maintenanceModeAllowedInitFlag = "network.maintenance_mode_allowed"
|
||||||
|
homomorphicHashDisabledCLIFlag = "homomorphic-disabled"
|
||||||
|
withdrawFeeInitFlag = "network.fee.withdraw"
|
||||||
|
withdrawFeeCLIFlag = "withdraw-fee"
|
||||||
|
containerDumpFlag = "dump"
|
||||||
|
containerContractFlag = "container-contract"
|
||||||
|
containerIDsFlag = "cid"
|
||||||
|
refillGasAmountFlag = "gas"
|
||||||
|
walletAccountFlag = "account"
|
||||||
|
notaryDepositTillFlag = "till"
|
||||||
|
localDumpFlag = "local-dump"
|
||||||
|
protoConfigPath = "protocol"
|
||||||
|
walletAddressFlag = "wallet-address"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// RootCmd is a root command of config section.
|
||||||
|
RootCmd = &cobra.Command{
|
||||||
|
Use: "morph",
|
||||||
|
Short: "Section for morph network configuration commands",
|
||||||
|
}
|
||||||
|
|
||||||
|
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(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
||||||
|
},
|
||||||
|
RunE: generateAlphabetCreds,
|
||||||
|
}
|
||||||
|
|
||||||
|
initCmd = &cobra.Command{
|
||||||
|
Use: "init",
|
||||||
|
Short: "Initialize side chain network with smart-contracts and network settings",
|
||||||
|
PreRun: func(cmd *cobra.Command, _ []string) {
|
||||||
|
_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
||||||
|
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
||||||
|
_ = viper.BindPFlag(epochDurationInitFlag, cmd.Flags().Lookup(epochDurationCLIFlag))
|
||||||
|
_ = viper.BindPFlag(maxObjectSizeInitFlag, cmd.Flags().Lookup(maxObjectSizeCLIFlag))
|
||||||
|
_ = viper.BindPFlag(incomeRateInitFlag, cmd.Flags().Lookup(incomeRateCLIFlag))
|
||||||
|
_ = viper.BindPFlag(homomorphicHashDisabledInitFlag, cmd.Flags().Lookup(homomorphicHashDisabledCLIFlag))
|
||||||
|
_ = viper.BindPFlag(auditFeeInitFlag, cmd.Flags().Lookup(auditFeeCLIFlag))
|
||||||
|
_ = viper.BindPFlag(candidateFeeInitFlag, cmd.Flags().Lookup(candidateFeeCLIFlag))
|
||||||
|
_ = viper.BindPFlag(containerFeeInitFlag, cmd.Flags().Lookup(containerFeeCLIFlag))
|
||||||
|
_ = viper.BindPFlag(containerAliasFeeInitFlag, cmd.Flags().Lookup(containerAliasFeeCLIFlag))
|
||||||
|
_ = viper.BindPFlag(withdrawFeeInitFlag, cmd.Flags().Lookup(withdrawFeeCLIFlag))
|
||||||
|
_ = viper.BindPFlag(protoConfigPath, cmd.Flags().Lookup(protoConfigPath))
|
||||||
|
_ = viper.BindPFlag(localDumpFlag, cmd.Flags().Lookup(localDumpFlag))
|
||||||
|
},
|
||||||
|
RunE: initializeSideChainCmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
generateStorageCmd = &cobra.Command{
|
||||||
|
Use: "generate-storage-wallet",
|
||||||
|
Short: "Generate storage node wallet for the morph network",
|
||||||
|
PreRun: func(cmd *cobra.Command, _ []string) {
|
||||||
|
_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
||||||
|
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(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(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
||||||
|
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
||||||
|
_ = viper.BindPFlag(refillGasAmountFlag, cmd.Flags().Lookup(refillGasAmountFlag))
|
||||||
|
},
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return refillGas(cmd, refillGasAmountFlag, false)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
forceNewEpoch = &cobra.Command{
|
||||||
|
Use: "force-new-epoch",
|
||||||
|
Short: "Create new NeoFS epoch event in the side chain",
|
||||||
|
PreRun: func(cmd *cobra.Command, _ []string) {
|
||||||
|
_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
||||||
|
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
||||||
|
},
|
||||||
|
RunE: forceNewEpochCmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
removeNodes = &cobra.Command{
|
||||||
|
Use: "remove-nodes key1 [key2 [...]]",
|
||||||
|
Short: "Remove storage nodes from the netmap",
|
||||||
|
Long: `Move nodes to the Offline state in the candidates list and tick an epoch to update the netmap`,
|
||||||
|
PreRun: func(cmd *cobra.Command, _ []string) {
|
||||||
|
_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
||||||
|
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
||||||
|
},
|
||||||
|
RunE: removeNodesCmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfig = &cobra.Command{
|
||||||
|
Use: "set-config key1=val1 [key2=val2 ...]",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Short: "Add/update global config value in the NeoFS network",
|
||||||
|
PreRun: func(cmd *cobra.Command, _ []string) {
|
||||||
|
_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
||||||
|
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
||||||
|
},
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
RunE: setConfigCmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
setPolicy = &cobra.Command{
|
||||||
|
Use: "set-policy [ExecFeeFactor=<n1>] [StoragePrice=<n2>] [FeePerByte=<n3>]",
|
||||||
|
DisableFlagsInUseLine: true,
|
||||||
|
Short: "Set global policy values",
|
||||||
|
PreRun: func(cmd *cobra.Command, _ []string) {
|
||||||
|
_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
||||||
|
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
||||||
|
},
|
||||||
|
RunE: setPolicyCmd,
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
return []string{"ExecFeeFactor=", "StoragePrice=", "FeePerByte="}, cobra.ShellCompDirectiveNoSpace
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dumpContractHashesCmd = &cobra.Command{
|
||||||
|
Use: "dump-hashes",
|
||||||
|
Short: "Dump deployed contract hashes",
|
||||||
|
PreRun: func(cmd *cobra.Command, _ []string) {
|
||||||
|
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
||||||
|
},
|
||||||
|
RunE: dumpContractHashes,
|
||||||
|
}
|
||||||
|
|
||||||
|
dumpNetworkConfigCmd = &cobra.Command{
|
||||||
|
Use: "dump-config",
|
||||||
|
Short: "Dump NeoFS network config",
|
||||||
|
PreRun: func(cmd *cobra.Command, _ []string) {
|
||||||
|
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
||||||
|
},
|
||||||
|
RunE: dumpNetworkConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
dumpBalancesCmd = &cobra.Command{
|
||||||
|
Use: "dump-balances",
|
||||||
|
Short: "Dump GAS balances",
|
||||||
|
PreRun: func(cmd *cobra.Command, _ []string) {
|
||||||
|
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
||||||
|
},
|
||||||
|
RunE: dumpBalances,
|
||||||
|
}
|
||||||
|
|
||||||
|
updateContractsCmd = &cobra.Command{
|
||||||
|
Use: "update-contracts",
|
||||||
|
Short: "Update NeoFS contracts",
|
||||||
|
PreRun: func(cmd *cobra.Command, _ []string) {
|
||||||
|
_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
||||||
|
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
||||||
|
},
|
||||||
|
RunE: updateContracts,
|
||||||
|
}
|
||||||
|
|
||||||
|
dumpContainersCmd = &cobra.Command{
|
||||||
|
Use: "dump-containers",
|
||||||
|
Short: "Dump NeoFS containers to file",
|
||||||
|
PreRun: func(cmd *cobra.Command, _ []string) {
|
||||||
|
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
||||||
|
},
|
||||||
|
RunE: dumpContainers,
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreContainersCmd = &cobra.Command{
|
||||||
|
Use: "restore-containers",
|
||||||
|
Short: "Restore NeoFS containers from file",
|
||||||
|
PreRun: func(cmd *cobra.Command, _ []string) {
|
||||||
|
_ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag))
|
||||||
|
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
||||||
|
},
|
||||||
|
RunE: restoreContainers,
|
||||||
|
}
|
||||||
|
|
||||||
|
listContainersCmd = &cobra.Command{
|
||||||
|
Use: "list-containers",
|
||||||
|
Short: "List NeoFS containers",
|
||||||
|
PreRun: func(cmd *cobra.Command, _ []string) {
|
||||||
|
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
||||||
|
},
|
||||||
|
RunE: listContainers,
|
||||||
|
}
|
||||||
|
|
||||||
|
depositNotaryCmd = &cobra.Command{
|
||||||
|
Use: "deposit-notary",
|
||||||
|
Short: "Deposit GAS for notary service",
|
||||||
|
PreRun: func(cmd *cobra.Command, _ []string) {
|
||||||
|
_ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag))
|
||||||
|
},
|
||||||
|
RunE: depositNotary,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RootCmd.AddCommand(generateAlphabetCmd)
|
||||||
|
generateAlphabetCmd.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
||||||
|
generateAlphabetCmd.Flags().Uint(alphabetSizeFlag, 7, "Amount of alphabet wallets to generate")
|
||||||
|
|
||||||
|
RootCmd.AddCommand(initCmd)
|
||||||
|
initCmd.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
||||||
|
initCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
||||||
|
initCmd.Flags().String(contractsInitFlag, "", "Path to archive with compiled NeoFS contracts (default fetched from latest github release)")
|
||||||
|
initCmd.Flags().Uint(epochDurationCLIFlag, 240, "Amount of side chain blocks in one NeoFS epoch")
|
||||||
|
initCmd.Flags().Uint(maxObjectSizeCLIFlag, 67108864, "Max single object size in bytes")
|
||||||
|
initCmd.Flags().Bool(homomorphicHashDisabledCLIFlag, false, "Disable object homomorphic hashing")
|
||||||
|
// Defaults are taken from neo-preodolenie.
|
||||||
|
initCmd.Flags().Uint64(containerFeeCLIFlag, 1000, "Container registration fee")
|
||||||
|
initCmd.Flags().Uint64(containerAliasFeeCLIFlag, 500, "Container alias fee")
|
||||||
|
initCmd.Flags().String(protoConfigPath, "", "Path to the consensus node configuration")
|
||||||
|
initCmd.Flags().String(localDumpFlag, "", "Path to the blocks dump file")
|
||||||
|
|
||||||
|
RootCmd.AddCommand(deployCmd)
|
||||||
|
|
||||||
|
RootCmd.AddCommand(generateStorageCmd)
|
||||||
|
generateStorageCmd.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
||||||
|
generateStorageCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
||||||
|
generateStorageCmd.Flags().String(storageWalletFlag, "", "Path to new storage node wallet")
|
||||||
|
generateStorageCmd.Flags().String(storageGasCLIFlag, "", "Initial amount of GAS to transfer")
|
||||||
|
generateStorageCmd.Flags().StringP(storageWalletLabelFlag, "l", "", "Wallet label")
|
||||||
|
|
||||||
|
RootCmd.AddCommand(forceNewEpoch)
|
||||||
|
forceNewEpoch.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
||||||
|
forceNewEpoch.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
||||||
|
|
||||||
|
RootCmd.AddCommand(removeNodes)
|
||||||
|
removeNodes.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
||||||
|
removeNodes.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
||||||
|
|
||||||
|
RootCmd.AddCommand(setPolicy)
|
||||||
|
setPolicy.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
||||||
|
setPolicy.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
||||||
|
|
||||||
|
RootCmd.AddCommand(dumpContractHashesCmd)
|
||||||
|
dumpContractHashesCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
||||||
|
dumpContractHashesCmd.Flags().String(customZoneFlag, "", "Custom zone to search.")
|
||||||
|
|
||||||
|
RootCmd.AddCommand(dumpNetworkConfigCmd)
|
||||||
|
dumpNetworkConfigCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
||||||
|
|
||||||
|
RootCmd.AddCommand(setConfig)
|
||||||
|
setConfig.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
||||||
|
setConfig.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
||||||
|
setConfig.Flags().Bool(forceConfigSet, false, "Force setting not well-known configuration key")
|
||||||
|
|
||||||
|
RootCmd.AddCommand(dumpBalancesCmd)
|
||||||
|
dumpBalancesCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
||||||
|
dumpBalancesCmd.Flags().BoolP(dumpBalancesStorageFlag, "s", false, "Dump balances of storage nodes from the current netmap")
|
||||||
|
dumpBalancesCmd.Flags().BoolP(dumpBalancesAlphabetFlag, "a", false, "Dump balances of alphabet contracts")
|
||||||
|
dumpBalancesCmd.Flags().BoolP(dumpBalancesProxyFlag, "p", false, "Dump balances of the proxy contract")
|
||||||
|
dumpBalancesCmd.Flags().Bool(dumpBalancesUseScriptHashFlag, false, "Use script-hash format for addresses")
|
||||||
|
|
||||||
|
RootCmd.AddCommand(updateContractsCmd)
|
||||||
|
updateContractsCmd.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
||||||
|
updateContractsCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
||||||
|
updateContractsCmd.Flags().String(contractsInitFlag, "", "Path to archive with compiled NeoFS contracts (default fetched from latest github release)")
|
||||||
|
|
||||||
|
RootCmd.AddCommand(dumpContainersCmd)
|
||||||
|
dumpContainersCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
||||||
|
dumpContainersCmd.Flags().String(containerDumpFlag, "", "File where to save dumped containers")
|
||||||
|
dumpContainersCmd.Flags().String(containerContractFlag, "", "Container contract hash (for networks without NNS)")
|
||||||
|
dumpContainersCmd.Flags().StringSlice(containerIDsFlag, nil, "Containers to dump")
|
||||||
|
|
||||||
|
RootCmd.AddCommand(restoreContainersCmd)
|
||||||
|
restoreContainersCmd.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
||||||
|
restoreContainersCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
||||||
|
restoreContainersCmd.Flags().String(containerDumpFlag, "", "File to restore containers from")
|
||||||
|
restoreContainersCmd.Flags().StringSlice(containerIDsFlag, nil, "Containers to restore")
|
||||||
|
|
||||||
|
RootCmd.AddCommand(listContainersCmd)
|
||||||
|
listContainersCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
||||||
|
listContainersCmd.Flags().String(containerContractFlag, "", "Container contract hash (for networks without NNS)")
|
||||||
|
|
||||||
|
RootCmd.AddCommand(refillGasCmd)
|
||||||
|
refillGasCmd.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir")
|
||||||
|
refillGasCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
||||||
|
refillGasCmd.Flags().String(storageWalletFlag, "", "Path to storage node wallet")
|
||||||
|
refillGasCmd.Flags().String(walletAddressFlag, "", "Address of wallet")
|
||||||
|
refillGasCmd.Flags().String(refillGasAmountFlag, "", "Additional amount of GAS to transfer")
|
||||||
|
refillGasCmd.MarkFlagsMutuallyExclusive(walletAddressFlag, storageWalletFlag)
|
||||||
|
|
||||||
|
RootCmd.AddCommand(cmdSubnet)
|
||||||
|
|
||||||
|
RootCmd.AddCommand(depositNotaryCmd)
|
||||||
|
depositNotaryCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint")
|
||||||
|
depositNotaryCmd.Flags().String(storageWalletFlag, "", "Path to storage node wallet")
|
||||||
|
depositNotaryCmd.Flags().String(walletAccountFlag, "", "Wallet account address")
|
||||||
|
depositNotaryCmd.Flags().String(refillGasAmountFlag, "", "Amount of GAS to deposit")
|
||||||
|
depositNotaryCmd.Flags().String(notaryDepositTillFlag, "", "Notary deposit duration in blocks")
|
||||||
|
}
|
1064
cmd/frostfs-adm/internal/modules/morph/subnet.go
Normal file
1064
cmd/frostfs-adm/internal/modules/morph/subnet.go
Normal file
File diff suppressed because it is too large
Load diff
21
cmd/frostfs-adm/internal/modules/morph/update.go
Normal file
21
cmd/frostfs-adm/internal/modules/morph/update.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package morph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func updateContracts(cmd *cobra.Command, _ []string) error {
|
||||||
|
wCtx, err := newInitializeContext(cmd, viper.GetViper())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("initialization error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wCtx.deployNNS(updateMethodName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return wCtx.updateContracts()
|
||||||
|
}
|
71
cmd/frostfs-adm/internal/modules/root.go
Normal file
71
cmd/frostfs-adm/internal/modules/root.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package modules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/config"
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/morph"
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules/storagecfg"
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/misc"
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/pkg/util/autocomplete"
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/pkg/util/gendoc"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
rootCmd = &cobra.Command{
|
||||||
|
Use: "frostfs-adm",
|
||||||
|
Short: "NeoFS Administrative Tool",
|
||||||
|
Long: `NeoFS Administrative Tool provides functions to setup and
|
||||||
|
manage NeoFS network deployment.`,
|
||||||
|
RunE: entryPoint,
|
||||||
|
SilenceUsage: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
configFlag = "config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cobra.OnInitialize(func() { initConfig(rootCmd) })
|
||||||
|
// we need to init viper config to bind viper and cobra configurations for
|
||||||
|
// rpc endpoint, alphabet wallet dir, key credentials, etc.
|
||||||
|
|
||||||
|
// use stdout as default output for cmd.Print()
|
||||||
|
rootCmd.SetOut(os.Stdout)
|
||||||
|
|
||||||
|
rootCmd.PersistentFlags().StringP(configFlag, "c", "", "Config file")
|
||||||
|
rootCmd.Flags().Bool("version", false, "Application version")
|
||||||
|
|
||||||
|
rootCmd.AddCommand(config.RootCmd)
|
||||||
|
rootCmd.AddCommand(morph.RootCmd)
|
||||||
|
rootCmd.AddCommand(storagecfg.RootCmd)
|
||||||
|
|
||||||
|
rootCmd.AddCommand(autocomplete.Command("frostfs-adm"))
|
||||||
|
rootCmd.AddCommand(gendoc.Command(rootCmd))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Execute() error {
|
||||||
|
return rootCmd.Execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
func entryPoint(cmd *cobra.Command, args []string) error {
|
||||||
|
printVersion, _ := cmd.Flags().GetBool("version")
|
||||||
|
if printVersion {
|
||||||
|
cmd.Print(misc.BuildInfo("NeoFS Adm"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.Usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
func initConfig(cmd *cobra.Command) {
|
||||||
|
configFile, err := cmd.Flags().GetString(configFlag)
|
||||||
|
if err != nil || configFile == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
viper.SetConfigType("yml")
|
||||||
|
viper.SetConfigFile(configFile)
|
||||||
|
_ = viper.ReadInConfig() // if config file is set but unavailable, ignore it
|
||||||
|
}
|
138
cmd/frostfs-adm/internal/modules/storagecfg/config.go
Normal file
138
cmd/frostfs-adm/internal/modules/storagecfg/config.go
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
package storagecfg
|
||||||
|
|
||||||
|
const configTemplate = `logger:
|
||||||
|
level: info # logger level: one of "debug", "info" (default), "warn", "error", "dpanic", "panic", "fatal"
|
||||||
|
|
||||||
|
node:
|
||||||
|
wallet:
|
||||||
|
path: {{ .Wallet.Path }} # path to a NEO wallet; ignored if key is presented
|
||||||
|
address: {{ .Wallet.Account }} # address of a NEO account in the wallet; ignored if key is presented
|
||||||
|
password: {{ .Wallet.Password }} # password for a NEO account in the wallet; ignored if key is presented
|
||||||
|
addresses: # list of addresses announced by Storage node in the Network map
|
||||||
|
- {{ .AnnouncedAddress }}
|
||||||
|
attribute_0: UN-LOCODE:{{ .Attribute.Locode }}
|
||||||
|
relay: {{ .Relay }} # start Storage node in relay mode without bootstrapping into the Network map
|
||||||
|
subnet:
|
||||||
|
exit_zero: false # toggle entrance to zero subnet (overrides corresponding attribute and occurrence in entries)
|
||||||
|
entries: [] # list of IDs of subnets to enter in a text format of NeoFS API protocol (overrides corresponding attributes)
|
||||||
|
|
||||||
|
grpc:
|
||||||
|
num: 1 # total number of listener endpoints
|
||||||
|
0:
|
||||||
|
endpoint: {{ .Endpoint }} # endpoint for gRPC server
|
||||||
|
tls:{{if .TLSCert}}
|
||||||
|
enabled: true # enable TLS for a gRPC connection (min version is TLS 1.2)
|
||||||
|
certificate: {{ .TLSCert }} # path to TLS certificate
|
||||||
|
key: {{ .TLSKey }} # path to TLS key
|
||||||
|
{{- else }}
|
||||||
|
enabled: false # disable TLS for a gRPC connection
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
control:
|
||||||
|
authorized_keys: # list of hex-encoded public keys that have rights to use the Control Service
|
||||||
|
{{- range .AuthorizedKeys }}
|
||||||
|
- {{.}}{{end}}
|
||||||
|
grpc:
|
||||||
|
endpoint: {{.ControlEndpoint}} # endpoint that is listened by the Control Service
|
||||||
|
|
||||||
|
morph:
|
||||||
|
dial_timeout: 20s # timeout for side chain NEO RPC client connection
|
||||||
|
cache_ttl: 15s # use TTL cache for side chain GET operations
|
||||||
|
rpc_endpoint: # side chain N3 RPC endpoints
|
||||||
|
{{- range .MorphRPC }}
|
||||||
|
- address: wss://{{.}}/ws{{end}}
|
||||||
|
{{if not .Relay }}
|
||||||
|
storage:
|
||||||
|
shard_pool_size: 15 # size of per-shard worker pools used for PUT operations
|
||||||
|
|
||||||
|
shard:
|
||||||
|
default: # section with the default shard parameters
|
||||||
|
metabase:
|
||||||
|
perm: 0644 # permissions for metabase files(directories: +x for current user and group)
|
||||||
|
|
||||||
|
blobstor:
|
||||||
|
perm: 0644 # permissions for blobstor files(directories: +x for current user and group)
|
||||||
|
depth: 2 # max depth of object tree storage in FS
|
||||||
|
small_object_size: 102400 # 100KiB, size threshold for "small" objects which are stored in key-value DB, not in FS, bytes
|
||||||
|
compress: true # turn on/off Zstandard compression (level 3) of stored objects
|
||||||
|
compression_exclude_content_types:
|
||||||
|
- audio/*
|
||||||
|
- video/*
|
||||||
|
|
||||||
|
blobovnicza:
|
||||||
|
size: 1073741824 # approximate size limit of single blobovnicza instance, total size will be: size*width^(depth+1), bytes
|
||||||
|
depth: 1 # max depth of object tree storage in key-value DB
|
||||||
|
width: 4 # max width of object tree storage in key-value DB
|
||||||
|
opened_cache_capacity: 50 # maximum number of opened database files
|
||||||
|
|
||||||
|
gc:
|
||||||
|
remover_batch_size: 200 # number of objects to be removed by the garbage collector
|
||||||
|
remover_sleep_interval: 5m # frequency of the garbage collector invocation
|
||||||
|
0:
|
||||||
|
mode: "read-write" # mode of the shard, must be one of the: "read-write" (default), "read-only"
|
||||||
|
|
||||||
|
metabase:
|
||||||
|
path: {{ .MetabasePath }} # path to the metabase
|
||||||
|
|
||||||
|
blobstor:
|
||||||
|
path: {{ .BlobstorPath }} # path to the blobstor
|
||||||
|
{{end}}`
|
||||||
|
|
||||||
|
const (
|
||||||
|
neofsMainnetAddress = "2cafa46838e8b564468ebd868dcafdd99dce6221"
|
||||||
|
balanceMainnetAddress = "dc1ec98d9d0c5f9dfade16144defe08cffc5ca55"
|
||||||
|
neofsTestnetAddress = "b65d8243ac63983206d17e5221af0653a7266fa1"
|
||||||
|
balanceTestnetAddress = "e0420c216003747626670d1424569c17c79015bf"
|
||||||
|
)
|
||||||
|
|
||||||
|
var n3config = map[string]struct {
|
||||||
|
MorphRPC []string
|
||||||
|
RPC []string
|
||||||
|
NeoFSContract string
|
||||||
|
BalanceContract string
|
||||||
|
}{
|
||||||
|
"testnet": {
|
||||||
|
MorphRPC: []string{
|
||||||
|
"rpc01.morph.testnet.fs.neo.org:51331",
|
||||||
|
"rpc02.morph.testnet.fs.neo.org:51331",
|
||||||
|
"rpc03.morph.testnet.fs.neo.org:51331",
|
||||||
|
"rpc04.morph.testnet.fs.neo.org:51331",
|
||||||
|
"rpc05.morph.testnet.fs.neo.org:51331",
|
||||||
|
"rpc06.morph.testnet.fs.neo.org:51331",
|
||||||
|
"rpc07.morph.testnet.fs.neo.org:51331",
|
||||||
|
},
|
||||||
|
RPC: []string{
|
||||||
|
"rpc01.testnet.n3.nspcc.ru:21331",
|
||||||
|
"rpc02.testnet.n3.nspcc.ru:21331",
|
||||||
|
"rpc03.testnet.n3.nspcc.ru:21331",
|
||||||
|
"rpc04.testnet.n3.nspcc.ru:21331",
|
||||||
|
"rpc05.testnet.n3.nspcc.ru:21331",
|
||||||
|
"rpc06.testnet.n3.nspcc.ru:21331",
|
||||||
|
"rpc07.testnet.n3.nspcc.ru:21331",
|
||||||
|
},
|
||||||
|
NeoFSContract: neofsTestnetAddress,
|
||||||
|
BalanceContract: balanceTestnetAddress,
|
||||||
|
},
|
||||||
|
"mainnet": {
|
||||||
|
MorphRPC: []string{
|
||||||
|
"rpc1.morph.fs.neo.org:40341",
|
||||||
|
"rpc2.morph.fs.neo.org:40341",
|
||||||
|
"rpc3.morph.fs.neo.org:40341",
|
||||||
|
"rpc4.morph.fs.neo.org:40341",
|
||||||
|
"rpc5.morph.fs.neo.org:40341",
|
||||||
|
"rpc6.morph.fs.neo.org:40341",
|
||||||
|
"rpc7.morph.fs.neo.org:40341",
|
||||||
|
},
|
||||||
|
RPC: []string{
|
||||||
|
"rpc1.n3.nspcc.ru:10331",
|
||||||
|
"rpc2.n3.nspcc.ru:10331",
|
||||||
|
"rpc3.n3.nspcc.ru:10331",
|
||||||
|
"rpc4.n3.nspcc.ru:10331",
|
||||||
|
"rpc5.n3.nspcc.ru:10331",
|
||||||
|
"rpc6.n3.nspcc.ru:10331",
|
||||||
|
"rpc7.n3.nspcc.ru:10331",
|
||||||
|
},
|
||||||
|
NeoFSContract: neofsMainnetAddress,
|
||||||
|
BalanceContract: balanceMainnetAddress,
|
||||||
|
},
|
||||||
|
}
|
417
cmd/frostfs-adm/internal/modules/storagecfg/root.go
Normal file
417
cmd/frostfs-adm/internal/modules/storagecfg/root.go
Normal file
|
@ -0,0 +1,417 @@
|
||||||
|
package storagecfg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
netutil "github.com/TrueCloudLab/frostfs-node/pkg/network"
|
||||||
|
"github.com/chzyer/readline"
|
||||||
|
"github.com/nspcc-dev/neo-go/cli/flags"
|
||||||
|
"github.com/nspcc-dev/neo-go/cli/input"
|
||||||
|
"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/rpcclient"
|
||||||
|
"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/nep17"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
walletFlag = "wallet"
|
||||||
|
accountFlag = "account"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultControlEndpoint = "localhost:8090"
|
||||||
|
defaultDataEndpoint = "localhost"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RootCmd is a root command of config section.
|
||||||
|
var RootCmd = &cobra.Command{
|
||||||
|
Use: "storage-config [-w wallet] [-a acccount] [<path-to-config>]",
|
||||||
|
Short: "Section for storage node configuration commands",
|
||||||
|
Run: storageConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
fs := RootCmd.Flags()
|
||||||
|
|
||||||
|
fs.StringP(walletFlag, "w", "", "Path to wallet")
|
||||||
|
fs.StringP(accountFlag, "a", "", "Wallet account")
|
||||||
|
}
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
AnnouncedAddress string
|
||||||
|
AuthorizedKeys []string
|
||||||
|
ControlEndpoint string
|
||||||
|
Endpoint string
|
||||||
|
TLSCert string
|
||||||
|
TLSKey string
|
||||||
|
MorphRPC []string
|
||||||
|
Attribute struct {
|
||||||
|
Locode string
|
||||||
|
}
|
||||||
|
Wallet struct {
|
||||||
|
Path string
|
||||||
|
Account string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
Relay bool
|
||||||
|
BlobstorPath string
|
||||||
|
MetabasePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func storageConfig(cmd *cobra.Command, args []string) {
|
||||||
|
var outPath string
|
||||||
|
if len(args) != 0 {
|
||||||
|
outPath = args[0]
|
||||||
|
} else {
|
||||||
|
outPath = getPath("File to write config at [./config.yml]: ")
|
||||||
|
if outPath == "" {
|
||||||
|
outPath = "./config.yml"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
historyPath := filepath.Join(os.TempDir(), "frostfs-adm.history")
|
||||||
|
readline.SetHistoryPath(historyPath)
|
||||||
|
|
||||||
|
var c config
|
||||||
|
|
||||||
|
c.Wallet.Path, _ = cmd.Flags().GetString(walletFlag)
|
||||||
|
if c.Wallet.Path == "" {
|
||||||
|
c.Wallet.Path = getPath("Path to the storage node wallet: ")
|
||||||
|
}
|
||||||
|
|
||||||
|
w, err := wallet.NewWalletFromFile(c.Wallet.Path)
|
||||||
|
fatalOnErr(err)
|
||||||
|
|
||||||
|
c.Wallet.Account, _ = cmd.Flags().GetString(accountFlag)
|
||||||
|
if c.Wallet.Account == "" {
|
||||||
|
addr := address.Uint160ToString(w.GetChangeAddress())
|
||||||
|
c.Wallet.Account = getWalletAccount(w, fmt.Sprintf("Wallet account [%s]: ", addr))
|
||||||
|
if c.Wallet.Account == "" {
|
||||||
|
c.Wallet.Account = addr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
accH, err := flags.ParseAddress(c.Wallet.Account)
|
||||||
|
fatalOnErr(err)
|
||||||
|
|
||||||
|
acc := w.GetAccount(accH)
|
||||||
|
if acc == nil {
|
||||||
|
fatalOnErr(errors.New("can't find account in wallet"))
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Wallet.Password, err = input.ReadPassword(fmt.Sprintf("Account password for %s: ", c.Wallet.Account))
|
||||||
|
fatalOnErr(err)
|
||||||
|
|
||||||
|
err = acc.Decrypt(c.Wallet.Password, keys.NEP2ScryptParams())
|
||||||
|
fatalOnErr(err)
|
||||||
|
|
||||||
|
c.AuthorizedKeys = append(c.AuthorizedKeys, hex.EncodeToString(acc.PrivateKey().PublicKey().Bytes()))
|
||||||
|
|
||||||
|
var network string
|
||||||
|
for {
|
||||||
|
network = getString("Choose network [mainnet]/testnet: ")
|
||||||
|
switch network {
|
||||||
|
case "":
|
||||||
|
network = "mainnet"
|
||||||
|
case "testnet", "mainnet":
|
||||||
|
default:
|
||||||
|
cmd.Println(`Network must be either "mainnet" or "testnet"`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
c.MorphRPC = n3config[network].MorphRPC
|
||||||
|
|
||||||
|
depositGas(cmd, acc, network)
|
||||||
|
|
||||||
|
c.Attribute.Locode = getString("UN-LOCODE attribute in [XX YYY] format: ")
|
||||||
|
var addr, port string
|
||||||
|
for {
|
||||||
|
c.AnnouncedAddress = getString("Publicly announced address: ")
|
||||||
|
validator := netutil.Address{}
|
||||||
|
err := validator.FromString(c.AnnouncedAddress)
|
||||||
|
if err != nil {
|
||||||
|
cmd.Println("Incorrect address format. See https://github.com/TrueCloudLab/frostfs-node/blob/master/pkg/network/address.go for details.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
uriAddr, err := url.Parse(validator.URIAddr())
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Errorf("unexpected error: %w", err))
|
||||||
|
}
|
||||||
|
addr = uriAddr.Hostname()
|
||||||
|
port = uriAddr.Port()
|
||||||
|
ip, err := net.ResolveIPAddr("ip", addr)
|
||||||
|
if err != nil {
|
||||||
|
cmd.Printf("Can't resolve IP address %s: %v\n", addr, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ip.IP.IsGlobalUnicast() {
|
||||||
|
cmd.Println("IP must be global unicast.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cmd.Printf("Resolved IP address: %s\n", ip.String())
|
||||||
|
|
||||||
|
_, err = strconv.ParseUint(port, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
cmd.Println("Port must be an integer.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultAddr := net.JoinHostPort(defaultDataEndpoint, port)
|
||||||
|
c.Endpoint = getString(fmt.Sprintf("Listening address [%s]: ", defaultAddr))
|
||||||
|
if c.Endpoint == "" {
|
||||||
|
c.Endpoint = defaultAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ControlEndpoint = getString(fmt.Sprintf("Listening address (control endpoint) [%s]: ", defaultControlEndpoint))
|
||||||
|
if c.ControlEndpoint == "" {
|
||||||
|
c.ControlEndpoint = defaultControlEndpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
c.TLSCert = getPath("TLS Certificate (optional): ")
|
||||||
|
if c.TLSCert != "" {
|
||||||
|
c.TLSKey = getPath("TLS Key: ")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Relay = getConfirmation(false, "Use node as a relay? yes/[no]: ")
|
||||||
|
if !c.Relay {
|
||||||
|
p := getPath("Path to the storage directory (all available storage will be used): ")
|
||||||
|
c.BlobstorPath = filepath.Join(p, "blob")
|
||||||
|
c.MetabasePath = filepath.Join(p, "meta")
|
||||||
|
}
|
||||||
|
|
||||||
|
out := applyTemplate(c)
|
||||||
|
fatalOnErr(os.WriteFile(outPath, out, 0644))
|
||||||
|
|
||||||
|
cmd.Println("Node is ready for work! Run `frostfs-node -config " + outPath + "`")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWalletAccount(w *wallet.Wallet, prompt string) string {
|
||||||
|
addrs := make([]readline.PrefixCompleterInterface, len(w.Accounts))
|
||||||
|
for i := range w.Accounts {
|
||||||
|
addrs[i] = readline.PcItem(w.Accounts[i].Address)
|
||||||
|
}
|
||||||
|
|
||||||
|
readline.SetAutoComplete(readline.NewPrefixCompleter(addrs...))
|
||||||
|
defer readline.SetAutoComplete(nil)
|
||||||
|
|
||||||
|
s, err := readline.Line(prompt)
|
||||||
|
fatalOnErr(err)
|
||||||
|
return strings.TrimSpace(s) // autocompleter can return a string with a trailing space
|
||||||
|
}
|
||||||
|
|
||||||
|
func getString(prompt string) string {
|
||||||
|
s, err := readline.Line(prompt)
|
||||||
|
fatalOnErr(err)
|
||||||
|
if s != "" {
|
||||||
|
_ = readline.AddHistory(s)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
type filenameCompleter struct{}
|
||||||
|
|
||||||
|
func (filenameCompleter) Do(line []rune, pos int) (newLine [][]rune, length int) {
|
||||||
|
prefix := string(line[:pos])
|
||||||
|
dir := filepath.Dir(prefix)
|
||||||
|
de, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range de {
|
||||||
|
name := filepath.Join(dir, de[i].Name())
|
||||||
|
if strings.HasPrefix(name, prefix) {
|
||||||
|
tail := []rune(strings.TrimPrefix(name, prefix))
|
||||||
|
if de[i].IsDir() {
|
||||||
|
tail = append(tail, filepath.Separator)
|
||||||
|
}
|
||||||
|
newLine = append(newLine, tail)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pos != 0 {
|
||||||
|
return newLine, pos - len([]rune(dir))
|
||||||
|
}
|
||||||
|
return newLine, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPath(prompt string) string {
|
||||||
|
readline.SetAutoComplete(filenameCompleter{})
|
||||||
|
defer readline.SetAutoComplete(nil)
|
||||||
|
|
||||||
|
p, err := readline.Line(prompt)
|
||||||
|
fatalOnErr(err)
|
||||||
|
|
||||||
|
if p == "" {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = readline.AddHistory(p)
|
||||||
|
|
||||||
|
abs, err := filepath.Abs(p)
|
||||||
|
if err != nil {
|
||||||
|
fatalOnErr(fmt.Errorf("can't create an absolute path: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return abs
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConfirmation(def bool, prompt string) bool {
|
||||||
|
for {
|
||||||
|
s, err := readline.Line(prompt)
|
||||||
|
fatalOnErr(err)
|
||||||
|
|
||||||
|
switch strings.ToLower(s) {
|
||||||
|
case "y", "yes":
|
||||||
|
return true
|
||||||
|
case "n", "no":
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
if len(s) == 0 {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyTemplate(c config) []byte {
|
||||||
|
tmpl, err := template.New("config").Parse(configTemplate)
|
||||||
|
fatalOnErr(err)
|
||||||
|
|
||||||
|
b := bytes.NewBuffer(nil)
|
||||||
|
fatalOnErr(tmpl.Execute(b, c))
|
||||||
|
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func fatalOnErr(err error) {
|
||||||
|
if err != nil {
|
||||||
|
_, _ = fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func depositGas(cmd *cobra.Command, acc *wallet.Account, network string) {
|
||||||
|
sideClient := initClient(n3config[network].MorphRPC)
|
||||||
|
balanceHash, _ := util.Uint160DecodeStringLE(n3config[network].BalanceContract)
|
||||||
|
|
||||||
|
sideActor, err := actor.NewSimple(sideClient, acc)
|
||||||
|
if err != nil {
|
||||||
|
fatalOnErr(fmt.Errorf("creating actor over side chain client: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
sideGas := nep17.NewReader(sideActor, balanceHash)
|
||||||
|
accSH := acc.Contract.ScriptHash()
|
||||||
|
|
||||||
|
balance, err := sideGas.BalanceOf(accSH)
|
||||||
|
if err != nil {
|
||||||
|
fatalOnErr(fmt.Errorf("side chain balance: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
ok := getConfirmation(false, fmt.Sprintf("Current NeoFS balance is %s, make a deposit? y/[n]: ",
|
||||||
|
fixedn.ToString(balance, 12)))
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
amountStr := getString("Enter amount in GAS: ")
|
||||||
|
amount, err := fixedn.FromString(amountStr, 8)
|
||||||
|
if err != nil {
|
||||||
|
fatalOnErr(fmt.Errorf("invalid amount: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
mainClient := initClient(n3config[network].RPC)
|
||||||
|
neofsHash, _ := util.Uint160DecodeStringLE(n3config[network].NeoFSContract)
|
||||||
|
|
||||||
|
mainActor, err := actor.NewSimple(mainClient, acc)
|
||||||
|
if err != nil {
|
||||||
|
fatalOnErr(fmt.Errorf("creating actor over main chain client: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
mainGas := nep17.New(mainActor, gas.Hash)
|
||||||
|
|
||||||
|
txHash, _, err := mainGas.Transfer(accSH, neofsHash, amount, nil)
|
||||||
|
if err != nil {
|
||||||
|
fatalOnErr(fmt.Errorf("sending TX to the NeoFS contract: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Print("Waiting for transactions to persist.")
|
||||||
|
tick := time.NewTicker(time.Second / 2)
|
||||||
|
defer tick.Stop()
|
||||||
|
|
||||||
|
timer := time.NewTimer(time.Second * 20)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
at := trigger.Application
|
||||||
|
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-tick.C:
|
||||||
|
_, err := mainClient.GetApplicationLog(txHash, &at)
|
||||||
|
if err == nil {
|
||||||
|
cmd.Print("\n")
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
cmd.Print(".")
|
||||||
|
case <-timer.C:
|
||||||
|
cmd.Printf("\nTimeout while waiting for transaction to persist.\n")
|
||||||
|
if getConfirmation(false, "Continue configuration? yes/[no]: ") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initClient(rpc []string) *rpcclient.Client {
|
||||||
|
var c *rpcclient.Client
|
||||||
|
var err error
|
||||||
|
|
||||||
|
shuffled := make([]string, len(rpc))
|
||||||
|
copy(shuffled, rpc)
|
||||||
|
rand.Shuffle(len(shuffled), func(i, j int) { shuffled[i], shuffled[j] = shuffled[j], shuffled[i] })
|
||||||
|
|
||||||
|
for _, endpoint := range shuffled {
|
||||||
|
c, err = rpcclient.New(context.Background(), "https://"+endpoint, rpcclient.Options{
|
||||||
|
DialTimeout: time.Second * 2,
|
||||||
|
RequestTimeout: time.Second * 5,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err = c.Init(); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
fatalOnErr(fmt.Errorf("can't create N3 client: %w", err))
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
13
cmd/frostfs-adm/main.go
Normal file
13
cmd/frostfs-adm/main.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-adm/internal/modules"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := modules.Execute(); err != nil {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
75
cmd/frostfs-cli/docs/sessions.md
Normal file
75
cmd/frostfs-cli/docs/sessions.md
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
# How NeoFS CLI uses session mechanism of the NeoFS
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
NeoFS sessions implement a mechanism for issuing a power of attorney by one
|
||||||
|
party to another. A trusted party can provide a so-called session token as
|
||||||
|
proof of the right to act on behalf of another member of the network. The
|
||||||
|
client of operations carried out with such a token will be the user who opened
|
||||||
|
the session. The token contains information which limits power of attorney like
|
||||||
|
action context or lifetime.
|
||||||
|
|
||||||
|
The client confirms trust in a third party by signing its public (session) key
|
||||||
|
with his private key. Any operation signed using private session key with
|
||||||
|
attached session token is treated as performed by the original client.
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
NeoFS CLI supports two ways to execute operation within a session depending on
|
||||||
|
whether the user of the command application is an original user (1) or a trusted
|
||||||
|
one (2).
|
||||||
|
|
||||||
|
### Dynamic
|
||||||
|
|
||||||
|
For case (1) CLI user can only open dynamic sessions. Protocol call
|
||||||
|
`SessionService.Create` is used for this purpose. As a result of the call, a
|
||||||
|
private session key will be generated on the server, thus making the remote
|
||||||
|
server trusted. This type of session is useful when the client needs to
|
||||||
|
transfer part of the responsibility for the formation of strict system elements
|
||||||
|
to the trusted server. At the moment, the approach is applicable only to
|
||||||
|
creating objects.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ frostfs-cli session create --rpc-endpoint <server_ip> --out ./blank_token
|
||||||
|
```
|
||||||
|
After this example command remote node holds session private key while its
|
||||||
|
public part is written into the session token encoded into the output file.
|
||||||
|
Later this token can be attached to the operations which support dynamic
|
||||||
|
sessions. Then the token will be finally formed and signed by CLI itself.
|
||||||
|
|
||||||
|
### Static
|
||||||
|
|
||||||
|
For case (2) CLI user can act on behalf of the person who issued the session
|
||||||
|
token to him. Unlike (1) the token must be fully prepared on the side of the
|
||||||
|
original client, and the CLI uses it only for reading. Ready token MUST have:
|
||||||
|
- correct context (object, container, etc.)
|
||||||
|
- valid lifetime
|
||||||
|
- public session key corresponding to the CLI key
|
||||||
|
- valid client signature
|
||||||
|
|
||||||
|
To sign the session token, exec:
|
||||||
|
```shell
|
||||||
|
$ frostfs-cli --wallet <client_wallet> util sign session-token --from ./blank_token --to ./token
|
||||||
|
```
|
||||||
|
Once the token is signed, it MUST NOT be modified.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Object
|
||||||
|
|
||||||
|
Here are sub-commands of `object` command which support only dynamic sessions (1):
|
||||||
|
- `put`
|
||||||
|
- `delete`
|
||||||
|
- `lock`
|
||||||
|
|
||||||
|
These commands accept blank token of the dynamically opened session or open
|
||||||
|
session internally if it has not been opened yet.
|
||||||
|
|
||||||
|
All other `object` sub-commands support only static sessions (2).
|
||||||
|
|
||||||
|
### Container
|
||||||
|
|
||||||
|
List of commands supporting sessions (static only):
|
||||||
|
- `create`
|
||||||
|
- `delete`
|
||||||
|
- `set-eacl`
|
34
cmd/frostfs-cli/docs/storage-node-xheaders.md
Normal file
34
cmd/frostfs-cli/docs/storage-node-xheaders.md
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
# Extended headers
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Extended headers are used for request/response. They may contain any user-defined headers
|
||||||
|
to be interpreted on application level.
|
||||||
|
Key name must be a unique valid UTF-8 string. Value can't be empty. Requests or
|
||||||
|
Responses with duplicated header names or headers with empty values are
|
||||||
|
considered invalid.
|
||||||
|
|
||||||
|
## Existing headers
|
||||||
|
|
||||||
|
There are some "well-known" headers starting with `__NEOFS__` prefix that
|
||||||
|
affect system behaviour:
|
||||||
|
|
||||||
|
* `__NEOFS__NETMAP_EPOCH` - netmap epoch to use for object placement calculation. The `value` is string
|
||||||
|
encoded `uint64` in decimal presentation. If set to '0' or omitted, the
|
||||||
|
current epoch only will be used.
|
||||||
|
* `__NEOFS__NETMAP_LOOKUP_DEPTH` - if object can't be found using current epoch's netmap, this header limits
|
||||||
|
how many past epochs the node can look up through. Depth is applied to a current epoch or the value
|
||||||
|
of `__NEOFS__NETMAP_EPOCH` attribute. The `value` is string encoded `uint64` in decimal presentation.
|
||||||
|
If set to '0' or not set, only the current epoch is used.
|
||||||
|
|
||||||
|
## `neofs-cli` commands with `--xhdr`
|
||||||
|
|
||||||
|
List of commands with support of extended headers:
|
||||||
|
* `container list-objects`
|
||||||
|
* `object delete/get/hash/head/lock/put/range/search`
|
||||||
|
* `storagegroup delete/get/list/put`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```shell
|
||||||
|
$ neofs-cli object put -r s01.neofs.devenv:8080 -w wallet.json --cid CID --file FILE --xhdr "__NEOFS__NETMAP_EPOCH=777"
|
||||||
|
```
|
892
cmd/frostfs-cli/internal/client/client.go
Normal file
892
cmd/frostfs-cli/internal/client/client.go
Normal file
|
@ -0,0 +1,892 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-sdk-go/accounting"
|
||||||
|
"github.com/TrueCloudLab/frostfs-sdk-go/client"
|
||||||
|
containerSDK "github.com/TrueCloudLab/frostfs-sdk-go/container"
|
||||||
|
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
|
"github.com/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||||
|
"github.com/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||||
|
"github.com/TrueCloudLab/frostfs-sdk-go/object"
|
||||||
|
oid "github.com/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||||
|
"github.com/TrueCloudLab/frostfs-sdk-go/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BalanceOfPrm groups parameters of BalanceOf operation.
|
||||||
|
type BalanceOfPrm struct {
|
||||||
|
commonPrm
|
||||||
|
client.PrmBalanceGet
|
||||||
|
}
|
||||||
|
|
||||||
|
// BalanceOfRes groups the resulting values of BalanceOf operation.
|
||||||
|
type BalanceOfRes struct {
|
||||||
|
cliRes *client.ResBalanceGet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Balance returns the current balance.
|
||||||
|
func (x BalanceOfRes) Balance() accounting.Decimal {
|
||||||
|
return x.cliRes.Amount()
|
||||||
|
}
|
||||||
|
|
||||||
|
// BalanceOf requests the current balance of a NeoFS user.
|
||||||
|
//
|
||||||
|
// Returns any error which prevented the operation from completing correctly in error return.
|
||||||
|
func BalanceOf(prm BalanceOfPrm) (res BalanceOfRes, err error) {
|
||||||
|
res.cliRes, err = prm.cli.BalanceGet(context.Background(), prm.PrmBalanceGet)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListContainersPrm groups parameters of ListContainers operation.
|
||||||
|
type ListContainersPrm struct {
|
||||||
|
commonPrm
|
||||||
|
client.PrmContainerList
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListContainersRes groups the resulting values of ListContainers operation.
|
||||||
|
type ListContainersRes struct {
|
||||||
|
cliRes *client.ResContainerList
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDList returns list of identifiers of user's containers.
|
||||||
|
func (x ListContainersRes) IDList() []cid.ID {
|
||||||
|
return x.cliRes.Containers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListContainers requests a list of NeoFS user's containers.
|
||||||
|
//
|
||||||
|
// Returns any error which prevented the operation from completing correctly in error return.
|
||||||
|
func ListContainers(prm ListContainersPrm) (res ListContainersRes, err error) {
|
||||||
|
res.cliRes, err = prm.cli.ContainerList(context.Background(), prm.PrmContainerList)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutContainerPrm groups parameters of PutContainer operation.
|
||||||
|
type PutContainerPrm struct {
|
||||||
|
commonPrm
|
||||||
|
client.PrmContainerPut
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutContainerRes groups the resulting values of PutContainer operation.
|
||||||
|
type PutContainerRes struct {
|
||||||
|
cnr cid.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns identifier of the created container.
|
||||||
|
func (x PutContainerRes) ID() cid.ID {
|
||||||
|
return x.cnr
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutContainer sends a request to save the container in NeoFS.
|
||||||
|
//
|
||||||
|
// Operation is asynchronous and not guaranteed even in the absence of errors.
|
||||||
|
// The required time is also not predictable.
|
||||||
|
//
|
||||||
|
// Success can be verified by reading by identifier.
|
||||||
|
//
|
||||||
|
// Returns any error which prevented the operation from completing correctly in error return.
|
||||||
|
func PutContainer(prm PutContainerPrm) (res PutContainerRes, err error) {
|
||||||
|
cliRes, err := prm.cli.ContainerPut(context.Background(), prm.PrmContainerPut)
|
||||||
|
if err == nil {
|
||||||
|
res.cnr = cliRes.ID()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetContainerPrm groups parameters of GetContainer operation.
|
||||||
|
type GetContainerPrm struct {
|
||||||
|
commonPrm
|
||||||
|
cliPrm client.PrmContainerGet
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContainer sets identifier of the container to be read.
|
||||||
|
func (x *GetContainerPrm) SetContainer(id cid.ID) {
|
||||||
|
x.cliPrm.SetContainer(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetContainerRes groups the resulting values of GetContainer operation.
|
||||||
|
type GetContainerRes struct {
|
||||||
|
cliRes *client.ResContainerGet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Container returns structured of the requested container.
|
||||||
|
func (x GetContainerRes) Container() containerSDK.Container {
|
||||||
|
return x.cliRes.Container()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetContainer reads a container from NeoFS by ID.
|
||||||
|
//
|
||||||
|
// Returns any error which prevented the operation from completing correctly in error return.
|
||||||
|
func GetContainer(prm GetContainerPrm) (res GetContainerRes, err error) {
|
||||||
|
res.cliRes, err = prm.cli.ContainerGet(context.Background(), prm.cliPrm)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsACLExtendable checks if ACL of the container referenced by the given identifier
|
||||||
|
// can be extended. Client connection MUST BE correctly established in advance.
|
||||||
|
func IsACLExtendable(c *client.Client, cnr cid.ID) (bool, error) {
|
||||||
|
var prm GetContainerPrm
|
||||||
|
prm.SetClient(c)
|
||||||
|
prm.SetContainer(cnr)
|
||||||
|
|
||||||
|
res, err := GetContainer(prm)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("get container from the NeoFS: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Container().BasicACL().Extendable(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteContainerPrm groups parameters of DeleteContainerPrm operation.
|
||||||
|
type DeleteContainerPrm struct {
|
||||||
|
commonPrm
|
||||||
|
client.PrmContainerDelete
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteContainerRes groups the resulting values of DeleteContainer operation.
|
||||||
|
type DeleteContainerRes struct{}
|
||||||
|
|
||||||
|
// DeleteContainer sends a request to remove a container from NeoFS by ID.
|
||||||
|
//
|
||||||
|
// Operation is asynchronous and not guaranteed even in the absence of errors.
|
||||||
|
// The required time is also not predictable.
|
||||||
|
//
|
||||||
|
// Success can be verified by reading by identifier.
|
||||||
|
//
|
||||||
|
// Returns any error which prevented the operation from completing correctly in error return.
|
||||||
|
func DeleteContainer(prm DeleteContainerPrm) (res DeleteContainerRes, err error) {
|
||||||
|
_, err = prm.cli.ContainerDelete(context.Background(), prm.PrmContainerDelete)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// EACLPrm groups parameters of EACL operation.
|
||||||
|
type EACLPrm struct {
|
||||||
|
commonPrm
|
||||||
|
client.PrmContainerEACL
|
||||||
|
}
|
||||||
|
|
||||||
|
// EACLRes groups the resulting values of EACL operation.
|
||||||
|
type EACLRes struct {
|
||||||
|
cliRes *client.ResContainerEACL
|
||||||
|
}
|
||||||
|
|
||||||
|
// EACL returns requested eACL table.
|
||||||
|
func (x EACLRes) EACL() eacl.Table {
|
||||||
|
return x.cliRes.Table()
|
||||||
|
}
|
||||||
|
|
||||||
|
// EACL reads eACL table from NeoFS by container ID.
|
||||||
|
//
|
||||||
|
// Returns any error which prevented the operation from completing correctly in error return.
|
||||||
|
func EACL(prm EACLPrm) (res EACLRes, err error) {
|
||||||
|
res.cliRes, err = prm.cli.ContainerEACL(context.Background(), prm.PrmContainerEACL)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEACLPrm groups parameters of SetEACL operation.
|
||||||
|
type SetEACLPrm struct {
|
||||||
|
commonPrm
|
||||||
|
client.PrmContainerSetEACL
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEACLRes groups the resulting values of SetEACL operation.
|
||||||
|
type SetEACLRes struct{}
|
||||||
|
|
||||||
|
// SetEACL requests to save an eACL table in NeoFS.
|
||||||
|
//
|
||||||
|
// Operation is asynchronous and no guaranteed even in the absence of errors.
|
||||||
|
// The required time is also not predictable.
|
||||||
|
//
|
||||||
|
// Success can be verified by reading by container identifier.
|
||||||
|
//
|
||||||
|
// Returns any error which prevented the operation from completing correctly in error return.
|
||||||
|
func SetEACL(prm SetEACLPrm) (res SetEACLRes, err error) {
|
||||||
|
_, err = prm.cli.ContainerSetEACL(context.Background(), prm.PrmContainerSetEACL)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkInfoPrm groups parameters of NetworkInfo operation.
|
||||||
|
type NetworkInfoPrm struct {
|
||||||
|
commonPrm
|
||||||
|
client.PrmNetworkInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkInfoRes groups the resulting values of NetworkInfo operation.
|
||||||
|
type NetworkInfoRes struct {
|
||||||
|
cliRes *client.ResNetworkInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkInfo returns structured information about the NeoFS network.
|
||||||
|
func (x NetworkInfoRes) NetworkInfo() netmap.NetworkInfo {
|
||||||
|
return x.cliRes.Info()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetworkInfo reads information about the NeoFS network.
|
||||||
|
//
|
||||||
|
// Returns any error which prevented the operation from completing correctly in error return.
|
||||||
|
func NetworkInfo(prm NetworkInfoPrm) (res NetworkInfoRes, err error) {
|
||||||
|
res.cliRes, err = prm.cli.NetworkInfo(context.Background(), prm.PrmNetworkInfo)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeInfoPrm groups parameters of NodeInfo operation.
|
||||||
|
type NodeInfoPrm struct {
|
||||||
|
commonPrm
|
||||||
|
client.PrmEndpointInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeInfoRes groups the resulting values of NodeInfo operation.
|
||||||
|
type NodeInfoRes struct {
|
||||||
|
cliRes *client.ResEndpointInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeInfo returns information about the node from netmap.
|
||||||
|
func (x NodeInfoRes) NodeInfo() netmap.NodeInfo {
|
||||||
|
return x.cliRes.NodeInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LatestVersion returns the latest NeoFS API version in use.
|
||||||
|
func (x NodeInfoRes) LatestVersion() version.Version {
|
||||||
|
return x.cliRes.LatestVersion()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeInfo requests information about the remote server from NeoFS netmap.
|
||||||
|
//
|
||||||
|
// Returns any error which prevented the operation from completing correctly in error return.
|
||||||
|
func NodeInfo(prm NodeInfoPrm) (res NodeInfoRes, err error) {
|
||||||
|
res.cliRes, err = prm.cli.EndpointInfo(context.Background(), prm.PrmEndpointInfo)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetMapSnapshotPrm groups parameters of NetMapSnapshot operation.
|
||||||
|
type NetMapSnapshotPrm struct {
|
||||||
|
commonPrm
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetMapSnapshotRes groups the resulting values of NetMapSnapshot operation.
|
||||||
|
type NetMapSnapshotRes struct {
|
||||||
|
cliRes *client.ResNetMapSnapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetMap returns current local snapshot of the NeoFS network map.
|
||||||
|
func (x NetMapSnapshotRes) NetMap() netmap.NetMap {
|
||||||
|
return x.cliRes.NetMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetMapSnapshot requests current network view of the remote server.
|
||||||
|
//
|
||||||
|
// Returns any error which prevented the operation from completing correctly in error return.
|
||||||
|
func NetMapSnapshot(prm NetMapSnapshotPrm) (res NetMapSnapshotRes, err error) {
|
||||||
|
res.cliRes, err = prm.cli.NetMapSnapshot(context.Background(), client.PrmNetMapSnapshot{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSessionPrm groups parameters of CreateSession operation.
|
||||||
|
type CreateSessionPrm struct {
|
||||||
|
commonPrm
|
||||||
|
client.PrmSessionCreate
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSessionRes groups the resulting values of CreateSession operation.
|
||||||
|
type CreateSessionRes struct {
|
||||||
|
cliRes *client.ResSessionCreate
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns session identifier.
|
||||||
|
func (x CreateSessionRes) ID() []byte {
|
||||||
|
return x.cliRes.ID()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionKey returns public session key in a binary format.
|
||||||
|
func (x CreateSessionRes) SessionKey() []byte {
|
||||||
|
return x.cliRes.PublicKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSession opens a new unlimited session with the remote node.
|
||||||
|
//
|
||||||
|
// Returns any error which prevented the operation from completing correctly in error return.
|
||||||
|
func CreateSession(prm CreateSessionPrm) (res CreateSessionRes, err error) {
|
||||||
|
res.cliRes, err = prm.cli.SessionCreate(context.Background(), prm.PrmSessionCreate)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutObjectPrm groups parameters of PutObject operation.
|
||||||
|
type PutObjectPrm struct {
|
||||||
|
commonObjectPrm
|
||||||
|
|
||||||
|
hdr *object.Object
|
||||||
|
|
||||||
|
rdr io.Reader
|
||||||
|
|
||||||
|
headerCallback func(*object.Object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHeader sets object header.
|
||||||
|
func (x *PutObjectPrm) SetHeader(hdr *object.Object) {
|
||||||
|
x.hdr = hdr
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPayloadReader sets reader of the object payload.
|
||||||
|
func (x *PutObjectPrm) SetPayloadReader(rdr io.Reader) {
|
||||||
|
x.rdr = rdr
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHeaderCallback sets callback which is called on the object after the header is received
|
||||||
|
// but before the payload is written.
|
||||||
|
func (x *PutObjectPrm) SetHeaderCallback(f func(*object.Object)) {
|
||||||
|
x.headerCallback = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutObjectRes groups the resulting values of PutObject operation.
|
||||||
|
type PutObjectRes struct {
|
||||||
|
id oid.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns identifier of the created object.
|
||||||
|
func (x PutObjectRes) ID() oid.ID {
|
||||||
|
return x.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutObject saves the object in NeoFS network.
|
||||||
|
//
|
||||||
|
// Returns any error which prevented the operation from completing correctly in error return.
|
||||||
|
func PutObject(prm PutObjectPrm) (*PutObjectRes, error) {
|
||||||
|
var putPrm client.PrmObjectPutInit
|
||||||
|
|
||||||
|
if prm.sessionToken != nil {
|
||||||
|
putPrm.WithinSession(*prm.sessionToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if prm.bearerToken != nil {
|
||||||
|
putPrm.WithBearerToken(*prm.bearerToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if prm.local {
|
||||||
|
putPrm.MarkLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
putPrm.WithXHeaders(prm.xHeaders...)
|
||||||
|
|
||||||
|
wrt, err := prm.cli.ObjectPutInit(context.Background(), putPrm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("init object writing: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if wrt.WriteHeader(*prm.hdr) {
|
||||||
|
if prm.headerCallback != nil {
|
||||||
|
prm.headerCallback(prm.hdr)
|
||||||
|
}
|
||||||
|
|
||||||
|
sz := prm.hdr.PayloadSize()
|
||||||
|
|
||||||
|
if data := prm.hdr.Payload(); len(data) > 0 {
|
||||||
|
if prm.rdr != nil {
|
||||||
|
prm.rdr = io.MultiReader(bytes.NewReader(data), prm.rdr)
|
||||||
|
} else {
|
||||||
|
prm.rdr = bytes.NewReader(data)
|
||||||
|
sz = uint64(len(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if prm.rdr != nil {
|
||||||
|
// TODO: (neofs-node#1198) explore better values or configure it
|
||||||
|
const defaultBufferSizePut = 4096
|
||||||
|
|
||||||
|
if sz == 0 || sz > defaultBufferSizePut {
|
||||||
|
sz = defaultBufferSizePut
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, sz)
|
||||||
|
|
||||||
|
var n int
|
||||||
|
|
||||||
|
for {
|
||||||
|
n, err = prm.rdr.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
if !wrt.WritePayloadChunk(buf[:n]) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("read payload: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cliRes, err := wrt.Close()
|
||||||
|
if err != nil { // here err already carries both status and client errors
|
||||||
|
return nil, fmt.Errorf("client failure: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PutObjectRes{
|
||||||
|
id: cliRes.StoredObjectID(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteObjectPrm groups parameters of DeleteObject operation.
|
||||||
|
type DeleteObjectPrm struct {
|
||||||
|
commonObjectPrm
|
||||||
|
objectAddressPrm
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteObjectRes groups the resulting values of DeleteObject operation.
|
||||||
|
type DeleteObjectRes struct {
|
||||||
|
tomb oid.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tombstone returns the ID of the created object with tombstone.
|
||||||
|
func (x DeleteObjectRes) Tombstone() oid.ID {
|
||||||
|
return x.tomb
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteObject marks an object to be removed from NeoFS through tombstone placement.
|
||||||
|
//
|
||||||
|
// Returns any error which prevented the operation from completing correctly in error return.
|
||||||
|
func DeleteObject(prm DeleteObjectPrm) (*DeleteObjectRes, error) {
|
||||||
|
var delPrm client.PrmObjectDelete
|
||||||
|
delPrm.FromContainer(prm.objAddr.Container())
|
||||||
|
delPrm.ByID(prm.objAddr.Object())
|
||||||
|
|
||||||
|
if prm.sessionToken != nil {
|
||||||
|
delPrm.WithinSession(*prm.sessionToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if prm.bearerToken != nil {
|
||||||
|
delPrm.WithBearerToken(*prm.bearerToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
delPrm.WithXHeaders(prm.xHeaders...)
|
||||||
|
|
||||||
|
cliRes, err := prm.cli.ObjectDelete(context.Background(), delPrm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("remove object via client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DeleteObjectRes{
|
||||||
|
tomb: cliRes.Tombstone(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectPrm groups parameters of GetObject operation.
|
||||||
|
type GetObjectPrm struct {
|
||||||
|
commonObjectPrm
|
||||||
|
objectAddressPrm
|
||||||
|
rawPrm
|
||||||
|
payloadWriterPrm
|
||||||
|
headerCallback func(*object.Object)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHeaderCallback sets callback which is called on the object after the header is received
|
||||||
|
// but before the payload is written.
|
||||||
|
func (p *GetObjectPrm) SetHeaderCallback(f func(*object.Object)) {
|
||||||
|
p.headerCallback = f
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectRes groups the resulting values of GetObject operation.
|
||||||
|
type GetObjectRes struct {
|
||||||
|
hdr *object.Object
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header returns the header of the request object.
|
||||||
|
func (x GetObjectRes) Header() *object.Object {
|
||||||
|
return x.hdr
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObject reads an object by address.
|
||||||
|
//
|
||||||
|
// Interrupts on any writer error. If successful, payload is written to the writer.
|
||||||
|
//
|
||||||
|
// Returns any error which prevented the operation from completing correctly in error return.
|
||||||
|
// For raw reading, returns *object.SplitInfoError error if object is virtual.
|
||||||
|
func GetObject(prm GetObjectPrm) (*GetObjectRes, error) {
|
||||||
|
var getPrm client.PrmObjectGet
|
||||||
|
getPrm.FromContainer(prm.objAddr.Container())
|
||||||
|
getPrm.ByID(prm.objAddr.Object())
|
||||||
|
|
||||||
|
if prm.sessionToken != nil {
|
||||||
|
getPrm.WithinSession(*prm.sessionToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if prm.bearerToken != nil {
|
||||||
|
getPrm.WithBearerToken(*prm.bearerToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if prm.raw {
|
||||||
|
getPrm.MarkRaw()
|
||||||
|
}
|
||||||
|
|
||||||
|
if prm.local {
|
||||||
|
getPrm.MarkLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
getPrm.WithXHeaders(prm.xHeaders...)
|
||||||
|
|
||||||
|
rdr, err := prm.cli.ObjectGetInit(context.Background(), getPrm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("init object reading on client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var hdr object.Object
|
||||||
|
|
||||||
|
if !rdr.ReadHeader(&hdr) {
|
||||||
|
_, err = rdr.Close()
|
||||||
|
return nil, fmt.Errorf("read object header: %w", err)
|
||||||
|
}
|
||||||
|
if prm.headerCallback != nil {
|
||||||
|
prm.headerCallback(&hdr)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(prm.wrt, rdr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("copy payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &GetObjectRes{
|
||||||
|
hdr: &hdr,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeadObjectPrm groups parameters of HeadObject operation.
|
||||||
|
type HeadObjectPrm struct {
|
||||||
|
commonObjectPrm
|
||||||
|
objectAddressPrm
|
||||||
|
rawPrm
|
||||||
|
|
||||||
|
mainOnly bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMainOnlyFlag sets flag to get only main fields of an object header in terms of NeoFS API.
|
||||||
|
func (x *HeadObjectPrm) SetMainOnlyFlag(v bool) {
|
||||||
|
x.mainOnly = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeadObjectRes groups the resulting values of HeadObject operation.
|
||||||
|
type HeadObjectRes struct {
|
||||||
|
hdr *object.Object
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header returns the requested object header.
|
||||||
|
func (x HeadObjectRes) Header() *object.Object {
|
||||||
|
return x.hdr
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeadObject reads an object header by address.
|
||||||
|
//
|
||||||
|
// Returns any error which prevented the operation from completing correctly in error return.
|
||||||
|
// For raw reading, returns *object.SplitInfoError error if object is virtual.
|
||||||
|
func HeadObject(prm HeadObjectPrm) (*HeadObjectRes, error) {
|
||||||
|
var cliPrm client.PrmObjectHead
|
||||||
|
cliPrm.FromContainer(prm.objAddr.Container())
|
||||||
|
cliPrm.ByID(prm.objAddr.Object())
|
||||||
|
|
||||||
|
if prm.sessionToken != nil {
|
||||||
|
cliPrm.WithinSession(*prm.sessionToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if prm.bearerToken != nil {
|
||||||
|
cliPrm.WithBearerToken(*prm.bearerToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if prm.raw {
|
||||||
|
cliPrm.MarkRaw()
|
||||||
|
}
|
||||||
|
|
||||||
|
if prm.local {
|
||||||
|
cliPrm.MarkLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPrm.WithXHeaders(prm.xHeaders...)
|
||||||
|
|
||||||
|
res, err := prm.cli.ObjectHead(context.Background(), cliPrm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read object header via client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var hdr object.Object
|
||||||
|
|
||||||
|
if !res.ReadHeader(&hdr) {
|
||||||
|
return nil, fmt.Errorf("missing header in response")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HeadObjectRes{
|
||||||
|
hdr: &hdr,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchObjectsPrm groups parameters of SearchObjects operation.
|
||||||
|
type SearchObjectsPrm struct {
|
||||||
|
commonObjectPrm
|
||||||
|
containerIDPrm
|
||||||
|
|
||||||
|
filters object.SearchFilters
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFilters sets search filters.
|
||||||
|
func (x *SearchObjectsPrm) SetFilters(filters object.SearchFilters) {
|
||||||
|
x.filters = filters
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchObjectsRes groups the resulting values of SearchObjects operation.
|
||||||
|
type SearchObjectsRes struct {
|
||||||
|
ids []oid.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDList returns identifiers of the matched objects.
|
||||||
|
func (x SearchObjectsRes) IDList() []oid.ID {
|
||||||
|
return x.ids
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchObjects selects objects from the container which match the filters.
|
||||||
|
//
|
||||||
|
// Returns any error which prevented the operation from completing correctly in error return.
|
||||||
|
func SearchObjects(prm SearchObjectsPrm) (*SearchObjectsRes, error) {
|
||||||
|
var cliPrm client.PrmObjectSearch
|
||||||
|
cliPrm.InContainer(prm.cnrID)
|
||||||
|
cliPrm.SetFilters(prm.filters)
|
||||||
|
|
||||||
|
if prm.sessionToken != nil {
|
||||||
|
cliPrm.WithinSession(*prm.sessionToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if prm.bearerToken != nil {
|
||||||
|
cliPrm.WithBearerToken(*prm.bearerToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if prm.local {
|
||||||
|
cliPrm.MarkLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPrm.WithXHeaders(prm.xHeaders...)
|
||||||
|
|
||||||
|
rdr, err := prm.cli.ObjectSearchInit(context.Background(), cliPrm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("init object search: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]oid.ID, 10)
|
||||||
|
var list []oid.ID
|
||||||
|
var n int
|
||||||
|
var ok bool
|
||||||
|
|
||||||
|
for {
|
||||||
|
n, ok = rdr.Read(buf)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
list = append(list, buf[i])
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = rdr.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read object list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SearchObjectsRes{
|
||||||
|
ids: list,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashPayloadRangesPrm groups parameters of HashPayloadRanges operation.
|
||||||
|
type HashPayloadRangesPrm struct {
|
||||||
|
commonObjectPrm
|
||||||
|
objectAddressPrm
|
||||||
|
|
||||||
|
tz bool
|
||||||
|
|
||||||
|
rngs []*object.Range
|
||||||
|
|
||||||
|
salt []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// TZ sets flag to request Tillich-Zemor hashes.
|
||||||
|
func (x *HashPayloadRangesPrm) TZ() {
|
||||||
|
x.tz = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRanges sets a list of payload ranges to hash.
|
||||||
|
func (x *HashPayloadRangesPrm) SetRanges(rngs []*object.Range) {
|
||||||
|
x.rngs = rngs
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSalt sets data for each range to be XOR'ed with.
|
||||||
|
func (x *HashPayloadRangesPrm) SetSalt(salt []byte) {
|
||||||
|
x.salt = salt
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashPayloadRangesRes groups the resulting values of HashPayloadRanges operation.
|
||||||
|
type HashPayloadRangesRes struct {
|
||||||
|
cliRes *client.ResObjectHash
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashList returns a list of hashes of the payload ranges keeping order.
|
||||||
|
func (x HashPayloadRangesRes) HashList() [][]byte {
|
||||||
|
return x.cliRes.Checksums()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashPayloadRanges requests hashes (by default SHA256) of the object payload ranges.
|
||||||
|
//
|
||||||
|
// Returns any error which prevented the operation from completing correctly in error return.
|
||||||
|
// Returns an error if number of received hashes differs with the number of requested ranges.
|
||||||
|
func HashPayloadRanges(prm HashPayloadRangesPrm) (*HashPayloadRangesRes, error) {
|
||||||
|
var cliPrm client.PrmObjectHash
|
||||||
|
cliPrm.FromContainer(prm.objAddr.Container())
|
||||||
|
cliPrm.ByID(prm.objAddr.Object())
|
||||||
|
|
||||||
|
if prm.local {
|
||||||
|
cliPrm.MarkLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPrm.UseSalt(prm.salt)
|
||||||
|
|
||||||
|
rngs := make([]uint64, 2*len(prm.rngs))
|
||||||
|
|
||||||
|
for i := range prm.rngs {
|
||||||
|
rngs[2*i] = prm.rngs[i].GetOffset()
|
||||||
|
rngs[2*i+1] = prm.rngs[i].GetLength()
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPrm.SetRangeList(rngs...)
|
||||||
|
|
||||||
|
if prm.tz {
|
||||||
|
cliPrm.TillichZemorAlgo()
|
||||||
|
}
|
||||||
|
|
||||||
|
if prm.sessionToken != nil {
|
||||||
|
cliPrm.WithinSession(*prm.sessionToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if prm.bearerToken != nil {
|
||||||
|
cliPrm.WithBearerToken(*prm.bearerToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPrm.WithXHeaders(prm.xHeaders...)
|
||||||
|
|
||||||
|
res, err := prm.cli.ObjectHash(context.Background(), cliPrm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read payload hashes via client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &HashPayloadRangesRes{
|
||||||
|
cliRes: res,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PayloadRangePrm groups parameters of PayloadRange operation.
|
||||||
|
type PayloadRangePrm struct {
|
||||||
|
commonObjectPrm
|
||||||
|
objectAddressPrm
|
||||||
|
rawPrm
|
||||||
|
payloadWriterPrm
|
||||||
|
|
||||||
|
rng *object.Range
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRange sets payload range to read.
|
||||||
|
func (x *PayloadRangePrm) SetRange(rng *object.Range) {
|
||||||
|
x.rng = rng
|
||||||
|
}
|
||||||
|
|
||||||
|
// PayloadRangeRes groups the resulting values of PayloadRange operation.
|
||||||
|
type PayloadRangeRes struct{}
|
||||||
|
|
||||||
|
// PayloadRange reads object payload range from NeoFS and writes it to the specified writer.
|
||||||
|
//
|
||||||
|
// Interrupts on any writer error.
|
||||||
|
//
|
||||||
|
// Returns any error which prevented the operation from completing correctly in error return.
|
||||||
|
// For raw reading, returns *object.SplitInfoError error if object is virtual.
|
||||||
|
func PayloadRange(prm PayloadRangePrm) (*PayloadRangeRes, error) {
|
||||||
|
var cliPrm client.PrmObjectRange
|
||||||
|
cliPrm.FromContainer(prm.objAddr.Container())
|
||||||
|
cliPrm.ByID(prm.objAddr.Object())
|
||||||
|
|
||||||
|
if prm.sessionToken != nil {
|
||||||
|
cliPrm.WithinSession(*prm.sessionToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if prm.bearerToken != nil {
|
||||||
|
cliPrm.WithBearerToken(*prm.bearerToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if prm.raw {
|
||||||
|
cliPrm.MarkRaw()
|
||||||
|
}
|
||||||
|
|
||||||
|
if prm.local {
|
||||||
|
cliPrm.MarkLocal()
|
||||||
|
}
|
||||||
|
|
||||||
|
cliPrm.SetOffset(prm.rng.GetOffset())
|
||||||
|
cliPrm.SetLength(prm.rng.GetLength())
|
||||||
|
|
||||||
|
cliPrm.WithXHeaders(prm.xHeaders...)
|
||||||
|
|
||||||
|
rdr, err := prm.cli.ObjectRangeInit(context.Background(), cliPrm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("init payload reading: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(prm.wrt, rdr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("copy payload: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new(PayloadRangeRes), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncContainerPrm groups parameters of SyncContainerSettings operation.
|
||||||
|
type SyncContainerPrm struct {
|
||||||
|
commonPrm
|
||||||
|
c *containerSDK.Container
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContainer sets a container that is required to be synced.
|
||||||
|
func (s *SyncContainerPrm) SetContainer(c *containerSDK.Container) {
|
||||||
|
s.c = c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncContainerRes groups resulting values of SyncContainerSettings
|
||||||
|
// operation.
|
||||||
|
type SyncContainerRes struct{}
|
||||||
|
|
||||||
|
// SyncContainerSettings reads global network config from NeoFS and
|
||||||
|
// syncs container settings with it.
|
||||||
|
//
|
||||||
|
// Interrupts on any writer error.
|
||||||
|
//
|
||||||
|
// Panics if a container passed as a parameter is nil.
|
||||||
|
func SyncContainerSettings(prm SyncContainerPrm) (*SyncContainerRes, error) {
|
||||||
|
if prm.c == nil {
|
||||||
|
panic("sync container settings with the network: nil container")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := client.SyncContainerWithNetwork(context.Background(), prm.c, prm.cli)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return new(SyncContainerRes), nil
|
||||||
|
}
|
12
cmd/frostfs-cli/internal/client/doc.go
Normal file
12
cmd/frostfs-cli/internal/client/doc.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// Package internal provides functionality for NeoFS CLI application communication with NeoFS network.
|
||||||
|
//
|
||||||
|
// The base client for accessing remote nodes via NeoFS API is a NeoFS SDK Go API client.
|
||||||
|
// However, although it encapsulates a useful piece of business logic (e.g. the signature mechanism),
|
||||||
|
// the NeoFS CLI application does not fully use the client's flexible interface.
|
||||||
|
//
|
||||||
|
// In this regard, this package provides functions over base API client necessary for the application.
|
||||||
|
// This allows you to concentrate the entire spectrum of the client's use in one place (this will be convenient
|
||||||
|
// both when updating the base client and for evaluating the UX of SDK library). So it is expected that all
|
||||||
|
// application packages will be limited to this package for the development of functionality requiring
|
||||||
|
// NeoFS API communication.
|
||||||
|
package internal
|
92
cmd/frostfs-cli/internal/client/prm.go
Normal file
92
cmd/frostfs-cli/internal/client/prm.go
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||||
|
"github.com/TrueCloudLab/frostfs-sdk-go/client"
|
||||||
|
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
|
oid "github.com/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||||
|
"github.com/TrueCloudLab/frostfs-sdk-go/session"
|
||||||
|
)
|
||||||
|
|
||||||
|
// here are small structures with public setters to share between parameter structures
|
||||||
|
|
||||||
|
type commonPrm struct {
|
||||||
|
cli *client.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetClient sets the base client for NeoFS API communication.
|
||||||
|
func (x *commonPrm) SetClient(cli *client.Client) {
|
||||||
|
x.cli = cli
|
||||||
|
}
|
||||||
|
|
||||||
|
type containerIDPrm struct {
|
||||||
|
cnrID cid.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContainerID sets the container identifier.
|
||||||
|
func (x *containerIDPrm) SetContainerID(id cid.ID) {
|
||||||
|
x.cnrID = id
|
||||||
|
}
|
||||||
|
|
||||||
|
type bearerTokenPrm struct {
|
||||||
|
bearerToken *bearer.Token
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBearerToken sets the bearer token to be attached to the request.
|
||||||
|
func (x *bearerTokenPrm) SetBearerToken(tok *bearer.Token) {
|
||||||
|
x.bearerToken = tok
|
||||||
|
}
|
||||||
|
|
||||||
|
type objectAddressPrm struct {
|
||||||
|
objAddr oid.Address
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *objectAddressPrm) SetAddress(addr oid.Address) {
|
||||||
|
x.objAddr = addr
|
||||||
|
}
|
||||||
|
|
||||||
|
type rawPrm struct {
|
||||||
|
raw bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRawFlag sets flag of raw request.
|
||||||
|
func (x *rawPrm) SetRawFlag(raw bool) {
|
||||||
|
x.raw = raw
|
||||||
|
}
|
||||||
|
|
||||||
|
type payloadWriterPrm struct {
|
||||||
|
wrt io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPayloadWriter sets the writer of the object payload.
|
||||||
|
func (x *payloadWriterPrm) SetPayloadWriter(wrt io.Writer) {
|
||||||
|
x.wrt = wrt
|
||||||
|
}
|
||||||
|
|
||||||
|
type commonObjectPrm struct {
|
||||||
|
commonPrm
|
||||||
|
bearerTokenPrm
|
||||||
|
|
||||||
|
sessionToken *session.Object
|
||||||
|
|
||||||
|
local bool
|
||||||
|
|
||||||
|
xHeaders []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTTL sets request TTL value.
|
||||||
|
func (x *commonObjectPrm) SetTTL(ttl uint32) {
|
||||||
|
x.local = ttl < 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetXHeaders sets request X-Headers.
|
||||||
|
func (x *commonObjectPrm) SetXHeaders(hs []string) {
|
||||||
|
x.xHeaders = hs
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetSessionToken sets the token of the session within which the request should be sent.
|
||||||
|
func (x *commonObjectPrm) SetSessionToken(tok *session.Object) {
|
||||||
|
x.sessionToken = tok
|
||||||
|
}
|
95
cmd/frostfs-cli/internal/client/sdk.go
Normal file
95
cmd/frostfs-cli/internal/client/sdk.go
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/pkg/network"
|
||||||
|
"github.com/TrueCloudLab/frostfs-sdk-go/client"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errInvalidEndpoint = errors.New("provided RPC endpoint is incorrect")
|
||||||
|
|
||||||
|
// GetSDKClientByFlag returns default frostfs-sdk-go client using the specified flag for the address.
|
||||||
|
// On error, outputs to stderr of cmd and exits with non-zero code.
|
||||||
|
func GetSDKClientByFlag(cmd *cobra.Command, key *ecdsa.PrivateKey, endpointFlag string) *client.Client {
|
||||||
|
cli, err := getSDKClientByFlag(cmd, key, endpointFlag)
|
||||||
|
if err != nil {
|
||||||
|
common.ExitOnErr(cmd, "can't create API client: %w", err)
|
||||||
|
}
|
||||||
|
return cli
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSDKClientByFlag(cmd *cobra.Command, key *ecdsa.PrivateKey, endpointFlag string) (*client.Client, error) {
|
||||||
|
var addr network.Address
|
||||||
|
|
||||||
|
err := addr.FromString(viper.GetString(endpointFlag))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%v: %w", errInvalidEndpoint, err)
|
||||||
|
}
|
||||||
|
return GetSDKClient(cmd, key, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSDKClient returns default frostfs-sdk-go client.
|
||||||
|
func GetSDKClient(cmd *cobra.Command, key *ecdsa.PrivateKey, addr network.Address) (*client.Client, error) {
|
||||||
|
var (
|
||||||
|
c client.Client
|
||||||
|
prmInit client.PrmInit
|
||||||
|
prmDial client.PrmDial
|
||||||
|
)
|
||||||
|
|
||||||
|
prmInit.SetDefaultPrivateKey(*key)
|
||||||
|
prmInit.ResolveNeoFSFailures()
|
||||||
|
prmDial.SetServerURI(addr.URIAddr())
|
||||||
|
if timeout := viper.GetDuration(commonflags.Timeout); timeout > 0 {
|
||||||
|
// In CLI we can only set a timeout for the whole operation.
|
||||||
|
// By also setting stream timeout we ensure that no operation hands
|
||||||
|
// for too long.
|
||||||
|
prmDial.SetTimeout(timeout)
|
||||||
|
prmDial.SetStreamTimeout(timeout)
|
||||||
|
|
||||||
|
common.PrintVerbose(cmd, "Set request timeout to %s.", timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Init(prmInit)
|
||||||
|
|
||||||
|
if err := c.Dial(prmDial); err != nil {
|
||||||
|
return nil, fmt.Errorf("can't init SDK client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCurrentEpoch returns current epoch.
|
||||||
|
func GetCurrentEpoch(ctx context.Context, cmd *cobra.Command, endpoint string) (uint64, error) {
|
||||||
|
var addr network.Address
|
||||||
|
|
||||||
|
if err := addr.FromString(endpoint); err != nil {
|
||||||
|
return 0, fmt.Errorf("can't parse RPC endpoint: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("can't generate key to sign query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := GetSDKClient(cmd, key, addr)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ni, err := c.NetworkInfo(ctx, client.PrmNetworkInfo{})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ni.Info().CurrentEpoch(), nil
|
||||||
|
}
|
49
cmd/frostfs-cli/internal/common/eacl.go
Normal file
49
cmd/frostfs-cli/internal/common/eacl.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/pkg/core/version"
|
||||||
|
"github.com/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||||
|
versionSDK "github.com/TrueCloudLab/frostfs-sdk-go/version"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errUnsupportedEACLFormat = errors.New("unsupported eACL format")
|
||||||
|
|
||||||
|
// ReadEACL reads extended ACL table from eaclPath.
|
||||||
|
func ReadEACL(cmd *cobra.Command, eaclPath string) *eacl.Table {
|
||||||
|
_, err := os.Stat(eaclPath) // check if `eaclPath` is an existing file
|
||||||
|
if err != nil {
|
||||||
|
ExitOnErr(cmd, "", errors.New("incorrect path to file with EACL"))
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintVerbose(cmd, "Reading EACL from file: %s", eaclPath)
|
||||||
|
|
||||||
|
data, err := os.ReadFile(eaclPath)
|
||||||
|
ExitOnErr(cmd, "can't read file with EACL: %w", err)
|
||||||
|
|
||||||
|
table := eacl.NewTable()
|
||||||
|
|
||||||
|
if err = table.UnmarshalJSON(data); err == nil {
|
||||||
|
validateAndFixEACLVersion(table)
|
||||||
|
PrintVerbose(cmd, "Parsed JSON encoded EACL table")
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = table.Unmarshal(data); err == nil {
|
||||||
|
validateAndFixEACLVersion(table)
|
||||||
|
PrintVerbose(cmd, "Parsed binary encoded EACL table")
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
|
ExitOnErr(cmd, "", errUnsupportedEACLFormat)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAndFixEACLVersion(table *eacl.Table) {
|
||||||
|
if !version.IsValid(table.Version()) {
|
||||||
|
table.SetVersion(versionSDK.Current())
|
||||||
|
}
|
||||||
|
}
|
28
cmd/frostfs-cli/internal/common/epoch.go
Normal file
28
cmd/frostfs-cli/internal/common/epoch.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseEpoch parses epoch argument. Second return value is true if
|
||||||
|
// the specified epoch is relative, and false otherwise.
|
||||||
|
func ParseEpoch(cmd *cobra.Command, flag string) (uint64, bool, error) {
|
||||||
|
s, _ := cmd.Flags().GetString(flag)
|
||||||
|
if len(s) == 0 {
|
||||||
|
return 0, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
relative := s[0] == '+'
|
||||||
|
if relative {
|
||||||
|
s = s[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
epoch, err := strconv.ParseUint(s, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, relative, fmt.Errorf("can't parse epoch for %s argument: %w", flag, err)
|
||||||
|
}
|
||||||
|
return epoch, relative, nil
|
||||||
|
}
|
50
cmd/frostfs-cli/internal/common/exit.go
Normal file
50
cmd/frostfs-cli/internal/common/exit.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
sdkstatus "github.com/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExitOnErr prints error and exits with a code that matches
|
||||||
|
// one of the common errors from sdk library. If no errors
|
||||||
|
// found, exits with 1 code.
|
||||||
|
// Does nothing if passed error in nil.
|
||||||
|
func ExitOnErr(cmd *cobra.Command, errFmt string, err error) {
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if errFmt != "" {
|
||||||
|
err = fmt.Errorf(errFmt, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
_ = iota
|
||||||
|
internal
|
||||||
|
aclDenied
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
code int
|
||||||
|
|
||||||
|
internalErr = new(sdkstatus.ServerInternal)
|
||||||
|
accessErr = new(sdkstatus.ObjectAccessDenied)
|
||||||
|
)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case errors.As(err, &internalErr):
|
||||||
|
code = internal
|
||||||
|
case errors.As(err, &accessErr):
|
||||||
|
code = aclDenied
|
||||||
|
err = fmt.Errorf("%w: %s", err, accessErr.Reason())
|
||||||
|
default:
|
||||||
|
code = internal
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.PrintErrln(err)
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
23
cmd/frostfs-cli/internal/common/json.go
Normal file
23
cmd/frostfs-cli/internal/common/json.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrettyPrintJSON prints m as an indented JSON to the cmd output.
|
||||||
|
func PrettyPrintJSON(cmd *cobra.Command, m json.Marshaler, entity string) {
|
||||||
|
data, err := m.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
PrintVerbose(cmd, "Can't convert %s to json: %w", entity, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
if err := json.Indent(buf, data, "", " "); err != nil {
|
||||||
|
PrintVerbose(cmd, "Can't pretty print json: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cmd.Println(buf)
|
||||||
|
}
|
49
cmd/frostfs-cli/internal/common/netmap.go
Normal file
49
cmd/frostfs-cli/internal/common/netmap.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrettyPrintNodeInfo print information about network node with given indent and index.
|
||||||
|
// To avoid printing attribute list use short parameter.
|
||||||
|
func PrettyPrintNodeInfo(cmd *cobra.Command, node netmap.NodeInfo,
|
||||||
|
index int, indent string, short bool) {
|
||||||
|
var strState string
|
||||||
|
|
||||||
|
switch {
|
||||||
|
default:
|
||||||
|
strState = "STATE_UNSUPPORTED"
|
||||||
|
case node.IsOnline():
|
||||||
|
strState = "ONLINE"
|
||||||
|
case node.IsOffline():
|
||||||
|
strState = "OFFLINE"
|
||||||
|
case node.IsMaintenance():
|
||||||
|
strState = "MAINTENANCE"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Printf("%sNode %d: %s %s ", indent, index+1, hex.EncodeToString(node.PublicKey()), strState)
|
||||||
|
|
||||||
|
netmap.IterateNetworkEndpoints(node, func(endpoint string) {
|
||||||
|
cmd.Printf("%s ", endpoint)
|
||||||
|
})
|
||||||
|
cmd.Println()
|
||||||
|
|
||||||
|
if !short {
|
||||||
|
node.IterateAttributes(func(key, value string) {
|
||||||
|
cmd.Printf("%s\t%s: %s\n", indent, key, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrettyPrintNetMap print information about network map.
|
||||||
|
func PrettyPrintNetMap(cmd *cobra.Command, nm netmap.NetMap) {
|
||||||
|
cmd.Println("Epoch:", nm.Epoch())
|
||||||
|
|
||||||
|
nodes := nm.Nodes()
|
||||||
|
for i := range nodes {
|
||||||
|
PrettyPrintNodeInfo(cmd, nodes[i], i, "", false)
|
||||||
|
}
|
||||||
|
}
|
66
cmd/frostfs-cli/internal/common/token.go
Normal file
66
cmd/frostfs-cli/internal/common/token.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadBearerToken reads bearer token from the path provided in a specified flag.
|
||||||
|
func ReadBearerToken(cmd *cobra.Command, flagname string) *bearer.Token {
|
||||||
|
path, err := cmd.Flags().GetString(flagname)
|
||||||
|
ExitOnErr(cmd, "", err)
|
||||||
|
|
||||||
|
if len(path) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintVerbose(cmd, "Reading bearer token from file [%s]...", path)
|
||||||
|
|
||||||
|
var tok bearer.Token
|
||||||
|
|
||||||
|
err = ReadBinaryOrJSON(cmd, &tok, path)
|
||||||
|
ExitOnErr(cmd, "invalid bearer token: %v", err)
|
||||||
|
|
||||||
|
return &tok
|
||||||
|
}
|
||||||
|
|
||||||
|
// BinaryOrJSON is an interface of entities which provide json.Unmarshaler
|
||||||
|
// and NeoFS binary decoder.
|
||||||
|
type BinaryOrJSON interface {
|
||||||
|
Unmarshal([]byte) error
|
||||||
|
json.Unmarshaler
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadBinaryOrJSON reads file data using provided path and decodes
|
||||||
|
// BinaryOrJSON from the data.
|
||||||
|
func ReadBinaryOrJSON(cmd *cobra.Command, dst BinaryOrJSON, fPath string) error {
|
||||||
|
PrintVerbose(cmd, "Reading file [%s]...", fPath)
|
||||||
|
|
||||||
|
// try to read session token from file
|
||||||
|
data, err := os.ReadFile(fPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read file <%s>: %w", fPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintVerbose(cmd, "Trying to decode binary...")
|
||||||
|
|
||||||
|
err = dst.Unmarshal(data)
|
||||||
|
if err != nil {
|
||||||
|
PrintVerbose(cmd, "Failed to decode binary: %v", err)
|
||||||
|
|
||||||
|
PrintVerbose(cmd, "Trying to decode JSON...")
|
||||||
|
|
||||||
|
err = dst.UnmarshalJSON(data)
|
||||||
|
if err != nil {
|
||||||
|
PrintVerbose(cmd, "Failed to decode JSON: %v", err)
|
||||||
|
return errors.New("invalid format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
46
cmd/frostfs-cli/internal/common/verbose.go
Normal file
46
cmd/frostfs-cli/internal/common/verbose.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||||
|
"github.com/TrueCloudLab/frostfs-sdk-go/checksum"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrintVerbose prints to the stdout if the commonflags.Verbose flag is on.
|
||||||
|
func PrintVerbose(cmd *cobra.Command, format string, a ...interface{}) {
|
||||||
|
if viper.GetBool(commonflags.Verbose) {
|
||||||
|
cmd.Printf(format+"\n", a...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrettyPrintUnixTime interprets s as unix timestamp and prints it as
|
||||||
|
// a date. Is s is invalid, "malformed" is returned.
|
||||||
|
func PrettyPrintUnixTime(s string) string {
|
||||||
|
unixTime, err := strconv.ParseInt(s, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "malformed"
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp := time.Unix(unixTime, 0)
|
||||||
|
|
||||||
|
return timestamp.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintChecksum prints checksum.
|
||||||
|
func PrintChecksum(cmd *cobra.Command, name string, recv func() (checksum.Checksum, bool)) {
|
||||||
|
var strVal string
|
||||||
|
|
||||||
|
cs, csSet := recv()
|
||||||
|
if csSet {
|
||||||
|
strVal = hex.EncodeToString(cs.Value())
|
||||||
|
} else {
|
||||||
|
strVal = "<empty>"
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Printf("%s: %s\n", name, strVal)
|
||||||
|
}
|
33
cmd/frostfs-cli/internal/commonflags/api.go
Normal file
33
cmd/frostfs-cli/internal/commonflags/api.go
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package commonflags
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TTL = "ttl"
|
||||||
|
TTLShorthand = ""
|
||||||
|
TTLDefault = 2
|
||||||
|
TTLUsage = "TTL value in request meta header"
|
||||||
|
|
||||||
|
XHeadersKey = "xhdr"
|
||||||
|
XHeadersShorthand = "x"
|
||||||
|
XHeadersUsage = "Request X-Headers in form of Key=Value"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitAPI inits common flags for storage node services.
|
||||||
|
func InitAPI(cmd *cobra.Command) {
|
||||||
|
ff := cmd.Flags()
|
||||||
|
|
||||||
|
ff.StringSliceP(XHeadersKey, XHeadersShorthand, []string{}, XHeadersUsage)
|
||||||
|
ff.Uint32P(TTL, TTLShorthand, TTLDefault, TTLUsage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindAPI binds API flags of storage node services to the viper.
|
||||||
|
func BindAPI(cmd *cobra.Command) {
|
||||||
|
ff := cmd.Flags()
|
||||||
|
|
||||||
|
_ = viper.BindPFlag(TTL, ff.Lookup(TTL))
|
||||||
|
_ = viper.BindPFlag(XHeadersKey, ff.Lookup(XHeadersKey))
|
||||||
|
}
|
9
cmd/frostfs-cli/internal/commonflags/expiration.go
Normal file
9
cmd/frostfs-cli/internal/commonflags/expiration.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package commonflags
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ExpireAt is a flag for setting last epoch of an object or a token.
|
||||||
|
ExpireAt = "expire-at"
|
||||||
|
// Lifetime is a flag for setting the lifetime of an object or a token,
|
||||||
|
// starting from the current epoch.
|
||||||
|
Lifetime = "lifetime"
|
||||||
|
)
|
84
cmd/frostfs-cli/internal/commonflags/flags.go
Normal file
84
cmd/frostfs-cli/internal/commonflags/flags.go
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
package commonflags
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Common CLI flag keys, shorthands, default
|
||||||
|
// values and their usage descriptions.
|
||||||
|
const (
|
||||||
|
GenerateKey = "generate-key"
|
||||||
|
generateKeyShorthand = "g"
|
||||||
|
generateKeyDefault = false
|
||||||
|
generateKeyUsage = "Generate new private key"
|
||||||
|
|
||||||
|
WalletPath = "wallet"
|
||||||
|
WalletPathShorthand = "w"
|
||||||
|
WalletPathDefault = ""
|
||||||
|
WalletPathUsage = "Path to the wallet or binary key"
|
||||||
|
|
||||||
|
Account = "address"
|
||||||
|
AccountShorthand = ""
|
||||||
|
AccountDefault = ""
|
||||||
|
AccountUsage = "Address of wallet account"
|
||||||
|
|
||||||
|
RPC = "rpc-endpoint"
|
||||||
|
RPCShorthand = "r"
|
||||||
|
RPCDefault = ""
|
||||||
|
RPCUsage = "Remote node address (as 'multiaddr' or '<host>:<port>')"
|
||||||
|
|
||||||
|
Timeout = "timeout"
|
||||||
|
TimeoutShorthand = "t"
|
||||||
|
TimeoutDefault = 15 * time.Second
|
||||||
|
TimeoutUsage = "Timeout for an operation"
|
||||||
|
|
||||||
|
Verbose = "verbose"
|
||||||
|
VerboseShorthand = "v"
|
||||||
|
VerboseUsage = "Verbose output"
|
||||||
|
|
||||||
|
ForceFlag = "force"
|
||||||
|
ForceFlagShorthand = "f"
|
||||||
|
|
||||||
|
CIDFlag = "cid"
|
||||||
|
CIDFlagUsage = "Container ID."
|
||||||
|
|
||||||
|
OIDFlag = "oid"
|
||||||
|
OIDFlagUsage = "Object ID."
|
||||||
|
)
|
||||||
|
|
||||||
|
// Init adds common flags to the command:
|
||||||
|
// - GenerateKey,
|
||||||
|
// - WalletPath,
|
||||||
|
// - Account,
|
||||||
|
// - RPC,
|
||||||
|
// - Timeout.
|
||||||
|
func Init(cmd *cobra.Command) {
|
||||||
|
InitWithoutRPC(cmd)
|
||||||
|
|
||||||
|
ff := cmd.Flags()
|
||||||
|
ff.StringP(RPC, RPCShorthand, RPCDefault, RPCUsage)
|
||||||
|
ff.DurationP(Timeout, TimeoutShorthand, TimeoutDefault, TimeoutUsage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitWithoutRPC is similar to Init but doesn't create the RPC flag.
|
||||||
|
func InitWithoutRPC(cmd *cobra.Command) {
|
||||||
|
ff := cmd.Flags()
|
||||||
|
|
||||||
|
ff.BoolP(GenerateKey, generateKeyShorthand, generateKeyDefault, generateKeyUsage)
|
||||||
|
ff.StringP(WalletPath, WalletPathShorthand, WalletPathDefault, WalletPathUsage)
|
||||||
|
ff.StringP(Account, AccountShorthand, AccountDefault, AccountUsage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind binds common command flags to the viper.
|
||||||
|
func Bind(cmd *cobra.Command) {
|
||||||
|
ff := cmd.Flags()
|
||||||
|
|
||||||
|
_ = viper.BindPFlag(GenerateKey, ff.Lookup(GenerateKey))
|
||||||
|
_ = viper.BindPFlag(WalletPath, ff.Lookup(WalletPath))
|
||||||
|
_ = viper.BindPFlag(Account, ff.Lookup(Account))
|
||||||
|
_ = viper.BindPFlag(RPC, ff.Lookup(RPC))
|
||||||
|
_ = viper.BindPFlag(Timeout, ff.Lookup(Timeout))
|
||||||
|
}
|
3
cmd/frostfs-cli/internal/commonflags/json.go
Normal file
3
cmd/frostfs-cli/internal/commonflags/json.go
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
package commonflags
|
||||||
|
|
||||||
|
const JSON = "json"
|
19
cmd/frostfs-cli/internal/commonflags/session.go
Normal file
19
cmd/frostfs-cli/internal/commonflags/session.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package commonflags
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const SessionToken = "session"
|
||||||
|
|
||||||
|
// InitSession registers SessionToken flag representing filepath to the token
|
||||||
|
// of the session with the given name. Supports NeoFS-binary and JSON files.
|
||||||
|
func InitSession(cmd *cobra.Command, name string) {
|
||||||
|
cmd.Flags().String(
|
||||||
|
SessionToken,
|
||||||
|
"",
|
||||||
|
fmt.Sprintf("Filepath to a JSON- or binary-encoded token of the %s session", name),
|
||||||
|
)
|
||||||
|
}
|
126
cmd/frostfs-cli/internal/key/key_test.go
Normal file
126
cmd/frostfs-cli/internal/key/key_test.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
package key
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||||
|
"github.com/nspcc-dev/neo-go/cli/input"
|
||||||
|
"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"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testCmd = &cobra.Command{
|
||||||
|
Use: "test",
|
||||||
|
Short: "test",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {},
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_getOrGenerate(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
|
||||||
|
wallPath := filepath.Join(dir, "wallet.json")
|
||||||
|
w, err := wallet.NewWallet(wallPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
badWallPath := filepath.Join(dir, "bad_wallet.json")
|
||||||
|
require.NoError(t, os.WriteFile(badWallPath, []byte("bad content"), os.ModePerm))
|
||||||
|
|
||||||
|
acc1, err := wallet.NewAccount()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, acc1.Encrypt("pass", keys.NEP2ScryptParams()))
|
||||||
|
w.AddAccount(acc1)
|
||||||
|
|
||||||
|
acc2, err := wallet.NewAccount()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, acc2.Encrypt("pass", keys.NEP2ScryptParams()))
|
||||||
|
acc2.Default = true
|
||||||
|
w.AddAccount(acc2)
|
||||||
|
require.NoError(t, w.Save())
|
||||||
|
|
||||||
|
keyPath := filepath.Join(dir, "binary.key")
|
||||||
|
rawKey, err := keys.NewPrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, os.WriteFile(keyPath, rawKey.Bytes(), os.ModePerm))
|
||||||
|
|
||||||
|
wifKey, err := keys.NewPrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
nep2Key, err := keys.NewPrivateKey()
|
||||||
|
require.NoError(t, err)
|
||||||
|
nep2, err := keys.NEP2Encrypt(nep2Key, "pass", keys.NEP2ScryptParams())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
in := bytes.NewBuffer(nil)
|
||||||
|
input.Terminal = term.NewTerminal(input.ReadWriter{
|
||||||
|
Reader: in,
|
||||||
|
Writer: io.Discard,
|
||||||
|
}, "")
|
||||||
|
|
||||||
|
checkKeyError(t, filepath.Join(dir, "badfile"), ErrFs)
|
||||||
|
checkKeyError(t, badWallPath, ErrInvalidKey)
|
||||||
|
|
||||||
|
t.Run("wallet", func(t *testing.T) {
|
||||||
|
checkKeyError(t, wallPath, ErrInvalidPassword)
|
||||||
|
|
||||||
|
in.WriteString("invalid\r")
|
||||||
|
checkKeyError(t, wallPath, ErrInvalidPassword)
|
||||||
|
|
||||||
|
in.WriteString("pass\r")
|
||||||
|
checkKey(t, wallPath, acc2.PrivateKey()) // default account
|
||||||
|
|
||||||
|
viper.Set(commonflags.Account, acc1.Address)
|
||||||
|
in.WriteString("pass\r")
|
||||||
|
checkKey(t, wallPath, acc1.PrivateKey())
|
||||||
|
|
||||||
|
viper.Set(commonflags.Account, "not an address")
|
||||||
|
checkKeyError(t, wallPath, ErrInvalidAddress)
|
||||||
|
|
||||||
|
acc, err := wallet.NewAccount()
|
||||||
|
require.NoError(t, err)
|
||||||
|
viper.Set(commonflags.Account, acc.Address)
|
||||||
|
checkKeyError(t, wallPath, ErrInvalidAddress)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("WIF", func(t *testing.T) {
|
||||||
|
checkKeyError(t, wifKey.WIF(), ErrFs)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("NEP-2", func(t *testing.T) {
|
||||||
|
checkKeyError(t, nep2, ErrFs)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("raw key", func(t *testing.T) {
|
||||||
|
checkKey(t, keyPath, rawKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("generate", func(t *testing.T) {
|
||||||
|
viper.Set(commonflags.GenerateKey, true)
|
||||||
|
actual, err := getOrGenerate(testCmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, actual)
|
||||||
|
for _, p := range []*keys.PrivateKey{nep2Key, rawKey, wifKey, acc1.PrivateKey(), acc2.PrivateKey()} {
|
||||||
|
require.NotEqual(t, p, actual, "expected new key to be generated")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkKeyError(t *testing.T, desc string, err error) {
|
||||||
|
viper.Set(commonflags.WalletPath, desc)
|
||||||
|
_, actualErr := getOrGenerate(testCmd)
|
||||||
|
require.ErrorIs(t, actualErr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkKey(t *testing.T, desc string, expected *keys.PrivateKey) {
|
||||||
|
viper.Set(commonflags.WalletPath, desc)
|
||||||
|
actual, err := getOrGenerate(testCmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, &expected.PrivateKey, actual)
|
||||||
|
}
|
62
cmd/frostfs-cli/internal/key/raw.go
Normal file
62
cmd/frostfs-cli/internal/key/raw.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package key
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errCantGenerateKey = errors.New("can't generate new private key")
|
||||||
|
|
||||||
|
// Get returns private key from wallet or binary file.
|
||||||
|
// Ideally we want to touch file-system on the last step.
|
||||||
|
// This function assumes that all flags were bind to viper in a `PersistentPreRun`.
|
||||||
|
func Get(cmd *cobra.Command) *ecdsa.PrivateKey {
|
||||||
|
pk, err := get(cmd)
|
||||||
|
common.ExitOnErr(cmd, "can't fetch private key: %w", err)
|
||||||
|
return pk
|
||||||
|
}
|
||||||
|
|
||||||
|
func get(cmd *cobra.Command) (*ecdsa.PrivateKey, error) {
|
||||||
|
keyDesc := viper.GetString(commonflags.WalletPath)
|
||||||
|
data, err := os.ReadFile(keyDesc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrFs, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
priv, err := keys.NewPrivateKeyFromBytes(data)
|
||||||
|
if err != nil {
|
||||||
|
w, err := wallet.NewWalletFromFile(keyDesc)
|
||||||
|
if err == nil {
|
||||||
|
return FromWallet(cmd, w, viper.GetString(commonflags.Account))
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("%w: %v", ErrInvalidKey, err)
|
||||||
|
}
|
||||||
|
return &priv.PrivateKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrGenerate is similar to get but generates a new key if commonflags.GenerateKey is set.
|
||||||
|
func GetOrGenerate(cmd *cobra.Command) *ecdsa.PrivateKey {
|
||||||
|
pk, err := getOrGenerate(cmd)
|
||||||
|
common.ExitOnErr(cmd, "can't fetch private key: %w", err)
|
||||||
|
return pk
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOrGenerate(cmd *cobra.Command) (*ecdsa.PrivateKey, error) {
|
||||||
|
if viper.GetBool(commonflags.GenerateKey) {
|
||||||
|
priv, err := keys.NewPrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %v", errCantGenerateKey, err)
|
||||||
|
}
|
||||||
|
return &priv.PrivateKey, nil
|
||||||
|
}
|
||||||
|
return get(cmd)
|
||||||
|
}
|
70
cmd/frostfs-cli/internal/key/wallet.go
Normal file
70
cmd/frostfs-cli/internal/key/wallet.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package key
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||||
|
"github.com/nspcc-dev/neo-go/cli/flags"
|
||||||
|
"github.com/nspcc-dev/neo-go/cli/input"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Key-related errors.
|
||||||
|
var (
|
||||||
|
ErrFs = errors.New("unable to read file from given path")
|
||||||
|
ErrInvalidKey = errors.New("provided key is incorrect, only wallet or binary key supported")
|
||||||
|
ErrInvalidAddress = errors.New("--address option must be specified and valid")
|
||||||
|
ErrInvalidPassword = errors.New("invalid password for the encrypted key")
|
||||||
|
)
|
||||||
|
|
||||||
|
// FromWallet returns private key of the wallet account.
|
||||||
|
func FromWallet(cmd *cobra.Command, w *wallet.Wallet, addrStr string) (*ecdsa.PrivateKey, error) {
|
||||||
|
var (
|
||||||
|
addr util.Uint160
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if addrStr == "" {
|
||||||
|
common.PrintVerbose(cmd, "Using default wallet address")
|
||||||
|
addr = w.GetChangeAddress()
|
||||||
|
} else {
|
||||||
|
addr, err = flags.ParseAddress(addrStr)
|
||||||
|
if err != nil {
|
||||||
|
common.PrintVerbose(cmd, "Can't parse address: %s", addrStr)
|
||||||
|
return nil, ErrInvalidAddress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
acc := w.GetAccount(addr)
|
||||||
|
if acc == nil {
|
||||||
|
common.PrintVerbose(cmd, "Can't find wallet account for %s", addrStr)
|
||||||
|
return nil, ErrInvalidAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
pass, err := getPassword()
|
||||||
|
if err != nil {
|
||||||
|
common.PrintVerbose(cmd, "Can't read password: %v", err)
|
||||||
|
return nil, ErrInvalidPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := acc.Decrypt(pass, keys.NEP2ScryptParams()); err != nil {
|
||||||
|
common.PrintVerbose(cmd, "Can't decrypt account: %v", err)
|
||||||
|
return nil, ErrInvalidPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
return &acc.PrivateKey().PrivateKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPassword() (string, error) {
|
||||||
|
// this check allows empty passwords
|
||||||
|
if viper.IsSet("password") {
|
||||||
|
return viper.GetString("password"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return input.ReadPassword("Enter password > ")
|
||||||
|
}
|
7
cmd/frostfs-cli/main.go
Normal file
7
cmd/frostfs-cli/main.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import cmd "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
70
cmd/frostfs-cli/modules/accounting/balance.go
Normal file
70
cmd/frostfs-cli/modules/accounting/balance.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package accounting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
internalclient "github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/pkg/util/precision"
|
||||||
|
"github.com/TrueCloudLab/frostfs-sdk-go/accounting"
|
||||||
|
"github.com/TrueCloudLab/frostfs-sdk-go/user"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ownerFlag = "owner"
|
||||||
|
)
|
||||||
|
|
||||||
|
var accountingBalanceCmd = &cobra.Command{
|
||||||
|
Use: "balance",
|
||||||
|
Short: "Get internal balance of NeoFS account",
|
||||||
|
Long: `Get internal balance of NeoFS account`,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
var idUser user.ID
|
||||||
|
|
||||||
|
pk := key.GetOrGenerate(cmd)
|
||||||
|
|
||||||
|
balanceOwner, _ := cmd.Flags().GetString(ownerFlag)
|
||||||
|
if balanceOwner == "" {
|
||||||
|
user.IDFromKey(&idUser, pk.PublicKey)
|
||||||
|
} else {
|
||||||
|
common.ExitOnErr(cmd, "can't decode owner ID wallet address: %w", idUser.DecodeString(balanceOwner))
|
||||||
|
}
|
||||||
|
|
||||||
|
cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)
|
||||||
|
|
||||||
|
var prm internalclient.BalanceOfPrm
|
||||||
|
prm.SetClient(cli)
|
||||||
|
prm.SetAccount(idUser)
|
||||||
|
|
||||||
|
res, err := internalclient.BalanceOf(prm)
|
||||||
|
common.ExitOnErr(cmd, "rpc error: %w", err)
|
||||||
|
|
||||||
|
// print to stdout
|
||||||
|
prettyPrintDecimal(cmd, res.Balance())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func initAccountingBalanceCmd() {
|
||||||
|
ff := accountingBalanceCmd.Flags()
|
||||||
|
|
||||||
|
ff.StringP(commonflags.WalletPath, commonflags.WalletPathShorthand, commonflags.WalletPathDefault, commonflags.WalletPathUsage)
|
||||||
|
ff.StringP(commonflags.Account, commonflags.AccountShorthand, commonflags.AccountDefault, commonflags.AccountUsage)
|
||||||
|
ff.StringP(commonflags.RPC, commonflags.RPCShorthand, commonflags.RPCDefault, commonflags.RPCUsage)
|
||||||
|
ff.String(ownerFlag, "", "owner of balance account (omit to use owner from private key)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func prettyPrintDecimal(cmd *cobra.Command, decimal accounting.Decimal) {
|
||||||
|
if viper.GetBool(commonflags.Verbose) {
|
||||||
|
cmd.Println("value:", decimal.Value())
|
||||||
|
cmd.Println("precision:", decimal.Precision())
|
||||||
|
} else {
|
||||||
|
amountF8 := precision.Convert(decimal.Precision(), 8, big.NewInt(decimal.Value()))
|
||||||
|
|
||||||
|
cmd.Println(fixedn.ToString(amountF8, 8))
|
||||||
|
}
|
||||||
|
}
|
27
cmd/frostfs-cli/modules/accounting/root.go
Normal file
27
cmd/frostfs-cli/modules/accounting/root.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package accounting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cmd represents the accounting command.
|
||||||
|
var Cmd = &cobra.Command{
|
||||||
|
Use: "accounting",
|
||||||
|
Short: "Operations with accounts and balances",
|
||||||
|
Long: `Operations with accounts and balances`,
|
||||||
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
flags := cmd.Flags()
|
||||||
|
|
||||||
|
_ = viper.BindPFlag(commonflags.WalletPath, flags.Lookup(commonflags.WalletPath))
|
||||||
|
_ = viper.BindPFlag(commonflags.Account, flags.Lookup(commonflags.Account))
|
||||||
|
_ = viper.BindPFlag(commonflags.RPC, flags.Lookup(commonflags.RPC))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Cmd.AddCommand(accountingBalanceCmd)
|
||||||
|
|
||||||
|
initAccountingBalanceCmd()
|
||||||
|
}
|
28
cmd/frostfs-cli/modules/acl/basic/print.go
Normal file
28
cmd/frostfs-cli/modules/acl/basic/print.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package basic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/util"
|
||||||
|
"github.com/TrueCloudLab/frostfs-sdk-go/container/acl"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var printACLCmd = &cobra.Command{
|
||||||
|
Use: "print",
|
||||||
|
Short: "Pretty print basic ACL from the HEX representation",
|
||||||
|
Example: `frostfs-cli acl basic print 0x1C8C8CCC`,
|
||||||
|
Long: `Pretty print basic ACL from the HEX representation.
|
||||||
|
Few roles have exclusive default access to set of operation, even if particular bit deny it.
|
||||||
|
Container have access to the operations of the data replication mechanism:
|
||||||
|
Get, Head, Put, Search, Hash.
|
||||||
|
InnerRing members are allowed to data audit ops only:
|
||||||
|
Get, Head, Hash, Search.`,
|
||||||
|
Run: printACL,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
}
|
||||||
|
|
||||||
|
func printACL(cmd *cobra.Command, args []string) {
|
||||||
|
var bacl acl.Basic
|
||||||
|
common.ExitOnErr(cmd, "unable to parse basic acl: %w", bacl.DecodeString(args[0]))
|
||||||
|
util.PrettyPrintTableBACL(cmd, &bacl)
|
||||||
|
}
|
14
cmd/frostfs-cli/modules/acl/basic/root.go
Normal file
14
cmd/frostfs-cli/modules/acl/basic/root.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package basic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Cmd = &cobra.Command{
|
||||||
|
Use: "basic",
|
||||||
|
Short: "Operations with Basic Access Control Lists",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Cmd.AddCommand(printACLCmd)
|
||||||
|
}
|
127
cmd/frostfs-cli/modules/acl/extended/create.go
Normal file
127
cmd/frostfs-cli/modules/acl/extended/create.go
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
package extended
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/common"
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
|
||||||
|
"github.com/TrueCloudLab/frostfs-node/cmd/frostfs-cli/modules/util"
|
||||||
|
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||||
|
"github.com/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var createCmd = &cobra.Command{
|
||||||
|
Use: "create",
|
||||||
|
Short: "Create extended ACL from the text representation",
|
||||||
|
Long: `Create extended ACL from the text representation.
|
||||||
|
|
||||||
|
Rule consist of these blocks: <action> <operation> [<filter1> ...] [<target1> ...]
|
||||||
|
|
||||||
|
Action is 'allow' or 'deny'.
|
||||||
|
|
||||||
|
Operation is an object service verb: 'get', 'head', 'put', 'search', 'delete', 'getrange', or 'getrangehash'.
|
||||||
|
|
||||||
|
Filter consists of <typ>:<key><match><value>
|
||||||
|
Typ is 'obj' for object applied filter or 'req' for request applied filter.
|
||||||
|
Key is a valid unicode string corresponding to object or request header key.
|
||||||
|
Well-known system object headers start with '$Object:' prefix.
|
||||||
|
User defined headers start without prefix.
|
||||||
|
Read more about filter keys at github.com/TrueCloudLab/frostfs-api/blob/master/proto-docs/acl.md#message-eaclrecordfilter
|
||||||
|
Match is '=' for matching and '!=' for non-matching filter.
|
||||||
|
Value is a valid unicode string corresponding to object or request header value.
|
||||||
|
|
||||||
|
Target is
|
||||||
|
'user' for container owner,
|
||||||
|
'system' for Storage nodes in container and Inner Ring nodes,
|
||||||
|
'others' for all other request senders,
|
||||||
|
'pubkey:<key1>,<key2>,...' for exact request sender, where <key> is a hex-encoded 33-byte public key.
|
||||||
|
|
||||||
|
When both '--rule' and '--file' arguments are used, '--rule' records will be placed higher in resulting extended ACL table.
|
||||||
|
`,
|
||||||
|
Example: `frostfs-cli acl extended create --cid EutHBsdT1YCzHxjCfQHnLPL1vFrkSyLSio4vkphfnEk -f rules.txt --out table.json
|
||||||
|
frostfs-cli acl extended create --cid EutHBsdT1YCzHxjCfQHnLPL1vFrkSyLSio4vkphfnEk -r 'allow get obj:Key=Value others' -r 'deny put others'`,
|
||||||
|
Run: createEACL,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
createCmd.Flags().StringArrayP("rule", "r", nil, "Extended ACL table record to apply")
|
||||||
|
createCmd.Flags().StringP("file", "f", "", "Read list of extended ACL table records from text file")
|
||||||
|
createCmd.Flags().StringP("out", "o", "", "Save JSON formatted extended ACL table in file")
|
||||||
|
createCmd.Flags().StringP(commonflags.CIDFlag, "", "", commonflags.CIDFlagUsage)
|
||||||
|
|
||||||
|
_ = cobra.MarkFlagFilename(createCmd.Flags(), "file")
|
||||||
|
_ = cobra.MarkFlagFilename(createCmd.Flags(), "out")
|
||||||
|
}
|
||||||
|
|
||||||
|
func createEACL(cmd *cobra.Command, _ []string) {
|
||||||
|
rules, _ := cmd.Flags().GetStringArray("rule")
|
||||||
|
fileArg, _ := cmd.Flags().GetString("file")
|
||||||
|
outArg, _ := cmd.Flags().GetString("out")
|
||||||
|
cidArg, _ := cmd.Flags().GetString(commonflags.CIDFlag)
|
||||||
|
|
||||||
|
var containerID cid.ID
|
||||||
|
if cidArg != "" {
|
||||||
|
if err := containerID.DecodeString(cidArg); err != nil {
|
||||||
|
cmd.PrintErrf("invalid container ID: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rulesFile, err := getRulesFromFile(fileArg)
|
||||||
|
if err != nil {
|
||||||
|
cmd.PrintErrf("can't read rules from file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
rules = append(rules, rulesFile...)
|
||||||
|
if len(rules) == 0 {
|
||||||
|
cmd.PrintErrln("no extended ACL rules has been provided")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
tb := eacl.NewTable()
|
||||||
|
common.ExitOnErr(cmd, "unable to parse provided rules: %w", util.ParseEACLRules(tb, rules))
|
||||||
|
|
||||||
|
tb.SetCID(containerID)
|
||||||
|
|
||||||
|
data, err := tb.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
cmd.PrintErrln(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
err = json.Indent(buf, data, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
cmd.PrintErrln(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(outArg) == 0 {
|
||||||
|
cmd.Println(buf)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(outArg, buf.Bytes(), 0644)
|
||||||
|
if err != nil {
|
||||||
|
cmd.PrintErrln(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRulesFromFile(filename string) ([]string, error) {
|
||||||
|
if len(filename) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Split(strings.TrimSpace(string(data)), "\n"), nil
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue