├── .circleci └── config.yml ├── .github ├── pull_request_template.md └── workflows │ ├── build_upload.yml │ ├── docker_image.yml │ └── go.yml ├── .gitignore ├── CHANGELOG.md ├── FUNDING.json ├── LICENSE-APACHE ├── LICENSE-MIT ├── Makefile ├── README.md ├── SECURITY.md ├── api ├── api_common.go ├── api_miner.go ├── client │ ├── client.go │ ├── gateway_client.go │ └── submitblock_client.go ├── docgen │ ├── example.go │ └── main.go └── proxy_gen.go ├── build ├── clock.go ├── tools.go └── version.go ├── cli ├── cmd.go ├── helper.go ├── log.go └── version.go ├── cmd ├── address.go ├── init.go ├── main.go ├── record.go ├── run.go ├── stop.go └── winner.go ├── docs ├── en │ ├── api-v0-methods-miner.md │ ├── config-desc.md │ ├── jaeger-tracing.md │ └── metrics.md └── zh │ ├── config-desc.md │ ├── design-spec.md │ ├── getting-start.md │ ├── journal-event.md │ ├── metrics.md │ └── mine-record.md ├── f3participant ├── multiparticipation.go └── participation.go ├── gen └── api │ └── proxygen.go ├── go.mod ├── go.sum ├── lib ├── journal │ ├── alerting │ │ ├── alerts.go │ │ └── alerts_test.go │ ├── env.go │ ├── fsjournal │ │ ├── fs.go │ │ └── fs_test.go │ ├── mockjournal │ │ └── journal.go │ ├── nil.go │ ├── registry.go │ ├── registry_test.go │ └── types.go ├── logger │ └── levels.go ├── metrics │ ├── expoter.go │ └── metrics.go └── tracing │ └── setup.go ├── miner ├── api.go ├── miner_test.go ├── minerwpp.go ├── miningmgr.go ├── mock │ └── mock_post_provider.go ├── multiminer.go ├── util.go └── warmup.go ├── node ├── builder.go ├── config │ ├── apiinfo.go │ ├── apiinfo_test.go │ ├── def.go │ ├── def_test.go │ ├── gatewaydef.go │ ├── load.go │ ├── load_test.go │ └── migrate │ │ └── def_nv170.go ├── impl │ ├── common │ │ └── common.go │ └── miner.go ├── modules │ ├── helpers │ │ └── helpers.go │ ├── mine-recorder │ │ ├── inner_recorder.go │ │ ├── mod.go │ │ └── record_test.go │ ├── miner-manager │ │ ├── api.go │ │ ├── auth_manager.go │ │ └── mock │ │ │ └── miner_manager.go │ ├── miner.go │ ├── services.go │ ├── slashfilter │ │ ├── api.go │ │ ├── local.go │ │ ├── mock.go │ │ ├── slashfilter.go │ │ └── slashfilter_test.go │ └── storage.go ├── options.go └── repo │ ├── fsrepo.go │ ├── fsrepo_ds.go │ ├── fsrepo_test.go │ ├── interface.go │ ├── migrate.go │ └── repo_test.go ├── scripts ├── build-bundle.sh └── publish-release.sh └── types ├── api.go ├── logs.go ├── miner.go ├── shutdown.go └── storage.go /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## 关联的Issues (Related Issues) 2 | 3 | 4 | 5 | close 6 | 7 | ## 改动 (Proposed Changes) 8 | 9 | 10 | 11 | 12 | ## 附注 (Additional Info) 13 | 14 | 15 | 16 | ## 自查清单 (Checklist) 17 | 18 | 在你认为本 PR 满足被审阅的标准之前,需要确保 / Before you mark the PR ready for review, please make sure that: 19 | - [ ] 符合Venus项目管理规范中关于PR的[相关标准](https://github.com/ipfs-force-community/dev-guidances/blob/master/%E9%A1%B9%E7%9B%AE%E7%AE%A1%E7%90%86/Venus/PR%E5%91%BD%E5%90%8D%E8%A7%84%E8%8C%83.md) / The PR follows the PR standards set out in the Venus project management guidelines 20 | - [ ] 具有清晰明确的[commit message](https://github.com/ipfs-force-community/dev-guidances/blob/master/%E8%B4%A8%E9%87%8F%E7%AE%A1%E7%90%86/%E4%BB%A3%E7%A0%81/git%E4%BD%BF%E7%94%A8/commit-message%E9%A3%8E%E6%A0%BC%E8%A7%84%E8%8C%83.md) / All commits have a clear commit message. 21 | - [ ] 包含相关的的[测试用例](https://github.com/ipfs-force-community/dev-guidances/blob/master/%E8%B4%A8%E9%87%8F%E7%AE%A1%E7%90%86/%E4%BB%A3%E7%A0%81/%E4%BB%A3%E7%A0%81%E5%BA%93/%E6%A3%80%E6%9F%A5%E9%A1%B9/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95.md)或者不需要新增测试用例 / This PR has tests for new functionality or change in behaviour or not need to add new tests. 22 | - [ ] 包含相关的的指南以及[文档](https://github.com/ipfs-force-community/dev-guidances/tree/master/%E8%B4%A8%E9%87%8F%E7%AE%A1%E7%90%86/%E6%96%87%E6%A1%A3)或者不需要新增文档 / This PR has updated usage guidelines and documentation or not need 23 | - [ ] 通过必要的检查项 / All checks are green 24 | -------------------------------------------------------------------------------- /.github/workflows/build_upload.yml: -------------------------------------------------------------------------------- 1 | name: build and upload 2 | 3 | on: 4 | push: 5 | branches: ['prep/**', 'release/**', 'test/**', master] 6 | tags: ['**'] 7 | 8 | jobs: 9 | build_upload: 10 | uses: filecoin-project/venus/.github/workflows/common_build_upload.yml@master 11 | with: 12 | bin_name: 'sophon-miner' 13 | has_ffi: false 14 | secrets: 15 | OSS_KEY_ID: ${{secrets.OSS_KEY_ID}} 16 | OSS_KEY_SECRET: ${{secrets.OSS_KEY_SECRET}} 17 | OSS_ENDPOINT: ${{secrets.OSS_ENDPOINT}} 18 | OSS_BUCKET: ${{secrets.OSS_BUCKET}} 19 | FTP_HOST: ${{secrets.FTP_HOST}} 20 | FTP_USER: ${{secrets.FTP_USER}} 21 | FTP_PWD: ${{secrets.FTP_PWD}} 22 | GODEYE_URL: ${{secrets.GODEYE_URL}} 23 | token: ${{github.token}} 24 | -------------------------------------------------------------------------------- /.github/workflows/docker_image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: ['prep/**', 'release/**', 'test/**', master] 6 | tags: ['**'] 7 | 8 | jobs: 9 | 10 | build_docker_image: 11 | uses: filecoin-project/venus/.github/workflows/common_docker_image.yml@master 12 | secrets: 13 | DOCKER_PASSWORD: ${{secrets.DOCKER_PASSWORD}} 14 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: 8 | - '**' 9 | 10 | jobs: 11 | 12 | check: 13 | uses: filecoin-project/venus/.github/workflows/common_go.yml@master 14 | with: 15 | has_ffi: false 16 | test_timeout: 20 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /sophon-miner 2 | /bench.json 3 | /lotuspond/front/node_modules 4 | /lotuspond/front/build 5 | extern/filecoin-ffi/rust/target 6 | **/*.a 7 | **/*.pc 8 | /**/*/.DS_STORE 9 | .DS_STORE 10 | build/.* 11 | build/paramfetch.sh 12 | /vendor 13 | /blocks.dot 14 | /blocks.svg 15 | /bundle 16 | /darwin 17 | /linux 18 | 19 | *-fuzz.zip 20 | bin/ipget 21 | bin/tmp/* 22 | .idea 23 | .vscode 24 | scratchpad 25 | .vscode 26 | *.out 27 | dockerfile 28 | sophon-miner.log -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # sophon-miner changelog 2 | 3 | ## v1.15.0 4 | 5 | * Feat/add more metrics [[#238](https://github.com/ipfs-force-community/sophon-miner/pull/238)] 6 | 7 | ## 1.14.0 8 | 9 | * refactor: Only pass the information needed to computeTicket [[#222](https://github.com/ipfs-force-community/sophon-miner/pull/222)] 10 | * add some explanation of parameters / 添加参数注释 [[#221](https://github.com/ipfs-force-community/sophon-miner/pull/221)] 11 | * doc: enable record [[#225](https://github.com/ipfs-force-community/sophon-miner/pull/225)] 12 | * fix: get beacon earlier [[#227](https://github.com/ipfs-force-community/sophon-miner/pull/227)] 13 | * Merge pull request #227 from ipfs-force-community/fix/get-beacon-earlier [[#228](https://github.com/ipfs-force-community/sophon-miner/pull/228)] 14 | * opt: add DialMinerRPC [[#230](https://github.com/ipfs-force-community/sophon-miner/pull/230)] 15 | * chore: bump version to v1.14.0-rc1 [[#232](https://github.com/ipfs-force-community/sophon-miner/pull/232)] 16 | * chore: update venus [[#233](https://github.com/ipfs-force-community/sophon-miner/pull/233)] 17 | * feat: add warning for PropagationDelaySecs [[#234](https://github.com/ipfs-force-community/sophon-miner/pull/234)] 18 | * fix: return error message [[#235](https://github.com/ipfs-force-community/sophon-miner/pull/235)] 19 | 20 | ## 1.13.0 / 2023-08-31 21 | 22 | * Fix/query record incomplete in [[#220](https://github.com/ipfs-force-community/sophon-miner/pull/220)] 23 | * feat: control concurrency when call CountWinner in [[#223](https://github.com/ipfs-force-community/sophon-miner/pull/223)] 24 | 25 | ## 1.13.0-rc2 / 2023-08-18 26 | 27 | ### New Feature 28 | * feat: disable mine record by default [[#218](https://github.com/ipfs-force-community/sophon-miner/pull/218)] 29 | 30 | ## 1.13.0-rc1 / 2023-08-18 31 | 32 | ### New Feature 33 | * feat: add api to list blocks in [[#211](https://github.com/ipfs-force-community/sophon-miner/pull/211)] 34 | * feat: add venus name space header in [[#213](https://github.com/ipfs-force-community/sophon-miner/pull/213)] 35 | * feat: add interface to query record of mining in [[#215](https://github.com/ipfs-force-community/sophon-miner/pull/215)] 36 | 37 | ### Bug Fix 38 | * fix: add null round count in [[#216](https://github.com/ipfs-force-community/sophon-miner/pull/216)] 39 | 40 | ### Documentation And Chores 41 | * doc: update config description / 更新config的解释 in [[#209](https://github.com/ipfs-force-community/sophon-miner/pull/209)] 42 | * chore: remove useless config code / 删除没用的配置代码 in [[#210](https://github.com/ipfs-force-community/sophon-miner/pull/210)] 43 | * chore: merge release v1.12 in [[#208](https://github.com/ipfs-force-community/sophon-miner/pull/208)] 44 | 45 | ## 1.12.0 46 | 47 | ## 1.12.0-rc2 / 2023-06-20 48 | 49 | ### Bug Fixes 50 | * fix: cmds compatible old repo [[#203](https://github.com/ipfs-force-community/sophon-miner/pull/203)] 51 | 52 | 53 | ## 1.12.0-rc1 / 2023-06-14 54 | 55 | ### New Features 56 | * feat: repo migrate [[#197](https://github.com/ipfs-force-community/sophon-miner/pull/197)] 57 | 58 | ### Bug Fixes 59 | * fix: Correctly handle null round [[#198](https://github.com/ipfs-force-community/sophon-miner/pull/198)] 60 | * fix: data race of mockChain [[#199](https://github.com/ipfs-force-community/sophon-miner/pull/199)] 61 | 62 | 63 | ## 1.11.0 / 2023-04-23 64 | 65 | * bump up version to v1.11.0 66 | * update sophon-auth to v1.11.0 67 | * update venus to v1.11.0 68 | 69 | ## 1.11.0-rc1 / 2023-04-18 70 | 71 | ### New Features 72 | * feat: mysql table migrate for miner_blocks / 更新数据库字段为可空或者添加默认值[[#168](https://github.com/ipfs-force-community/sophon-miner/pull/168)] 73 | * feat: update AuthClient which with token /客户端token验证 [[#169](https://github.com/ipfs-force-community/sophon-miner/pull/169)] 74 | * feat: add status api / 添加状态检测接口 [[#172](https://github.com/ipfs-force-community/sophon-miner/pull/172)] 75 | * feat: add docker push /增加推送到镜像仓库的功能 [[#183](https://github.com/ipfs-force-community/sophon-miner/pull/183)] 76 | 77 | ### Improvements 78 | * opt: chain-forked check /链分叉判断时只对同周期内判断 [[#181](https://github.com/ipfs-force-community/sophon-miner/pull/181)] 79 | * opt: set the select messages timeout / 选择消息设置5秒超时 [[#184](https://github.com/ipfs-force-community/sophon-miner/pull/184)] 80 | 81 | ### Bug Fixes 82 | * fix:check gateway fail /修复gateway配置检查失败的问题 [[#177](https://github.com/ipfs-force-community/sophon-miner/pull/177)] 83 | * fix: config check / 修复配置检测失败的问题 [[#178]( https://github.com/ipfs-force-community/sophon-miner/pull/178)] 84 | 85 | ## 1.10.0 / 2023-03-02 86 | 87 | ### Improvements 88 | 89 | - 数据表 `miner_blocks` 字段设置默认值,`winning_at` 允许为 `null` 90 | 91 | ## 1.10.0-rc1 / 2023-02-17 92 | 93 | ### New features 94 | - feat: user data isolation / 增加用户数据隔离 (#163) ([ipfs-force-community/sophon-miner#163](https://github.com/ipfs-force-community/sophon-miner/pull/163)) 95 | 96 | 97 | ## 1.9.0 / 2022-12-30 98 | 99 | ### Dependency Updates 100 | 101 | - 升级venus组件的版本 102 | 103 | 104 | ## 1.8.0 / 2022-11-16 105 | 106 | ### Dependency Updates 107 | 108 | - github.com/filecoin-project/venus (-> v1.8.0) 109 | - github.com/ipfs-force-community/sophon-auth (-> v1.8.0) 110 | 111 | 112 | ## 1.8.0-rc5 / 2022-11-03 113 | 114 | ### Improvements 115 | 116 | - 增加 `miners` 是否出块的控制开关,需要 `sophon-auth` 版本 >= v1.8.0-rc4. 117 | 118 | 119 | ## 1.8.0-rc4 / 2022-10-26 120 | 121 | ### Fixes 122 | 123 | - 修复计算历史出块权时panic 124 | 125 | ## 1.8.0-rc3 / 2022-10-20 126 | 127 | ### Improvements 128 | 129 | - 不记录没有获胜的出块Timeout 130 | 131 | ### 注意事项 132 | 133 | 从 `1.7.*` 升级会自动迁移配置文件,从 `1.6.*` 升级需重新初始化`Repo`(init) 134 | 135 | 136 | ## 1.8.0-rc2 / 2022-10-19 137 | 138 | ### Improvements 139 | 140 | - 配置项 `MySQL.ConnMaxLifeTime` 改为字符窜格式: `60 -> "1m0s"`; 141 | - `PropagationDelaySecs` 和 `MinerOnceTimeout` 由配置文件设置; 142 | - Repo目录增加 `version`用于自动升级配置文件等。 143 | 144 | ### 注意事项 145 | 146 | 从 `1.7.*` 升级会自动迁移配置文件,从 `1.6.*` 升级需重新初始化`Repo`(init) 147 | 148 | 149 | 150 | ## 1.6.1 / 2022-07-22 151 | 152 | ### Improvements 153 | 154 | - 网络参数从同步节点请求,移除本地配置; 155 | - 简化配置文件,参考 配置文件解析; 156 | - 移除 `venus-shared` 中已有的数据类型; 157 | - 移除没有实际作用的代码; 158 | - 矿工由 `sophon-auth` 管理,移除本地的矿工管理模块; 159 | - 移除对 `filecoin-ffi` 的依赖; 160 | - 新增配置项说明文档; 161 | - 新增快速启动文档; 162 | - 优化出块,在创建区块前再次尝试获取上一轮`base`,做如下处理: 163 | - 获得较优 `base`(满足获胜条件并有更多的区块),则选择后者进行出块,避免获取 `base` 不足引起的孤块; 164 | - `base` 不匹配(发生了链分叉,之前获得的 `base` 计算的出块权是无效的),不进行无意义的区块创建。 165 | - 修复采用 `mysql` 数据库时 `parenet_key` 字段长度不足问题。 166 | 167 | ### 注意事项 168 | 169 | 升级到此版本涉及配置文件的改动,有两个选择: 170 | - 重新初始化 `repo`; 171 | - 手动修改配置文件(参考config-desc.md) 172 | -------------------------------------------------------------------------------- /FUNDING.json: -------------------------------------------------------------------------------- 1 | { 2 | "drips": { 3 | "filecoin": { 4 | "ownedBy": "0xD4a3361da05DD7eD15DF0dDE1361b393D48f55F9" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 2 | 3 | http://www.apache.org/licenses/LICENSE-2.0 4 | 5 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 6 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/usr/bin/env bash 2 | 3 | all: build 4 | .PHONY: all docker 5 | 6 | GOVERSION:=$(shell go version | cut -d' ' -f 3 | cut -d. -f 2) 7 | ifeq ($(shell expr $(GOVERSION) \< 17), 1) 8 | $(warning Your Golang version is go 1.$(GOVERSION)) 9 | $(error Update Golang to version to at least 1.18.1) 10 | endif 11 | 12 | CLEAN:= 13 | BINS:= 14 | 15 | BUILD_TARGET=sophon-miner 16 | 17 | ldflags=-X=github.com/ipfs-force-community/sophon-miner/build.CurrentCommit='+git$(subst -,.,$(shell git describe --always --match=NeVeRmAtCh --dirty 2>/dev/null || git rev-parse --short HEAD 2>/dev/null))' 18 | ifneq ($(strip $(LDFLAGS)),) 19 | ldflags+=-extldflags=$(LDFLAGS) 20 | endif 21 | 22 | GOFLAGS+=-ldflags="$(ldflags)" 23 | 24 | build: miner 25 | .PHONY: build 26 | 27 | miner: 28 | rm -f $(BUILD_TARGET) 29 | go build $(GOFLAGS) -o $(BUILD_TARGET) ./cmd/ 30 | 31 | debug: 32 | rm -f $(BUILD_TARGET) 33 | go build $(GOFLAGS) -gcflags=all="-N -l" -o $(BUILD_TARGET) ./cmd/ 34 | 35 | .PHONY: miner 36 | BINS+=sophon-miner 37 | 38 | docsgen: 39 | go build $(GOFLAGS) -o docgen-md ./api/docgen 40 | ./docgen-md > ./docs/en/api-v0-methods-miner.md 41 | rm docgen-md 42 | 43 | # MISC 44 | 45 | buildall: $(BINS) 46 | 47 | clean: 48 | rm -rf $(CLEAN) $(BINS) 49 | .PHONY: clean 50 | 51 | dist-clean: 52 | git clean -xdff 53 | git submodule deinit --all -f 54 | .PHONY: dist-clean 55 | 56 | gen: 57 | go run ./gen/api 58 | goimports -w api 59 | .PHONY: gen 60 | 61 | print-%: 62 | @echo $*=$($*) 63 | 64 | 65 | # docker 66 | .PHONY: docker 67 | 68 | mock: 69 | go run github.com/golang/mock/mockgen -destination=./miner/mock/mock_post_provider.go -source=./miner/util.go -package mock 70 | 71 | TAG:=test 72 | docker: 73 | ifdef DOCKERFILE 74 | cp $(DOCKERFILE) ./dockerfile 75 | else 76 | curl -o dockerfile https://raw.githubusercontent.com/filecoin-project/venus-docs/master/script/docker/dockerfile 77 | endif 78 | docker build --build-arg https_proxy=$(BUILD_DOCKER_PROXY) --build-arg BUILD_TARGET=$(BUILD_TARGET) -t sophon-miner . 79 | docker tag sophon-miner filvenus/sophon-miner:$(TAG) 80 | ifdef PRIVATE_REGISTRY 81 | docker tag sophon-miner $(PRIVATE_REGISTRY)/filvenus/sophon-miner:$(TAG) 82 | endif 83 | 84 | docker-push: docker 85 | docker push $(PRIVATE_REGISTRY)/filvenus/sophon-miner:$(TAG) 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Sophon Logo 4 | 5 |

6 | 7 |

sophon-miner

8 | 9 |

10 | 11 | 12 |
13 |

14 | 15 | The sophon-miner is used to mine for multiple miners, the advantage is that: 16 | 1. The miners in the mining pool do not need to deploy their own mining programs. 17 | 2. It is possible to increase the tps and miner fees for packaging messages. 18 | 3. Miners can package messages for each other to avoid the problem of selfish mining. 19 | 20 | Use [Venus Issues](https://github.com/filecoin-project/venus/issues) for reporting issues about this repository. 21 | 22 | ## Building & Documentation 23 | 24 | For instructions on how to build, install and setup venus, please visit [https://venus.filecoin.io/](https://venus.filecoin.io/). 25 | 26 | ## Reporting a Vulnerability 27 | 28 | Please send an email to security@filecoin.org. See our [security policy](SECURITY.md) for more details. 29 | 30 | ## Related packages 31 | 32 | These repos are independent and reusable modules, but are tightly integrated into Venus to make up a fully featured Filecoin implementation: 33 | - [specs-actors](https://github.com/filecoin-project/specs-actors) which has its own [kanban work tracker available here](https://app.zenhub.com/workspaces/actors-5ee6f3aa87591f0016c05685/board) 34 | 35 | ## Contribute 36 | 37 | Venus is a universally open project and welcomes contributions of all kinds: code, docs, and more. However, before making a contribution, we ask you to heed these recommendations: 38 | 39 | 1. If the proposal entails a protocol change, please first submit a [Filecoin Improvement Proposal](https://github.com/filecoin-project/FIPs). 40 | 2. If the change is complex and requires prior discussion, [open an issue](https://github.com/ipfs-force-community/sophon-miner/issues) to request feedback before you start working on a pull request. This is to avoid disappointment and sunk costs, in case the change is not actually needed or accepted. 41 | 3. Please refrain from submitting PRs to adapt existing code to subjective preferences. The changeset should contain functional or technical improvements/enhancements, bug fixes, new features, or some other clear material contribution. Simple stylistic changes are likely to be rejected in order to reduce code churn. 42 | 43 | When implementing a change: 44 | 45 | 1. Adhere to the standard Go formatting guidelines, e.g. [Effective Go](https://golang.org/doc/effective_go.html). Run `go fmt`. 46 | 2. Stick to the idioms and patterns used in the codebase. Familiar-looking code has a higher chance of being accepted than eerie code. Pay attention to commonly used variable and parameter names, avoidance of naked returns, error handling patterns, etc. 47 | 3. Comments: follow the advice on the [Commentary](https://golang.org/doc/effective_go.html#commentary) section of Effective Go. 48 | 4. Minimize code churn. Modify only what is strictly necessary. Well-encapsulated changesets will get a quicker response from maintainers. 49 | 5. Lint your code with [`golangci-lint`](https://golangci-lint.run) (CI will reject your PR if unlinted). 50 | 6. Add tests. 51 | 7. Title the PR in a meaningful way and describe the rationale and the thought process in the PR description. 52 | 8. Write clean, thoughtful, and detailed [commit messages](https://chris.beams.io/posts/git-commit/). This is even more important than the PR description, because commit messages are stored _inside_ the Git history. One good rule is: if you are happy posting the commit message as the PR description, then it's a good commit message. 53 | 54 | ## License 55 | 56 | Dual-licensed under [MIT](https://github.com/ipfs-force-community/sophon-miner/blob/master/LICENSE-MIT) + [Apache 2.0](https://github.com/ipfs-force-community/sophon-miner/blob/master/LICENSE-APACHE) 57 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | For reporting security vulnerabilities/bugs, please consult our Security Policy and Responsible Disclosure Program information at https://github.com/filecoin-project/community/blob/master/SECURITY.md. Security vulnerabilities should be reported via our [Vulnerability Reporting channels](https://github.com/filecoin-project/community/blob/master/SECURITY.md#vulnerability-reporting) and will be eligible for a [Bug Bounty](https://security.filecoin.io/bug-bounty/). 6 | 7 | Please try to provide a clear description of any bugs reported, along with how to reproduce the bug if possible. More detailed bug reports (especially those with a PoC included) will help us move forward much faster. Additionally, please avoid reporting bugs that already have open issues. Take a moment to search the issue list of the related GitHub repositories before writing up a new report. 8 | 9 | Here are some examples of bugs we would consider to be security vulnerabilities: 10 | 11 | * If you can spend from a `multisig` wallet you do not control the keys for. 12 | * If you can cause a miner to be slashed without them actually misbehaving. 13 | * If you can maintain power without submitting windowed posts regularly. 14 | * If you can craft a message that causes venus nodes to panic. 15 | * If you can cause your miner to win significantly more blocks than it should. 16 | * If you can craft a message that causes a persistent fork in the network. 17 | * If you can cause the total amount of Filecoin in the network to no longer be 2 billion. 18 | 19 | This is not an exhaustive list, but should provide some idea of what we consider as a security vulnerability, . 20 | 21 | ## Reporting a non security bug 22 | 23 | For non-security bugs, please simply file a GitHub [issue](https://github.com/filecoin-project/venus/issues/new/choose). 24 | -------------------------------------------------------------------------------- /api/api_common.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/google/uuid" 7 | 8 | sharedTypes "github.com/filecoin-project/venus/venus-shared/types" 9 | ) 10 | 11 | type Common interface { 12 | 13 | // Version provides information about API provider 14 | Version(context.Context) (sharedTypes.Version, error) //perm:read 15 | 16 | LogList(context.Context) ([]string, error) //perm:write 17 | LogSetLevel(context.Context, string, string) error //perm:write 18 | 19 | // trigger graceful shutdown 20 | Shutdown(context.Context) error //perm:admin 21 | 22 | // Session returns a random UUID of api provider session 23 | Session(context.Context) (uuid.UUID, error) //perm:read 24 | 25 | Closing(context.Context) (<-chan struct{}, error) //perm:read 26 | } 27 | -------------------------------------------------------------------------------- /api/api_miner.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/ipfs-force-community/sophon-miner/types" 7 | 8 | "github.com/filecoin-project/go-address" 9 | "github.com/filecoin-project/go-state-types/abi" 10 | ) 11 | 12 | type MinerAPI interface { 13 | Common 14 | 15 | UpdateAddress(context.Context, int64, int64) ([]types.MinerInfo, error) //perm:admin 16 | ListAddress(context.Context) ([]types.MinerInfo, error) //perm:read 17 | StatesForMining(context.Context, []address.Address) ([]types.MinerState, error) //perm:read 18 | CountWinners(context.Context, []address.Address, abi.ChainEpoch, abi.ChainEpoch) ([]types.CountWinners, error) //perm:read 19 | ListBlocks(ctx context.Context, params *types.BlocksQueryParams) ([]types.MinedBlock, error) //perm:read 20 | WarmupForMiner(context.Context, address.Address) error //perm:write 21 | Start(context.Context, []address.Address) error //perm:admin 22 | Stop(context.Context, []address.Address) error //perm:admin 23 | 24 | QueryRecord(ctx context.Context, params *types.QueryRecordParams) ([]map[string]string, error) //perm:read 25 | } 26 | -------------------------------------------------------------------------------- /api/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/filecoin-project/go-jsonrpc" 9 | 10 | venus_api "github.com/filecoin-project/venus/venus-shared/api" 11 | "github.com/ipfs-force-community/sophon-miner/api" 12 | ) 13 | 14 | const MajorVersion = 0 15 | const APINamespace = "miner.MinerAPI" 16 | const MethodNamespace = "Filecoin" 17 | 18 | // NewCommonRPC creates a new http jsonrpc client. 19 | func NewCommonRPC(ctx context.Context, addr string, requestHeader http.Header) (api.Common, jsonrpc.ClientCloser, error) { 20 | var res api.CommonStruct 21 | closer, err := jsonrpc.NewMergeClient(ctx, addr, MethodNamespace, 22 | []interface{}{ 23 | &res.Internal, 24 | }, 25 | requestHeader, 26 | ) 27 | 28 | return &res, closer, err 29 | } 30 | 31 | // NewMinerRPC creates a new http jsonrpc client for miner 32 | func NewMinerRPC(ctx context.Context, addr string, requestHeader http.Header, opts ...jsonrpc.Option) (api.MinerAPI, jsonrpc.ClientCloser, error) { 33 | requestHeader.Set(venus_api.VenusAPINamespaceHeader, APINamespace) 34 | var res api.MinerAPIStruct 35 | closer, err := jsonrpc.NewMergeClient(ctx, addr, MethodNamespace, 36 | []interface{}{ 37 | &res.CommonStruct.Internal, 38 | &res.Internal, 39 | }, 40 | requestHeader, 41 | opts..., 42 | ) 43 | 44 | return &res, closer, err 45 | } 46 | 47 | // DialMinerRPC is a more convinient way of building client, as it resolves any format (url, multiaddr) of addr string. 48 | func DialMinerRPC(ctx context.Context, addr string, token string, requestHeader http.Header, opts ...jsonrpc.Option) (api.MinerAPI, jsonrpc.ClientCloser, error) { 49 | ainfo := venus_api.NewAPIInfo(addr, token) 50 | endpoint, err := ainfo.DialArgs(venus_api.VerString(MajorVersion)) 51 | if err != nil { 52 | return nil, nil, fmt.Errorf("get dial args: %w", err) 53 | } 54 | 55 | if requestHeader == nil { 56 | requestHeader = http.Header{} 57 | } 58 | requestHeader.Set(venus_api.VenusAPINamespaceHeader, APINamespace) 59 | ainfo.SetAuthHeader(requestHeader) 60 | 61 | var res api.MinerAPIStruct 62 | closer, err := jsonrpc.NewMergeClient(ctx, endpoint, MethodNamespace, venus_api.GetInternalStructs(&res), requestHeader, opts...) 63 | 64 | return &res, closer, err 65 | } 66 | -------------------------------------------------------------------------------- /api/client/gateway_client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/filecoin-project/go-jsonrpc" 8 | 9 | "github.com/ipfs-force-community/sophon-miner/node/config" 10 | 11 | gatewayAPIV2 "github.com/filecoin-project/venus/venus-shared/api/gateway/v2" 12 | ) 13 | 14 | func NewGatewayRPC(ctx context.Context, cfg *config.GatewayNode) (gatewayAPIV2.IGateway, jsonrpc.ClientCloser, error) { 15 | var err error 16 | addrs, err := cfg.DialArgs() 17 | if err != nil { 18 | return nil, nil, fmt.Errorf("could not get DialArgs: %w", err) 19 | } 20 | 21 | var gatewayAPI gatewayAPIV2.IGateway = nil 22 | var closer jsonrpc.ClientCloser 23 | for _, addr := range addrs { 24 | gatewayAPI, closer, err = gatewayAPIV2.NewIGatewayRPC(ctx, addr, cfg.AuthHeader()) 25 | if err == nil { 26 | return gatewayAPI, closer, err 27 | } 28 | } 29 | 30 | return gatewayAPI, closer, err 31 | } 32 | -------------------------------------------------------------------------------- /api/client/submitblock_client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/filecoin-project/go-jsonrpc" 8 | 9 | "github.com/ipfs-force-community/sophon-miner/node/config" 10 | 11 | v1 "github.com/filecoin-project/venus/venus-shared/api/chain/v1" 12 | ) 13 | 14 | func NewFullNodeRPC(ctx context.Context, apiInfo *config.APIInfo) (v1.FullNode, jsonrpc.ClientCloser, error) { 15 | addr, err := apiInfo.DialArgs("v1") 16 | if err != nil { 17 | return nil, nil, fmt.Errorf("could not get DialArgs: %w", err) 18 | } 19 | 20 | return v1.NewFullNodeRPC(ctx, addr, apiInfo.AuthHeader()) 21 | } 22 | -------------------------------------------------------------------------------- /api/docgen/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "go/parser" 7 | "go/token" 8 | "path/filepath" 9 | "reflect" 10 | "strings" 11 | "unicode" 12 | 13 | "golang.org/x/text/cases" 14 | "golang.org/x/text/language" 15 | 16 | "github.com/filecoin-project/go-address" 17 | "github.com/filecoin-project/go-jsonrpc/auth" 18 | "github.com/filecoin-project/go-state-types/abi" 19 | 20 | "github.com/ipfs-force-community/sophon-miner/api" 21 | 22 | sharedTypes "github.com/filecoin-project/venus/venus-shared/types" 23 | ) 24 | 25 | var ExampleValues = map[reflect.Type]interface{}{ 26 | reflect.TypeOf(auth.Permission("")): auth.Permission("write"), 27 | reflect.TypeOf(""): "string value", 28 | reflect.TypeOf(uint64(42)): uint64(42), 29 | reflect.TypeOf(int(42)): int(42), 30 | reflect.TypeOf(uint(42)): uint(42), 31 | reflect.TypeOf(byte(7)): byte(7), 32 | reflect.TypeOf([]byte{}): []byte("byte array"), 33 | reflect.TypeOf(map[string]string{}): make(map[string]string), 34 | } 35 | 36 | func addExample(v interface{}) { 37 | ExampleValues[reflect.TypeOf(v)] = v 38 | } 39 | 40 | func init() { 41 | addr, err := address.NewIDAddress(1234) 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | ExampleValues[reflect.TypeOf(addr)] = addr 47 | 48 | addExample(sharedTypes.APIVersion(1)) 49 | addExample(abi.ChainEpoch(10101)) 50 | addExample(int64(9)) 51 | addExample(true) 52 | 53 | // miner specific 54 | //si := uint64(12) 55 | //addExample(&si) 56 | //addExample(abi.ActorID(1000)) 57 | } 58 | 59 | func GetAPIType() (i interface{}, t reflect.Type, permStruct []reflect.Type) { 60 | i = &api.MinerAPIStruct{} 61 | t = reflect.TypeOf(new(struct{ api.MinerAPI })).Elem() 62 | permStruct = append(permStruct, reflect.TypeOf(api.MinerAPIStruct{}.Internal)) 63 | permStruct = append(permStruct, reflect.TypeOf(api.CommonStruct{}.Internal)) 64 | 65 | return 66 | } 67 | 68 | func ExampleValue(method string, t, parent reflect.Type) interface{} { 69 | v, ok := ExampleValues[t] 70 | if ok { 71 | return v 72 | } 73 | 74 | switch t.Kind() { 75 | case reflect.Slice: 76 | out := reflect.New(t).Elem() 77 | out = reflect.Append(out, reflect.ValueOf(ExampleValue(method, t.Elem(), t))) 78 | return out.Interface() 79 | case reflect.Chan: 80 | return ExampleValue(method, t.Elem(), nil) 81 | case reflect.Struct: 82 | es := exampleStruct(method, t, parent) 83 | v := reflect.ValueOf(es).Elem().Interface() 84 | ExampleValues[t] = v 85 | return v 86 | case reflect.Array: 87 | out := reflect.New(t).Elem() 88 | for i := 0; i < t.Len(); i++ { 89 | out.Index(i).Set(reflect.ValueOf(ExampleValue(method, t.Elem(), t))) 90 | } 91 | return out.Interface() 92 | 93 | case reflect.Ptr: 94 | if t.Elem().Kind() == reflect.Struct { 95 | es := exampleStruct(method, t.Elem(), t) 96 | //ExampleValues[t] = es 97 | return es 98 | } 99 | case reflect.Interface: 100 | return struct{}{} 101 | } 102 | 103 | panic(fmt.Sprintf("No example value for type: %s (method '%s')", t, method)) 104 | } 105 | 106 | func exampleStruct(method string, t, parent reflect.Type) interface{} { 107 | ns := reflect.New(t) 108 | for i := 0; i < t.NumField(); i++ { 109 | f := t.Field(i) 110 | if shouldIgnoreField(f, parent) { 111 | continue 112 | } 113 | 114 | caser := cases.Title(language.English) 115 | if caser.String(f.Name) == f.Name { 116 | ns.Elem().Field(i).Set(reflect.ValueOf(ExampleValue(method, f.Type, t))) 117 | } 118 | } 119 | 120 | return ns.Interface() 121 | } 122 | 123 | func shouldIgnoreField(f reflect.StructField, parentType reflect.Type) bool { 124 | if f.Type == parentType { 125 | return true 126 | } 127 | 128 | if len(f.Name) == 0 { 129 | return true 130 | } 131 | 132 | if !token.IsExported(f.Name) { 133 | return true 134 | } 135 | 136 | jtag := f.Tag.Get("json") 137 | if len(jtag) == 0 { 138 | return false 139 | } 140 | 141 | return strings.Split(jtag, ",")[0] == "-" 142 | } 143 | 144 | type Visitor struct { 145 | Root string 146 | Methods map[string]ast.Node 147 | } 148 | 149 | func (v *Visitor) Visit(node ast.Node) ast.Visitor { 150 | st, ok := node.(*ast.TypeSpec) 151 | if !ok { 152 | return v 153 | } 154 | 155 | if st.Name.Name != v.Root { 156 | return nil 157 | } 158 | 159 | iface := st.Type.(*ast.InterfaceType) 160 | for _, m := range iface.Methods.List { 161 | if len(m.Names) > 0 { 162 | v.Methods[m.Names[0].Name] = m 163 | } 164 | } 165 | 166 | return v 167 | } 168 | 169 | const NoComment = "There are not yet any comments for this method." 170 | 171 | func ParseApiASTInfo() (comments map[string]string, groupDocs map[string]string) { //nolint:golint 172 | fset := token.NewFileSet() 173 | apiDir, err := filepath.Abs("api") 174 | if err != nil { 175 | fmt.Println("filepath absolute error: ", err) 176 | return 177 | } 178 | apiFile, err := filepath.Abs("api/api_miner.go") 179 | if err != nil { 180 | fmt.Println("filepath absolute error: ", err, "file:", apiFile) 181 | return 182 | } 183 | pkgs, err := parser.ParseDir(fset, apiDir, nil, parser.AllErrors|parser.ParseComments) 184 | if err != nil { 185 | fmt.Println("parse error: ", err) 186 | return 187 | } 188 | 189 | ap := pkgs["api"] 190 | f := ap.Files[apiFile] 191 | cmap := ast.NewCommentMap(fset, f, f.Comments) 192 | 193 | v := &Visitor{"MinerAPI", make(map[string]ast.Node)} 194 | ast.Walk(v, ap) 195 | 196 | comments = make(map[string]string) 197 | groupDocs = make(map[string]string) 198 | for mn, node := range v.Methods { 199 | filteredComments := cmap.Filter(node).Comments() 200 | if len(filteredComments) == 0 { 201 | comments[mn] = NoComment 202 | } else { 203 | for _, c := range filteredComments { 204 | if strings.HasPrefix(c.Text(), "MethodGroup:") { 205 | parts := strings.Split(c.Text(), "\n") 206 | groupName := strings.TrimSpace(parts[0][12:]) 207 | comment := strings.Join(parts[1:], "\n") 208 | groupDocs[groupName] = comment 209 | 210 | break 211 | } 212 | } 213 | 214 | l := len(filteredComments) - 1 215 | if len(filteredComments) > 1 { 216 | l = len(filteredComments) - 2 217 | } 218 | last := filteredComments[l].Text() 219 | if !strings.HasPrefix(last, "MethodGroup:") { 220 | comments[mn] = last 221 | } else { 222 | comments[mn] = NoComment 223 | } 224 | } 225 | } 226 | return comments, groupDocs 227 | } 228 | 229 | type MethodGroup struct { 230 | GroupName string 231 | Header string 232 | Methods []*Method 233 | } 234 | 235 | type Method struct { 236 | Comment string 237 | Name string 238 | InputExample string 239 | ResponseExample string 240 | } 241 | 242 | func MethodGroupFromName(mn string) string { 243 | i := strings.IndexFunc(mn[1:], func(r rune) bool { 244 | return unicode.IsUpper(r) 245 | }) 246 | if i < 0 { 247 | return "" 248 | } 249 | return mn[:i+1] 250 | } 251 | -------------------------------------------------------------------------------- /api/docgen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "sort" 8 | "strings" 9 | ) 10 | 11 | func main() { 12 | comments, groupComments := ParseApiASTInfo() 13 | 14 | groups := make(map[string]*MethodGroup) 15 | 16 | _, t, permStruct := GetAPIType() 17 | 18 | for i := 0; i < t.NumMethod(); i++ { 19 | m := t.Method(i) 20 | 21 | groupName := MethodGroupFromName(m.Name) 22 | 23 | g, ok := groups[groupName] 24 | if !ok { 25 | g = new(MethodGroup) 26 | g.Header = groupComments[groupName] 27 | g.GroupName = groupName 28 | groups[groupName] = g 29 | } 30 | 31 | var args []interface{} 32 | ft := m.Func.Type() 33 | for j := 2; j < ft.NumIn(); j++ { 34 | inp := ft.In(j) 35 | args = append(args, ExampleValue(m.Name, inp, nil)) 36 | } 37 | 38 | v, err := json.MarshalIndent(args, "", " ") 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | outv := ExampleValue(m.Name, ft.Out(0), nil) 44 | 45 | ov, err := json.MarshalIndent(outv, "", " ") 46 | if err != nil { 47 | panic(err) 48 | } 49 | 50 | g.Methods = append(g.Methods, &Method{ 51 | Name: m.Name, 52 | Comment: comments[m.Name], 53 | InputExample: string(v), 54 | ResponseExample: string(ov), 55 | }) 56 | } 57 | 58 | var groupslice []*MethodGroup 59 | for _, g := range groups { 60 | groupslice = append(groupslice, g) 61 | } 62 | 63 | sort.Slice(groupslice, func(i, j int) bool { 64 | return groupslice[i].GroupName < groupslice[j].GroupName 65 | }) 66 | 67 | fmt.Printf("# Groups\n") 68 | 69 | for _, g := range groupslice { 70 | fmt.Printf("* [%s](#%s)\n", g.GroupName, g.GroupName) 71 | for _, method := range g.Methods { 72 | fmt.Printf(" * [%s](#%s)\n", method.Name, method.Name) 73 | } 74 | } 75 | 76 | for _, g := range groupslice { 77 | g := g 78 | fmt.Printf("## %s\n", g.GroupName) 79 | fmt.Printf("%s\n\n", g.Header) 80 | 81 | sort.Slice(g.Methods, func(i, j int) bool { 82 | return g.Methods[i].Name < g.Methods[j].Name 83 | }) 84 | 85 | for _, m := range g.Methods { 86 | fmt.Printf("### %s\n", m.Name) 87 | fmt.Printf("%s\n\n", m.Comment) 88 | 89 | var meth reflect.StructField 90 | var ok bool 91 | for _, ps := range permStruct { 92 | meth, ok = ps.FieldByName(m.Name) 93 | if ok { 94 | break 95 | } 96 | } 97 | if !ok { 98 | panic("no perms for method: " + m.Name) 99 | } 100 | 101 | perms := meth.Tag.Get("perm") 102 | 103 | fmt.Printf("Perms: %s\n\n", perms) 104 | 105 | if strings.Count(m.InputExample, "\n") > 0 { 106 | fmt.Printf("Inputs:\n```json\n%s\n```\n\n", m.InputExample) 107 | } else { 108 | fmt.Printf("Inputs: `%s`\n\n", m.InputExample) 109 | } 110 | 111 | if strings.Count(m.ResponseExample, "\n") > 0 { 112 | fmt.Printf("Response:\n```json\n%s\n```\n\n", m.ResponseExample) 113 | } else { 114 | fmt.Printf("Response: `%s`\n\n", m.ResponseExample) 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /build/clock.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | import "github.com/raulk/clock" 4 | 5 | // Clock is the global clock for the system. In standard builds, 6 | // we use a real-time clock, which maps to the `time` package. 7 | // 8 | // Tests that need control of time can replace this variable with 9 | // clock.NewMock(). Always use real time for socket/stream deadlines. 10 | var Clock = clock.New() 11 | -------------------------------------------------------------------------------- /build/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package build 5 | 6 | // in order to gen check: ci 7 | import ( 8 | _ "github.com/golang/mock/mockgen" 9 | _ "golang.org/x/tools/cmd/stringer" 10 | ) 11 | -------------------------------------------------------------------------------- /build/version.go: -------------------------------------------------------------------------------- 1 | package build 2 | 3 | var CurrentCommit string 4 | 5 | // BuildVersion is the local build version, set by build system 6 | const BuildVersion = "1.18.0" 7 | const Version = "1180" 8 | 9 | func UserVersion() string { 10 | return BuildVersion + CurrentCommit 11 | } 12 | -------------------------------------------------------------------------------- /cli/cmd.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | logging "github.com/ipfs/go-log/v2" 12 | "github.com/mitchellh/go-homedir" 13 | "github.com/urfave/cli/v2" 14 | 15 | "github.com/filecoin-project/go-jsonrpc" 16 | 17 | "github.com/ipfs-force-community/sophon-miner/api" 18 | "github.com/ipfs-force-community/sophon-miner/api/client" 19 | "github.com/ipfs-force-community/sophon-miner/node/config" 20 | "github.com/ipfs-force-community/sophon-miner/node/repo" 21 | 22 | v1 "github.com/filecoin-project/venus/venus-shared/api/chain/v1" 23 | ) 24 | 25 | var log = logging.Logger("cli") 26 | 27 | const ( 28 | metadataTraceContext = "traceContext" 29 | ) 30 | 31 | func GetAPIInfo(ctx *cli.Context) (config.APIInfo, error) { 32 | p, err := homedir.Expand(ctx.String("repo")) 33 | if err != nil { 34 | return config.APIInfo{}, fmt.Errorf("could not expand home dir: %w", err) 35 | } 36 | 37 | r, err := repo.NewFS(p) 38 | if err != nil { 39 | return config.APIInfo{}, fmt.Errorf("could not open repo at path: %s; %w", p, err) 40 | } 41 | 42 | // todo: rm compatibility for repo when appropriate 43 | exist, _ := r.Exists() 44 | if !exist { 45 | r, err = repo.NewFS("~/.venusminer") 46 | if err != nil { 47 | return config.APIInfo{}, err 48 | } 49 | } 50 | 51 | ma, err := r.APIEndpoint() 52 | if err != nil { 53 | return config.APIInfo{}, fmt.Errorf("could not get api endpoint: %w", err) 54 | } 55 | 56 | token, err := r.APIToken() 57 | if err != nil { 58 | log.Warnf("Couldn't load CLI token, capabilities may be limited: %v", err) 59 | } 60 | 61 | return config.APIInfo{ 62 | Addr: ma.String(), 63 | Token: string(token), 64 | }, nil 65 | } 66 | 67 | func GetRawAPI(ctx *cli.Context, version string) (string, http.Header, error) { 68 | ainfo, err := GetAPIInfo(ctx) 69 | if err != nil { 70 | return "", nil, fmt.Errorf("could not get API info: %w", err) 71 | } 72 | 73 | addr, err := ainfo.DialArgs(version) 74 | if err != nil { 75 | return "", nil, fmt.Errorf("could not get DialArgs: %w", err) 76 | } 77 | 78 | return addr, ainfo.AuthHeader(), nil 79 | } 80 | 81 | func GetMinerAPI(ctx *cli.Context) (api.MinerAPI, jsonrpc.ClientCloser, error) { 82 | addr, headers, err := GetRawAPI(ctx, "v0") 83 | if err != nil { 84 | return nil, nil, err 85 | } 86 | 87 | return client.NewMinerRPC(ctx.Context, addr, headers) 88 | } 89 | 90 | func GetFullNodeAPI(ctx *cli.Context, fn *config.APIInfo, version string) (v1.FullNode, jsonrpc.ClientCloser, error) { 91 | addr, err := fn.DialArgs(version) 92 | if err != nil { 93 | return nil, nil, fmt.Errorf("could not get DialArgs: %w", err) 94 | } 95 | 96 | return v1.NewFullNodeRPC(ctx.Context, addr, fn.AuthHeader()) 97 | } 98 | 99 | func DaemonContext(cctx *cli.Context) context.Context { 100 | if mtCtx, ok := cctx.App.Metadata[metadataTraceContext]; ok { 101 | return mtCtx.(context.Context) 102 | } 103 | 104 | return context.Background() 105 | } 106 | 107 | // ReqContext returns context for cli execution. Calling it for the first time 108 | // installs SIGTERM handler that will close returned context. 109 | // Not safe for concurrent execution. 110 | func ReqContext(cctx *cli.Context) context.Context { 111 | tCtx := DaemonContext(cctx) 112 | 113 | ctx, done := context.WithCancel(tCtx) 114 | sigChan := make(chan os.Signal, 2) 115 | go func() { 116 | <-sigChan 117 | done() 118 | }() 119 | signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP) 120 | 121 | return ctx 122 | } 123 | 124 | var CommonCommands = []*cli.Command{ 125 | logCmd, 126 | VersionCmd, 127 | } 128 | -------------------------------------------------------------------------------- /cli/helper.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | 8 | ufcli "github.com/urfave/cli/v2" 9 | ) 10 | 11 | type PrintHelpErr struct { 12 | Err error 13 | Ctx *ufcli.Context 14 | } 15 | 16 | func (e *PrintHelpErr) Error() string { 17 | return e.Err.Error() 18 | } 19 | 20 | func (e *PrintHelpErr) Unwrap() error { 21 | return e.Err 22 | } 23 | 24 | func (e *PrintHelpErr) Is(o error) bool { 25 | _, ok := o.(*PrintHelpErr) 26 | return ok 27 | } 28 | 29 | func RunApp(app *ufcli.App) { 30 | if err := app.Run(os.Args); err != nil { 31 | fmt.Fprintf(os.Stderr, "ERROR: %s\n\n", err) // nolint:errcheck 32 | var phe *PrintHelpErr 33 | if errors.As(err, &phe) { 34 | _ = ufcli.ShowCommandHelp(phe.Ctx, phe.Ctx.Command.Name) 35 | } 36 | os.Exit(1) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cli/log.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | var logCmd = &cli.Command{ 10 | Name: "log", 11 | Usage: "Module log level management", 12 | Subcommands: []*cli.Command{ 13 | logList, 14 | logSetLevel, 15 | }, 16 | } 17 | 18 | var logList = &cli.Command{ 19 | Name: "list", 20 | Usage: "List log systems", 21 | Action: func(cctx *cli.Context) error { 22 | api, closer, err := GetMinerAPI(cctx) 23 | if err != nil { 24 | return err 25 | } 26 | defer closer() 27 | 28 | ctx := ReqContext(cctx) 29 | 30 | systems, err := api.LogList(ctx) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | for _, system := range systems { 36 | fmt.Println(system) 37 | } 38 | 39 | return nil 40 | }, 41 | } 42 | 43 | var logSetLevel = &cli.Command{ 44 | Name: "set-level", 45 | Usage: "Set log level", 46 | ArgsUsage: "[level]", 47 | Description: `Set the log level for logging systems: 48 | 49 | The system flag can be specified multiple times. 50 | 51 | eg) log set-level --system chain --system chainxchg debug 52 | 53 | Available Levels: 54 | debug 55 | info 56 | warn 57 | error 58 | 59 | Environment Variables: 60 | GOLOG_LOG_LEVEL - Default log level for all log systems 61 | GOLOG_LOG_FMT - Change output log format (json, nocolor) 62 | GOLOG_FILE - Write logs to file 63 | GOLOG_OUTPUT - Specify whether to output to file, stderr, stdout or a combination, i.e. file+stderr 64 | `, 65 | Flags: []cli.Flag{ 66 | &cli.StringSliceFlag{ 67 | Name: "system", 68 | Usage: "limit to log system", 69 | Value: &cli.StringSlice{}, 70 | }, 71 | }, 72 | Action: func(cctx *cli.Context) error { 73 | api, closer, err := GetMinerAPI(cctx) 74 | if err != nil { 75 | return err 76 | } 77 | defer closer() 78 | ctx := ReqContext(cctx) 79 | 80 | if !cctx.Args().Present() { 81 | return fmt.Errorf("level is required") 82 | } 83 | 84 | systems := cctx.StringSlice("system") 85 | if len(systems) == 0 { 86 | var err error 87 | systems, err = api.LogList(ctx) 88 | if err != nil { 89 | return err 90 | } 91 | } 92 | 93 | for _, system := range systems { 94 | if err := api.LogSetLevel(ctx, system, cctx.Args().First()); err != nil { 95 | return fmt.Errorf("setting log level on %s: %w", system, err) 96 | } 97 | } 98 | 99 | return nil 100 | }, 101 | } 102 | -------------------------------------------------------------------------------- /cli/version.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | var VersionCmd = &cli.Command{ 10 | Name: "version", 11 | Usage: "Print version", 12 | Action: func(cctx *cli.Context) error { 13 | api, closer, err := GetMinerAPI(cctx) 14 | if err != nil { 15 | return err 16 | } 17 | defer closer() 18 | 19 | ctx := ReqContext(cctx) 20 | v, err := api.Version(ctx) 21 | if err != nil { 22 | return err 23 | } 24 | fmt.Println("Daemon: ", v) 25 | 26 | fmt.Print("Local: ") 27 | cli.VersionPrinter(cctx) 28 | return nil 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /cmd/address.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/urfave/cli/v2" 8 | 9 | "github.com/filecoin-project/go-address" 10 | lcli "github.com/ipfs-force-community/sophon-miner/cli" 11 | ) 12 | 13 | var addressCmd = &cli.Command{ 14 | Name: "address", 15 | Usage: "manage the miner address", 16 | Subcommands: []*cli.Command{ 17 | updateCmd, 18 | listCmd, 19 | stateCmd, 20 | startMiningCmd, 21 | stopMiningCmd, 22 | warmupCmd, 23 | }, 24 | } 25 | 26 | var updateCmd = &cli.Command{ 27 | Name: "update", 28 | Usage: "reacquire address from sophon-auth", 29 | Flags: []cli.Flag{ 30 | &cli.Int64Flag{ 31 | Name: "skip", 32 | Required: false, 33 | }, 34 | &cli.Int64Flag{ 35 | Name: "limit", 36 | Required: false, 37 | }, 38 | }, 39 | Action: func(cctx *cli.Context) error { 40 | postApi, closer, err := lcli.GetMinerAPI(cctx) 41 | if err != nil { 42 | return err 43 | } 44 | defer closer() 45 | 46 | skip := cctx.Int64("skip") 47 | limit := cctx.Int64("limit") 48 | 49 | miners, err := postApi.UpdateAddress(cctx.Context, skip, limit) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | formatJson, err := json.MarshalIndent(miners, "", "\t") 55 | if err != nil { 56 | return err 57 | } 58 | fmt.Println(string(formatJson)) 59 | 60 | return nil 61 | }, 62 | } 63 | 64 | var listCmd = &cli.Command{ 65 | Name: "list", 66 | Usage: "print miners", 67 | Flags: []cli.Flag{}, 68 | Action: func(cctx *cli.Context) error { 69 | postApi, closer, err := lcli.GetMinerAPI(cctx) 70 | if err != nil { 71 | return err 72 | } 73 | defer closer() 74 | 75 | miners, err := postApi.ListAddress(cctx.Context) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | formatJson, err := json.MarshalIndent(miners, "", "\t") 81 | if err != nil { 82 | return err 83 | } 84 | fmt.Println(string(formatJson)) 85 | return nil 86 | 87 | }, 88 | } 89 | 90 | var stateCmd = &cli.Command{ 91 | Name: "state", 92 | Usage: "print state of mining", 93 | Flags: []cli.Flag{}, 94 | ArgsUsage: "[address ...]", 95 | Action: func(cctx *cli.Context) error { 96 | postApi, closer, err := lcli.GetMinerAPI(cctx) 97 | if err != nil { 98 | return err 99 | } 100 | defer closer() 101 | 102 | var addrs []address.Address 103 | for i, s := range cctx.Args().Slice() { 104 | minerAddr, err := address.NewFromString(s) 105 | if err != nil { 106 | return fmt.Errorf("parsing %d-th miner: %w", i, err) 107 | } 108 | 109 | addrs = append(addrs, minerAddr) 110 | } 111 | 112 | states, err := postApi.StatesForMining(cctx.Context, addrs) 113 | if err != nil { 114 | return err 115 | } 116 | 117 | formatJson, err := json.MarshalIndent(states, "", "\t") 118 | if err != nil { 119 | return err 120 | } 121 | fmt.Println(string(formatJson)) 122 | return nil 123 | 124 | }, 125 | } 126 | 127 | var startMiningCmd = &cli.Command{ 128 | Name: "start", 129 | Usage: "start mining for specified miner, if not specified, it means all", 130 | Flags: []cli.Flag{}, 131 | ArgsUsage: "[address ...]", 132 | Action: func(cctx *cli.Context) error { 133 | postApi, closer, err := lcli.GetMinerAPI(cctx) 134 | if err != nil { 135 | return err 136 | } 137 | defer closer() 138 | ctx := lcli.ReqContext(cctx) 139 | 140 | var addrs []address.Address 141 | for i, s := range cctx.Args().Slice() { 142 | minerAddr, err := address.NewFromString(s) 143 | if err != nil { 144 | return fmt.Errorf("parsing %d-th miner: %w", i, err) 145 | } 146 | 147 | addrs = append(addrs, minerAddr) 148 | } 149 | 150 | err = postApi.Start(ctx, addrs) 151 | if err != nil { 152 | return err 153 | } 154 | 155 | fmt.Println("start mining success.") 156 | return nil 157 | }, 158 | } 159 | 160 | var stopMiningCmd = &cli.Command{ 161 | Name: "stop", 162 | Usage: "stop mining for specified miner, if not specified, it means all", 163 | Flags: []cli.Flag{}, 164 | ArgsUsage: "[address ...]", 165 | Action: func(cctx *cli.Context) error { 166 | postApi, closer, err := lcli.GetMinerAPI(cctx) 167 | if err != nil { 168 | return err 169 | } 170 | defer closer() 171 | ctx := lcli.ReqContext(cctx) 172 | 173 | var addrs []address.Address 174 | for i, s := range cctx.Args().Slice() { 175 | minerAddr, err := address.NewFromString(s) 176 | if err != nil { 177 | return fmt.Errorf("parsing %d-th miner: %w", i, err) 178 | } 179 | 180 | addrs = append(addrs, minerAddr) 181 | } 182 | 183 | err = postApi.Stop(ctx, addrs) 184 | if err != nil { 185 | return err 186 | } 187 | 188 | fmt.Println("stop mining success.") 189 | return nil 190 | }, 191 | } 192 | 193 | var warmupCmd = &cli.Command{ 194 | Name: "warmup", 195 | Usage: "winPoSt warmup for miner", 196 | Flags: []cli.Flag{}, 197 | ArgsUsage: "", 198 | Action: func(cctx *cli.Context) error { 199 | if count := cctx.Args().Len(); count < 1 { 200 | return cli.ShowSubcommandHelp(cctx) 201 | } 202 | 203 | minerApi, closer, err := lcli.GetMinerAPI(cctx) 204 | if err != nil { 205 | return err 206 | } 207 | defer closer() 208 | 209 | minerAddr, err := address.NewFromString(cctx.Args().First()) 210 | if err != nil { 211 | return fmt.Errorf("parsing miner: %w", err) 212 | } 213 | 214 | err = minerApi.WarmupForMiner(cctx.Context, minerAddr) 215 | if err == nil { 216 | fmt.Println("warmup success.") 217 | } else { 218 | fmt.Println("warmup failed: ", err.Error()) 219 | } 220 | 221 | return nil 222 | }, 223 | } 224 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | logging "github.com/ipfs/go-log/v2" 7 | "github.com/urfave/cli/v2" 8 | "go.opencensus.io/trace" 9 | 10 | "github.com/ipfs-force-community/sophon-miner/build" 11 | lcli "github.com/ipfs-force-community/sophon-miner/cli" 12 | "github.com/ipfs-force-community/sophon-miner/lib/logger" 13 | ) 14 | 15 | var log = logging.Logger("main") 16 | 17 | const FlagMinerRepo = "repo" 18 | 19 | func main() { 20 | logger.SetupLogLevels() 21 | 22 | local := []*cli.Command{ 23 | initCmd, 24 | runCmd, 25 | stopCmd, 26 | addressCmd, 27 | winnerCmd, 28 | recordCmd, 29 | } 30 | 31 | ctx, span := trace.StartSpan(context.Background(), "/cli") 32 | defer span.End() 33 | 34 | app := &cli.App{ 35 | Name: "sophon-miner", 36 | Usage: "Filecoin decentralized storage network miner", 37 | Version: build.UserVersion(), 38 | EnableBashCompletion: true, 39 | Flags: []cli.Flag{ 40 | &cli.StringFlag{ 41 | Name: FlagMinerRepo, 42 | EnvVars: []string{ 43 | "SOPHON_MINER_PATH", 44 | "VENUS_MINER_PATH", // TODO Deprecated after V1.13.* 45 | }, 46 | Aliases: []string{"miner-repo"}, 47 | Value: "~/.sophon-miner", 48 | Usage: "Specify miner repo path, env SOPHON_MINER_PATH", 49 | }, 50 | }, 51 | 52 | Commands: append(local, lcli.CommonCommands...), 53 | } 54 | app.Setup() 55 | app.Metadata["traceContext"] = ctx 56 | 57 | lcli.RunApp(app) 58 | } 59 | -------------------------------------------------------------------------------- /cmd/record.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | 8 | "github.com/filecoin-project/go-address" 9 | "github.com/filecoin-project/go-state-types/abi" 10 | lcli "github.com/ipfs-force-community/sophon-miner/cli" 11 | "github.com/ipfs-force-community/sophon-miner/types" 12 | "github.com/urfave/cli/v2" 13 | ) 14 | 15 | var recordCmd = &cli.Command{ 16 | Name: "record", 17 | Usage: "the record about mine block", 18 | Subcommands: []*cli.Command{ 19 | queryCmd, 20 | }, 21 | } 22 | 23 | var queryCmd = &cli.Command{ 24 | Name: "query", 25 | Usage: "query record", 26 | ArgsUsage: " ", 27 | Flags: []cli.Flag{ 28 | &cli.UintFlag{ 29 | Name: "limit", 30 | Usage: "query nums record of limit", 31 | Value: 1, 32 | }, 33 | }, 34 | Action: func(cctx *cli.Context) error { 35 | if cctx.NArg() != 2 { 36 | return cli.ShowSubcommandHelp(cctx) 37 | } 38 | 39 | minerAddr, err := address.NewFromString(cctx.Args().Get(0)) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | epoch, err := strconv.ParseUint(cctx.Args().Get(1), 10, 64) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | minerAPI, closer, err := lcli.GetMinerAPI(cctx) 50 | if err != nil { 51 | return err 52 | } 53 | defer closer() 54 | 55 | res, err := minerAPI.QueryRecord(lcli.ReqContext(cctx), &types.QueryRecordParams{ 56 | Epoch: abi.ChainEpoch(epoch), 57 | Limit: cctx.Uint("limit"), 58 | Miner: minerAddr, 59 | }) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | formatJson, err := json.MarshalIndent(res, "", "\t") 65 | if err != nil { 66 | return err 67 | } 68 | 69 | fmt.Println(string(formatJson)) 70 | return nil 71 | }, 72 | } 73 | -------------------------------------------------------------------------------- /cmd/stop.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "net/http/pprof" 5 | 6 | "github.com/urfave/cli/v2" 7 | 8 | lcli "github.com/ipfs-force-community/sophon-miner/cli" 9 | ) 10 | 11 | var stopCmd = &cli.Command{ 12 | Name: "stop", 13 | Usage: "Stop running sophon-miner daemon", 14 | Flags: []cli.Flag{}, 15 | Action: func(cctx *cli.Context) error { 16 | api, closer, err := lcli.GetMinerAPI(cctx) 17 | if err != nil { 18 | return err 19 | } 20 | defer closer() 21 | 22 | err = api.Shutdown(lcli.ReqContext(cctx)) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | return nil 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /cmd/winner.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/filecoin-project/go-address" 8 | "github.com/filecoin-project/go-state-types/abi" 9 | "github.com/urfave/cli/v2" 10 | 11 | lcli "github.com/ipfs-force-community/sophon-miner/cli" 12 | ) 13 | 14 | var winnerCmd = &cli.Command{ 15 | Name: "winner", 16 | Usage: "block right management", 17 | Subcommands: []*cli.Command{ 18 | countCmd, 19 | }, 20 | } 21 | 22 | var countCmd = &cli.Command{ 23 | Name: "count", 24 | Usage: "Count the block rights of the specified miner", 25 | Flags: []cli.Flag{ 26 | &cli.Int64Flag{ 27 | Name: "epoch-start", 28 | Required: true, 29 | }, 30 | &cli.Int64Flag{ 31 | Name: "epoch-end", 32 | Required: true, 33 | }, 34 | }, 35 | ArgsUsage: "[address ...]", 36 | Action: func(cctx *cli.Context) error { 37 | minerAPI, closer, err := lcli.GetMinerAPI(cctx) 38 | if err != nil { 39 | return err 40 | } 41 | defer closer() 42 | 43 | var addrs []address.Address 44 | for i, s := range cctx.Args().Slice() { 45 | minerAddr, err := address.NewFromString(s) 46 | if err != nil { 47 | return fmt.Errorf("parsing %d-th miner: %w", i, err) 48 | } 49 | 50 | addrs = append(addrs, minerAddr) 51 | } 52 | 53 | epochStart := cctx.Int64("epoch-start") 54 | epochEnd := cctx.Int64("epoch-end") 55 | 56 | winners, err := minerAPI.CountWinners(cctx.Context, addrs, abi.ChainEpoch(epochStart), abi.ChainEpoch(epochEnd)) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | formatJson, err := json.MarshalIndent(winners, "", "\t") 62 | if err != nil { 63 | return err 64 | } 65 | fmt.Println(string(formatJson)) 66 | return nil 67 | 68 | }, 69 | } 70 | -------------------------------------------------------------------------------- /docs/en/api-v0-methods-miner.md: -------------------------------------------------------------------------------- 1 | # Groups 2 | * [](#) 3 | * [Closing](#Closing) 4 | * [Session](#Session) 5 | * [Shutdown](#Shutdown) 6 | * [Start](#Start) 7 | * [Stop](#Stop) 8 | * [Version](#Version) 9 | * [Count](#Count) 10 | * [CountWinners](#CountWinners) 11 | * [List](#List) 12 | * [ListAddress](#ListAddress) 13 | * [ListBlocks](#ListBlocks) 14 | * [Log](#Log) 15 | * [LogList](#LogList) 16 | * [LogSetLevel](#LogSetLevel) 17 | * [Query](#Query) 18 | * [QueryRecord](#QueryRecord) 19 | * [States](#States) 20 | * [StatesForMining](#StatesForMining) 21 | * [Update](#Update) 22 | * [UpdateAddress](#UpdateAddress) 23 | * [Warmup](#Warmup) 24 | * [WarmupForMiner](#WarmupForMiner) 25 | ## 26 | 27 | 28 | ### Closing 29 | 30 | 31 | Perms: read 32 | 33 | Inputs: `null` 34 | 35 | Response: `{}` 36 | 37 | ### Session 38 | 39 | 40 | Perms: read 41 | 42 | Inputs: `null` 43 | 44 | Response: `"07070707-0707-0707-0707-070707070707"` 45 | 46 | ### Shutdown 47 | 48 | 49 | Perms: admin 50 | 51 | Inputs: `null` 52 | 53 | Response: `{}` 54 | 55 | ### Start 56 | 57 | 58 | Perms: admin 59 | 60 | Inputs: 61 | ```json 62 | [ 63 | [ 64 | "t01234" 65 | ] 66 | ] 67 | ``` 68 | 69 | Response: `{}` 70 | 71 | ### Stop 72 | 73 | 74 | Perms: admin 75 | 76 | Inputs: 77 | ```json 78 | [ 79 | [ 80 | "t01234" 81 | ] 82 | ] 83 | ``` 84 | 85 | Response: `{}` 86 | 87 | ### Version 88 | 89 | 90 | Perms: read 91 | 92 | Inputs: `null` 93 | 94 | Response: 95 | ```json 96 | { 97 | "Version": "string value", 98 | "APIVersion": 0 99 | } 100 | ``` 101 | 102 | ## Count 103 | 104 | 105 | ### CountWinners 106 | 107 | 108 | Perms: read 109 | 110 | Inputs: 111 | ```json 112 | [ 113 | [ 114 | "t01234" 115 | ], 116 | 10101, 117 | 10101 118 | ] 119 | ``` 120 | 121 | Response: 122 | ```json 123 | [ 124 | { 125 | "miner": "t01234", 126 | "totalWinCount": 0, 127 | "msg": "string value", 128 | "winEpochList": null 129 | } 130 | ] 131 | ``` 132 | 133 | ## List 134 | 135 | 136 | ### ListAddress 137 | 138 | 139 | Perms: read 140 | 141 | Inputs: `null` 142 | 143 | Response: 144 | ```json 145 | [ 146 | { 147 | "Addr": "t01234", 148 | "Id": "string value", 149 | "Name": "string value", 150 | "OpenMining": false 151 | } 152 | ] 153 | ``` 154 | 155 | ### ListBlocks 156 | 157 | 158 | Perms: read 159 | 160 | Inputs: 161 | ```json 162 | [ 163 | { 164 | "Miners": [ 165 | "t01234" 166 | ], 167 | "Limit": 42, 168 | "Offset": 42 169 | } 170 | ] 171 | ``` 172 | 173 | Response: 174 | ```json 175 | [ 176 | { 177 | "ParentEpoch": 0, 178 | "ParentKey": "", 179 | "Epoch": 9, 180 | "Miner": "string value", 181 | "Cid": "string value", 182 | "WinningAt": "0001-01-01T00:00:00Z", 183 | "MineState": 0, 184 | "Consuming": 9 185 | } 186 | ] 187 | ``` 188 | 189 | ## Log 190 | 191 | 192 | ### LogList 193 | 194 | 195 | Perms: write 196 | 197 | Inputs: `null` 198 | 199 | Response: 200 | ```json 201 | [ 202 | "string value" 203 | ] 204 | ``` 205 | 206 | ### LogSetLevel 207 | 208 | 209 | Perms: write 210 | 211 | Inputs: 212 | ```json 213 | [ 214 | "string value", 215 | "string value" 216 | ] 217 | ``` 218 | 219 | Response: `{}` 220 | 221 | ## Query 222 | 223 | 224 | ### QueryRecord 225 | 226 | 227 | Perms: read 228 | 229 | Inputs: 230 | ```json 231 | [ 232 | { 233 | "Miner": "t01234", 234 | "Epoch": 10101, 235 | "Limit": 42 236 | } 237 | ] 238 | ``` 239 | 240 | Response: 241 | ```json 242 | [ 243 | {} 244 | ] 245 | ``` 246 | 247 | ## States 248 | 249 | 250 | ### StatesForMining 251 | 252 | 253 | Perms: read 254 | 255 | Inputs: 256 | ```json 257 | [ 258 | [ 259 | "t01234" 260 | ] 261 | ] 262 | ``` 263 | 264 | Response: 265 | ```json 266 | [ 267 | { 268 | "Addr": "t01234", 269 | "IsMining": false, 270 | "Err": [ 271 | "string value" 272 | ] 273 | } 274 | ] 275 | ``` 276 | 277 | ## Update 278 | 279 | 280 | ### UpdateAddress 281 | 282 | 283 | Perms: admin 284 | 285 | Inputs: 286 | ```json 287 | [ 288 | 9, 289 | 9 290 | ] 291 | ``` 292 | 293 | Response: 294 | ```json 295 | [ 296 | { 297 | "Addr": "t01234", 298 | "Id": "string value", 299 | "Name": "string value", 300 | "OpenMining": false 301 | } 302 | ] 303 | ``` 304 | 305 | ## Warmup 306 | 307 | 308 | ### WarmupForMiner 309 | 310 | 311 | Perms: write 312 | 313 | Inputs: 314 | ```json 315 | [ 316 | "t01234" 317 | ] 318 | ``` 319 | 320 | Response: `{}` 321 | 322 | -------------------------------------------------------------------------------- /docs/en/config-desc.md: -------------------------------------------------------------------------------- 1 | # Configuration parsing 2 | 3 |    The file is located in `~/.sophon-miner/config.toml` by default, which is generated when the command `sophon-miner init` is executed. Behavior comments starting with `#` in the file. 4 | 5 | 6 | ## Old version 7 | 8 | version number `< v1.7.0` 9 | 10 | ```toml 11 | # venus fullnode API 12 | ListenAPI = "/ip4/127.0.0.1/tcp/3453" 13 | Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiYWRtaW4iLCJwZXJtIjoiYWRtaW4iLCJleHQiOiIifQ.RRSdeQ-c1Ei-8roAj-L-wpOr-y6PssDorbGijMPxjoc" 14 | # deprecated, replaced by `slash filter` 15 | BlockRecord = "cache" 16 | 17 | [API] 18 | ListenAddress = "/ip4/0.0.0.0/tcp/12308/http" 19 | 20 | # venus-gateway API 21 | [Gateway] 22 | ListenAPI = ["/ip4/127.0.0.1/tcp/45132"] 23 | Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiYWRtaW4iLCJwZXJtIjoiYWRtaW4iLCJleHQiOiIifQ.RRSdeQ-c1Ei-8roAj-L-wpOr-y6PssDorbGijMPxjoc" 24 | 25 | [Db] 26 | # deprecated, replaced by ~sophon-auth` 27 | Type = "auth" 28 | # `slash filter` 29 | SFType = "mysql" 30 | [Db.MySQL] 31 | Conn = "root:kuangfengjuexizhan@tcp(192.168.200.2:3308)/sophon-miner-butterfly-200-19?charset=utf8mb4&parseTime=True&loc=Local&timeout=10s" 32 | MaxOpenConn = 100 33 | MaxIdleConn = 10 34 | ConnMaxLifeTime = 60 35 | Debug = false 36 | [Db.Auth] 37 | ListenAPI = "http://127.0.0.1:8989" 38 | Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiYWRtaW4iLCJwZXJtIjoiYWRtaW4iLCJleHQiOiIifQ.RRSdeQ-c1Ei-8roAj-L-wpOr-y6PssDorbGijMPxjoc" 39 | 40 | # Jaeger Tracing 41 | [Tracing] 42 | JaegerTracingEnabled = false 43 | JaegerEndpoint = "localhost:6831" 44 | ProbabilitySampler = 1.0 45 | ServerName = "sophon-miner" 46 | ``` 47 | 48 | ## New version 49 | 50 | version number `>= v1.7.0` 51 | 52 | ```toml 53 | PropagationDelaySecs = 12 54 | MinerOnceTimeout = "15s" 55 | MpoolSelectDelaySecs = 0 56 | 57 | # `sophon-miner` host address and port 58 | [API] 59 | ListenAddress = "/ip4/0.0.0.0/tcp/12308" 60 | 61 | # venus fullnode API 62 | [FullNode] 63 | Addr = "/ip4/127.0.0.1/tcp/3453" 64 | Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiY2hhaW4tc2VydmljZSIsInBlcm0iOiJhZG1pbiIsImV4dCI6IiJ9.DxlsJO-XrrdQLvJdA6wdWJxeYOhZt_kMYMHc7NdfQNw" 65 | 66 | # venus-gateway API 67 | [Gateway] 68 | ListenAPI = ["/ip4/127.0.0.1/tcp/45132"] 69 | Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiY2hhaW4tc2VydmljZSIsInBlcm0iOiJhZG1pbiIsImV4dCI6IiJ9.DxlsJO-XrrdQLvJdA6wdWJxeYOhZt_kMYMHc7NdfQNw" 70 | 71 | # sophon-auth API 72 | [Auth] 73 | Addr = "http://127.0.0.1:8989" 74 | Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiY2hhaW4tc2VydmljZSIsInBlcm0iOiJhZG1pbiIsImV4dCI6IiJ9.DxlsJO-XrrdQLvJdA6wdWJxeYOhZt_kMYMHc7NdfQNw" 75 | 76 | # `slash filter` 77 | [SlashFilter] 78 | Type = "local" # optional: local,mysql 79 | [SlashFilter.MySQL] 80 | Conn = "" 81 | MaxOpenConn = 100 82 | MaxIdleConn = 10 83 | ConnMaxLifeTime = 60 84 | Debug = false 85 | 86 | # Jaeger Tracing 87 | [Tracing] 88 | JaegerTracingEnabled = false 89 | JaegerEndpoint = "localhost:6831" 90 | ProbabilitySampler = 1.0 91 | ServerName = "sophon-miner" 92 | ``` 93 | 94 | ### Metrics Configuration parsing 95 | 96 | A basic configuration example of `Metrics` is as follows: 97 | ```toml 98 | [Metrics] 99 | # Whether to enable metrics statistics, the default is false 100 | Enabled = false 101 | 102 | [Metrics.Exporter] 103 | # Metric exporter type, currently optional: prometheus or graphite, default is prometheus 104 | Type = "prometheus" 105 | 106 | [Metrics.Exporter.Prometheus] 107 | #multiaddr 108 | EndPoint = "/ip4/0.0.0.0/tcp/12310" 109 | # Naming convention: "a_b_c", no "-" 110 | Namespace = "miner" 111 | # Indicator registry type, optional: default (with the environment indicators that the program runs) or define (custom) 112 | RegistryType = "define" 113 | # prometheus service path 114 | Path = "/debug/metrics" 115 | # Reporting period, in seconds (s) 116 | ReportingPeriod = 10 117 | 118 | [Metrics.Exporter.Graphite] 119 | # Naming convention: "a_b_c", no "-" 120 | Namespace = "miner" 121 | # graphite exporter collector service address 122 | Host = "127.0.0.1" 123 | # graphite exporter collector service listening port 124 | Port = 12310 125 | # Reporting period, in seconds (s) 126 | ReportingPeriod = 10 127 | ``` 128 | 129 | If you choose `Metrics.Exporter` as `Prometheus`, you can quickly view metrics through the command line: 130 | 131 | ``` 132 | $ curl http://127.0.0.1: 133 | # HELP miner_getbaseinfo_ms Duration of GetBaseInfo in miner 134 | # TYPE miner_getbaseinfo_ms histogram 135 | miner_getbaseinfo_ms_bucket{miner_id="t010938",le="100"} 50 136 | miner_getbaseinfo_ms_bucket{miner_id="t010938",le="200"} 51 137 | miner_getbaseinfo_ms_bucket{miner_id="t010938",le="400"} 51 138 | miner_getbaseinfo_ms_bucket{miner_id="t010938",le="600"} 51 139 | miner_getbaseinfo_ms_bucket{miner_id="t010938",le="800"} 51 140 | miner_getbaseinfo_ms_bucket{miner_id="t010938",le="1000"} 51 141 | miner_getbaseinfo_ms_bucket{miner_id="t010938",le="2000"} 51 142 | miner_getbaseinfo_ms_bucket{miner_id="t010938",le="20000"} 51 143 | miner_getbaseinfo_ms_bucket{miner_id="t010938",le="+Inf"} 51 144 | miner_getbaseinfo_ms_sum{miner_id="t010938"} 470.23516 145 | ... ... 146 | ``` 147 | > If you encounter the error `curl: (56) Recv failure: Connection reset by peer`, use the native `ip` address as follows: 148 | ``` 149 | $ curl http://: 150 | ``` 151 | -------------------------------------------------------------------------------- /docs/en/jaeger-tracing.md: -------------------------------------------------------------------------------- 1 | # Jaeger Tracing 2 | 3 | `sophon-miner` has tracing built into many of its internals. To view the traces, first download [Jaeger](https://www.jaegertracing.io/download/) (Choose the 'all-in-one' binary). Then run it somewhere, start up the sophon-miner daemon, and open up localhost:16686 in your browser. 4 | 5 | ## Open Census 6 | 7 | `sophon-miner` uses [OpenCensus](https://opencensus.io/) for tracing application flow. This generates spans through the execution of annotated code paths. 8 | 9 | Currently it is set up to use Jaeger, though other tracing backends should be fairly easy to swap in. 10 | 11 | ## Running Locally 12 | 13 | To easily run and view tracing locally, first, install jaeger. The easiest way to do this is to [download the binaries](https://www.jaegertracing.io/download/) and then run the `jaeger-all-in-one` binary. This will start up jaeger, listen for spans on `localhost:6831`, and expose a web UI for viewing traces on `http://localhost:16686/`. 14 | 15 | Now, to start sending traces from Venus-Miner to Jaeger, set the environment variable `SOPHON_MINER_JAEGER` to `localhost:6831`, and start the `sophon-miner run`. 16 | 17 | Now, to view any generated traces, open up `http://localhost:16686/` in your browser. 18 | 19 | ## Adding Spans 20 | 21 | To annotate a new codepath with spans, add the following lines to the top of the function you wish to trace: 22 | 23 | ```go 24 | ctx, span := trace.StartSpan(ctx, "put function name here") 25 | defer span.End() 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/en/metrics.md: -------------------------------------------------------------------------------- 1 | ## `sophon-miner` metrics 2 | 3 | For the configuration of enabling indicators and exporters, please refer to[metrics config](./config-desc.md) 4 | 5 | ``` 6 | // Type: Histogram, Tag: minerID, Semantics: Time to get `BaseInfo` (unit: milliseconds) 7 | GetBaseInfoDuration = stats.Float64("getbaseinfo_ms", "Duration of GetBaseInfo in miner", stats.UnitMilliseconds) 8 | // Type: histogram, Tag: minerID, Semantics: time-consuming to calculate `ticket` (unit: milliseconds) 9 | ComputeTicketDuration = stats.Float64("computeticket_ms", "Duration of ComputeTicket in miner", stats.UnitMilliseconds) 10 | // Type: Histogram, Tag: minerID, Semantics: Time to calculate whether to win the block right (unit: milliseconds) 11 | IsRoundWinnerDuration = stats.Float64("isroundwinner_ms", "Duration of IsRoundWinner in miner", stats.UnitMilliseconds) 12 | // Type: Histogram, Tag: minerID, Semantics: Time to calculate winning proof (unit: seconds) 13 | ComputeProofDuration = stats.Float64("computeproof_s", "Duration of ComputeProof in miner", stats.UnitSeconds) 14 | 15 | // Type: Counter, Tag: minerID, Semantics: The number of successful blocks, which means that the local verification 16 | // passed and the broadcast was successful. It was not recognized or excluded due to insufficient base or consensus error. 17 | NumberOfBlock = stats.Int64("number_of_block", "Number of production blocks", stats.UnitDimensionless) 18 | // Type: Counter, Tag: minerID, Semantics: The number of times to obtain the right to produce blocks, 19 | // if the calculation is on the fork chain, the block production will not be recognized 20 | NumberOfIsRoundWinner = stats.Int64("number_of_isroundwinner", "Number of is round winner", stats.UnitDimensionless) 21 | 22 | // Type: Counter, Tag: minerID, Semantics: The number of times that the block failed due to the timeout of calculating the winning proof 23 | NumberOfMiningTimeout = stats.Int64("number_of_mining_timeout", "Number of mining failures due to compute proof timeout", stats.UnitDimensionless) 24 | // Type: Counter, Tag: minerID, Semantics: The number of times the block was abandoned due to the chain fork, 25 | // the block generation on the forked chain is meaningless, so the subsequent logic will be abandoned 26 | // if the chain fork is verified before the block is generated 27 | NumberOfMiningChainFork = stats.Int64("number_of_mining_chain_fork", "Number of mining failures due to chain fork", stats.UnitDimensionless) 28 | // Type: Counter, Tag: minerID, Semantics: Number of block errors due to other errors 29 | NumberOfMiningError = stats.Int64("number_of_mining_error", "Number of mining failures due to error", stats.UnitDimensionless) 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/zh/config-desc.md: -------------------------------------------------------------------------------- 1 | # 配置文件解析 2 | 3 | `sophon-miner` 的配置文件默认位于 `~/.sophon-miner/config.toml`,执行命令 `sophon-miner init` 时生成。文件中以 `#` 开头的行为注释。 4 | 5 | ## 新版本 6 | 7 | 版本号 `>= v1.7.0` 的版本. 8 | 9 | ```toml 10 | # 等待base延迟的秒数 11 | PropagationDelaySecs = 12 12 | # 计算出块证明等待超时 13 | MinerOnceTimeout = "15s" 14 | # 选择消息API的超时(单位:秒),0-不启用 15 | MpoolSelectDelaySecs = 0 16 | 17 | # `sophon-miner` 服务监听地址 18 | [API] 19 | ListenAddress = "/ip4/127.0.0.1/tcp/12308" 20 | 21 | [FullNode] 22 | Addr = "/ip4/127.0.0.1/tcp/3453" 23 | # [Gateway],[Auth],[FullNode] 是同一个token 24 | Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiZWx2aW4iLCJwZXJtIjoiYWRtaW4iLCJleHQiOiIifQ.y2vEhxUd2Q9N6kYKe1nsKA4Xelam_ZIVRw7Ul_LgNkk" 25 | 26 | [Gateway] 27 | ListenAPI = ["/ip4/0.0.0.0/tcp/4569"] 28 | # [Gateway],[Auth],[FullNode] 是同一个token 29 | Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiZWx2aW4iLCJwZXJtIjoiYWRtaW4iLCJleHQiOiIifQ.y2vEhxUd2Q9N6kYKe1nsKA4Xelam_ZIVRw7Ul_LgNkk" 30 | 31 | [Auth] 32 | Addr = "127.0.0.1:8989" 33 | # [Gateway],[Auth],[FullNode] 是同一个token 34 | Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiZWx2aW4iLCJwZXJtIjoiYWRtaW4iLCJleHQiOiIifQ.y2vEhxUd2Q9N6kYKe1nsKA4Xelam_ZIVRw7Ul_LgNkk" 35 | 36 | [SlashFilter] 37 | Type = "local" 38 | [SlashFilter.MySQL] 39 | Conn = "" 40 | MaxOpenConn = 100 41 | MaxIdleConn = 10 42 | ConnMaxLifeTime = "1m0s" 43 | Debug = false 44 | 45 | [Tracing] 46 | JaegerTracingEnabled = false 47 | ProbabilitySampler = 1.0 48 | JaegerEndpoint = "localhost:6831" 49 | ServerName = "" 50 | 51 | [Metrics] 52 | # 是否开启metrics指标统计,默认为false 53 | Enabled = false 54 | 55 | [Metrics.Exporter] 56 | # 指标导出器类型,目前可选:prometheus或graphite,默认为prometheus 57 | Type = "prometheus" 58 | 59 | [Metrics.Exporter.Prometheus] 60 | # multiaddr 61 | EndPoint = "/ip4/0.0.0.0/tcp/12310" 62 | # 命名规范: "a_b_c", 不能带"-" 63 | Namespace = "miner" 64 | # 指标注册表类型,可选:default(默认,会附带程序运行的环境指标)或 define(自定义) 65 | RegistryType = "define" 66 | # prometheus 服务路径 67 | Path = "/debug/metrics" 68 | # 上报周期,单位为 秒(s) 69 | ReportingPeriod = 10 70 | 71 | [Metrics.Exporter.Graphite] 72 | # 命名规范: "a_b_c", 不能带"-" 73 | Namespace = "miner" 74 | # graphite exporter 收集器服务地址 75 | Host = "127.0.0.1" 76 | # graphite exporter 收集器服务监听端口 77 | Port = 12310 78 | # 上报周期,单位为 秒(s) 79 | ReportingPeriod = 10 80 | ``` 81 | 82 | 如果选择 `Metrics.Exporter` 为 `Prometheus`, 可通过命令行快速查看指标: 83 | 84 | ```bash 85 | $ curl http://127.0.0.1:12310/debug/metrics 86 | # HELP miner_getbaseinfo_ms Duration of GetBaseInfo in miner 87 | # TYPE miner_getbaseinfo_ms histogram 88 | miner_getbaseinfo_ms_bucket{miner_id="t010938",le="100"} 50 89 | miner_getbaseinfo_ms_bucket{miner_id="t010938",le="200"} 51 90 | miner_getbaseinfo_ms_bucket{miner_id="t010938",le="400"} 51 91 | miner_getbaseinfo_ms_bucket{miner_id="t010938",le="600"} 51 92 | miner_getbaseinfo_ms_bucket{miner_id="t010938",le="800"} 51 93 | miner_getbaseinfo_ms_bucket{miner_id="t010938",le="1000"} 51 94 | miner_getbaseinfo_ms_bucket{miner_id="t010938",le="2000"} 51 95 | miner_getbaseinfo_ms_bucket{miner_id="t010938",le="20000"} 51 96 | miner_getbaseinfo_ms_bucket{miner_id="t010938",le="+Inf"} 51 97 | miner_getbaseinfo_ms_sum{miner_id="t010938"} 470.23516 98 | ... ... 99 | ``` 100 | > 如果遇到错误 `curl: (56) Recv failure: Connection reset by peer`, 请使用本机 `ip` 地址, 如下所示: 101 | ```bash 102 | $ curl http://:12310/debug/metrics 103 | ``` 104 | 105 | ### `SubmitNodes` 配置项解析 106 | 107 | 当选择消息的节点和广播区块的节点 `mpool` 中的消息不一致时, 有可能导致新出的区块无法验证,如区块中打包的消息在广播节点的 `mpool` 中不存在, 故引入`SubmitNodes`配置项, 保证区块正常上链. 108 | 109 | 默认的配置是空的,如下: 110 | ```toml 111 | SubmitNodes = [] 112 | ``` 113 | 114 | 当使用了 `chain-co` 组建时,需要将对接的所有同步节点配置到 `SubmitNodes`, 参考配置如下: 115 | 116 | ```toml 117 | [[SubmitNodes]] 118 | Addr = "/192.168.200.108/tcp/3453" 119 | Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiY2hhaW4tc2VydmljZSIsInBlcm0iOiJhZG1pbiIsImV4dCI6IiJ9.DxlsJO-XrrdQLvJdA6wdWJxeYOhZt_kMYMHc7NdfQNw" 120 | 121 | [[SubmitNodes]] 122 | Addr = "/ip4/192.168.200.107/tcp/3453" 123 | Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiY2hhaW4tc2VydmljZSIsInBlcm0iOiJhZG1pbiIsImV4dCI6IiJ9.DxlsJO-XrrdQLvJdA6wdWJxeYOhZt_kMYMHc7NdfQNw" 124 | ``` 125 | 126 | ## 旧版本 127 | 128 | 旧版本指的是版本号 `< v1.7.0` 的版本 129 | 130 | ```toml 131 | # 链服务监听地址 132 | ListenAPI = "/ip4/127.0.0.1/tcp/3453" 133 | Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiYWRtaW4iLCJwZXJtIjoiYWRtaW4iLCJleHQiOiIifQ.RRSdeQ-c1Ei-8roAj-L-wpOr-y6PssDorbGijMPxjoc" 134 | # 生产的区块记录方式,已废弃,由 `slash filter` 取代 135 | BlockRecord = "cache" 136 | 137 | # `sophon-miner` 服务监听地址 138 | [API] 139 | ListenAddress = "/ip4/0.0.0.0/tcp/12308/http" 140 | RemoteListenAddress = "" 141 | Timeout = "30s" 142 | 143 | # 事件网关服务监听地址 144 | [Gateway] 145 | ListenAPI = ["/ip4/127.0.0.1/tcp/45132"] 146 | Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiYWRtaW4iLCJwZXJtIjoiYWRtaW4iLCJleHQiOiIifQ.RRSdeQ-c1Ei-8roAj-L-wpOr-y6PssDorbGijMPxjoc" 147 | 148 | # 数据库信息 149 | [Db] 150 | # 矿工管理方式,已废弃,从 `sophon-auth` 获取 151 | Type = "auth" 152 | # `slash filter` 模块区块存取方式 153 | SFType = "mysql" 154 | [Db.MySQL] 155 | Conn = "root:kuangfengjuexizhan@tcp(192.168.200.2:3308)/sophon-miner-butterfly-200-19?charset=utf8mb4&parseTime=True&loc=Local&timeout=10s" 156 | MaxOpenConn = 100 157 | MaxIdleConn = 10 158 | ConnMaxLifeTime = 60 159 | Debug = false 160 | [Db.Auth] 161 | ListenAPI = "http://127.0.0.1:8989" 162 | Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiYWRtaW4iLCJwZXJtIjoiYWRtaW4iLCJleHQiOiIifQ.RRSdeQ-c1Ei-8roAj-L-wpOr-y6PssDorbGijMPxjoc" 163 | 164 | # Jaeger Tracing 服务信息,默认不启用 165 | [Tracing] 166 | JaegerTracingEnabled = false 167 | JaegerEndpoint = "localhost:6831" 168 | ProbabilitySampler = 1.0 169 | ServerName = "sophon-miner" 170 | ``` 171 | 172 | -------------------------------------------------------------------------------- /docs/zh/design-spec.md: -------------------------------------------------------------------------------- 1 | # `sophon-miner` 2 | 3 | ## 简介 4 | 5 |   `sophon-miner` 是 `Venus` 矿池中的链服务组件之一,是矿工出块的调度器。与 `PL` 实现的 `lotus-miner` 不同的是: 6 | 7 | - 支持多个矿工出块,`lotus-miner` 的一个实例负责单个矿工的出块; 8 | 9 | - 与 `Venus` 矿池中其他组件配合完成矿工的出块流程: 10 | - 调用 `venus-wallet` 进行签名及签名验证; 11 | - 调用 `venus-sealer`(将弃用) 或 `venus-cluster` 计算获胜证明。 12 | 13 | - 多个矿工合作出块,保证收益最大化。如:一个周期有多个出块时,打包不重复的消息,获得更多的小费,也可以保证消息及时上链,提升网络的TPS。 14 | 15 | 16 | ## 功能模块 17 | 18 |   `sophon-miner` 的主要模块有:矿工管理,出块管理。 19 | 20 | ### 矿工管理 21 | 22 | `Venus` 矿池中的用户(或称为矿工)是由 `sophon-auth` 组件管理的,其记录了每个矿工的基础信息及身份验证信息。`sophon-miner` 从 `sophon-auth` 拉取最新的矿工列表,并周期性地进行区块生产流程。 23 | 24 | `sophon-miner` 可以暂停矿工列表中任意矿工的出块,比如某个矿工的签名节点失联时,可以手动暂停该矿工的出块流程,等待签名正常后再开启出块。 25 | 26 | `sophon-miner` 执行 `update` 更新矿工列表,通常在某些矿工退出矿池或有新的矿工加入矿池时进行。 27 | 28 | ### 出块管理 29 | 30 | `sophon-miner` 的一轮出块流程如下: 31 | 32 | - 请求同步节点获取 `Base`,即 `parent Tipset`(通常是最近一次有出块周期的 `Block`集)及空轮数(空轮表示该周期没有任何矿工出块); 33 | 34 | - 统计本周期获得出块权的矿工及出块必要数据,如随机数,选中扇区信息等; 35 | 36 | - 为每个获得出块权的矿工计算获胜证明,选择消息,创建区块; 37 | 38 | - 验证本周期生产的区块合法性(是否存在共识错误,因为广播具有共识错误的区块会受到一定的 `Fil` 惩罚),广播区块。 39 | -------------------------------------------------------------------------------- /docs/zh/getting-start.md: -------------------------------------------------------------------------------- 1 | ## `sophon-miner` 快速启动 2 | 3 | ## 实例初始化 4 | 5 | ### 初始化 `Repo` 6 | 7 | **注意⚠️** 8 | 1. `init` 命令会连接 `--api`flag 指定的链节点,所以必须先启动链节点,才能初始化成功。 9 | 10 | `< v1.7.0` 版本: 11 | ```shell script 12 | $ ./sophon-miner init 13 | # For nettype, choose from mainnet, debug, 2k, calibnet 14 | --nettype=calibnet \ 15 | --auth-api= \ 16 | --token= \ 17 | --gateway-api=/ip4//tcp/PORT \ 18 | --api=/ip4//tcp/PORT \ 19 | --slash-filter local 20 | ``` 21 | 22 | `>= v1.7.0` 版本: 23 | ```shell script 24 | $ ./sophon-miner init 25 | --api=/ip4//tcp/PORT \ 26 | --gateway-api=/ip4//tcp/PORT \ 27 | --auth-api \ 28 | --token \ 29 | --slash-filter local 30 | ``` 31 | 32 | ### 启动 `sophon-miner` 33 | 34 | ```shell script 35 | $ nohup ./sophon-miner run > miner.log 2>&1 & 36 | ``` 37 | 38 | ## 矿工管理 39 | 40 | `sophon-miner` 启动时会从 `sophon-auth` 中拉取矿工列表,然后预执行一遍出块流程,如果失败可用 `state` 命令查看原因: 41 | 42 | ```shell script 43 | $ ./sophon-miner address state 44 | [ 45 | { 46 | "Addr": "", 47 | "IsMining": true, 48 | "Err": null 49 | } 50 | ] 51 | ``` 52 | 53 | 查看当前的矿工列表: 54 | 55 | ```shell script 56 | $ ./sophon-miner address list 57 | ``` 58 | 59 | 60 | `start` 或 `stop` 命令可以开始或暂停某个矿工的的出块流程,如暂停 `f01008` 的出块流程: 61 | 62 | ```shell script 63 | $ ./sophon-miner address stop f01008 64 | ``` 65 | > 注意:暂停后即使矿工有算力也不会执行出块,故在故障修复后需执行 `start` 开启出块流程。 66 | 67 | 68 | 有矿工加入或退出矿池,或矿工信息有变化时,需要重新从`venus_auth`拉取矿工列表: 69 | 70 | ```shell script 71 | $ ./sophon-miner address update 72 | ``` 73 | 74 | ## 出块权统计 75 | 76 | 统计矿工在指定链高度区间内获得的出块权: 77 | 78 | ```shell script 79 | ./sophon-miner winner count --epoch-start= --epoch-end= 80 | ``` 81 | 82 | > 这个是统计历史出块权的,不是预测未来的,所以: 1. `epoch-end` > `epoch-start`; 2. `epoch-end` 须小于当前链高度。 83 | -------------------------------------------------------------------------------- /docs/zh/journal-event.md: -------------------------------------------------------------------------------- 1 | # Journal 2 | 3 | `Journal` 通过注册事件的方式,记录系统中特定的状态,将其输出到指定存储系统中,目前仅实现了文件系统。 4 | 5 | `Journal` 主要功能有两个: 6 | 7 | - `Journal Event`:注册事件,跟踪事件; 8 | - `Alerting`:依赖于`Journal Event` 的报警机制。 9 | 10 | 通过这两个功能,有助于辅助运维工作,跟踪系统的运行状况。 11 | 12 | 13 | ## Journal Event 14 | 15 | 通过事件方式记录需要的系统状态。可以是文件系统,也可以是其他存储系统,仅需实现接口: 16 | 17 | ```go 18 | type EventTypeRegistry interface { 19 | 20 | // RegisterEventType introduces a new event type to a journal, and 21 | // returns an EventType token that components can later use to check whether 22 | // journalling for that type is enabled/suppressed, and to tag journal 23 | // entries appropriately. 24 | RegisterEventType(system, event string) EventType 25 | } 26 | 27 | type Journal interface { 28 | EventTypeRegistry 29 | 30 | // RecordEvent records this event to the journal, if and only if the 31 | // EventType is enabled. If so, it calls the supplier function to obtain 32 | // the payload to record. 33 | // 34 | // Implementations MUST recover from panics raised by the supplier function. 35 | RecordEvent(evtType EventType, supplier func() interface{}) 36 | 37 | // Close closes this journal for further writing. 38 | Close() error 39 | } 40 | ``` 41 | 42 | 在 `sophon-miner` 系统中注册了以下事件: `System: "miner", Event: "block_mined"`,用于记录矿工的出块。在日志中以 `json` 方式保存: 43 | ```json 44 | {"System":"miner","Event":"block_mined","Timestamp":"2022-02-08T09:44:45.49744285+08:00","Data":{"cid":{"/":"bafy2bzacebur5zuktumidwj7hm6tv42f2cy6khwtk4zjz4434b5hfrdq2xcns"},"epoch":91552,"miner":"t01031","nulls":0,"parents":[{"/":"bafy2bzacect4kjtcml4uvgvmqtmbrdvh34zb73b5pqr7udjtzd5pnpa3lq5g4"}],"timestamp":1644284700}} 45 | ``` 46 | 47 | ## Alerting 48 | 49 | `Alerting` 封装了 `Journal` 用于跟踪事件,通过 `AddAlertType`注册报警类型,调用`Raise` 及 `Resolve` 发起或解决报警。 50 | 51 | ```go 52 | type Alerting struct { 53 | j journal.Journal 54 | 55 | lk sync.Mutex 56 | alerts map[AlertType]Alert 57 | } 58 | ``` 59 | `Alerting` 可用于监控系统资源,辅助运维做出合理的资源分配。 60 | -------------------------------------------------------------------------------- /docs/zh/metrics.md: -------------------------------------------------------------------------------- 1 | ## `sophon-miner` 指标 2 | 3 | 有关开启指标,导出器的配置请参考[metrics config](./config-desc.md) 4 | 5 | ``` 6 | // 类型:直方图,Tag:minerID,语义:获取 `BaseInfo` 耗时(单位:毫秒) 7 | GetBaseInfoDuration = stats.Float64("getbaseinfo_ms", "Duration of GetBaseInfo in miner", stats.UnitMilliseconds) 8 | // 类型:直方图,Tag:minerID,语义:计算 `ticket` 耗时(单位:毫秒) 9 | ComputeTicketDuration = stats.Float64("computeticket_ms", "Duration of ComputeTicket in miner", stats.UnitMilliseconds) 10 | // 类型:直方图,Tag:minerID,语义:计算是否赢得出块权耗时(单位:毫秒) 11 | IsRoundWinnerDuration = stats.Float64("isroundwinner_ms", "Duration of IsRoundWinner in miner", stats.UnitMilliseconds) 12 | // 类型:直方图,Tag:minerID,语义:计算获胜证明耗时(单位:秒) 13 | ComputeProofDuration = stats.Float64("computeproof_s", "Duration of ComputeProof in miner", stats.UnitSeconds) 14 | 15 | // 类型:计数器,Tag:minerID,语义:成功出块次数,指本地验证通过并成功广播,因base不足或共识错误没被承认没排除 16 | NumberOfBlock = stats.Int64("number_of_block", "Number of production blocks", stats.UnitDimensionless) 17 | // 类型:计数器,Tag:minerID,语义:获得出块权的次数,如果计算时处于分叉链上,则出块不会被承认 18 | NumberOfIsRoundWinner = stats.Int64("number_of_isroundwinner", "Number of is round winner", stats.UnitDimensionless) 19 | 20 | // 类型:计数器,Tag:minerID,语义:因计算获胜证明超时导致出块失败的次数 21 | NumberOfMiningTimeout = stats.Int64("number_of_mining_timeout", "Number of mining failures due to compute proof timeout", stats.UnitDimensionless) 22 | // 类型:计数器,Tag:minerID,语义:因链分叉放弃出块的次数,在分叉链上的出块没有意义,故在出块前验证到链分叉则放弃后续逻辑 23 | NumberOfMiningChainFork = stats.Int64("number_of_mining_chain_fork", "Number of mining failures due to chain fork", stats.UnitDimensionless) 24 | // 类型:计数器,Tag:minerID,语义:因其他错误导致出块错误的次数 25 | NumberOfMiningError = stats.Int64("number_of_mining_error", "Number of mining failures due to error", stats.UnitDimensionless) 26 | ``` 27 | 28 | 29 | ### rpc 30 | 31 | ``` 32 | # 调用无效RPC方法的次数 33 | RPCInvalidMethod = stats.Int64("rpc/invalid_method", "Total number of invalid RPC methods called", stats.UnitDimensionless) 34 | # RPC请求失败的次数 35 | RPCRequestError = stats.Int64("rpc/request_error", "Total number of request errors handled", stats.UnitDimensionless) 36 | # RPC响应失败的次数 37 | RPCResponseError = stats.Int64("rpc/response_error", "Total number of responses errors handled", stats.UnitDimensionless) 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/zh/mine-record.md: -------------------------------------------------------------------------------- 1 | ## 出块记录说明 2 | 3 | ### 出块记录是什么? 4 | 5 | 出块记录是 `venus-miner` 在计算出块时,保留在数据库中的,关于计算出块过程中出现的一些关键信息以及时间信息。出块记录一方面,可以用于链服务管理者排查出块过程中存在的问题,另一方面可以为链服务的用户,提供更加详细的出块信息,方便用户进行出块相关方面的分析。 6 | 7 | #### 出块记录的内容 8 | 一个典型的出块记录会包含一下字段: 9 | ```json 10 | { 11 | // 高度以及 miner 当下的基础信息 12 | "epoch": "814088", 13 | "miner": "t01038", 14 | "worker": "t3rjjw3z2rtxsplsbga4u...z4nylkbl4bbo4thh7ja", 15 | "minerPower": "1182013654564864", 16 | "networkPower": "5496119577509888", 17 | 18 | // 出块计算时间是否晚于理想时间节点 19 | "lateStart": "false", 20 | 21 | // 出块计算的父块信息 22 | "baseEpoch": "814087", 23 | "nullRounds": "0", 24 | "baseDelta": "21.004469998s", 25 | "beaconEpoch": "3221959", 26 | 27 | // 是否被允许参加出块选举 28 | "isEligible": "true", 29 | 30 | // 赢票,如果该字段没有出现,说明没有获得出块权,或者出块过程中出现了报错 31 | "winCount": "1", 32 | 33 | // 出块过程中产生的关键信息,例如:miner 没有获得出块权等 34 | "info":"", 35 | 36 | // 出块过程中的报错信息 37 | "error":"", 38 | 39 | // 出块过程中的起始时间戳,以及各阶段耗时 40 | "start": "2023-08-11 18:16:51.013248275 +0800 CST m=+67.262220230", 41 | "getBaseInfo": "8.442663ms", 42 | "computeElectionProof": "33.231212ms", 43 | "computeTicket": "33.637688ms", 44 | "seed": "5.689287ms", 45 | "computePostProof": "4.155131366s", 46 | "selectMessage": "3.640518079s", 47 | "createBlock": "35.231231ms", 48 | "end": "2023-08-11 18:16:58.925227964 +0800 CST m=+75.174199917", 49 | } 50 | ``` 51 | 52 | 值得注意的是,上面各个字段都不是一定会出现的,例如:`error` 字段,如果出块过程中没有报错,那么这个字段就不会出现在出块记录中。如果没有获取到出块权的话,也不会有 `win_count` 字段。 53 | 54 | ### 使用 55 | 56 | 57 | 该功能启用后,`sophon-miner` 会记录七天的出块记录。可以使用 cli 查询,和请求接口查询等方式获取出块记录。 58 | 59 | #### 启用 60 | 61 | mine recorder 默认是关闭的,可以通过配置文件启用。 62 | 63 | ```toml 64 | [Recorder] 65 | # 是否启用 66 | # 默认 false 67 | Enable = true 68 | 69 | # 记录过期时间 70 | # 默认为 7天, 也就是 & * 2880 71 | ExpireEpoch = 0 72 | 73 | # 每次查询的最大记录数 74 | # 默认为 288 75 | MaxRecordPerQuery = 0 76 | ``` 77 | 78 | #### 通过 cli 查询出块记录 79 | 80 | ```sh 81 | sophon-miner record query 82 | NAME: 83 | sophon-miner record query - query record 84 | 85 | USAGE: 86 | sophon-miner record query [command options] 87 | 88 | OPTIONS: 89 | --limit value query nums record of limit (default: 1) 90 | 91 | # 举例: 查询 t01038 在 814000 ~ 814199 的出块记录 92 | sophon-miner record query --limit 200 t01038 814000 93 | ``` 94 | 95 | #### 通过接口查询出块记录 96 | 97 | Path: `/rpc/v0` 98 | 99 | Method: `POST` 100 | 101 | Header: 102 | 103 | `X-VENUS-API-NAMESPACE: miner.MinerAPI`, 104 | 105 | `Content-Type: application/json`, 106 | 107 | `Authorization: Bearer eyJhbGci...OiJIUzI` 108 | 109 | Body: 110 | ```json 111 | { 112 | "method": "Filecoin.QueryRecord", 113 | "params": [ 114 | { 115 | "Miner": "t01038", 116 | "Epoch": 814000, //开始计算的高度 117 | "Limit": 200 //开始高度后的多少高度 118 | } 119 | ], 120 | "id": 0 121 | } 122 | ``` 123 | 124 | Response: 125 | ```json 126 | [ 127 | { 128 | "epoch": "814000", 129 | "miner": "t01038", 130 | "worker": "t3rjjw3z2rtxsplsbga4u...z4nylkbl4bbo4thh7ja", 131 | "minerPower": "1182013654564864", 132 | "networkPower": "5496119577509888", 133 | "lateStart": "false", 134 | "baseEpoch": "814087", 135 | "nullRounds": "0", 136 | "baseDelta": "21.004469998s", 137 | "beaconEpoch": "813999", 138 | "isEligible": "true", 139 | "winCount": "1", 140 | "info":"", 141 | "error":"", 142 | "start": "2023-08-11 18:16:51.013248275 +0800 CST m=+67.262220230", 143 | "getBaseInfo": "8.442663ms", 144 | "computeElectionProof": "33.231212ms", 145 | "computeTicket": "33.637688ms", 146 | "seed": "5.689287ms", 147 | "computePostProof": "4.155131366s", 148 | "selectMessage": "3.640518079s", 149 | "createBlock": "35.231231ms", 150 | "end": "2023-08-11 18:16:58.925227964 +0800 CST m=+75.174199917" 151 | }, 152 | ,,,,, 153 | { 154 | "epoch": "814199", 155 | "miner": "t01038", 156 | "worker": "t3rjjw3z2rtxsplsbga4u...z4nylkbl4bbo4thh7ja", 157 | "minerPower": "1182013654564864", 158 | "networkPower": "5496119577509888", 159 | "lateStart": "false", 160 | "baseEpoch": "814198", 161 | "nullRounds": "0", 162 | "baseDelta": "21.004469998s", 163 | "beaconEpoch": "3221959", 164 | "isEligible": "true", 165 | "winCount": "1", 166 | "info":"", 167 | "error":"", 168 | "start": "2023-08-11 18:16:51.013248275 +0800 CST m=+67.262220230", 169 | "getBaseInfo": "8.442663ms", 170 | "computeElectionProof": "33.231212ms", 171 | "computeTicket": "33.637688ms", 172 | "seed": "5.689287ms", 173 | "computePostProof": "4.155131366s", 174 | "selectMessage": "3.640518079s", 175 | "createBlock": "35.231231ms", 176 | "end": "2023-08-11 18:16:58.925227964 +0800 CST m=+75.174199917" 177 | }, 178 | ] 179 | ``` 180 | 181 | ##### 使用示例 182 | 183 | ```sh 184 | # 使用 curl 命令查询 185 | curl http://gateway:45132/rpc/v0 -v -X POST -H "X-VENUS-API-NAMESPACE: miner.MinerAPI" -H "Content-Type: application/json" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs...HTQgbqUna4" -d '{"method": "Filecoin.QueryRecord", "params": [ { "Miner": "t01002" , "Epoch":27200 , "Limit":200 } 186 | ], "id": 0}' 187 | ``` 188 | -------------------------------------------------------------------------------- /f3participant/multiparticipation.go: -------------------------------------------------------------------------------- 1 | package f3participant 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/filecoin-project/go-address" 9 | v1api "github.com/filecoin-project/venus/venus-shared/api/chain/v1" 10 | "github.com/ipfs-force-community/sophon-miner/node/modules/helpers" 11 | miner_manager "github.com/ipfs-force-community/sophon-miner/node/modules/miner-manager" 12 | "github.com/jpillora/backoff" 13 | ) 14 | 15 | const ( 16 | // maxCheckProgressAttempts defines the maximum number of failed attempts 17 | // before we abandon the current lease and restart the participation process. 18 | // 19 | // The default backoff takes 12 attempts to reach a maximum delay of 1 minute. 20 | // Allowing for 13 failures results in approximately 2 minutes of backoff since 21 | // the lease was granted. Given a lease validity of up to 5 instances, this means 22 | // we would give up on checking the lease during its mid-validity period; 23 | // typically when we would try to renew the participation ticket. Hence, the value 24 | // to 13. 25 | checkProgressMaxAttempts = 13 26 | 27 | // F3LeaseTerm The number of instances the miner will attempt to lease from nodes. 28 | F3LeaseTerm = 5 29 | ) 30 | 31 | type MultiParticipant struct { 32 | participants map[address.Address]*Participant 33 | minerManager miner_manager.MinerManageAPI 34 | 35 | newParticipant func(context.Context, address.Address) *Participant 36 | } 37 | 38 | func NewMultiParticipant(ctx helpers.MetricsCtx, 39 | node v1api.FullNode, 40 | minerManager miner_manager.MinerManageAPI, 41 | ) (*MultiParticipant, error) { 42 | newParticipant := func(ctx context.Context, participant address.Address) *Participant { 43 | return NewParticipant( 44 | ctx, 45 | node, 46 | participant, 47 | &backoff.Backoff{ 48 | Min: 1 * time.Second, 49 | Max: 1 * time.Minute, 50 | Factor: 1.5, 51 | }, 52 | checkProgressMaxAttempts, 53 | F3LeaseTerm, 54 | ) 55 | } 56 | 57 | miners, err := minerManager.List(ctx) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | mp := &MultiParticipant{ 63 | participants: make(map[address.Address]*Participant), 64 | minerManager: minerManager, 65 | newParticipant: newParticipant, 66 | } 67 | 68 | for _, minerInfo := range miners { 69 | if !minerManager.IsOpenMining(ctx, minerInfo.Addr) { 70 | continue 71 | } 72 | p := newParticipant(ctx, minerInfo.Addr) 73 | mp.participants[minerInfo.Addr] = p 74 | } 75 | 76 | return mp, nil 77 | } 78 | 79 | func (mp *MultiParticipant) Start(ctx context.Context) error { 80 | for _, p := range mp.participants { 81 | if err := p.Start(ctx); err != nil { 82 | return err 83 | } 84 | } 85 | go mp.MonitorMiner(ctx) 86 | 87 | return nil 88 | } 89 | 90 | func (mp *MultiParticipant) Stop(ctx context.Context) error { 91 | for _, p := range mp.participants { 92 | if err := p.Stop(ctx); err != nil { 93 | return fmt.Errorf("failed to stop participant %v, err: %v", p, err) 94 | } 95 | } 96 | return nil 97 | } 98 | 99 | func (mp *MultiParticipant) addParticipant(ctx helpers.MetricsCtx, participant address.Address) error { 100 | p, ok := mp.participants[participant] 101 | if ok { 102 | return nil 103 | } 104 | mp.participants[participant] = mp.newParticipant(ctx, participant) 105 | log.Infof("add participate %s", participant) 106 | 107 | return p.Start(ctx) 108 | } 109 | 110 | func (mp *MultiParticipant) removeParticipant(ctx context.Context, participant address.Address) error { 111 | p, ok := mp.participants[participant] 112 | if !ok { 113 | return nil 114 | } 115 | delete(mp.participants, participant) 116 | log.Infof("remove participate %s", participant) 117 | 118 | return p.Stop(ctx) 119 | } 120 | 121 | func (mp *MultiParticipant) MonitorMiner(ctx context.Context) { 122 | ticker := time.NewTicker(10 * time.Minute) 123 | defer ticker.Stop() 124 | 125 | for { 126 | select { 127 | case <-ctx.Done(): 128 | return 129 | case <-ticker.C: 130 | miners, err := mp.minerManager.List(ctx) 131 | if err != nil { 132 | log.Errorf("failed to list miners: %v", err) 133 | continue 134 | } 135 | 136 | for _, minerInfo := range miners { 137 | if !mp.minerManager.IsOpenMining(ctx, minerInfo.Addr) { 138 | if err := mp.removeParticipant(ctx, minerInfo.Addr); err != nil { 139 | log.Errorf("failed to remove participate %s: %v", minerInfo.Addr, err) 140 | } 141 | continue 142 | } 143 | 144 | if _, ok := mp.participants[minerInfo.Addr]; !ok { 145 | if err := mp.addParticipant(ctx, minerInfo.Addr); err != nil { 146 | log.Errorf("failed to add participate %s: %v", minerInfo.Addr, err) 147 | } 148 | } 149 | } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /lib/journal/alerting/alerts.go: -------------------------------------------------------------------------------- 1 | package alerting 2 | 3 | import ( 4 | "encoding/json" 5 | "sort" 6 | "sync" 7 | "time" 8 | 9 | "github.com/ipfs-force-community/sophon-miner/lib/journal" 10 | logging "github.com/ipfs/go-log/v2" 11 | ) 12 | 13 | var log = logging.Logger("alerting") 14 | 15 | // Alerting provides simple stateful alert system. Consumers can register alerts, 16 | // which can be raised and resolved. 17 | // 18 | // When an alert is raised or resolved, a related journal entry is recorded. 19 | type Alerting struct { 20 | j journal.Journal 21 | 22 | lk sync.Mutex 23 | alerts map[AlertType]Alert 24 | } 25 | 26 | // AlertType is a unique alert identifier 27 | type AlertType struct { 28 | System, Subsystem string 29 | } 30 | 31 | // AlertEvent contains information about alert state transition 32 | type AlertEvent struct { 33 | Type string // either 'raised' or 'resolved' 34 | Message json.RawMessage 35 | Time time.Time 36 | } 37 | 38 | type Alert struct { 39 | Type AlertType 40 | Active bool 41 | 42 | LastActive *AlertEvent // NOTE: pointer for nullability, don't mutate the referenced object! 43 | LastResolved *AlertEvent 44 | 45 | journalType journal.EventType 46 | } 47 | 48 | func NewAlertingSystem(j journal.Journal) *Alerting { 49 | return &Alerting{ 50 | j: j, 51 | 52 | alerts: map[AlertType]Alert{}, 53 | } 54 | } 55 | 56 | func (a *Alerting) AddAlertType(system, subsystem string) AlertType { 57 | a.lk.Lock() 58 | defer a.lk.Unlock() 59 | 60 | at := AlertType{ 61 | System: system, 62 | Subsystem: subsystem, 63 | } 64 | 65 | if _, exists := a.alerts[at]; exists { 66 | return at 67 | } 68 | 69 | et := a.j.RegisterEventType(system, subsystem) 70 | 71 | a.alerts[at] = Alert{ 72 | Type: at, 73 | Active: false, 74 | journalType: et, 75 | } 76 | 77 | return at 78 | } 79 | 80 | func (a *Alerting) update(at AlertType, message interface{}, upd func(Alert, json.RawMessage) Alert) { 81 | a.lk.Lock() 82 | defer a.lk.Unlock() 83 | 84 | alert, ok := a.alerts[at] 85 | if !ok { 86 | log.Errorw("unknown alert", "type", at, "message", message) 87 | } 88 | 89 | rawMsg, err := json.Marshal(message) 90 | if err != nil { 91 | log.Errorw("marshaling alert message failed", "type", at, "error", err) 92 | rawMsg, err = json.Marshal(&struct { 93 | AlertError string 94 | }{ 95 | AlertError: err.Error(), 96 | }) 97 | log.Errorw("marshaling marshaling error failed", "type", at, "error", err) 98 | } 99 | 100 | a.alerts[at] = upd(alert, rawMsg) 101 | } 102 | 103 | // Raise marks the alert condition as active and records related event in the journal 104 | func (a *Alerting) Raise(at AlertType, message interface{}) { 105 | log.Errorw("alert raised", "type", at, "message", message) 106 | 107 | a.update(at, message, func(alert Alert, rawMsg json.RawMessage) Alert { 108 | alert.Active = true 109 | alert.LastActive = &AlertEvent{ 110 | Type: "raised", 111 | Message: rawMsg, 112 | Time: time.Now(), 113 | } 114 | 115 | a.j.RecordEvent(alert.journalType, func() interface{} { 116 | return alert.LastActive 117 | }) 118 | 119 | return alert 120 | }) 121 | } 122 | 123 | // Resolve marks the alert condition as resolved and records related event in the journal 124 | func (a *Alerting) Resolve(at AlertType, message interface{}) { 125 | log.Errorw("alert resolved", "type", at, "message", message) 126 | 127 | a.update(at, message, func(alert Alert, rawMsg json.RawMessage) Alert { 128 | alert.Active = false 129 | alert.LastResolved = &AlertEvent{ 130 | Type: "resolved", 131 | Message: rawMsg, 132 | Time: time.Now(), 133 | } 134 | 135 | a.j.RecordEvent(alert.journalType, func() interface{} { 136 | return alert.LastResolved 137 | }) 138 | 139 | return alert 140 | }) 141 | } 142 | 143 | // GetAlerts returns all registered (active and inactive) alerts 144 | func (a *Alerting) GetAlerts() []Alert { 145 | a.lk.Lock() 146 | defer a.lk.Unlock() 147 | 148 | out := make([]Alert, 0, len(a.alerts)) 149 | for _, alert := range a.alerts { 150 | out = append(out, alert) 151 | } 152 | sort.Slice(out, func(i, j int) bool { 153 | if out[i].Type.System != out[j].Type.System { 154 | return out[i].Type.System < out[j].Type.System 155 | } 156 | 157 | return out[i].Type.Subsystem < out[j].Type.Subsystem 158 | }) 159 | 160 | return out 161 | } 162 | -------------------------------------------------------------------------------- /lib/journal/alerting/alerts_test.go: -------------------------------------------------------------------------------- 1 | // stm: #unit 2 | package alerting 3 | 4 | import ( 5 | "encoding/json" 6 | "testing" 7 | 8 | mock "github.com/golang/mock/gomock" 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/ipfs-force-community/sophon-miner/lib/journal" 12 | "github.com/ipfs-force-community/sophon-miner/lib/journal/mockjournal" 13 | ) 14 | 15 | func TestAlerting(t *testing.T) { 16 | mockCtrl := mock.NewController(t) 17 | defer mockCtrl.Finish() 18 | j := mockjournal.NewMockJournal(mockCtrl) 19 | 20 | a := NewAlertingSystem(j) 21 | 22 | j.EXPECT().RegisterEventType("s1", "b1").Return(journal.EventType{System: "s1", Event: "b1"}) 23 | // stm: @VENUSMINER_ALERTING_ADD_ALERT_TYPE_001 24 | al1 := a.AddAlertType("s1", "b1") 25 | 26 | j.EXPECT().RegisterEventType("s2", "b2").Return(journal.EventType{System: "s2", Event: "b2"}) 27 | // stm: @VENUSMINER_ALERTING_ADD_ALERT_TYPE_001 28 | al2 := a.AddAlertType("s2", "b2") 29 | 30 | l := a.GetAlerts() 31 | require.Len(t, l, 2) 32 | require.Equal(t, al1, l[0].Type) 33 | require.Equal(t, al2, l[1].Type) 34 | 35 | for _, alert := range l { 36 | require.False(t, alert.Active) 37 | require.Nil(t, alert.LastActive) 38 | require.Nil(t, alert.LastResolved) 39 | } 40 | 41 | j.EXPECT().RecordEvent(a.alerts[al1].journalType, mock.Any()) 42 | a.Raise(al1, "test") 43 | 44 | for _, alert := range l { // check for no magic mutations 45 | require.False(t, alert.Active) 46 | require.Nil(t, alert.LastActive) 47 | require.Nil(t, alert.LastResolved) 48 | } 49 | 50 | // stm: @VENUSMINER_ALERTING_GET_ALERTS_001 51 | l = a.GetAlerts() 52 | require.Len(t, l, 2) 53 | require.Equal(t, al1, l[0].Type) 54 | require.Equal(t, al2, l[1].Type) 55 | 56 | require.True(t, l[0].Active) 57 | require.NotNil(t, l[0].LastActive) 58 | require.Equal(t, "raised", l[0].LastActive.Type) 59 | require.Equal(t, json.RawMessage(`"test"`), l[0].LastActive.Message) 60 | require.Nil(t, l[0].LastResolved) 61 | 62 | require.False(t, l[1].Active) 63 | require.Nil(t, l[1].LastActive) 64 | require.Nil(t, l[1].LastResolved) 65 | 66 | var lastResolved *AlertEvent 67 | 68 | resolveMesage := "test resolve" 69 | j.EXPECT().RecordEvent(a.alerts[al1].journalType, mock.Any()).Do( 70 | func(arg0 interface{}, cb func() interface{}) { 71 | lastResolved = cb().(*AlertEvent) 72 | }) 73 | // stm: @VENUSMINER_ALERTING_RAISE_001 74 | a.Resolve(al1, resolveMesage) 75 | l = a.GetAlerts() 76 | 77 | var resolvedAlert *Alert 78 | for _, alert := range l { 79 | if alert.Type.System == "s1" && alert.Type.Subsystem == "b1" { 80 | resolvedAlert = &alert 81 | break 82 | } 83 | } 84 | require.NotNil(t, resolvedAlert) 85 | require.NotNil(t, resolvedAlert.LastResolved) 86 | require.False(t, resolvedAlert.Active) 87 | require.Equal(t, resolvedAlert.LastResolved, lastResolved) 88 | } 89 | -------------------------------------------------------------------------------- /lib/journal/env.go: -------------------------------------------------------------------------------- 1 | package journal 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | // envJournalDisabledEvents is the environment variable through which disabled 8 | // journal events can be customized. 9 | const envDisabledEvents = "SOPHON_MINER_JOURNAL_DISABLED_EVENTS" 10 | 11 | func EnvDisabledEvents() DisabledEvents { 12 | if env, ok := os.LookupEnv(envDisabledEvents); ok { 13 | if ret, err := ParseDisabledEvents(env); err == nil { 14 | return ret 15 | } 16 | } 17 | // fallback if env variable is not set, or if it failed to parse. 18 | return DefaultDisabledEvents 19 | } 20 | -------------------------------------------------------------------------------- /lib/journal/fsjournal/fs.go: -------------------------------------------------------------------------------- 1 | package fsjournal 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | logging "github.com/ipfs/go-log/v2" 10 | 11 | "github.com/ipfs-force-community/sophon-miner/build" 12 | "github.com/ipfs-force-community/sophon-miner/lib/journal" 13 | "github.com/ipfs-force-community/sophon-miner/node/repo" 14 | ) 15 | 16 | var log = logging.Logger("fsjournal") 17 | 18 | const RFC3339nocolon = "2006-01-02T150405Z0700" 19 | 20 | // fsJournal is a basic journal backed by files on a filesystem. 21 | type fsJournal struct { 22 | journal.EventTypeRegistry 23 | 24 | dir string 25 | sizeLimit int64 26 | 27 | fi *os.File 28 | fSize int64 29 | 30 | incoming chan *journal.Event 31 | 32 | closing chan struct{} 33 | closed chan struct{} 34 | } 35 | 36 | // OpenFSJournal constructs a rolling filesystem journal, with a default 37 | // per-file size limit of 1GiB. 38 | func OpenFSJournal(lr repo.LockedRepo, disabled journal.DisabledEvents) (journal.Journal, error) { 39 | dir := filepath.Join(lr.Path(), "journal") 40 | if err := os.MkdirAll(dir, 0755); err != nil { 41 | return nil, fmt.Errorf("failed to mk directory %s for file journal: %w", dir, err) 42 | } 43 | 44 | f := &fsJournal{ 45 | EventTypeRegistry: journal.NewEventTypeRegistry(disabled), 46 | dir: dir, 47 | sizeLimit: 1 << 30, 48 | incoming: make(chan *journal.Event, 32), 49 | closing: make(chan struct{}), 50 | closed: make(chan struct{}), 51 | } 52 | 53 | if err := f.rollJournalFile(); err != nil { 54 | return nil, err 55 | } 56 | 57 | go f.runLoop() 58 | 59 | return f, nil 60 | } 61 | 62 | func (f *fsJournal) RecordEvent(evtType journal.EventType, supplier func() interface{}) { 63 | defer func() { 64 | if r := recover(); r != nil { 65 | log.Warnf("recovered from panic while recording journal event; type=%s, err=%v", evtType, r) 66 | } 67 | }() 68 | 69 | if !evtType.Enabled() { 70 | return 71 | } 72 | 73 | je := &journal.Event{ 74 | EventType: evtType, 75 | Timestamp: build.Clock.Now(), 76 | Data: supplier(), 77 | } 78 | select { 79 | case f.incoming <- je: 80 | case <-f.closing: 81 | log.Warnw("journal closed but tried to log event", "event", je) 82 | } 83 | } 84 | 85 | func (f *fsJournal) Close() error { 86 | close(f.closing) 87 | <-f.closed 88 | return nil 89 | } 90 | 91 | func (f *fsJournal) putEvent(evt *journal.Event) error { 92 | b, err := json.Marshal(evt) 93 | if err != nil { 94 | return err 95 | } 96 | n, err := f.fi.Write(append(b, '\n')) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | f.fSize += int64(n) 102 | 103 | if f.fSize >= f.sizeLimit { 104 | _ = f.rollJournalFile() 105 | } 106 | 107 | return nil 108 | } 109 | 110 | func (f *fsJournal) rollJournalFile() error { 111 | if f.fi != nil { 112 | _ = f.fi.Close() 113 | } 114 | current := filepath.Join(f.dir, "sophon-miner-journal.ndjson") 115 | rolled := filepath.Join(f.dir, fmt.Sprintf( 116 | "sophon-miner-journal-%s.ndjson", 117 | build.Clock.Now().Format(RFC3339nocolon), 118 | )) 119 | 120 | // check if journal file exists 121 | if fi, err := os.Stat(current); err == nil && !fi.IsDir() { 122 | err := os.Rename(current, rolled) 123 | if err != nil { 124 | return fmt.Errorf("failed to roll journal file: %w", err) 125 | } 126 | } 127 | 128 | nfi, err := os.Create(current) 129 | if err != nil { 130 | return fmt.Errorf("failed to create journal file: %w", err) 131 | } 132 | 133 | f.fi = nfi 134 | f.fSize = 0 135 | 136 | return nil 137 | } 138 | 139 | func (f *fsJournal) runLoop() { 140 | defer close(f.closed) 141 | 142 | for { 143 | select { 144 | case je := <-f.incoming: 145 | if err := f.putEvent(je); err != nil { 146 | log.Errorw("failed to write out journal event", "event", je, "err", err) 147 | } 148 | case <-f.closing: 149 | _ = f.fi.Close() 150 | return 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /lib/journal/fsjournal/fs_test.go: -------------------------------------------------------------------------------- 1 | // stm: #unit 2 | package fsjournal 3 | 4 | import ( 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/ipfs-force-community/sophon-miner/lib/journal" 10 | "github.com/ipfs-force-community/sophon-miner/node/repo" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestFsJournal(t *testing.T) { 15 | tmp := t.TempDir() 16 | repo, err := repo.NewFS(tmp) 17 | require.NoError(t, err) 18 | lr, err := repo.Lock() 19 | require.NoError(t, err) 20 | 21 | dir := filepath.Join(lr.Path(), "journal") 22 | require.NoError(t, os.WriteFile(dir, []byte("file exists\n"), 0644)) 23 | 24 | // stm: @VENUSMINER_JOURNAL_ENV_DISABLED_EVENTS_001 25 | envDisableEvent := journal.EnvDisabledEvents() 26 | require.Zero(t, len(envDisableEvent)) 27 | 28 | { 29 | // 'tmpdir/journal' is a file would cause an error 30 | // stm: @VENUSMINER_FSJOURNAL_OPEN_FS_JOURNAL_002 31 | _, err = OpenFSJournal(lr, envDisableEvent) 32 | require.Error(t, err) 33 | require.NoError(t, os.RemoveAll(dir)) 34 | } 35 | 36 | jlFile := filepath.Join(dir, "sophon-miner-journal.ndjson") 37 | { 38 | // If there is an error on rollJournalFile return nil and error. 39 | // stm: @VENUSMINER_FSJOURNAL_OPEN_FS_JOURNAL_003 40 | require.NoError(t, os.MkdirAll(jlFile, 0755)) 41 | 42 | _, err := OpenFSJournal(lr, envDisableEvent) 43 | require.Error(t, err) 44 | require.NoError(t, os.RemoveAll(dir)) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/journal/mockjournal/journal.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/ipfs-force-community/sophon-miner/lib/journal (interfaces: Journal) 3 | 4 | // Package mockjournal is a generated GoMock package. 5 | package mockjournal 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | journal "github.com/ipfs-force-community/sophon-miner/lib/journal" 12 | ) 13 | 14 | // MockJournal is a mock of Journal interface. 15 | type MockJournal struct { 16 | ctrl *gomock.Controller 17 | recorder *MockJournalMockRecorder 18 | } 19 | 20 | // MockJournalMockRecorder is the mock recorder for MockJournal. 21 | type MockJournalMockRecorder struct { 22 | mock *MockJournal 23 | } 24 | 25 | // NewMockJournal creates a new mock instance. 26 | func NewMockJournal(ctrl *gomock.Controller) *MockJournal { 27 | mock := &MockJournal{ctrl: ctrl} 28 | mock.recorder = &MockJournalMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *MockJournal) EXPECT() *MockJournalMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // Close mocks base method. 38 | func (m *MockJournal) Close() error { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "Close") 41 | ret0, _ := ret[0].(error) 42 | return ret0 43 | } 44 | 45 | // Close indicates an expected call of Close. 46 | func (mr *MockJournalMockRecorder) Close() *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockJournal)(nil).Close)) 49 | } 50 | 51 | // RecordEvent mocks base method. 52 | func (m *MockJournal) RecordEvent(arg0 journal.EventType, arg1 func() interface{}) { 53 | m.ctrl.T.Helper() 54 | m.ctrl.Call(m, "RecordEvent", arg0, arg1) 55 | } 56 | 57 | // RecordEvent indicates an expected call of RecordEvent. 58 | func (mr *MockJournalMockRecorder) RecordEvent(arg0, arg1 interface{}) *gomock.Call { 59 | mr.mock.ctrl.T.Helper() 60 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordEvent", reflect.TypeOf((*MockJournal)(nil).RecordEvent), arg0, arg1) 61 | } 62 | 63 | // RegisterEventType mocks base method. 64 | func (m *MockJournal) RegisterEventType(arg0, arg1 string) journal.EventType { 65 | m.ctrl.T.Helper() 66 | ret := m.ctrl.Call(m, "RegisterEventType", arg0, arg1) 67 | ret0, _ := ret[0].(journal.EventType) 68 | return ret0 69 | } 70 | 71 | // RegisterEventType indicates an expected call of RegisterEventType. 72 | func (mr *MockJournalMockRecorder) RegisterEventType(arg0, arg1 interface{}) *gomock.Call { 73 | mr.mock.ctrl.T.Helper() 74 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterEventType", reflect.TypeOf((*MockJournal)(nil).RegisterEventType), arg0, arg1) 75 | } 76 | -------------------------------------------------------------------------------- /lib/journal/nil.go: -------------------------------------------------------------------------------- 1 | package journal 2 | 3 | type nilJournal struct{} 4 | 5 | // nilj is a singleton nil journal. 6 | var nilj Journal = &nilJournal{} 7 | 8 | func NilJournal() Journal { 9 | return nilj 10 | } 11 | 12 | func (n *nilJournal) RegisterEventType(_, _ string) EventType { return EventType{} } 13 | 14 | func (n *nilJournal) RecordEvent(_ EventType, _ func() interface{}) {} 15 | 16 | func (n *nilJournal) Close() error { return nil } 17 | -------------------------------------------------------------------------------- /lib/journal/registry.go: -------------------------------------------------------------------------------- 1 | package journal 2 | 3 | import "sync" 4 | 5 | // EventTypeRegistry is a component that constructs tracked EventType tokens, 6 | // for usage with a Journal. 7 | type EventTypeRegistry interface { 8 | 9 | // RegisterEventType introduces a new event type to a journal, and 10 | // returns an EventType token that components can later use to check whether 11 | // journalling for that type is enabled/suppressed, and to tag journal 12 | // entries appropriately. 13 | RegisterEventType(system, event string) EventType 14 | } 15 | 16 | // eventTypeRegistry is an embeddable mixin that takes care of tracking disabled 17 | // event types, and returning initialized/safe EventTypes when requested. 18 | type eventTypeRegistry struct { 19 | sync.Mutex 20 | 21 | m map[string]EventType 22 | } 23 | 24 | var _ EventTypeRegistry = (*eventTypeRegistry)(nil) 25 | 26 | func NewEventTypeRegistry(disabled DisabledEvents) EventTypeRegistry { 27 | ret := &eventTypeRegistry{ 28 | m: make(map[string]EventType, len(disabled)+32), // + extra capacity. 29 | } 30 | 31 | for _, et := range disabled { 32 | et.enabled, et.safe = false, true 33 | ret.m[et.System+":"+et.Event] = et 34 | } 35 | 36 | return ret 37 | } 38 | 39 | func (d *eventTypeRegistry) RegisterEventType(system, event string) EventType { 40 | d.Lock() 41 | defer d.Unlock() 42 | 43 | key := system + ":" + event 44 | if et, ok := d.m[key]; ok { 45 | return et 46 | } 47 | 48 | et := EventType{ 49 | System: system, 50 | Event: event, 51 | enabled: true, 52 | safe: true, 53 | } 54 | 55 | d.m[key] = et 56 | return et 57 | } 58 | -------------------------------------------------------------------------------- /lib/journal/registry_test.go: -------------------------------------------------------------------------------- 1 | // stm: #unit 2 | package journal 3 | 4 | import ( 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestDisabledEvents(t *testing.T) { 11 | req := require.New(t) 12 | 13 | // stm: @VENUSMINER_JOURNAL_NEW_EVENT_TYPE_REGISTRY_001, @VENUSMINER_JOURNAL_REGISTER_EVENT_TYPE_001, @VENUSMINER_JOURNAL_ENABLED_001 14 | test := func(dis DisabledEvents) func(*testing.T) { 15 | return func(t *testing.T) { 16 | registry := NewEventTypeRegistry(dis) 17 | 18 | reg1 := registry.RegisterEventType("system1", "disabled1") 19 | reg2 := registry.RegisterEventType("system1", "disabled2") 20 | 21 | req.False(reg1.Enabled()) 22 | req.False(reg2.Enabled()) 23 | req.True(reg1.safe) 24 | req.True(reg2.safe) 25 | 26 | reg3 := registry.RegisterEventType("system3", "enabled3") 27 | req.True(reg3.Enabled()) 28 | req.True(reg3.safe) 29 | } 30 | } 31 | 32 | t.Run("direct", test(DisabledEvents{ 33 | EventType{System: "system1", Event: "disabled1"}, 34 | EventType{System: "system1", Event: "disabled2"}, 35 | })) 36 | 37 | // stm: @VENUSMINER_JOURNAL_PARSE_DISABLED_EVENTS_001 38 | dis, err := ParseDisabledEvents("system1:disabled1,system1:disabled2") 39 | req.NoError(err) 40 | 41 | t.Run("parsed", test(dis)) 42 | 43 | dis, err = ParseDisabledEvents(" system1:disabled1 , system1:disabled2 ") 44 | req.NoError(err) 45 | 46 | t.Run("parsed_spaces", test(dis)) 47 | } 48 | 49 | func TestParseDisableEvents(t *testing.T) { 50 | // stm: @VENUSMINER_JOURNAL_PARSE_DISABLED_EVENTS_001 51 | _, err := ParseDisabledEvents("system1:disabled1:failed,system1:disabled2") 52 | require.Error(t, err) 53 | } 54 | -------------------------------------------------------------------------------- /lib/journal/types.go: -------------------------------------------------------------------------------- 1 | package journal 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | var ( 10 | // DefaultDisabledEvents lists the journal events disabled by 11 | // default, usually because they are considered noisy. 12 | DefaultDisabledEvents = DisabledEvents{} 13 | ) 14 | 15 | // DisabledEvents is the set of event types whose journaling is suppressed. 16 | type DisabledEvents []EventType 17 | 18 | // ParseDisabledEvents parses a string of the form: "system1:event1,system1:event2[,...]" 19 | // into a DisabledEvents object, returning an error if the string failed to parse. 20 | // 21 | // It sanitizes strings via strings.TrimSpace. 22 | func ParseDisabledEvents(s string) (DisabledEvents, error) { 23 | s = strings.TrimSpace(s) // sanitize 24 | evts := strings.Split(s, ",") 25 | ret := make(DisabledEvents, 0, len(evts)) 26 | for _, evt := range evts { 27 | evt = strings.TrimSpace(evt) // sanitize 28 | s := strings.Split(evt, ":") 29 | if len(s) != 2 { 30 | return nil, fmt.Errorf("invalid event type: %s", s) 31 | } 32 | ret = append(ret, EventType{System: s[0], Event: s[1]}) 33 | } 34 | return ret, nil 35 | } 36 | 37 | // EventType represents the signature of an event. 38 | type EventType struct { 39 | System string 40 | Event string 41 | 42 | // enabled stores whether this event type is enabled. 43 | enabled bool 44 | 45 | // safe is a sentinel marker that's set to true if this EventType was 46 | // constructed correctly (via Journal#RegisterEventType). 47 | safe bool 48 | } 49 | 50 | func (et EventType) String() string { 51 | return et.System + ":" + et.Event 52 | } 53 | 54 | // Enabled returns whether this event type is enabled in the journaling 55 | // subsystem. Users are advised to check this before actually attempting to 56 | // add a journal entry, as it helps bypass object construction for events that 57 | // would be discarded anyway. 58 | // 59 | // All event types are enabled by default, and specific event types can only 60 | // be disabled at Journal construction time. 61 | func (et EventType) Enabled() bool { 62 | return et.safe && et.enabled 63 | } 64 | 65 | func NewEventType(sys, evt string, enable, safe bool) EventType { 66 | return EventType{System: sys, Event: evt, enabled: true, safe: true} 67 | } 68 | 69 | //go:generate go run github.com/golang/mock/mockgen -destination=mockjournal/journal.go -package=mockjournal . Journal 70 | 71 | // Journal represents an audit trail of system actions. 72 | // 73 | // Every entry is tagged with a timestamp, a system name, and an event name. 74 | // The supplied data can be any type, as long as it is JSON serializable, 75 | // including structs, map[string]interface{}, or primitive types. 76 | // 77 | // For cleanliness and type safety, we recommend to use typed events. See the 78 | // *Evt struct types in this package for more info. 79 | type Journal interface { 80 | EventTypeRegistry 81 | 82 | // RecordEvent records this event to the journal, if and only if the 83 | // EventType is enabled. If so, it calls the supplier function to obtain 84 | // the payload to record. 85 | // 86 | // Implementations MUST recover from panics raised by the supplier function. 87 | RecordEvent(evtType EventType, supplier func() interface{}) 88 | 89 | // Close closes this journal for further writing. 90 | Close() error 91 | } 92 | 93 | // Event represents a journal entry. 94 | // 95 | // See godocs on Journal for more information. 96 | type Event struct { 97 | EventType 98 | 99 | Timestamp time.Time 100 | Data interface{} 101 | } 102 | -------------------------------------------------------------------------------- /lib/logger/levels.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "os" 5 | 6 | logging "github.com/ipfs/go-log/v2" 7 | ) 8 | 9 | func SetupLogLevels() { 10 | if _, set := os.LookupEnv("GOLOG_LOG_LEVEL"); !set { 11 | _ = logging.SetLogLevel("*", "INFO") 12 | _ = logging.SetLogLevel("miner", "DEBUG") 13 | } 14 | // Always mute RtRefreshManager because it breaks terminals 15 | _ = logging.SetLogLevel("dht/RtRefreshManager", "FATAL") 16 | } 17 | -------------------------------------------------------------------------------- /lib/metrics/expoter.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "go.opencensus.io/stats/view" 8 | 9 | "github.com/ipfs-force-community/metrics" 10 | ) 11 | 12 | func SetupMetrics(ctx context.Context, metricsConfig *metrics.MetricsConfig) error { 13 | if err := view.Register( 14 | MinerNodeViews..., 15 | ); err != nil { 16 | return fmt.Errorf("cannot register the view: %w", err) 17 | } 18 | 19 | return metrics.SetupMetrics(ctx, metricsConfig) 20 | } 21 | -------------------------------------------------------------------------------- /lib/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | rpcMetrics "github.com/filecoin-project/go-jsonrpc/metrics" 8 | "github.com/ipfs-force-community/metrics" 9 | "go.opencensus.io/stats" 10 | "go.opencensus.io/stats/view" 11 | "go.opencensus.io/tag" 12 | ) 13 | 14 | // Global Tags 15 | var ( 16 | MinerID, _ = tag.NewKey("miner_id") 17 | ) 18 | 19 | var ( 20 | NumberOfBlock = stats.Int64("number_of_block", "Number of production blocks", stats.UnitDimensionless) 21 | NumberOfIsRoundWinner = stats.Int64("number_of_isroundwinner", "Number of is round winner", stats.UnitDimensionless) 22 | 23 | NumberOfMiningTimeout = stats.Int64("number_of_mining_timeout", "Number of mining failures due to compute proof timeout", stats.UnitDimensionless) 24 | NumberOfMiningChainFork = stats.Int64("number_of_mining_chain_fork", "Number of mining failures due to chain fork", stats.UnitDimensionless) 25 | NumberOfMiningError = stats.Int64("number_of_mining_error", "Number of mining failures due to error", stats.UnitDimensionless) 26 | ) 27 | 28 | var ( 29 | NumberOfBlockView = &view.View{ 30 | Measure: NumberOfBlock, 31 | Aggregation: view.Count(), 32 | TagKeys: []tag.Key{MinerID}, 33 | } 34 | IsRoundWinnerView = &view.View{ 35 | Measure: NumberOfIsRoundWinner, 36 | Aggregation: view.Count(), 37 | TagKeys: []tag.Key{MinerID}, 38 | } 39 | NumberOfMiningTimeoutView = &view.View{ 40 | Measure: NumberOfMiningTimeout, 41 | Aggregation: view.Count(), 42 | TagKeys: []tag.Key{MinerID}, 43 | } 44 | NumberOfMiningChainForkView = &view.View{ 45 | Measure: NumberOfMiningChainFork, 46 | Aggregation: view.Count(), 47 | TagKeys: []tag.Key{MinerID}, 48 | } 49 | NumberOfMiningErrorView = &view.View{ 50 | Measure: NumberOfMiningError, 51 | Aggregation: view.Count(), 52 | TagKeys: []tag.Key{MinerID}, 53 | } 54 | ) 55 | 56 | var ( 57 | MinerNumInState = metrics.NewInt64WithCategory("miner/num", "miner num in vary state", "") 58 | 59 | GetBaseInfoDuration = metrics.NewTimerMs("mine/getbaseinfo", "Duration of GetBaseInfo in miner", MinerID) 60 | ComputeTicketDuration = metrics.NewTimerMs("mine/computeticket", "Duration of ComputeTicket in miner", MinerID) 61 | CheckRoundWinnerDuration = metrics.NewTimerMs("mine/checkroundwinner", "Duration of Check Round Winner in miner", MinerID) 62 | ComputeProofDuration = metrics.NewTimerMs("mine/computeproof", "Duration of ComputeProof in miner", MinerID) 63 | ) 64 | 65 | var MinerNodeViews = append([]*view.View{ 66 | NumberOfBlockView, 67 | IsRoundWinnerView, 68 | NumberOfMiningTimeoutView, 69 | NumberOfMiningChainForkView, 70 | NumberOfMiningErrorView, 71 | }, rpcMetrics.DefaultViews...) 72 | 73 | func SinceInMilliseconds(startTime time.Time) float64 { 74 | return float64(time.Since(startTime).Nanoseconds()) / 1e6 75 | } 76 | 77 | func TimerMilliseconds(ctx context.Context, m *stats.Float64Measure, minerID string) func() { 78 | start := time.Now() 79 | return func() { 80 | ctx, _ = tag.New( 81 | ctx, 82 | tag.Upsert(MinerID, minerID), 83 | ) 84 | stats.Record(ctx, m.M(SinceInMilliseconds(start))) 85 | } 86 | } 87 | 88 | func SinceInSeconds(startTime time.Time) float64 { 89 | return float64(time.Since(startTime).Microseconds()) / 1e6 90 | } 91 | 92 | func TimerSeconds(ctx context.Context, m *stats.Float64Measure, minerID string) func() { 93 | start := time.Now() 94 | return func() { 95 | ctx, _ = tag.New( 96 | ctx, 97 | tag.Upsert(MinerID, minerID), 98 | ) 99 | stats.Record(ctx, m.M(SinceInSeconds(start))) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/tracing/setup.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "os" 5 | 6 | "contrib.go.opencensus.io/exporter/jaeger" 7 | logging "github.com/ipfs/go-log/v2" 8 | "go.opencensus.io/trace" 9 | 10 | "github.com/ipfs-force-community/metrics" 11 | ) 12 | 13 | var log = logging.Logger("tracing") 14 | 15 | func SetupJaegerTracing(cfg *metrics.TraceConfig) *jaeger.Exporter { 16 | if !cfg.JaegerTracingEnabled { 17 | return nil 18 | } 19 | 20 | agentEndpointURI := cfg.JaegerEndpoint 21 | if _, ok := os.LookupEnv("SOPHON_MINER_JAEGER"); ok { 22 | agentEndpointURI = os.Getenv("SOPHON_MINER_JAEGER") 23 | } 24 | 25 | je, err := jaeger.NewExporter(jaeger.Options{ 26 | AgentEndpoint: agentEndpointURI, 27 | Process: jaeger.Process{ 28 | ServiceName: cfg.ServerName, 29 | }, 30 | }) 31 | if err != nil { 32 | log.Errorw("Failed to create the Jaeger exporter", "error", err) 33 | return nil 34 | } 35 | 36 | trace.RegisterExporter(je) 37 | trace.ApplyConfig(trace.Config{ 38 | DefaultSampler: trace.ProbabilitySampler(cfg.ProbabilitySampler), 39 | }) 40 | 41 | log.Infof("register tracing exporter:%s, service name:%s", cfg.JaegerEndpoint, cfg.ServerName) 42 | 43 | return je 44 | } 45 | -------------------------------------------------------------------------------- /miner/api.go: -------------------------------------------------------------------------------- 1 | package miner 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/filecoin-project/go-address" 7 | "github.com/filecoin-project/go-state-types/abi" 8 | 9 | "github.com/ipfs-force-community/sophon-miner/types" 10 | ) 11 | 12 | type MiningAPI interface { 13 | IMinerMining 14 | IMinerManager 15 | } 16 | 17 | type IMinerMining interface { 18 | Start(context.Context) error 19 | Stop(context.Context) error 20 | ManualStart(context.Context, []address.Address) error 21 | ManualStop(context.Context, []address.Address) error 22 | } 23 | 24 | type IMinerManager interface { 25 | UpdateAddress(context.Context, int64, int64) ([]types.MinerInfo, error) 26 | ListAddress(context.Context) ([]types.MinerInfo, error) 27 | StatesForMining(context.Context, []address.Address) ([]types.MinerState, error) 28 | CountWinners(context.Context, []address.Address, abi.ChainEpoch, abi.ChainEpoch) ([]types.CountWinners, error) 29 | ListBlocks(ctx context.Context, params *types.BlocksQueryParams) ([]types.MinedBlock, error) 30 | WarmupForMiner(context.Context, address.Address) error 31 | QueryRecord(ctx context.Context, params *types.QueryRecordParams) ([]map[string]string, error) 32 | } 33 | -------------------------------------------------------------------------------- /miner/minerwpp.go: -------------------------------------------------------------------------------- 1 | package miner 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/filecoin-project/go-address" 9 | "github.com/filecoin-project/go-state-types/abi" 10 | "github.com/filecoin-project/go-state-types/network" 11 | 12 | "github.com/ipfs-force-community/sophon-miner/api/client" 13 | "github.com/ipfs-force-community/sophon-miner/build" 14 | "github.com/ipfs-force-community/sophon-miner/node/config" 15 | 16 | "github.com/filecoin-project/venus/pkg/constants" 17 | "github.com/filecoin-project/venus/venus-shared/actors/builtin" 18 | v1 "github.com/filecoin-project/venus/venus-shared/api/chain/v1" 19 | "github.com/filecoin-project/venus/venus-shared/types" 20 | ) 21 | 22 | type MiningWpp struct { 23 | mAddr address.Address 24 | gatewayNode *config.GatewayNode 25 | 26 | winnRpt abi.RegisteredPoStProof 27 | } 28 | 29 | func NewWinningPoStProver(api v1.FullNode, gatewayNode *config.GatewayNode, mAddr address.Address) (*MiningWpp, error) { 30 | mi, err := api.StateMinerInfo(context.TODO(), mAddr, types.EmptyTSK) 31 | if err != nil { 32 | return nil, fmt.Errorf("getting sector size: %w", err) 33 | } 34 | 35 | if constants.InsecurePoStValidation { 36 | log.Warn("*****************************************************************************") 37 | log.Warn(" Generating fake PoSt proof! You should only see this while running tests! ") 38 | log.Warn("*****************************************************************************") 39 | } 40 | 41 | return &MiningWpp{gatewayNode: gatewayNode, mAddr: mAddr, winnRpt: mi.WindowPoStProofType}, nil 42 | } 43 | 44 | var _ WinningPoStProver = (*MiningWpp)(nil) 45 | 46 | func (wpp *MiningWpp) GenerateCandidates(ctx context.Context, randomness abi.PoStRandomness, eligibleSectorCount uint64) ([]uint64, error) { 47 | panic("should not be called") 48 | } 49 | 50 | func (wpp *MiningWpp) ComputeProof(ctx context.Context, ssi []builtin.ExtendedSectorInfo, rand abi.PoStRandomness, currEpoch abi.ChainEpoch, nv network.Version) ([]builtin.PoStProof, error) { 51 | if constants.InsecurePoStValidation { 52 | return []builtin.PoStProof{{ProofBytes: []byte("valid proof")}}, nil 53 | } 54 | 55 | log.Infof("Computing WinningPoSt ;%+v; %v", ssi, rand) 56 | 57 | start := build.Clock.Now() 58 | 59 | api, closer, err := client.NewGatewayRPC(ctx, wpp.gatewayNode) 60 | if err != nil { 61 | return nil, err 62 | } 63 | defer closer() 64 | 65 | proofBuf, err := api.ComputeProof(ctx, wpp.mAddr, ssi, rand, currEpoch, nv) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | log.Infof("GenerateWinningPoSt took %s", time.Since(start)) 71 | return proofBuf, nil 72 | } 73 | -------------------------------------------------------------------------------- /miner/mock/mock_post_provider.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ./miner/util.go 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | abi "github.com/filecoin-project/go-state-types/abi" 12 | network "github.com/filecoin-project/go-state-types/network" 13 | builtin "github.com/filecoin-project/venus/venus-shared/actors/builtin" 14 | gomock "github.com/golang/mock/gomock" 15 | ) 16 | 17 | // MockWinningPoStProver is a mock of WinningPoStProver interface. 18 | type MockWinningPoStProver struct { 19 | ctrl *gomock.Controller 20 | recorder *MockWinningPoStProverMockRecorder 21 | } 22 | 23 | // MockWinningPoStProverMockRecorder is the mock recorder for MockWinningPoStProver. 24 | type MockWinningPoStProverMockRecorder struct { 25 | mock *MockWinningPoStProver 26 | } 27 | 28 | // NewMockWinningPoStProver creates a new mock instance. 29 | func NewMockWinningPoStProver(ctrl *gomock.Controller) *MockWinningPoStProver { 30 | mock := &MockWinningPoStProver{ctrl: ctrl} 31 | mock.recorder = &MockWinningPoStProverMockRecorder{mock} 32 | return mock 33 | } 34 | 35 | // EXPECT returns an object that allows the caller to indicate expected use. 36 | func (m *MockWinningPoStProver) EXPECT() *MockWinningPoStProverMockRecorder { 37 | return m.recorder 38 | } 39 | 40 | // ComputeProof mocks base method. 41 | func (m *MockWinningPoStProver) ComputeProof(arg0 context.Context, arg1 []builtin.ExtendedSectorInfo, arg2 abi.PoStRandomness, arg3 abi.ChainEpoch, arg4 network.Version) ([]builtin.PoStProof, error) { 42 | m.ctrl.T.Helper() 43 | ret := m.ctrl.Call(m, "ComputeProof", arg0, arg1, arg2, arg3, arg4) 44 | ret0, _ := ret[0].([]builtin.PoStProof) 45 | ret1, _ := ret[1].(error) 46 | return ret0, ret1 47 | } 48 | 49 | // ComputeProof indicates an expected call of ComputeProof. 50 | func (mr *MockWinningPoStProverMockRecorder) ComputeProof(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { 51 | mr.mock.ctrl.T.Helper() 52 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ComputeProof", reflect.TypeOf((*MockWinningPoStProver)(nil).ComputeProof), arg0, arg1, arg2, arg3, arg4) 53 | } 54 | 55 | // GenerateCandidates mocks base method. 56 | func (m *MockWinningPoStProver) GenerateCandidates(arg0 context.Context, arg1 abi.PoStRandomness, arg2 uint64) ([]uint64, error) { 57 | m.ctrl.T.Helper() 58 | ret := m.ctrl.Call(m, "GenerateCandidates", arg0, arg1, arg2) 59 | ret0, _ := ret[0].([]uint64) 60 | ret1, _ := ret[1].(error) 61 | return ret0, ret1 62 | } 63 | 64 | // GenerateCandidates indicates an expected call of GenerateCandidates. 65 | func (mr *MockWinningPoStProverMockRecorder) GenerateCandidates(arg0, arg1, arg2 interface{}) *gomock.Call { 66 | mr.mock.ctrl.T.Helper() 67 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GenerateCandidates", reflect.TypeOf((*MockWinningPoStProver)(nil).GenerateCandidates), arg0, arg1, arg2) 68 | } 69 | -------------------------------------------------------------------------------- /miner/util.go: -------------------------------------------------------------------------------- 1 | package miner 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/binary" 7 | "fmt" 8 | 9 | "github.com/minio/blake2b-simd" 10 | 11 | "github.com/filecoin-project/go-address" 12 | "github.com/filecoin-project/go-state-types/abi" 13 | "github.com/filecoin-project/go-state-types/crypto" 14 | "github.com/filecoin-project/go-state-types/network" 15 | 16 | "github.com/filecoin-project/venus/venus-shared/actors/builtin" 17 | "github.com/filecoin-project/venus/venus-shared/types" 18 | "github.com/filecoin-project/venus/venus-shared/types/wallet" 19 | ) 20 | 21 | type WinningPoStProver interface { 22 | GenerateCandidates(context.Context, abi.PoStRandomness, uint64) ([]uint64, error) 23 | ComputeProof(context.Context, []builtin.ExtendedSectorInfo, abi.PoStRandomness, abi.ChainEpoch, network.Version) ([]builtin.PoStProof, error) 24 | } 25 | 26 | type SignFunc func(ctx context.Context, signer address.Address, accounts []string, toSign []byte, meta types.MsgMeta) (*crypto.Signature, error) 27 | 28 | func ComputeVRF(ctx context.Context, sign SignFunc, account string, worker address.Address, sigInput []byte) ([]byte, error) { 29 | sig, err := sign(ctx, worker, []string{account}, sigInput, types.MsgMeta{Type: types.MTDrawRandomParam}) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | if sig.Type != crypto.SigTypeBLS { 35 | return nil, fmt.Errorf("miner worker address was not a BLS key") 36 | } 37 | 38 | return sig.Data, nil 39 | } 40 | 41 | func IsRoundWinner( 42 | ctx context.Context, 43 | round abi.ChainEpoch, 44 | account string, 45 | miner address.Address, 46 | brand types.BeaconEntry, 47 | mbi *types.MiningBaseInfo, 48 | sign SignFunc, 49 | ) (*types.ElectionProof, error) { 50 | 51 | buf := new(bytes.Buffer) 52 | if err := miner.MarshalCBOR(buf); err != nil { 53 | return nil, fmt.Errorf("failed to cbor marshal address: %w", err) 54 | } 55 | 56 | electionRand := new(bytes.Buffer) 57 | drp := &wallet.DrawRandomParams{ 58 | Rbase: brand.Data, 59 | Pers: crypto.DomainSeparationTag_ElectionProofProduction, 60 | Round: round, 61 | Entropy: buf.Bytes(), 62 | } 63 | err := drp.MarshalCBOR(electionRand) 64 | if err != nil { 65 | return nil, fmt.Errorf("failed to marshal randomness: %w", err) 66 | } 67 | //electionRand, err := chain.DrawRandomness(brand.Data, crypto.DomainSeparationTag_ElectionProofProduction, round, buf.Bytes()) 68 | //if err != nil { 69 | // return nil, fmt.Errorf("failed to draw randomness: %w", err) 70 | //} 71 | 72 | vrfout, err := ComputeVRF(ctx, sign, account, mbi.WorkerKey, electionRand.Bytes()) 73 | if err != nil { 74 | return nil, fmt.Errorf("failed to compute VRF: %w", err) 75 | } 76 | 77 | ep := &types.ElectionProof{VRFProof: vrfout} 78 | j := ep.ComputeWinCount(mbi.MinerPower, mbi.NetworkPower) 79 | ep.WinCount = j 80 | if j < 1 { 81 | return nil, nil 82 | } 83 | 84 | return ep, nil 85 | } 86 | 87 | // DrawRandomness todo 在venus处理好后,这里的删除,用venus中的函数 88 | func DrawRandomness(rbase []byte, pers crypto.DomainSeparationTag, round abi.ChainEpoch, entropy []byte) ([]byte, error) { 89 | h := blake2b.New256() 90 | if err := binary.Write(h, binary.BigEndian, int64(pers)); err != nil { 91 | return nil, fmt.Errorf("deriving randomness: %s", err) 92 | } 93 | VRFDigest := blake2b.Sum256(rbase) 94 | _, err := h.Write(VRFDigest[:]) 95 | if err != nil { 96 | return nil, fmt.Errorf("hashing VRFDigest: %s", err) 97 | } 98 | if err := binary.Write(h, binary.BigEndian, round); err != nil { 99 | return nil, fmt.Errorf("deriving randomness: %s", err) 100 | } 101 | _, err = h.Write(entropy) 102 | if err != nil { 103 | return nil, fmt.Errorf("hashing entropy: %s", err) 104 | } 105 | 106 | return h.Sum(nil), nil 107 | } 108 | 109 | // ReorgOps takes two tipsets (which can be at different heights), and walks 110 | // their corresponding chains backwards one step at a time until we find 111 | // a common ancestor. It then returns the respective chain segments that fork 112 | // from the identified ancestor, in reverse order, where the first element of 113 | // each slice is the supplied tipset, and the last element is the common 114 | // ancestor. 115 | // 116 | // If an error happens along the way, we return the error with nil slices. 117 | // todo should move this code into store.ReorgOps. anywhere use this function should invoke store.ReorgOps 118 | // todo 因依赖filecoin-ffi,暂时从venus复制的,venus处理好依赖后删除,用venus中的 119 | func ReorgOps(lts func(context.Context, types.TipSetKey) (*types.TipSet, error), a, b *types.TipSet) ([]*types.TipSet, []*types.TipSet, error) { 120 | left := a 121 | right := b 122 | 123 | var leftChain, rightChain []*types.TipSet 124 | for !left.Equals(right) { 125 | if left.Height() > right.Height() { 126 | leftChain = append(leftChain, left) 127 | par, err := lts(context.TODO(), left.Parents()) 128 | if err != nil { 129 | return nil, nil, err 130 | } 131 | 132 | left = par 133 | } else { 134 | rightChain = append(rightChain, right) 135 | par, err := lts(context.TODO(), right.Parents()) 136 | if err != nil { 137 | log.Infof("failed to fetch right.Parents: %s", err) 138 | return nil, nil, err 139 | } 140 | 141 | right = par 142 | } 143 | } 144 | 145 | return leftChain, rightChain, nil 146 | } 147 | -------------------------------------------------------------------------------- /miner/warmup.go: -------------------------------------------------------------------------------- 1 | package miner 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "fmt" 7 | "math" 8 | "time" 9 | 10 | "github.com/filecoin-project/go-address" 11 | "github.com/filecoin-project/go-bitfield" 12 | "github.com/filecoin-project/go-state-types/abi" 13 | proof7 "github.com/filecoin-project/specs-actors/v7/actors/runtime/proof" 14 | 15 | "github.com/filecoin-project/venus/venus-shared/types" 16 | ) 17 | 18 | func (m *Miner) winPoStWarmup(ctx context.Context) error { 19 | m.lkWPP.Lock() 20 | defer m.lkWPP.Unlock() 21 | for addr, wpp := range m.minerWPPMap { 22 | tAddr := addr 23 | epp := wpp.epp 24 | go func() { 25 | err := m.winPostWarmupForMiner(ctx, tAddr, epp) 26 | if err != nil { 27 | log.Warnw("mining warm up failed", "miner", tAddr, "err", err.Error()) 28 | m.minerWPPMap[tAddr].err = append(m.minerWPPMap[tAddr].err, time.Now().Format("2006-01-02 15:04:05 ")+err.Error()) 29 | } 30 | }() 31 | } 32 | 33 | return nil 34 | } 35 | 36 | func (m *Miner) WarmupForMiner(ctx context.Context, mAddr address.Address) error { 37 | m.lkWPP.Lock() 38 | wpp, ok := m.minerWPPMap[mAddr] 39 | m.lkWPP.Unlock() 40 | 41 | if ok { 42 | return m.winPostWarmupForMiner(ctx, mAddr, wpp.epp) 43 | } 44 | 45 | return fmt.Errorf("%s not exist", mAddr) 46 | } 47 | 48 | func (m *Miner) winPostWarmupForMiner(ctx context.Context, addr address.Address, epp WinningPoStProver) error { 49 | deadlines, err := m.api.StateMinerDeadlines(ctx, addr, types.EmptyTSK) 50 | if err != nil { 51 | return fmt.Errorf("getting deadlines: %w", err) 52 | } 53 | 54 | var sector abi.SectorNumber = math.MaxUint64 55 | 56 | out: 57 | for dlIdx := range deadlines { 58 | partitions, err := m.api.StateMinerPartitions(ctx, addr, uint64(dlIdx), types.EmptyTSK) 59 | if err != nil { 60 | return fmt.Errorf("getting partitions for deadline %d: %w", dlIdx, err) 61 | } 62 | 63 | for _, partition := range partitions { 64 | b, err := partition.ActiveSectors.First() 65 | if err == bitfield.ErrNoBitsSet { 66 | continue 67 | } 68 | if err != nil { 69 | return err 70 | } 71 | 72 | sector = abi.SectorNumber(b) 73 | break out 74 | } 75 | } 76 | 77 | if sector == math.MaxUint64 { 78 | log.Info("skipping winning PoSt warmup, no sectors") 79 | return nil 80 | } 81 | 82 | log.Infow("starting winning PoSt warmup", "sector", sector) 83 | start := time.Now() 84 | 85 | var r abi.PoStRandomness = make([]byte, abi.RandomnessLength) 86 | _, _ = rand.Read(r) 87 | 88 | si, err := m.api.StateSectorGetInfo(ctx, addr, sector, types.EmptyTSK) 89 | if err != nil { 90 | return fmt.Errorf("getting sector info: %w", err) 91 | } 92 | 93 | if si == nil { 94 | return fmt.Errorf("sector not found %d", sector) 95 | } 96 | 97 | ts, err := m.api.ChainHead(ctx) 98 | if err != nil { 99 | return fmt.Errorf("getting chain head") 100 | } 101 | nv, err := m.api.StateNetworkVersion(ctx, ts.Key()) 102 | if err != nil { 103 | return fmt.Errorf("getting network version") 104 | } 105 | 106 | _, err = epp.ComputeProof(ctx, []proof7.ExtendedSectorInfo{ 107 | { 108 | SealProof: si.SealProof, 109 | SectorNumber: sector, 110 | SealedCID: si.SealedCID, 111 | SectorKey: si.SectorKeyCID, 112 | }, 113 | }, r, ts.Height(), nv) 114 | if err != nil { 115 | return fmt.Errorf("failed to compute proof: %w", err) 116 | } 117 | 118 | log.Infow("winning PoSt warmup successful", "took", time.Since(start)) 119 | return nil 120 | } 121 | 122 | func (m *Miner) doWinPoStWarmup(ctx context.Context) { 123 | err := m.winPoStWarmup(ctx) 124 | if err != nil { 125 | log.Errorw("winning PoSt warmup failed", "error", err) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /node/config/apiinfo.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | 7 | "github.com/multiformats/go-multiaddr" 8 | manet "github.com/multiformats/go-multiaddr/net" 9 | ) 10 | 11 | type APIInfo struct { 12 | Addr string 13 | Token string 14 | } 15 | 16 | func defaultAPIInfo() *APIInfo { 17 | return &APIInfo{ 18 | Addr: "/ip4/0.0.0.0/tcp/12308/http", 19 | Token: "", 20 | } 21 | } 22 | 23 | func (a APIInfo) DialArgs(version string) (string, error) { 24 | ma, err := multiaddr.NewMultiaddr(a.Addr) 25 | if err == nil { 26 | _, addr, err := manet.DialArgs(ma) 27 | if err != nil { 28 | return "", err 29 | } 30 | 31 | return "ws://" + addr + "/rpc/" + version, nil 32 | } 33 | 34 | _, err = url.Parse(a.Addr) 35 | if err != nil { 36 | return "", err 37 | } 38 | return a.Addr + "/rpc/" + version, nil 39 | } 40 | 41 | func (a APIInfo) Host() (string, error) { 42 | ma, err := multiaddr.NewMultiaddr(a.Addr) 43 | if err == nil { 44 | _, addr, err := manet.DialArgs(ma) 45 | if err != nil { 46 | return "", err 47 | } 48 | 49 | return addr, nil 50 | } 51 | 52 | spec, err := url.Parse(a.Addr) 53 | if err != nil { 54 | return "", err 55 | } 56 | return spec.Host, nil 57 | } 58 | 59 | func (a APIInfo) AuthHeader() http.Header { 60 | if len(a.Token) != 0 { 61 | headers := http.Header{} 62 | headers.Add("Authorization", "Bearer "+string(a.Token)) 63 | return headers 64 | } 65 | log.Warn("Sealer API Token not set and requested, capabilities might be limited.") 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /node/config/apiinfo_test.go: -------------------------------------------------------------------------------- 1 | // stm: #unit 2 | package config 3 | 4 | import ( 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestAPIInfo(t *testing.T) { 11 | apiInfo := defaultAPIInfo() 12 | // stm: @VENUSMINER_NODECONFIGAPIINFO_DIAL_ARGS_001 13 | url, err := apiInfo.DialArgs("0") 14 | assert.NoError(t, err) 15 | assert.Greater(t, len(url), 0) 16 | 17 | // stm: @VENUSMINER_NODECONFIGAPIINFO_HOST_001 18 | host, err := apiInfo.Host() 19 | assert.NoError(t, err) 20 | assert.Equal(t, host, "0.0.0.0:12308") 21 | 22 | // stm: @VENUSMINER_NODECONFIGAPIINFO_AUTH_HEADER_001 23 | apiInfo.Token = "hello" 24 | header := apiInfo.AuthHeader() 25 | assert.Equal(t, header.Get("Authorization"), "Bearer "+apiInfo.Token) 26 | } 27 | -------------------------------------------------------------------------------- /node/config/def.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "time" 7 | 8 | logging "github.com/ipfs/go-log/v2" 9 | 10 | "github.com/ipfs-force-community/metrics" 11 | ) 12 | 13 | var log = logging.Logger("config") 14 | 15 | // Duration is a wrapper type for Duration 16 | // for decoding and encoding from/to TOML 17 | type Duration time.Duration 18 | 19 | // UnmarshalText implements interface for TOML decoding 20 | func (dur *Duration) UnmarshalText(text []byte) error { 21 | d, err := time.ParseDuration(string(text)) 22 | if err != nil { 23 | return err 24 | } 25 | *dur = Duration(d) 26 | return err 27 | } 28 | 29 | func (dur Duration) MarshalText() ([]byte, error) { 30 | d := time.Duration(dur) 31 | return []byte(d.String()), nil 32 | } 33 | 34 | type MySQLConfig struct { 35 | Conn string 36 | MaxOpenConn int // 100 37 | MaxIdleConn int // 10 38 | ConnMaxLifeTime Duration // 60s 39 | Debug bool 40 | } 41 | 42 | type SlashFilterConfig struct { 43 | Type string 44 | MySQL MySQLConfig 45 | } 46 | 47 | type RecorderConfig struct { 48 | Enable bool 49 | ExpireEpoch uint64 50 | MaxRecordPerQuery uint 51 | } 52 | 53 | func newSlashFilterConfig() *SlashFilterConfig { 54 | return &SlashFilterConfig{ 55 | Type: "local", 56 | MySQL: MySQLConfig{ 57 | Conn: "", 58 | MaxOpenConn: 100, 59 | MaxIdleConn: 10, 60 | ConnMaxLifeTime: Duration(60 * time.Second), 61 | Debug: false, 62 | }, 63 | } 64 | } 65 | 66 | type API struct { 67 | ListenAddress string 68 | } 69 | 70 | type MinerConfig struct { 71 | API API 72 | FullNode *APIInfo 73 | Gateway *GatewayNode 74 | Auth *APIInfo 75 | SubmitNodes []*APIInfo 76 | F3Node *APIInfo 77 | 78 | PropagationDelaySecs uint64 79 | MpoolSelectDelaySecs uint64 80 | MinerOnceTimeout Duration 81 | 82 | SlashFilter *SlashFilterConfig 83 | Recorder *RecorderConfig 84 | 85 | Tracing *metrics.TraceConfig 86 | Metrics *metrics.MetricsConfig 87 | } 88 | 89 | func DefaultMinerConfig() *MinerConfig { 90 | minerCfg := &MinerConfig{ 91 | API: API{ 92 | ListenAddress: "/ip4/127.0.0.1/tcp/12308", 93 | }, 94 | FullNode: defaultAPIInfo(), 95 | Gateway: newDefaultGatewayNode(), 96 | Auth: defaultAPIInfo(), 97 | PropagationDelaySecs: 12, 98 | MinerOnceTimeout: Duration(time.Second * 15), 99 | MpoolSelectDelaySecs: 0, 100 | SlashFilter: newSlashFilterConfig(), 101 | Tracing: metrics.DefaultTraceConfig(), 102 | Metrics: metrics.DefaultMetricsConfig(), 103 | } 104 | 105 | return minerCfg 106 | } 107 | 108 | func Check(cfg *MinerConfig) error { 109 | if len(cfg.API.ListenAddress) == 0 { 110 | return fmt.Errorf("must config listen address") 111 | } 112 | 113 | if len(cfg.FullNode.Addr) == 0 || len(cfg.FullNode.Token) == 0 { 114 | return fmt.Errorf("must config full node url and token") 115 | } 116 | 117 | _, err := url.Parse(cfg.Auth.Addr) 118 | if err != nil { 119 | return fmt.Errorf("auth url format not correct %s %w", cfg.Auth.Addr, err) 120 | } 121 | 122 | if len(cfg.Gateway.ListenAPI) == 0 { 123 | return fmt.Errorf("config at lease one gateway url") 124 | } 125 | 126 | if cfg.SlashFilter.Type == "mysql" { 127 | if len(cfg.SlashFilter.MySQL.Conn) == 0 { 128 | return fmt.Errorf("mysql dsn must set when slash filter is mysql") 129 | } 130 | } else if cfg.SlashFilter.Type == "local" { 131 | } else { 132 | return fmt.Errorf("not support slash filter %s", cfg.SlashFilter.Type) 133 | } 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /node/config/def_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/BurntSushi/toml" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestDefaultMinerRoundtrip(t *testing.T) { 14 | c := DefaultMinerConfig() 15 | 16 | var s string 17 | { 18 | buf := new(bytes.Buffer) 19 | _, _ = buf.WriteString("# Default config:\n") 20 | e := toml.NewEncoder(buf) 21 | require.NoError(t, e.Encode(c)) 22 | 23 | s = buf.String() 24 | } 25 | 26 | c2, err := FromReader(strings.NewReader(s), DefaultMinerConfig()) 27 | require.NoError(t, err) 28 | 29 | require.True(t, reflect.DeepEqual(c, c2)) 30 | } 31 | -------------------------------------------------------------------------------- /node/config/gatewaydef.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/filecoin-project/venus/venus-shared/api" 7 | ) 8 | 9 | type GatewayNode struct { 10 | ListenAPI []string 11 | Token string 12 | } 13 | 14 | func (gw *GatewayNode) DialArgs() ([]string, error) { 15 | var mAddrs []string 16 | 17 | for _, apiAddr := range gw.ListenAPI { 18 | apiInfo := api.NewAPIInfo(apiAddr, gw.Token) 19 | addr, err := apiInfo.DialArgs("v2") 20 | if err != nil { 21 | log.Errorf("dial ma err: %s", err.Error()) 22 | continue 23 | } 24 | 25 | mAddrs = append(mAddrs, addr) 26 | } 27 | 28 | return mAddrs, nil 29 | } 30 | 31 | func (gw *GatewayNode) AuthHeader() http.Header { 32 | if len(gw.Token) != 0 { 33 | headers := http.Header{} 34 | headers.Add("Authorization", "Bearer "+string(gw.Token)) 35 | return headers 36 | } 37 | log.Warn("Sealer API Token not set and requested, capabilities might be limited.") 38 | return nil 39 | } 40 | 41 | func newDefaultGatewayNode() *GatewayNode { 42 | return &GatewayNode{ 43 | ListenAPI: []string{}, 44 | Token: "", 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /node/config/load.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "github.com/BurntSushi/toml" 10 | ) 11 | 12 | // FromFile loads config from a specified file overriding defaults specified in 13 | // the def parameter. If file does not exist or is empty defaults are assumed. 14 | func FromFile(path string, def interface{}) (interface{}, error) { 15 | file, err := os.Open(path) 16 | switch { 17 | case os.IsNotExist(err): 18 | return def, nil 19 | case err != nil: 20 | return nil, err 21 | } 22 | 23 | defer file.Close() //nolint:errcheck // The file is RO 24 | return FromReader(file, def) 25 | } 26 | 27 | // FromReader loads config from a reader instance. 28 | func FromReader(reader io.Reader, def interface{}) (interface{}, error) { 29 | cfg := def 30 | _, err := toml.NewDecoder(reader).Decode(cfg) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | return cfg, nil 36 | } 37 | 38 | func ConfigComment(t interface{}) ([]byte, error) { 39 | buf := new(bytes.Buffer) 40 | _, _ = buf.WriteString("# Default config:\n") 41 | e := toml.NewEncoder(buf) 42 | if err := e.Encode(t); err != nil { 43 | return nil, fmt.Errorf("encoding config: %w", err) 44 | } 45 | b := buf.Bytes() 46 | b = bytes.ReplaceAll(b, []byte("\n"), []byte("\n#")) 47 | b = bytes.ReplaceAll(b, []byte("#["), []byte("[")) 48 | return b, nil 49 | } 50 | -------------------------------------------------------------------------------- /node/config/load_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestDecodeNothing(t *testing.T) { 12 | assert := assert.New(t) 13 | 14 | { 15 | cfg, err := FromFile(os.DevNull, DefaultMinerConfig()) 16 | assert.Nil(err, "error should be nil") 17 | assert.Equal(DefaultMinerConfig(), cfg, 18 | "config from empty file should be the same as default") 19 | } 20 | 21 | { 22 | cfg, err := FromFile("./does-not-exist.toml", DefaultMinerConfig()) 23 | assert.Nil(err, "error should be nil") 24 | assert.Equal(DefaultMinerConfig(), cfg, 25 | "config from not exisiting file should be the same as default") 26 | } 27 | } 28 | 29 | func TestParitalConfig(t *testing.T) { 30 | assert := assert.New(t) 31 | cfgString := ` 32 | [FullNode] 33 | ` 34 | expected := DefaultMinerConfig() 35 | 36 | { 37 | cfg, err := FromReader(bytes.NewReader([]byte(cfgString)), DefaultMinerConfig()) 38 | assert.NoError(err, "error should be nil") 39 | assert.Equal(expected, cfg, 40 | "config from reader should contain changes") 41 | } 42 | 43 | { 44 | f, err := os.CreateTemp("", "config-*.toml") 45 | fname := f.Name() 46 | 47 | assert.NoError(err, "tmp file shold not error") 48 | _, err = f.WriteString(cfgString) 49 | assert.NoError(err, "writing to tmp file should not error") 50 | err = f.Close() 51 | assert.NoError(err, "closing tmp file should not error") 52 | defer os.Remove(fname) //nolint:errcheck 53 | 54 | cfg, err := FromFile(fname, DefaultMinerConfig()) 55 | assert.Nil(err, "error should be nil") 56 | assert.Equal(expected, cfg, 57 | "config from reader should contain changes") 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /node/config/migrate/def_nv170.go: -------------------------------------------------------------------------------- 1 | package migrate 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/ipfs-force-community/metrics" 7 | 8 | "github.com/ipfs-force-community/sophon-miner/node/config" 9 | ) 10 | 11 | type MySQLConfig struct { 12 | Conn string 13 | MaxOpenConn int // 100 14 | MaxIdleConn int // 10 15 | ConnMaxLifeTime time.Duration // 60(s) 16 | Debug bool 17 | } 18 | 19 | type SlashFilterConfig struct { 20 | Type string 21 | MySQL MySQLConfig 22 | } 23 | 24 | type MinerConfig struct { 25 | FullNode *config.APIInfo 26 | Gateway *config.GatewayNode 27 | Auth *config.APIInfo 28 | 29 | SlashFilter *SlashFilterConfig 30 | 31 | Tracing *metrics.TraceConfig 32 | Metrics *metrics.MetricsConfig 33 | } 34 | 35 | func (cfgV170 *MinerConfig) ToMinerConfig(cfg *config.MinerConfig) { 36 | cfg.FullNode = cfgV170.FullNode 37 | cfg.Gateway = cfgV170.Gateway 38 | cfg.Auth = cfgV170.Auth 39 | cfg.SlashFilter.Type = cfgV170.SlashFilter.Type 40 | cfg.SlashFilter.MySQL.Conn = cfgV170.SlashFilter.MySQL.Conn 41 | cfg.SlashFilter.MySQL.MaxOpenConn = cfgV170.SlashFilter.MySQL.MaxOpenConn 42 | cfg.SlashFilter.MySQL.MaxIdleConn = cfgV170.SlashFilter.MySQL.MaxIdleConn 43 | cfg.SlashFilter.MySQL.ConnMaxLifeTime = config.Duration(cfgV170.SlashFilter.MySQL.ConnMaxLifeTime) 44 | cfg.SlashFilter.MySQL.Debug = cfgV170.SlashFilter.MySQL.Debug 45 | cfg.Tracing = cfgV170.Tracing 46 | cfg.Metrics = cfgV170.Metrics 47 | } 48 | -------------------------------------------------------------------------------- /node/impl/common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/google/uuid" 7 | logging "github.com/ipfs/go-log/v2" 8 | "go.uber.org/fx" 9 | 10 | "github.com/ipfs-force-community/sophon-miner/api" 11 | "github.com/ipfs-force-community/sophon-miner/build" 12 | "github.com/ipfs-force-community/sophon-miner/types" 13 | 14 | sharedTypes "github.com/filecoin-project/venus/venus-shared/types" 15 | ) 16 | 17 | var session = uuid.New() 18 | 19 | type CommonAPI struct { 20 | fx.In 21 | 22 | ShutdownChan types.ShutdownChan 23 | } 24 | 25 | var apiVersion = sharedTypes.NewVer(1, 2, 0) 26 | 27 | func (a *CommonAPI) Version(context.Context) (sharedTypes.Version, error) { 28 | return sharedTypes.Version{ 29 | Version: build.UserVersion(), 30 | APIVersion: apiVersion, 31 | }, nil 32 | } 33 | 34 | func (a *CommonAPI) LogList(context.Context) ([]string, error) { 35 | return logging.GetSubsystems(), nil 36 | } 37 | 38 | func (a *CommonAPI) LogSetLevel(ctx context.Context, subsystem, level string) error { 39 | return logging.SetLogLevel(subsystem, level) 40 | } 41 | 42 | func (a *CommonAPI) Shutdown(ctx context.Context) error { 43 | a.ShutdownChan <- struct{}{} 44 | return nil 45 | } 46 | 47 | func (a *CommonAPI) Session(ctx context.Context) (uuid.UUID, error) { 48 | return session, nil 49 | } 50 | 51 | func (a *CommonAPI) Closing(ctx context.Context) (<-chan struct{}, error) { 52 | return make(chan struct{}), nil // relies on jsonrpc closing 53 | } 54 | 55 | var _ api.Common = &CommonAPI{} 56 | -------------------------------------------------------------------------------- /node/impl/miner.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/filecoin-project/go-address" 7 | "github.com/filecoin-project/go-state-types/abi" 8 | "github.com/ipfs-force-community/sophon-auth/jwtclient" 9 | 10 | "github.com/ipfs-force-community/sophon-miner/api" 11 | "github.com/ipfs-force-community/sophon-miner/miner" 12 | "github.com/ipfs-force-community/sophon-miner/node/impl/common" 13 | "github.com/ipfs-force-community/sophon-miner/types" 14 | ) 15 | 16 | type MinerAPI struct { 17 | common.CommonAPI 18 | miner.MiningAPI 19 | 20 | AuthClient jwtclient.IAuthClient 21 | } 22 | 23 | var _ api.MinerAPI = &MinerAPI{} 24 | 25 | func (m *MinerAPI) UpdateAddress(ctx context.Context, skip int64, limit int64) ([]types.MinerInfo, error) { 26 | return m.MiningAPI.UpdateAddress(ctx, skip, limit) 27 | } 28 | 29 | func (m *MinerAPI) ListAddress(ctx context.Context) ([]types.MinerInfo, error) { 30 | mis, err := m.MiningAPI.ListAddress(ctx) 31 | if err != nil { 32 | return nil, err 33 | } 34 | ret := filter(mis, func(mi types.MinerInfo) bool { 35 | return jwtclient.CheckPermissionByMiner(ctx, m.AuthClient, mi.Addr) == nil 36 | }) 37 | 38 | return ret, nil 39 | } 40 | 41 | func (m *MinerAPI) StatesForMining(ctx context.Context, addrs []address.Address) ([]types.MinerState, error) { 42 | addrsAllowed := filter(addrs, func(addr address.Address) bool { 43 | return jwtclient.CheckPermissionByMiner(ctx, m.AuthClient, addr) == nil 44 | }) 45 | return m.MiningAPI.StatesForMining(ctx, addrsAllowed) 46 | } 47 | 48 | func (m *MinerAPI) CountWinners(ctx context.Context, addrs []address.Address, start abi.ChainEpoch, end abi.ChainEpoch) ([]types.CountWinners, error) { 49 | addrsAllowed := filter(addrs, func(addr address.Address) bool { 50 | return jwtclient.CheckPermissionByMiner(ctx, m.AuthClient, addr) == nil 51 | }) 52 | return m.MiningAPI.CountWinners(ctx, addrsAllowed, start, end) 53 | } 54 | 55 | func (m *MinerAPI) ListBlocks(ctx context.Context, params *types.BlocksQueryParams) ([]types.MinedBlock, error) { 56 | addrsAllowed := filter(params.Miners, func(addr address.Address) bool { 57 | return jwtclient.CheckPermissionByMiner(ctx, m.AuthClient, addr) == nil 58 | }) 59 | params.Miners = addrsAllowed 60 | return m.MiningAPI.ListBlocks(ctx, params) 61 | } 62 | 63 | func (m *MinerAPI) WarmupForMiner(ctx context.Context, maddr address.Address) error { 64 | if err := jwtclient.CheckPermissionByMiner(ctx, m.AuthClient, maddr); err != nil { 65 | return err 66 | } 67 | return m.MiningAPI.WarmupForMiner(ctx, maddr) 68 | } 69 | 70 | func (m *MinerAPI) Start(ctx context.Context, addrs []address.Address) error { 71 | addrsAllowed := filter(addrs, func(addr address.Address) bool { 72 | return jwtclient.CheckPermissionByMiner(ctx, m.AuthClient, addr) == nil 73 | }) 74 | return m.MiningAPI.ManualStart(ctx, addrsAllowed) 75 | } 76 | 77 | func (m *MinerAPI) Stop(ctx context.Context, addrs []address.Address) error { 78 | addrsAllowed := filter(addrs, func(addr address.Address) bool { 79 | return jwtclient.CheckPermissionByMiner(ctx, m.AuthClient, addr) == nil 80 | }) 81 | return m.MiningAPI.ManualStop(ctx, addrsAllowed) 82 | } 83 | 84 | func (m *MinerAPI) QueryRecord(ctx context.Context, params *types.QueryRecordParams) ([]map[string]string, error) { 85 | if err := jwtclient.CheckPermissionByMiner(ctx, m.AuthClient, params.Miner); err != nil { 86 | return nil, err 87 | } 88 | return m.MiningAPI.QueryRecord(ctx, params) 89 | } 90 | 91 | func filter[T any](src []T, pass func(T) bool) []T { 92 | ret := make([]T, 0) 93 | for _, addr := range src { 94 | if pass(addr) { 95 | ret = append(ret, addr) 96 | } 97 | } 98 | return ret 99 | } 100 | -------------------------------------------------------------------------------- /node/modules/helpers/helpers.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "context" 5 | 6 | "go.uber.org/fx" 7 | ) 8 | 9 | // MetricsCtx is a context wrapper with metrics 10 | type MetricsCtx context.Context 11 | 12 | // LifecycleCtx creates a context which will be cancelled when lifecycle stops 13 | // 14 | // This is a hack which we need because most of our services use contexts in a 15 | // wrong way 16 | func LifecycleCtx(mctx MetricsCtx, lc fx.Lifecycle) context.Context { 17 | ctx, cancel := context.WithCancel(mctx) 18 | lc.Append(fx.Hook{ 19 | OnStop: func(_ context.Context) error { 20 | cancel() 21 | return nil 22 | }, 23 | }) 24 | return ctx 25 | } 26 | -------------------------------------------------------------------------------- /node/modules/mine-recorder/inner_recorder.go: -------------------------------------------------------------------------------- 1 | package minerecorder 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/filecoin-project/go-address" 9 | "github.com/filecoin-project/go-state-types/abi" 10 | "github.com/ipfs/go-datastore" 11 | ) 12 | 13 | var ( 14 | ErrDatastoreNotSet = fmt.Errorf("database not set") 15 | ErrRecorderDisabled = fmt.Errorf("recorder disabled") 16 | ) 17 | var innerRecorder Recorder 18 | var enable = false 19 | 20 | func SetDatastore(ds datastore.Datastore) { 21 | innerRecorder = NewDefaultRecorder(ds) 22 | enable = true 23 | } 24 | 25 | func checkAvailable() error { 26 | if !enable { 27 | return ErrRecorderDisabled 28 | } 29 | return nil 30 | } 31 | 32 | func Record(ctx context.Context, miner address.Address, epoch abi.ChainEpoch, r Records) error { 33 | err := checkAvailable() 34 | if err != nil { 35 | return err 36 | } 37 | return innerRecorder.Record(ctx, miner, epoch, r) 38 | } 39 | 40 | func Query(ctx context.Context, miner address.Address, epoch abi.ChainEpoch, limit uint) ([]Records, error) { 41 | err := checkAvailable() 42 | if err != nil { 43 | return nil, err 44 | } 45 | return innerRecorder.Query(ctx, miner, epoch, limit) 46 | } 47 | 48 | type subRecorder struct { 49 | miner address.Address 50 | epoch abi.ChainEpoch 51 | } 52 | 53 | func (s *subRecorder) Record(ctx context.Context, r Records) { 54 | err := checkAvailable() 55 | if errors.Is(err, ErrRecorderDisabled) { 56 | log.Debugf("recorder disabled, skip record") 57 | return 58 | } else if err != nil { 59 | log.Warnf("record failed: %s", err.Error()) 60 | } 61 | err = innerRecorder.Record(ctx, s.miner, s.epoch, r) 62 | if err != nil { 63 | log.Warnf("record failed: %s", err.Error()) 64 | } 65 | } 66 | 67 | func Sub(miner address.Address, epoch abi.ChainEpoch) SubRecorder { 68 | return &subRecorder{ 69 | miner: miner, 70 | epoch: epoch, 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /node/modules/mine-recorder/mod.go: -------------------------------------------------------------------------------- 1 | package minerecorder 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/binary" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "sync" 11 | 12 | logging "github.com/ipfs/go-log/v2" 13 | 14 | "github.com/filecoin-project/go-address" 15 | "github.com/filecoin-project/go-state-types/abi" 16 | "github.com/ipfs/go-datastore" 17 | "github.com/ipfs/go-datastore/namespace" 18 | "github.com/ipfs/go-datastore/query" 19 | ) 20 | 21 | var log = logging.Logger("mine-recorder") 22 | 23 | var ( 24 | // MaxRecordPerQuery is the max record can be query in one time 25 | MaxRecordPerQuery uint = 288 26 | // ExpireEpoch is the num of epoch that record will be expired, default is 7 * 2880 epoch, about 7 days 27 | ExpireEpoch = abi.ChainEpoch(7 * 2880) 28 | 29 | DatastoreNamespaceKey = datastore.NewKey("/mine-record") 30 | ) 31 | 32 | var ( 33 | ErrorRecordNotFound = datastore.ErrNotFound 34 | ErrorExceedMaxRecordPerQuery = fmt.Errorf("query exceed MaxRecordPerQuery(%d)", MaxRecordPerQuery) 35 | ) 36 | 37 | var ( 38 | metaMinEpochKey = datastore.NewKey("/index/min-epoch") 39 | ) 40 | 41 | type Recorder interface { 42 | Record(ctx context.Context, miner address.Address, epoch abi.ChainEpoch, r Records) error 43 | Query(ctx context.Context, miner address.Address, from abi.ChainEpoch, limit uint) ([]Records, error) 44 | } 45 | 46 | type SubRecorder interface { 47 | Record(ctx context.Context, r Records) 48 | } 49 | 50 | type Records = map[string]string 51 | 52 | func key(miner address.Address, epoch abi.ChainEpoch) datastore.Key { 53 | return datastore.NewKey("/" + epoch.String() + "/" + miner.String()) 54 | } 55 | 56 | type DefaultRecorder struct { 57 | ds datastore.Datastore 58 | mut sync.RWMutex 59 | latestRecordEpoch abi.ChainEpoch 60 | } 61 | 62 | var _ Recorder = (*DefaultRecorder)(nil) 63 | 64 | func NewDefaultRecorder(ds datastore.Datastore) *DefaultRecorder { 65 | return &DefaultRecorder{ 66 | ds: namespace.Wrap(ds, DatastoreNamespaceKey), 67 | } 68 | } 69 | 70 | func (d *DefaultRecorder) Record(ctx context.Context, miner address.Address, epoch abi.ChainEpoch, r Records) error { 71 | before, err := d.get(ctx, miner, epoch) 72 | if err != nil && !errors.Is(err, ErrorRecordNotFound) { 73 | return err 74 | } 75 | 76 | r = coverMap(before, r) 77 | 78 | err = d.put(ctx, miner, epoch, r) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | err = d.cleanExpire(ctx, epoch) 84 | if err != nil { 85 | return err 86 | } 87 | return nil 88 | } 89 | 90 | func (d *DefaultRecorder) get(ctx context.Context, miner address.Address, epoch abi.ChainEpoch) (Records, error) { 91 | key := key(miner, epoch) 92 | val, err := d.ds.Get(ctx, key) 93 | if err != nil { 94 | return nil, fmt.Errorf("get record (%s) fail: %w", key, err) 95 | } 96 | var record Records 97 | err = json.Unmarshal(val, &record) 98 | if err != nil { 99 | return nil, err 100 | } 101 | return record, nil 102 | } 103 | 104 | func (d *DefaultRecorder) put(ctx context.Context, miner address.Address, epoch abi.ChainEpoch, r Records) error { 105 | key := key(miner, epoch) 106 | val, err := json.Marshal(&r) 107 | if err != nil { 108 | return err 109 | } 110 | err = d.ds.Put(ctx, key, val) 111 | if err != nil { 112 | return err 113 | } 114 | return nil 115 | } 116 | 117 | func (d *DefaultRecorder) Query(ctx context.Context, miner address.Address, from abi.ChainEpoch, limit uint) ([]Records, error) { 118 | if limit > MaxRecordPerQuery { 119 | return nil, ErrorExceedMaxRecordPerQuery 120 | } 121 | if limit == 0 { 122 | limit = 1 123 | } 124 | ret := make([]Records, 0, limit) 125 | to := from + abi.ChainEpoch(limit) 126 | for ; from < to; from++ { 127 | r, err := d.get(ctx, miner, from) 128 | if errors.Is(err, ErrorRecordNotFound) { 129 | // ignore record not found 130 | log.Warnf("query record: %s on %d : %s", miner, from, err.Error()) 131 | continue 132 | } else if err != nil { 133 | return nil, err 134 | } 135 | covered := coverMap(r, Records{"miner": miner.String(), "epoch": from.String()}) 136 | ret = append(ret, covered) 137 | } 138 | 139 | if len(ret) != int(limit) { 140 | log.Infof("query record: %s from %d to %d ,found %d ", miner, from-abi.ChainEpoch(limit), from, len(ret)) 141 | } else { 142 | log.Debugf("query record: %s from %d to %d ,found %d ", miner, from-abi.ChainEpoch(limit), limit, len(ret)) 143 | } 144 | return ret, nil 145 | } 146 | 147 | func (d *DefaultRecorder) cleanExpire(ctx context.Context, epoch abi.ChainEpoch) error { 148 | d.mut.RLock() 149 | latestRecordEpoch := d.latestRecordEpoch 150 | d.mut.RUnlock() 151 | 152 | if epoch > latestRecordEpoch { 153 | needClean := false 154 | d.mut.Lock() 155 | // double check, prevent latestRecordEpoch was updated by other goroutine before get write lock 156 | if epoch > d.latestRecordEpoch { 157 | d.latestRecordEpoch = epoch 158 | needClean = true 159 | } 160 | d.mut.Unlock() 161 | 162 | if needClean { 163 | deadline := epoch - ExpireEpoch 164 | err := d.cleanBefore(ctx, deadline) 165 | if err != nil { 166 | return err 167 | } 168 | } 169 | } 170 | return nil 171 | } 172 | 173 | func (d *DefaultRecorder) cleanBefore(ctx context.Context, deadline abi.ChainEpoch) error { 174 | minEpochInByte, err := d.ds.Get(ctx, metaMinEpochKey) 175 | if errors.Is(err, datastore.ErrNotFound) { 176 | // no min epoch record 177 | minEpochInByte = intoBytes(deadline) 178 | err = d.ds.Put(ctx, metaMinEpochKey, minEpochInByte) 179 | return err 180 | } else if err != nil { 181 | return fmt.Errorf("get min epoch: %w", err) 182 | } 183 | 184 | var minEpoch abi.ChainEpoch 185 | fromBytes(minEpochInByte, &minEpoch) 186 | 187 | if deadline > minEpoch { 188 | // delete record range [minEpoch, deadline) 189 | for i := minEpoch; i < deadline; i++ { 190 | res, err := d.ds.Query(ctx, query.Query{ 191 | Prefix: "/" + i.String(), 192 | KeysOnly: true, 193 | }) 194 | if err != nil { 195 | return fmt.Errorf("query record: %w", err) 196 | } 197 | for r := range res.Next() { 198 | if r.Error != nil { 199 | return fmt.Errorf("query record: %w", r.Error) 200 | } 201 | err := d.ds.Delete(ctx, datastore.NewKey(r.Entry.Key)) 202 | if err != nil { 203 | return fmt.Errorf("delete record: %w", err) 204 | } 205 | } 206 | } 207 | 208 | deadlineInByte := intoBytes(deadline) 209 | err = d.ds.Put(ctx, metaMinEpochKey, deadlineInByte) 210 | if err != nil { 211 | return err 212 | } 213 | } 214 | return nil 215 | } 216 | 217 | func intoBytes(v any) []byte { 218 | buf := bytes.NewBuffer([]byte{}) 219 | binary.Write(buf, binary.BigEndian, v) // nolint: errcheck 220 | return buf.Bytes() 221 | } 222 | 223 | func fromBytes(b []byte, v any) { 224 | buf := bytes.NewBuffer(b) 225 | binary.Read(buf, binary.BigEndian, v) // nolint: errcheck 226 | } 227 | 228 | // mergeMap cover src onto dst, the key in dst will overwrite the key in src 229 | func coverMap[K comparable, V any](before, after map[K]V) map[K]V { 230 | newMap := make(map[K]V) 231 | for k, v := range before { 232 | newMap[k] = v 233 | } 234 | for k, v := range after { 235 | newMap[k] = v 236 | } 237 | return newMap 238 | } 239 | -------------------------------------------------------------------------------- /node/modules/mine-recorder/record_test.go: -------------------------------------------------------------------------------- 1 | package minerecorder 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/filecoin-project/go-address" 8 | "github.com/filecoin-project/go-state-types/abi" 9 | "github.com/ipfs/go-datastore" 10 | leveldb "github.com/ipfs/go-ds-leveldb" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | var ( 15 | RecordExample = make(map[string]string) 16 | ) 17 | 18 | func TestDefaultRecorder(t *testing.T) { 19 | ctx := context.Background() 20 | 21 | t.Run("record and get", func(t *testing.T) { 22 | db := createDatastore(t) 23 | r := NewDefaultRecorder(db) 24 | defer db.Close() 25 | 26 | err := r.Record(ctx, newIDAddress(1), abi.ChainEpoch(0), RecordExample) 27 | require.NoError(t, err) 28 | ret, err := r.get(ctx, newIDAddress(1), abi.ChainEpoch(0)) 29 | require.NoError(t, err) 30 | require.Equal(t, RecordExample, ret) 31 | }) 32 | 33 | t.Run("query record not exist", func(t *testing.T) { 34 | db := createDatastore(t) 35 | r := NewDefaultRecorder(db) 36 | defer db.Close() 37 | 38 | _, err := r.get(ctx, newIDAddress(1), abi.ChainEpoch(9)) 39 | require.ErrorIs(t, err, ErrorRecordNotFound) 40 | 41 | err = r.Record(ctx, newIDAddress(1), abi.ChainEpoch(9), RecordExample) 42 | require.NoError(t, err) 43 | err = r.Record(ctx, newIDAddress(1), abi.ChainEpoch(10), RecordExample) 44 | require.NoError(t, err) 45 | err = r.Record(ctx, newIDAddress(1), abi.ChainEpoch(11), RecordExample) 46 | require.NoError(t, err) 47 | err = r.Record(ctx, newIDAddress(1), abi.ChainEpoch(14), RecordExample) 48 | require.NoError(t, err) 49 | 50 | rets, err := r.Query(ctx, newIDAddress(1), abi.ChainEpoch(5), 10) 51 | require.NoError(t, err) 52 | require.Equal(t, 4, len(rets)) 53 | }) 54 | 55 | t.Run("query exceed MaxRecordPerQuery", func(t *testing.T) { 56 | db := createDatastore(t) 57 | r := NewDefaultRecorder(db) 58 | defer db.Close() 59 | 60 | _, err := r.Query(ctx, newIDAddress(1), abi.ChainEpoch(0), MaxRecordPerQuery+1) 61 | require.ErrorIs(t, err, ErrorExceedMaxRecordPerQuery) 62 | }) 63 | 64 | t.Run("auto clean", func(t *testing.T) { 65 | ExpireEpoch = abi.ChainEpoch(10) 66 | db := createDatastore(t) 67 | r := NewDefaultRecorder(db) 68 | defer db.Close() 69 | 70 | // pre fill 71 | err := r.Record(ctx, newIDAddress(1), abi.ChainEpoch(0), RecordExample) 72 | require.NoError(t, err) 73 | err = r.Record(ctx, newIDAddress(2), abi.ChainEpoch(0), RecordExample) 74 | require.NoError(t, err) 75 | 76 | ret, err := r.get(ctx, newIDAddress(1), abi.ChainEpoch(0)) 77 | require.NoError(t, err) 78 | require.Equal(t, RecordExample, ret) 79 | 80 | // approach expiration 81 | err = r.Record(ctx, newIDAddress(1), abi.ChainEpoch(ExpireEpoch), RecordExample) 82 | require.NoError(t, err) 83 | 84 | ret, err = r.get(ctx, newIDAddress(1), abi.ChainEpoch(0)) 85 | require.NoError(t, err) 86 | require.Equal(t, RecordExample, ret) 87 | 88 | // exceed expiration 89 | err = r.Record(ctx, newIDAddress(1), abi.ChainEpoch(ExpireEpoch+1), RecordExample) 90 | require.NoError(t, err) 91 | 92 | _, err = r.get(ctx, newIDAddress(1), abi.ChainEpoch(0)) 93 | require.ErrorIs(t, err, ErrorRecordNotFound) 94 | _, err = r.get(ctx, newIDAddress(2), abi.ChainEpoch(0)) 95 | require.ErrorIs(t, err, ErrorRecordNotFound) 96 | 97 | ret, err = r.get(ctx, newIDAddress(1), abi.ChainEpoch(ExpireEpoch)) 98 | require.NoError(t, err) 99 | require.Equal(t, RecordExample, ret) 100 | }) 101 | 102 | t.Run("sub recorder", func(t *testing.T) { 103 | db := createDatastore(t) 104 | SetDatastore(db) 105 | 106 | rcd := Sub(newIDAddress(1), abi.ChainEpoch(0)) 107 | rcd.Record(ctx, Records{"key": "val"}) 108 | 109 | ret, err := Query(ctx, newIDAddress(1), abi.ChainEpoch(0), 1) 110 | require.NoError(t, err) 111 | require.Equal(t, 1, len(ret)) 112 | require.Equal(t, "val", ret[0]["key"]) 113 | }) 114 | } 115 | 116 | func TestCoverMap(t *testing.T) { 117 | t.Run("cover map", func(t *testing.T) { 118 | before := Records{ 119 | "key1": "val1", 120 | "key2": "val2", 121 | } 122 | after := Records{ 123 | "key1": "val4", 124 | "key3": "val3", 125 | } 126 | ret := coverMap(before, after) 127 | require.Equal(t, Records{ 128 | "key1": "val4", 129 | "key2": "val2", 130 | "key3": "val3", 131 | }, ret) 132 | }) 133 | } 134 | 135 | func createDatastore(t testing.TB) datastore.Batching { 136 | path := t.TempDir() + "/leveldb" 137 | db, err := leveldb.NewDatastore(path, nil) 138 | require.NoError(t, err) 139 | return db 140 | } 141 | 142 | func newIDAddress(id uint64) address.Address { 143 | ret, err := address.NewIDAddress(id) 144 | if err != nil { 145 | panic("create id address fail") 146 | } 147 | return ret 148 | } 149 | 150 | func BenchmarkQuery(b *testing.B) { 151 | ctx := context.Background() 152 | db := createDatastore(b) 153 | r := NewDefaultRecorder(db) 154 | defer db.Close() 155 | 156 | for i := 0; i < 1000*000*1000; i++ { 157 | err := r.Record(ctx, newIDAddress(1), abi.ChainEpoch(i), RecordExample) 158 | require.NoError(b, err) 159 | } 160 | 161 | b.ResetTimer() 162 | for i := 0; i < b.N; i++ { 163 | _, err := r.Query(ctx, newIDAddress(1), abi.ChainEpoch(0), 2000) 164 | require.NoError(b, err) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /node/modules/miner-manager/api.go: -------------------------------------------------------------------------------- 1 | package miner_manager 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/filecoin-project/go-address" 7 | 8 | "github.com/ipfs-force-community/sophon-miner/types" 9 | ) 10 | 11 | //go:generate go run github.com/golang/mock/mockgen -destination=mock/miner_manager.go -package=mock . MinerManageAPI 12 | 13 | type MinerManageAPI interface { 14 | Has(ctx context.Context, mAddr address.Address) bool 15 | Get(ctx context.Context, mAddr address.Address) (*types.MinerInfo, error) 16 | IsOpenMining(ctx context.Context, mAddr address.Address) bool 17 | OpenMining(ctx context.Context, mAddr address.Address) (*types.MinerInfo, error) 18 | CloseMining(ctx context.Context, mAddr address.Address) error 19 | List(ctx context.Context) (map[address.Address]*types.MinerInfo, error) 20 | Update(ctx context.Context, skip, limit int64) (map[address.Address]*types.MinerInfo, error) 21 | } 22 | -------------------------------------------------------------------------------- /node/modules/miner-manager/auth_manager.go: -------------------------------------------------------------------------------- 1 | package miner_manager 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/filecoin-project/go-address" 9 | logging "github.com/ipfs/go-log/v2" 10 | 11 | "github.com/ipfs-force-community/sophon-auth/jwtclient" 12 | 13 | "github.com/ipfs-force-community/sophon-miner/types" 14 | ) 15 | 16 | const CoMinersLimit = 20000 17 | 18 | var ( 19 | ErrNotFound = fmt.Errorf("not found") 20 | ) 21 | 22 | var log = logging.Logger("auth-miners") 23 | 24 | type MinerManage struct { 25 | authClient jwtclient.IAuthClient 26 | 27 | miners map[address.Address]*types.MinerInfo 28 | lk sync.Mutex 29 | } 30 | 31 | func NewVenusAuth(url, token string) func() (jwtclient.IAuthClient, error) { 32 | return func() (jwtclient.IAuthClient, error) { 33 | return jwtclient.NewAuthClient(url, token) 34 | } 35 | } 36 | 37 | func NewMinerManager(authClient jwtclient.IAuthClient) (MinerManageAPI, error) { 38 | m := &MinerManage{authClient: authClient} 39 | _, err := m.Update(context.TODO(), 0, 0) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | return m, nil 45 | } 46 | 47 | func (m *MinerManage) Has(ctx context.Context, mAddr address.Address) bool { 48 | m.lk.Lock() 49 | defer m.lk.Unlock() 50 | 51 | _, ok := m.miners[mAddr] 52 | return ok 53 | } 54 | 55 | func (m *MinerManage) Get(ctx context.Context, mAddr address.Address) (*types.MinerInfo, error) { 56 | m.lk.Lock() 57 | defer m.lk.Unlock() 58 | 59 | if _, ok := m.miners[mAddr]; ok { 60 | return m.miners[mAddr], nil 61 | } 62 | 63 | return nil, ErrNotFound 64 | } 65 | 66 | func (m *MinerManage) IsOpenMining(ctx context.Context, mAddr address.Address) bool { 67 | m.lk.Lock() 68 | defer m.lk.Unlock() 69 | 70 | if _, ok := m.miners[mAddr]; ok { 71 | return m.miners[mAddr].OpenMining 72 | } 73 | 74 | return false 75 | } 76 | 77 | func (m *MinerManage) OpenMining(ctx context.Context, mAddr address.Address) (*types.MinerInfo, error) { 78 | m.lk.Lock() 79 | defer m.lk.Unlock() 80 | 81 | if minerInfo, ok := m.miners[mAddr]; ok { 82 | _, err := m.authClient.UpsertMiner(ctx, minerInfo.Name, minerInfo.Addr.String(), true) 83 | if err != nil { 84 | return nil, err 85 | } 86 | minerInfo.OpenMining = true 87 | return minerInfo, nil 88 | } 89 | 90 | return nil, ErrNotFound 91 | } 92 | 93 | func (m *MinerManage) CloseMining(ctx context.Context, mAddr address.Address) error { 94 | m.lk.Lock() 95 | defer m.lk.Unlock() 96 | 97 | if minerInfo, ok := m.miners[mAddr]; ok { 98 | _, err := m.authClient.UpsertMiner(ctx, minerInfo.Name, minerInfo.Addr.String(), false) 99 | if err != nil { 100 | return err 101 | } 102 | minerInfo.OpenMining = false 103 | return nil 104 | } 105 | 106 | return ErrNotFound 107 | } 108 | 109 | func (m *MinerManage) List(ctx context.Context) (map[address.Address]*types.MinerInfo, error) { 110 | m.lk.Lock() 111 | defer m.lk.Unlock() 112 | 113 | return m.miners, nil 114 | } 115 | 116 | func (m *MinerManage) Update(ctx context.Context, skip, limit int64) (map[address.Address]*types.MinerInfo, error) { 117 | m.lk.Lock() 118 | defer m.lk.Unlock() 119 | 120 | if limit == 0 { 121 | limit = CoMinersLimit 122 | } 123 | 124 | users, err := m.authClient.ListUsersWithMiners(ctx, skip, limit, 0) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | miners := make(map[address.Address]*types.MinerInfo, 0) 130 | for _, user := range users { 131 | if user.State != 1 { 132 | log.Warnf("user: %s state is disabled, it's miners won't be updated", user.Name) 133 | continue 134 | } 135 | 136 | for _, miner := range user.Miners { 137 | miners[miner.Miner] = &types.MinerInfo{ 138 | Addr: miner.Miner, 139 | Id: user.Id, 140 | Name: miner.User, 141 | OpenMining: miner.OpenMining, 142 | } 143 | } 144 | } 145 | m.miners = miners 146 | 147 | return m.miners, nil 148 | } 149 | 150 | var _ MinerManageAPI = &MinerManage{} 151 | -------------------------------------------------------------------------------- /node/modules/miner-manager/mock/miner_manager.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/ipfs-force-community/sophon-miner/node/modules/miner-manager (interfaces: MinerManageAPI) 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | address "github.com/filecoin-project/go-address" 12 | gomock "github.com/golang/mock/gomock" 13 | types "github.com/ipfs-force-community/sophon-miner/types" 14 | ) 15 | 16 | // MockMinerManageAPI is a mock of MinerManageAPI interface. 17 | type MockMinerManageAPI struct { 18 | ctrl *gomock.Controller 19 | recorder *MockMinerManageAPIMockRecorder 20 | } 21 | 22 | // MockMinerManageAPIMockRecorder is the mock recorder for MockMinerManageAPI. 23 | type MockMinerManageAPIMockRecorder struct { 24 | mock *MockMinerManageAPI 25 | } 26 | 27 | // NewMockMinerManageAPI creates a new mock instance. 28 | func NewMockMinerManageAPI(ctrl *gomock.Controller) *MockMinerManageAPI { 29 | mock := &MockMinerManageAPI{ctrl: ctrl} 30 | mock.recorder = &MockMinerManageAPIMockRecorder{mock} 31 | return mock 32 | } 33 | 34 | // EXPECT returns an object that allows the caller to indicate expected use. 35 | func (m *MockMinerManageAPI) EXPECT() *MockMinerManageAPIMockRecorder { 36 | return m.recorder 37 | } 38 | 39 | // CloseMining mocks base method. 40 | func (m *MockMinerManageAPI) CloseMining(arg0 context.Context, arg1 address.Address) error { 41 | m.ctrl.T.Helper() 42 | ret := m.ctrl.Call(m, "CloseMining", arg0, arg1) 43 | ret0, _ := ret[0].(error) 44 | return ret0 45 | } 46 | 47 | // CloseMining indicates an expected call of CloseMining. 48 | func (mr *MockMinerManageAPIMockRecorder) CloseMining(arg0, arg1 interface{}) *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseMining", reflect.TypeOf((*MockMinerManageAPI)(nil).CloseMining), arg0, arg1) 51 | } 52 | 53 | // Get mocks base method. 54 | func (m *MockMinerManageAPI) Get(arg0 context.Context, arg1 address.Address) (*types.MinerInfo, error) { 55 | m.ctrl.T.Helper() 56 | ret := m.ctrl.Call(m, "Get", arg0, arg1) 57 | ret0, _ := ret[0].(*types.MinerInfo) 58 | ret1, _ := ret[1].(error) 59 | return ret0, ret1 60 | } 61 | 62 | // Get indicates an expected call of Get. 63 | func (mr *MockMinerManageAPIMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call { 64 | mr.mock.ctrl.T.Helper() 65 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockMinerManageAPI)(nil).Get), arg0, arg1) 66 | } 67 | 68 | // Has mocks base method. 69 | func (m *MockMinerManageAPI) Has(arg0 context.Context, arg1 address.Address) bool { 70 | m.ctrl.T.Helper() 71 | ret := m.ctrl.Call(m, "Has", arg0, arg1) 72 | ret0, _ := ret[0].(bool) 73 | return ret0 74 | } 75 | 76 | // Has indicates an expected call of Has. 77 | func (mr *MockMinerManageAPIMockRecorder) Has(arg0, arg1 interface{}) *gomock.Call { 78 | mr.mock.ctrl.T.Helper() 79 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Has", reflect.TypeOf((*MockMinerManageAPI)(nil).Has), arg0, arg1) 80 | } 81 | 82 | // IsOpenMining mocks base method. 83 | func (m *MockMinerManageAPI) IsOpenMining(arg0 context.Context, arg1 address.Address) bool { 84 | m.ctrl.T.Helper() 85 | ret := m.ctrl.Call(m, "IsOpenMining", arg0, arg1) 86 | ret0, _ := ret[0].(bool) 87 | return ret0 88 | } 89 | 90 | // IsOpenMining indicates an expected call of IsOpenMining. 91 | func (mr *MockMinerManageAPIMockRecorder) IsOpenMining(arg0, arg1 interface{}) *gomock.Call { 92 | mr.mock.ctrl.T.Helper() 93 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsOpenMining", reflect.TypeOf((*MockMinerManageAPI)(nil).IsOpenMining), arg0, arg1) 94 | } 95 | 96 | // List mocks base method. 97 | func (m *MockMinerManageAPI) List(arg0 context.Context) (map[address.Address]*types.MinerInfo, error) { 98 | m.ctrl.T.Helper() 99 | ret := m.ctrl.Call(m, "List", arg0) 100 | ret0, _ := ret[0].(map[address.Address]*types.MinerInfo) 101 | ret1, _ := ret[1].(error) 102 | return ret0, ret1 103 | } 104 | 105 | // List indicates an expected call of List. 106 | func (mr *MockMinerManageAPIMockRecorder) List(arg0 interface{}) *gomock.Call { 107 | mr.mock.ctrl.T.Helper() 108 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockMinerManageAPI)(nil).List), arg0) 109 | } 110 | 111 | // OpenMining mocks base method. 112 | func (m *MockMinerManageAPI) OpenMining(arg0 context.Context, arg1 address.Address) (*types.MinerInfo, error) { 113 | m.ctrl.T.Helper() 114 | ret := m.ctrl.Call(m, "OpenMining", arg0, arg1) 115 | ret0, _ := ret[0].(*types.MinerInfo) 116 | ret1, _ := ret[1].(error) 117 | return ret0, ret1 118 | } 119 | 120 | // OpenMining indicates an expected call of OpenMining. 121 | func (mr *MockMinerManageAPIMockRecorder) OpenMining(arg0, arg1 interface{}) *gomock.Call { 122 | mr.mock.ctrl.T.Helper() 123 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenMining", reflect.TypeOf((*MockMinerManageAPI)(nil).OpenMining), arg0, arg1) 124 | } 125 | 126 | // Update mocks base method. 127 | func (m *MockMinerManageAPI) Update(arg0 context.Context, arg1, arg2 int64) (map[address.Address]*types.MinerInfo, error) { 128 | m.ctrl.T.Helper() 129 | ret := m.ctrl.Call(m, "Update", arg0, arg1, arg2) 130 | ret0, _ := ret[0].(map[address.Address]*types.MinerInfo) 131 | ret1, _ := ret[1].(error) 132 | return ret0, ret1 133 | } 134 | 135 | // Update indicates an expected call of Update. 136 | func (mr *MockMinerManageAPIMockRecorder) Update(arg0, arg1, arg2 interface{}) *gomock.Call { 137 | mr.mock.ctrl.T.Helper() 138 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockMinerManageAPI)(nil).Update), arg0, arg1, arg2) 139 | } 140 | -------------------------------------------------------------------------------- /node/modules/miner.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/filecoin-project/venus/pkg/constants" 7 | chainV1API "github.com/filecoin-project/venus/venus-shared/api/chain/v1" 8 | "github.com/ipfs-force-community/sophon-miner/f3participant" 9 | "github.com/ipfs-force-community/sophon-miner/lib/journal" 10 | "github.com/ipfs-force-community/sophon-miner/miner" 11 | "github.com/ipfs-force-community/sophon-miner/node/config" 12 | "github.com/ipfs-force-community/sophon-miner/node/modules/helpers" 13 | minermanager "github.com/ipfs-force-community/sophon-miner/node/modules/miner-manager" 14 | "github.com/ipfs-force-community/sophon-miner/node/modules/slashfilter" 15 | logging "github.com/ipfs/go-log/v2" 16 | "go.uber.org/fx" 17 | ) 18 | 19 | var log = logging.Logger("modules/miner") 20 | 21 | func NewMinerProcessor(lc fx.Lifecycle, 22 | mCtx helpers.MetricsCtx, 23 | api chainV1API.FullNode, 24 | cfg *config.MinerConfig, 25 | sfAPI slashfilter.SlashFilterAPI, 26 | minerManager minermanager.MinerManageAPI, 27 | j journal.Journal, 28 | ) (miner.MiningAPI, error) { 29 | ctx := helpers.LifecycleCtx(mCtx, lc) 30 | m, err := miner.NewMiner(ctx, api, cfg, minerManager, sfAPI, j) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | lc.Append(fx.Hook{ 36 | OnStart: func(ctx context.Context) error { 37 | if err := m.Start(ctx); err != nil { 38 | return err 39 | } 40 | return nil 41 | }, 42 | OnStop: func(ctx context.Context) error { 43 | return m.Stop(ctx) 44 | }, 45 | }) 46 | 47 | return m, err 48 | } 49 | 50 | func NewMultiParticipant(lc fx.Lifecycle, 51 | mCtx helpers.MetricsCtx, 52 | cfg *config.MinerConfig, 53 | minerManager minermanager.MinerManageAPI, 54 | ) (*f3participant.MultiParticipant, error) { 55 | if cfg.F3Node == nil || len(cfg.F3Node.Addr) == 0 { 56 | log.Warnf("f3 node is not configured") 57 | return nil, nil 58 | } 59 | if constants.DisableF3 { 60 | log.Warnf("f3 is disabled") 61 | return nil, nil 62 | } 63 | 64 | node, close, err := chainV1API.DialFullNodeRPC(mCtx, cfg.F3Node.Addr, cfg.F3Node.Token, nil) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | mp, err := f3participant.NewMultiParticipant(mCtx, node, minerManager) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | lc.Append(fx.Hook{ 75 | OnStart: func(ctx context.Context) error { 76 | return mp.Start(ctx) 77 | }, 78 | OnStop: func(ctx context.Context) error { 79 | close() 80 | return mp.Stop(ctx) 81 | }, 82 | }) 83 | 84 | return mp, nil 85 | } 86 | -------------------------------------------------------------------------------- /node/modules/services.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import ( 4 | "context" 5 | 6 | "go.uber.org/fx" 7 | 8 | "github.com/ipfs-force-community/sophon-miner/lib/journal" 9 | "github.com/ipfs-force-community/sophon-miner/lib/journal/fsjournal" 10 | "github.com/ipfs-force-community/sophon-miner/node/repo" 11 | ) 12 | 13 | func OpenFilesystemJournal(lr repo.LockedRepo, lc fx.Lifecycle, disabled journal.DisabledEvents) (journal.Journal, error) { 14 | jrnl, err := fsjournal.OpenFSJournal(lr, disabled) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | lc.Append(fx.Hook{ 20 | OnStop: func(_ context.Context) error { return jrnl.Close() }, 21 | }) 22 | 23 | return jrnl, err 24 | } 25 | -------------------------------------------------------------------------------- /node/modules/slashfilter/api.go: -------------------------------------------------------------------------------- 1 | package slashfilter 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/filecoin-project/go-state-types/abi" 8 | "github.com/ipfs-force-community/sophon-miner/types" 9 | 10 | vtypes "github.com/filecoin-project/venus/venus-shared/types" 11 | ) 12 | 13 | type BlockStoreType string 14 | 15 | const ( 16 | Local BlockStoreType = "local" 17 | MySQL BlockStoreType = "mysql" 18 | ) 19 | 20 | type SlashFilterAPI interface { 21 | HasBlock(ctx context.Context, bh *vtypes.BlockHeader) (bool, error) 22 | MinedBlock(ctx context.Context, bh *vtypes.BlockHeader, parentEpoch abi.ChainEpoch) error 23 | PutBlock(ctx context.Context, bh *vtypes.BlockHeader, parentEpoch abi.ChainEpoch, t time.Time, state types.StateMining) error 24 | ListBlock(ctx context.Context, params *types.BlocksQueryParams) ([]MinedBlock, error) 25 | } 26 | -------------------------------------------------------------------------------- /node/modules/slashfilter/local.go: -------------------------------------------------------------------------------- 1 | package slashfilter 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/ipfs/go-cid" 9 | "github.com/ipfs/go-datastore" 10 | "github.com/ipfs/go-datastore/namespace" 11 | 12 | "github.com/filecoin-project/go-state-types/abi" 13 | 14 | vtypes "github.com/filecoin-project/venus/venus-shared/types" 15 | 16 | "github.com/ipfs-force-community/sophon-miner/types" 17 | ) 18 | 19 | type localSlashFilter struct { 20 | byEpoch datastore.Datastore // double-fork mining faults, parent-grinding fault 21 | byParents datastore.Datastore // time-offset mining faults 22 | } 23 | 24 | func NewLocal(ds types.MetadataDS) SlashFilterAPI { 25 | return &localSlashFilter{ 26 | byEpoch: namespace.Wrap(ds, datastore.NewKey("/slashfilter/epoch")), 27 | byParents: namespace.Wrap(ds, datastore.NewKey("/slashfilter/parents")), 28 | } 29 | } 30 | 31 | func (f *localSlashFilter) HasBlock(ctx context.Context, bh *vtypes.BlockHeader) (bool, error) { 32 | epochKey := datastore.NewKey(fmt.Sprintf("/%s/%d", bh.Miner, bh.Height)) 33 | 34 | return f.byEpoch.Has(ctx, epochKey) 35 | } 36 | 37 | func (f *localSlashFilter) PutBlock(ctx context.Context, bh *vtypes.BlockHeader, parentEpoch abi.ChainEpoch, t time.Time, state types.StateMining) error { 38 | // Only successful block generation is recorded locally 39 | if state != types.Success { 40 | return nil 41 | } 42 | 43 | parentsKey := datastore.NewKey(fmt.Sprintf("/%s/%x", bh.Miner, vtypes.NewTipSetKey(bh.Parents...).Bytes())) 44 | if err := f.byParents.Put(ctx, parentsKey, bh.Cid().Bytes()); err != nil { 45 | return fmt.Errorf("putting byEpoch entry: %w", err) 46 | } 47 | 48 | epochKey := datastore.NewKey(fmt.Sprintf("/%s/%d", bh.Miner, bh.Height)) 49 | if err := f.byEpoch.Put(ctx, epochKey, bh.Cid().Bytes()); err != nil { 50 | return fmt.Errorf("putting byEpoch entry: %w", err) 51 | } 52 | 53 | return nil 54 | } 55 | 56 | func (f *localSlashFilter) MinedBlock(ctx context.Context, bh *vtypes.BlockHeader, parentEpoch abi.ChainEpoch) error { 57 | // double-fork mining (2 blocks at one epoch) --> HasBlock 58 | 59 | parentsKey := datastore.NewKey(fmt.Sprintf("/%s/%x", bh.Miner, vtypes.NewTipSetKey(bh.Parents...).Bytes())) 60 | { 61 | // time-offset mining faults (2 blocks with the same parents) 62 | if err := checkFault(ctx, f.byParents, parentsKey, bh, "time-offset mining faults"); err != nil { 63 | return err 64 | } 65 | } 66 | 67 | { 68 | // parent-grinding fault (didn't mine on top of our own block) 69 | 70 | // First check if we have mined a block on the parent epoch 71 | parentEpochKey := datastore.NewKey(fmt.Sprintf("/%s/%d", bh.Miner, parentEpoch)) 72 | have, err := f.byEpoch.Has(ctx, parentEpochKey) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | if have { 78 | // If we had, make sure it's in our parent tipset 79 | cidb, err := f.byEpoch.Get(ctx, parentEpochKey) 80 | if err != nil { 81 | return fmt.Errorf("getting other block cid: %w", err) 82 | } 83 | 84 | _, parent, err := cid.CidFromBytes(cidb) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | var found bool 90 | for _, c := range bh.Parents { 91 | if c.Equals(parent) { 92 | found = true 93 | } 94 | } 95 | 96 | if !found { 97 | return fmt.Errorf("produced block would trigger 'parent-grinding fault' consensus fault; miner: %s; bh: %s, expected parent: %s", bh.Miner, bh.Cid(), parent) 98 | } 99 | } 100 | } 101 | 102 | return nil 103 | } 104 | 105 | func (f *localSlashFilter) ListBlock(ctx context.Context, params *types.BlocksQueryParams) ([]MinedBlock, error) { 106 | return nil, fmt.Errorf("you are using levelDB, List Block is not supported") 107 | } 108 | 109 | func checkFault(ctx context.Context, t datastore.Datastore, key datastore.Key, bh *vtypes.BlockHeader, faultType string) error { 110 | fault, err := t.Has(ctx, key) 111 | if err != nil { 112 | return err 113 | } 114 | 115 | if fault { 116 | cidb, err := t.Get(ctx, key) 117 | if err != nil { 118 | return fmt.Errorf("getting other block cid: %w", err) 119 | } 120 | 121 | _, other, err := cid.CidFromBytes(cidb) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | if other == bh.Cid() { 127 | return nil 128 | } 129 | 130 | return fmt.Errorf("produced block would trigger '%s' consensus fault; miner: %s; bh: %s, other: %s", faultType, bh.Miner, bh.Cid(), other) 131 | } 132 | 133 | return nil 134 | } 135 | -------------------------------------------------------------------------------- /node/modules/slashfilter/mock.go: -------------------------------------------------------------------------------- 1 | package slashfilter 2 | 3 | import ( 4 | "github.com/ipfs/go-datastore" 5 | "gorm.io/driver/sqlite" 6 | "gorm.io/gorm" 7 | "gorm.io/gorm/logger" 8 | ) 9 | 10 | func NewMysqlMock() (SlashFilterAPI, *gorm.DB, error) { 11 | db, err := gorm.Open( 12 | sqlite.Open(":memory:"), 13 | &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) 14 | 15 | if err != nil { 16 | return nil, nil, err 17 | } 18 | 19 | sqlDb, err := db.DB() 20 | if err != nil { 21 | return nil, nil, err 22 | } 23 | 24 | // mock db work only allow one connection 25 | sqlDb.SetMaxOpenConns(1) 26 | 27 | if err := db.AutoMigrate(&MinedBlock{}); err != nil { 28 | return nil, nil, err 29 | } 30 | 31 | return &mysqlSlashFilter{_db: db}, db, nil 32 | } 33 | 34 | func NewLocalMock() (SlashFilterAPI, datastore.Datastore, error) { 35 | ds := datastore.NewMapDatastore() 36 | return NewLocal(ds), ds, nil 37 | } 38 | -------------------------------------------------------------------------------- /node/modules/slashfilter/slashfilter_test.go: -------------------------------------------------------------------------------- 1 | // stm: #uinit 2 | package slashfilter 3 | 4 | import ( 5 | "context" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/ipfs-force-community/sophon-miner/types" 11 | "github.com/ipfs/go-datastore" 12 | "github.com/ipfs/go-datastore/query" 13 | 14 | "github.com/filecoin-project/go-address" 15 | "github.com/filecoin-project/go-state-types/abi" 16 | "github.com/ipfs/go-cid" 17 | "github.com/stretchr/testify/require" 18 | 19 | "github.com/filecoin-project/venus/venus-shared/testutil" 20 | venusTypes "github.com/filecoin-project/venus/venus-shared/types" 21 | ) 22 | 23 | func wrapper(f func(*testing.T, SlashFilterAPI, func() error), sf SlashFilterAPI, clear func() error) func(t *testing.T) { 24 | return func(t *testing.T) { 25 | f(t, sf, clear) 26 | } 27 | } 28 | 29 | func makeMysqlTestCtx(t *testing.T) (SlashFilterAPI, func() error) { 30 | sf, db, err := NewMysqlMock() 31 | require.NoError(t, err) 32 | clear := func() error { 33 | return db.Delete(&MinedBlock{}, "1 = 1").Error 34 | } 35 | return sf, clear 36 | } 37 | 38 | func makeLocalTestCtx(t *testing.T) (SlashFilterAPI, func() error) { 39 | sf, ds, err := NewLocalMock() 40 | require.NoError(t, err) 41 | 42 | ctx := context.TODO() 43 | 44 | clear := func() error { 45 | rs, err := ds.Query(ctx, query.Query{Prefix: "/"}) 46 | if err != nil { 47 | return err 48 | } 49 | for { 50 | res, ok := rs.NextSync() 51 | if !ok { 52 | break 53 | } 54 | if err := ds.Delete(ctx, datastore.NewKey(res.Key)); err != nil { 55 | return err 56 | } 57 | } 58 | return nil 59 | } 60 | return sf, clear 61 | } 62 | 63 | func checkHasBlock(t *testing.T, api SlashFilterAPI, bh *venusTypes.BlockHeader, expect bool) { 64 | ctx := context.TODO() 65 | exists, err := api.HasBlock(ctx, bh) 66 | require.NoError(t, err) 67 | require.Equal(t, exists, expect) 68 | } 69 | 70 | func TestSlashFilter(t *testing.T) { 71 | // stm: @VENUSMINER_NODE_MODULES_AUTH_MANAGER_MINED_BLOCK_001 72 | t.Run("mysql slash filter tests", func(t *testing.T) { 73 | sf, clear := makeMysqlTestCtx(t) 74 | t.Run("put-block", wrapper(testPutBlock, sf, clear)) 75 | t.Run("mined-block", wrapper(testMinedBlock, sf, clear)) 76 | }) 77 | t.Run("local slash filter tests", func(t *testing.T) { 78 | sf, clear := makeLocalTestCtx(t) 79 | t.Run("put-block", wrapper(testPutBlock, sf, clear)) 80 | t.Run("mined-block", wrapper(testMinedBlock, sf, clear)) 81 | }) 82 | } 83 | 84 | func testPutBlock(t *testing.T, sf SlashFilterAPI, clear func() error) { 85 | require.NoError(t, clear()) 86 | 87 | const mdata = "f021344" 88 | maddr, err := address.NewFromString(mdata) 89 | require.NoErrorf(t, err, "parse miner address %s", mdata) 90 | 91 | var parents []cid.Cid 92 | testutil.Provide(t, &parents, testutil.WithSliceLen(2)) 93 | 94 | mockCid, _ := cid.Parse("bafkqaaa") 95 | 96 | h := abi.ChainEpoch(100) 97 | ph := abi.ChainEpoch(99) 98 | 99 | cases := []struct { 100 | bh *venusTypes.BlockHeader 101 | parentEpoch abi.ChainEpoch 102 | t time.Time 103 | state types.StateMining 104 | }{ 105 | { 106 | bh: &venusTypes.BlockHeader{ 107 | Parents: parents, 108 | Height: h, 109 | Miner: maddr, 110 | Ticket: nil, 111 | }, 112 | parentEpoch: ph, 113 | t: time.Now(), 114 | state: types.Mining, 115 | }, 116 | { 117 | bh: &venusTypes.BlockHeader{ 118 | Parents: parents, 119 | Height: h, 120 | Miner: maddr, 121 | Ticket: &venusTypes.Ticket{ 122 | VRFProof: []byte("====1====="), 123 | }, 124 | ParentStateRoot: mockCid, 125 | ParentMessageReceipts: mockCid, 126 | Messages: mockCid, 127 | }, 128 | parentEpoch: ph, 129 | t: time.Time{}, 130 | state: types.Success, 131 | }, 132 | } 133 | 134 | ctx := context.Background() 135 | 136 | // stm: @VENUSMINER_NODE_MODULES_SLASHFILTER_MYSQL_HAS_BLOCK_001, @VENUSMINER_NODE_MODULES_AUTH_MANAGER_PUT_BLOCK_001 137 | t.Run("new block", func(t *testing.T) { 138 | cs := cases[0] 139 | checkHasBlock(t, sf, cs.bh, false) 140 | require.NoError(t, sf.PutBlock(ctx, cs.bh, cs.parentEpoch, cs.t, cs.state)) 141 | // BlockHeader in case[0], don't have a ticket, `HasBlock` should return false. 142 | checkHasBlock(t, sf, cs.bh, false) 143 | }) 144 | 145 | t.Run("update block", func(t *testing.T) { 146 | cs := cases[1] 147 | err := sf.PutBlock(ctx, cs.bh, cs.parentEpoch, cs.t, cs.state) 148 | require.NoError(t, err) 149 | // After updated, block.Ticket is not nil, and its len(Cids) won't be zero. 150 | // hasBlock should be 'true' 151 | checkHasBlock(t, sf, cs.bh, true) 152 | }) 153 | } 154 | 155 | func testMinedBlock(t *testing.T, sf SlashFilterAPI, clear func() error) { 156 | const mdata = "f021344" 157 | maddr, err := address.NewFromString(mdata) 158 | require.NoErrorf(t, err, "parse miner address %s", mdata) 159 | 160 | var parents []cid.Cid 161 | testutil.Provide(t, &parents, testutil.WithSliceLen(2)) 162 | 163 | mockCid, _ := cid.Parse("bafkqaaa") 164 | bh := &venusTypes.BlockHeader{ 165 | Height: abi.ChainEpoch(100), 166 | Miner: maddr, 167 | Ticket: &venusTypes.Ticket{ 168 | VRFProof: []byte("====1====="), 169 | }, 170 | Parents: parents, 171 | ParentStateRoot: mockCid, 172 | ParentMessageReceipts: mockCid, 173 | Messages: mockCid, 174 | } 175 | ph := abi.ChainEpoch(99) 176 | 177 | ctx := context.Background() 178 | 179 | t.Run("general", func(t *testing.T) { 180 | require.NoError(t, clear()) 181 | err = sf.MinedBlock(ctx, bh, ph) 182 | require.Nil(t, err) 183 | }) 184 | 185 | t.Run("time-offset mining", func(t *testing.T) { 186 | require.NoError(t, clear()) 187 | require.NoError(t, sf.PutBlock(ctx, bh, ph, time.Now(), types.Success)) 188 | 189 | // change bh.Message to simulate case of time-offset mining fault(2 blocks with the same parents) 190 | testutil.Provide(t, &bh.Messages) 191 | err = sf.MinedBlock(ctx, bh, ph) 192 | require.Error(t, err) 193 | require.Greater(t, strings.Index(err.Error(), "time-offset"), 0) 194 | }) 195 | 196 | t.Run("parent-grinding", func(t *testing.T) { 197 | require.NoError(t, clear()) 198 | err = sf.PutBlock(ctx, bh, ph, time.Now(), types.Success) 199 | require.NoError(t, err) 200 | 201 | blockId := bh.Cid() 202 | 203 | // simulate case of "parent griding" fault 204 | // parents of new block header don't include previous block, 205 | testutil.Provide(t, &bh.Parents, testutil.WithSliceLen(2)) 206 | 207 | bh.Height = bh.Height + 1 208 | err = sf.MinedBlock(ctx, bh, ph+1) 209 | require.Error(t, err) 210 | require.Greater(t, strings.Index(err.Error(), "parent-grinding fault"), 0) 211 | 212 | // include parent block into its `parents`, expect success.. 213 | bh.Parents = append(bh.Parents, blockId) 214 | err = sf.MinedBlock(ctx, bh, ph+1) 215 | require.NoError(t, err) 216 | }) 217 | } 218 | -------------------------------------------------------------------------------- /node/modules/storage.go: -------------------------------------------------------------------------------- 1 | package modules 2 | 3 | import ( 4 | "context" 5 | 6 | types2 "github.com/ipfs-force-community/sophon-miner/types" 7 | 8 | "go.uber.org/fx" 9 | 10 | "github.com/ipfs-force-community/sophon-miner/node/modules/helpers" 11 | "github.com/ipfs-force-community/sophon-miner/node/repo" 12 | ) 13 | 14 | func LockedRepo(lr repo.LockedRepo) func(lc fx.Lifecycle) repo.LockedRepo { 15 | return func(lc fx.Lifecycle) repo.LockedRepo { 16 | lc.Append(fx.Hook{ 17 | OnStop: func(_ context.Context) error { 18 | return lr.Close() 19 | }, 20 | }) 21 | 22 | return lr 23 | } 24 | } 25 | 26 | func Datastore(lc fx.Lifecycle, mctx helpers.MetricsCtx, r repo.LockedRepo) (types2.MetadataDS, error) { 27 | ctx := helpers.LifecycleCtx(mctx, lc) 28 | mds, err := r.Datastore(ctx, "/metadata") 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | return mds, nil 34 | } 35 | -------------------------------------------------------------------------------- /node/options.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "reflect" 5 | 6 | "go.uber.org/fx" 7 | ) 8 | 9 | // Option is a functional option which can be used with the New function to 10 | // change how the node is constructed 11 | // 12 | // Options are applied in sequence 13 | type Option func(*Settings) error 14 | 15 | // Options groups multiple options into one 16 | func Options(opts ...Option) Option { 17 | return func(s *Settings) error { 18 | for _, opt := range opts { 19 | if err := opt(s); err != nil { 20 | return err 21 | } 22 | } 23 | return nil 24 | } 25 | } 26 | 27 | // Error is a special option which returns an error when applied 28 | func Error(err error) Option { 29 | return func(_ *Settings) error { 30 | return err 31 | } 32 | } 33 | 34 | func ApplyIf(check func(s *Settings) bool, opts ...Option) Option { 35 | return func(s *Settings) error { 36 | if check(s) { 37 | return Options(opts...)(s) 38 | } 39 | return nil 40 | } 41 | } 42 | 43 | func If(b bool, opts ...Option) Option { 44 | return ApplyIf(func(s *Settings) bool { 45 | return b 46 | }, opts...) 47 | } 48 | 49 | // Override option changes constructor for a given type 50 | func Override(typ, constructor interface{}) Option { 51 | return func(s *Settings) error { 52 | if i, ok := typ.(invoke); ok { 53 | s.invokes[i] = fx.Invoke(constructor) 54 | return nil 55 | } 56 | 57 | if c, ok := typ.(special); ok { 58 | s.modules[c] = fx.Provide(constructor) 59 | return nil 60 | } 61 | ctor := as(constructor, typ) 62 | rt := reflect.TypeOf(typ).Elem() 63 | 64 | s.modules[rt] = fx.Provide(ctor) 65 | return nil 66 | } 67 | } 68 | 69 | func Unset(typ interface{}) Option { 70 | return func(s *Settings) error { 71 | if i, ok := typ.(invoke); ok { 72 | s.invokes[i] = nil 73 | return nil 74 | } 75 | 76 | if c, ok := typ.(special); ok { 77 | delete(s.modules, c) 78 | return nil 79 | } 80 | rt := reflect.TypeOf(typ).Elem() 81 | 82 | delete(s.modules, rt) 83 | return nil 84 | } 85 | } 86 | 87 | // From(*T) -> func(t T) T {return t} 88 | func From(typ interface{}) interface{} { 89 | rt := []reflect.Type{reflect.TypeOf(typ).Elem()} 90 | ft := reflect.FuncOf(rt, rt, false) 91 | return reflect.MakeFunc(ft, func(args []reflect.Value) (results []reflect.Value) { 92 | return args 93 | }).Interface() 94 | } 95 | 96 | // from go-ipfs 97 | // as casts input constructor to a given interface (if a value is given, it 98 | // wraps it into a constructor). 99 | // 100 | // Note: this method may look like a hack, and in fact it is one. 101 | // This is here only because https://github.com/uber-go/fx/issues/673 wasn't 102 | // released yet 103 | // 104 | // Note 2: when making changes here, make sure this method stays at 105 | // 100% coverage. This makes it less likely it will be terribly broken 106 | func as(in interface{}, as interface{}) interface{} { 107 | outType := reflect.TypeOf(as) 108 | 109 | if outType.Kind() != reflect.Ptr { 110 | panic("outType is not a pointer") 111 | } 112 | 113 | if reflect.TypeOf(in).Kind() != reflect.Func { 114 | ctype := reflect.FuncOf(nil, []reflect.Type{outType.Elem()}, false) 115 | 116 | return reflect.MakeFunc(ctype, func(args []reflect.Value) (results []reflect.Value) { 117 | out := reflect.New(outType.Elem()) 118 | out.Elem().Set(reflect.ValueOf(in)) 119 | 120 | return []reflect.Value{out.Elem()} 121 | }).Interface() 122 | } 123 | 124 | inType := reflect.TypeOf(in) 125 | 126 | ins := make([]reflect.Type, inType.NumIn()) 127 | outs := make([]reflect.Type, inType.NumOut()) 128 | 129 | for i := range ins { 130 | ins[i] = inType.In(i) 131 | } 132 | outs[0] = outType.Elem() 133 | for i := range outs[1:] { 134 | outs[i+1] = inType.Out(i + 1) 135 | } 136 | 137 | ctype := reflect.FuncOf(ins, outs, false) 138 | 139 | return reflect.MakeFunc(ctype, func(args []reflect.Value) (results []reflect.Value) { 140 | outs := reflect.ValueOf(in).Call(args) 141 | 142 | out := reflect.New(outType.Elem()) 143 | if outs[0].Type().AssignableTo(outType.Elem()) { 144 | // Out: Iface = In: *Struct; Out: Iface = In: OtherIface 145 | out.Elem().Set(outs[0]) 146 | } else { 147 | // Out: Iface = &(In: Struct) 148 | t := reflect.New(outs[0].Type()) 149 | t.Elem().Set(outs[0]) 150 | out.Elem().Set(t) 151 | } 152 | outs[0] = out.Elem() 153 | 154 | return outs 155 | }).Interface() 156 | } 157 | -------------------------------------------------------------------------------- /node/repo/fsrepo_ds.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | ldbopts "github.com/syndtr/goleveldb/leveldb/opt" 10 | 11 | "github.com/ipfs/go-datastore" 12 | levelds "github.com/ipfs/go-ds-leveldb" 13 | measure "github.com/ipfs/go-ds-measure" 14 | ) 15 | 16 | type dsCtor func(path string, readonly bool) (datastore.Batching, error) 17 | 18 | var fsDatastores = map[string]dsCtor{ 19 | "metadata": levelDs, 20 | } 21 | 22 | func levelDs(path string, readonly bool) (datastore.Batching, error) { 23 | return levelds.NewDatastore(path, &levelds.Options{ 24 | Compression: ldbopts.NoCompression, 25 | NoSync: false, 26 | Strict: ldbopts.StrictAll, 27 | ReadOnly: readonly, 28 | }) 29 | } 30 | 31 | func (fsr *fsLockedRepo) openDatastores(readonly bool) (map[string]datastore.Batching, error) { 32 | if err := os.MkdirAll(fsr.join(fsDatastore), 0755); err != nil { 33 | return nil, fmt.Errorf("mkdir %s: %w", fsr.join(fsDatastore), err) 34 | } 35 | 36 | out := map[string]datastore.Batching{} 37 | 38 | for p, ctor := range fsDatastores { 39 | prefix := datastore.NewKey(p) 40 | 41 | // TODO: optimization: don't init datastores we don't need 42 | ds, err := ctor(fsr.join(filepath.Join(fsDatastore, p)), readonly) 43 | if err != nil { 44 | return nil, fmt.Errorf("opening datastore %s: %w", prefix, err) 45 | } 46 | 47 | ds = measure.New("fsrepo."+p, ds) 48 | 49 | out[datastore.NewKey(p).String()] = ds 50 | } 51 | 52 | return out, nil 53 | } 54 | 55 | func (fsr *fsLockedRepo) Datastore(_ context.Context, ns string) (datastore.Batching, error) { 56 | fsr.dsOnce.Do(func() { 57 | fsr.ds, fsr.dsErr = fsr.openDatastores(fsr.readonly) 58 | }) 59 | 60 | if fsr.dsErr != nil { 61 | return nil, fsr.dsErr 62 | } 63 | ds, ok := fsr.ds[ns] 64 | if ok { 65 | return ds, nil 66 | } 67 | return nil, fmt.Errorf("no such datastore: %s", ns) 68 | } 69 | -------------------------------------------------------------------------------- /node/repo/fsrepo_test.go: -------------------------------------------------------------------------------- 1 | // stm: #unit 2 | package repo 3 | 4 | import ( 5 | "os" 6 | "testing" 7 | 8 | "github.com/ipfs-force-community/sophon-miner/node/config" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func genFsRepo(t *testing.T) (*FsRepo, func()) { 13 | path, err := os.MkdirTemp("", "venus-repo-") 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | 18 | repo, err := NewFS(path) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | err = repo.Init() 24 | if err != ErrRepoExists && err != nil { 25 | t.Fatal(err) 26 | } 27 | return repo, func() { 28 | _ = os.RemoveAll(path) 29 | } 30 | } 31 | 32 | func TestFsBasic(t *testing.T) { 33 | // stm: @VENUSMINER_NODE_REPO_FSREPO_INIT_001 34 | repo, closer := genFsRepo(t) 35 | 36 | // stm: @VENUSMINER_NODE_REPO_FSREPO_EXISTS_001 37 | exists, err := repo.Exists() 38 | assert.NoError(t, err) 39 | assert.True(t, exists) 40 | 41 | cfg := config.DefaultMinerConfig() 42 | // stm: @VENUSMINER_NODE_REPO_FSREPO_UPDATE_001 43 | err = repo.Update(cfg) 44 | assert.NoError(t, err) 45 | 46 | defer closer() 47 | // stm: @VENUSMINER_NODE_REPO_FSREPO_API_ENDPOINT_001, @VENUSMINER_NODE_REPO_FSREPO_API_TOKEN_001, @VENUSMINER_NODE_REPO_FSREPO_API_CONFIG_001, @VENUSMINER_NODE_REPO_FSREPO_LOCK_001, @VENUSMINER_NODE_REPO_FSREPO_PATH_001, @VENUSMINER_NODE_REPO_FSREPO_CLOSE_001, @VENUSMINER_NODE_REPO_FSREPO_SET_CONFIG_001, @VENUSMINER_NODE_REPO_FSREPO_SET_API_ENDPOINT_001, @VENUSMINER_NODE_REPO_FSREPO_SET_API_TOKEN_001, @VENUSMINER_NODE_REPO_FSREPO_LIST_001, @VENUSMINER_NODE_REPO_FSREPO_GET_001, @VENUSMINER_NODE_REPO_FSREPO_PUT_001, @VENUSMINER_NODE_REPO_FSREPO_DELETE_001 48 | basicTest(t, repo) 49 | } 50 | -------------------------------------------------------------------------------- /node/repo/interface.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/ipfs/go-datastore" 8 | "github.com/multiformats/go-multiaddr" 9 | ) 10 | 11 | var ( 12 | ErrNoAPIEndpoint = errors.New("API not running (no endpoint)") 13 | ErrRepoAlreadyLocked = errors.New("repo is already locked") 14 | ErrClosedRepo = errors.New("repo is no longer open") 15 | ) 16 | 17 | type Repo interface { 18 | // APIEndpoint returns multiaddress for communication with Lotus API 19 | APIEndpoint() (multiaddr.Multiaddr, error) 20 | 21 | // APIToken returns JWT API Token for use in operations that require auth 22 | APIToken() ([]byte, error) 23 | 24 | // Lock locks the repo for exclusive use. 25 | Lock() (LockedRepo, error) 26 | } 27 | 28 | type LockedRepo interface { 29 | // Close closes repo and removes lock. 30 | Close() error 31 | 32 | // Returns datastore defined in this repo. 33 | // The supplied context must only be used to initialize the datastore. 34 | // The implementation should not retain the context for usage throughout 35 | // the lifecycle. 36 | Datastore(ctx context.Context, namespace string) (datastore.Batching, error) 37 | 38 | // Returns config in this repo 39 | Config() (interface{}, error) 40 | SetConfig(func(interface{})) error 41 | 42 | // SetAPIEndpoint sets the endpoint of the current API 43 | // so it can be read by API clients 44 | SetAPIEndpoint(multiaddr.Multiaddr) error 45 | 46 | // SetAPIToken sets JWT API Token for CLI 47 | SetAPIToken([]byte) error 48 | 49 | // Path returns absolute path of the repo 50 | Path() string 51 | 52 | // SetVersion sets the version number 53 | SetVersion(version string) error 54 | 55 | // Migrate used to upgrade the repo 56 | Migrate() error 57 | } 58 | -------------------------------------------------------------------------------- /node/repo/migrate.go: -------------------------------------------------------------------------------- 1 | package repo 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "strconv" 9 | 10 | "github.com/ipfs-force-community/sophon-miner/build" 11 | "github.com/ipfs-force-community/sophon-miner/node/config" 12 | "github.com/ipfs-force-community/sophon-miner/node/config/migrate" 13 | ) 14 | 15 | type UpgradeFunc func(*fsLockedRepo) error 16 | 17 | type versionInfo struct { 18 | version int 19 | upgrade UpgradeFunc 20 | } 21 | 22 | var versionMap = []versionInfo{ 23 | {version: 180, upgrade: Version180Upgrade}, 24 | } 25 | 26 | func Version180Upgrade(fsr *fsLockedRepo) error { 27 | cfgV, err := config.FromFile(fsr.configPath, &migrate.MinerConfig{}) 28 | if err != nil { 29 | return err 30 | } 31 | cfgV170, ok := cfgV.(*migrate.MinerConfig) 32 | 33 | if ok { 34 | // upgrade 35 | if err := fsr.SetConfig(func(i interface{}) { 36 | cfg := i.(*config.MinerConfig) 37 | cfgV170.ToMinerConfig(cfg) 38 | }); err != nil { 39 | return fmt.Errorf("modify config failed: %w", err) 40 | } 41 | } 42 | 43 | return nil 44 | } 45 | 46 | func (fsr *fsLockedRepo) Migrate() error { 47 | filePath := filepath.Join(fsr.path, fsVersion) 48 | f, err := os.Open(filePath) 49 | if err != nil && !os.IsNotExist(err) { 50 | return err 51 | } 52 | 53 | actVerStr := "0" 54 | if err == nil { 55 | defer f.Close() //nolint: errcheck 56 | 57 | data, err := io.ReadAll(f) 58 | if err != nil { 59 | return fmt.Errorf("failed to read %s: %w", filePath, err) 60 | } 61 | actVerStr = string(data) 62 | } 63 | 64 | actVer, _ := strconv.Atoi(actVerStr) 65 | for _, up := range versionMap { 66 | if up.version > actVer { 67 | if up.upgrade == nil { 68 | log.Infof("version %d no executable function", up.version) 69 | } else { 70 | err = up.upgrade(fsr) 71 | if err != nil { 72 | return fmt.Errorf("upgrade version to %d: %w", up.version, err) 73 | } 74 | log.Infof("success to upgrade version %d to %d", actVer, up.version) 75 | } 76 | actVer = up.version 77 | } 78 | } 79 | 80 | err = fsr.SetVersion(build.Version) 81 | if err != nil { 82 | return fmt.Errorf("modify version failed: %w", err) 83 | } 84 | 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /node/repo/repo_test.go: -------------------------------------------------------------------------------- 1 | // stm: #uint 2 | package repo 3 | 4 | import ( 5 | "testing" 6 | 7 | "github.com/multiformats/go-multiaddr" 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/ipfs-force-community/sophon-miner/node/config" 11 | ) 12 | 13 | func basicTest(t *testing.T, repo Repo) { 14 | apima, err := repo.APIEndpoint() 15 | if assert.Error(t, err) { 16 | assert.Equal(t, ErrNoAPIEndpoint, err) 17 | } 18 | assert.Nil(t, apima, "with no api endpoint, return should be nil") 19 | 20 | lrepo, err := repo.Lock() 21 | assert.NoError(t, err, "should be able to lock once") 22 | assert.NotNil(t, lrepo, "locked repo shouldn't be nil") 23 | 24 | { 25 | lrepo2, err := repo.Lock() 26 | if assert.Error(t, err) { 27 | assert.Equal(t, ErrRepoAlreadyLocked, err) 28 | } 29 | assert.Nil(t, lrepo2, "with locked repo errors, nil should be returned") 30 | } 31 | 32 | err = lrepo.Close() 33 | assert.NoError(t, err, "should be able to unlock") 34 | 35 | lrepo, err = repo.Lock() 36 | assert.NoError(t, err, "should be able to relock") 37 | assert.NotNil(t, lrepo, "locked repo shouldn't be nil") 38 | 39 | ma, err := multiaddr.NewMultiaddr("/ip4/127.0.0.1/tcp/43244") 40 | assert.NoError(t, err, "creating multiaddr shouldn't error") 41 | 42 | err = lrepo.SetAPIEndpoint(ma) 43 | assert.NoError(t, err, "setting multiaddr shouldn't error") 44 | 45 | apima, err = repo.APIEndpoint() 46 | assert.NoError(t, err, "setting multiaddr shouldn't error") 47 | assert.Equal(t, ma, apima, "returned API multiaddr should be the same") 48 | 49 | err = lrepo.SetAPIToken([]byte("api token")) 50 | assert.NoError(t, err) 51 | 52 | token, err := repo.APIToken() 53 | assert.NoError(t, err) 54 | assert.Equal(t, token, []byte("api token")) 55 | 56 | c1, err := lrepo.Config() 57 | assert.Equal(t, config.DefaultMinerConfig(), c1, "there should be a default config") 58 | assert.NoError(t, err, "config should not error") 59 | 60 | err = lrepo.Close() 61 | assert.NoError(t, err, "should be able to close") 62 | 63 | apima, err = repo.APIEndpoint() 64 | assert.NoError(t, err, "getting multiaddr shouldn't error") 65 | assert.Equal(t, ma, apima, "returned API multiaddr should be the same") 66 | 67 | lrepo, err = repo.Lock() 68 | assert.NoError(t, err, "should be able to relock") 69 | assert.NotNil(t, lrepo, "locked repo shouldn't be nil") 70 | } 71 | -------------------------------------------------------------------------------- /scripts/build-bundle.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | ARCHS=( 5 | "darwin" 6 | "linux" 7 | ) 8 | 9 | REQUIRED=( 10 | "ipfs" 11 | "sha512sum" 12 | ) 13 | for REQUIRE in "${REQUIRED[@]}" 14 | do 15 | command -v "${REQUIRE}" >/dev/null 2>&1 || echo >&2 "'${REQUIRE}' must be installed" 16 | done 17 | 18 | mkdir bundle 19 | pushd bundle 20 | 21 | BINARIES=( 22 | "sophon-miner" 23 | ) 24 | 25 | # start ipfs 26 | export IPFS_PATH=`mktemp -d` 27 | ipfs init 28 | ipfs daemon & 29 | PID="$!" 30 | trap "kill -9 ${PID}" EXIT 31 | sleep 30 32 | 33 | for ARCH in "${ARCHS[@]}" 34 | do 35 | mkdir -p "${ARCH}/sophon-miner" 36 | pushd "${ARCH}" 37 | for BINARY in "${BINARIES[@]}" 38 | do 39 | cp "../../${ARCH}/${BINARY}" "sophon-miner/" 40 | chmod +x "sophon-miner/${BINARY}" 41 | done 42 | 43 | tar -zcvf "../sophon-miner_${CIRCLE_TAG}_${ARCH}-amd64.tar.gz" sophon-miner 44 | popd 45 | rm -rf "${ARCH}" 46 | 47 | sha512sum "sophon-miner_${CIRCLE_TAG}_${ARCH}-amd64.tar.gz" > "sophon-miner_${CIRCLE_TAG}_${ARCH}-amd64.tar.gz.sha512" 48 | 49 | ipfs add -q "sophon-miner_${CIRCLE_TAG}_${ARCH}-amd64.tar.gz" > "sophon-miner_${CIRCLE_TAG}_${ARCH}-amd64.tar.gz.cid" 50 | done 51 | popd 52 | -------------------------------------------------------------------------------- /scripts/publish-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | pushd bundle 5 | 6 | # make sure we have a token set, api requests won't work otherwise 7 | if [ -z "${GITHUB_TOKEN}" ]; then 8 | echo "\${GITHUB_TOKEN} not set, publish failed" 9 | exit 1 10 | fi 11 | 12 | REQUIRED=( 13 | "jq" 14 | "curl" 15 | ) 16 | for REQUIRE in "${REQUIRED[@]}" 17 | do 18 | command -v "${REQUIRE}" >/dev/null 2>&1 || echo >&2 "'${REQUIRE}' must be installed" 19 | done 20 | 21 | #see if the release already exists by tag 22 | RELEASE_RESPONSE=` 23 | curl \ 24 | --header "Authorization: token ${GITHUB_TOKEN}" \ 25 | "https://api.github.com/repos/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/releases/tags/${CIRCLE_TAG}" 26 | ` 27 | RELEASE_ID=`echo "${RELEASE_RESPONSE}" | jq '.id'` 28 | 29 | if [ "${RELEASE_ID}" = "null" ]; then 30 | echo "creating release" 31 | 32 | RELEASE_DATA="{ 33 | \"tag_name\": \"${CIRCLE_TAG}\", 34 | \"target_commitish\": \"${CIRCLE_SHA1}\", 35 | \"name\": \"${CIRCLE_TAG}\", 36 | \"body\": \"\", 37 | \"prerelease\": false 38 | }" 39 | 40 | # create it if it doesn't exist yet 41 | RELEASE_RESPONSE=` 42 | curl \ 43 | --request POST \ 44 | --header "Authorization: token ${GITHUB_TOKEN}" \ 45 | --header "Content-Type: application/json" \ 46 | --data "${RELEASE_DATA}" \ 47 | "https://api.github.com/repos/$CIRCLE_PROJECT_USERNAME/${CIRCLE_PROJECT_REPONAME}/releases" 48 | ` 49 | else 50 | echo "release already exists" 51 | fi 52 | 53 | RELEASE_UPLOAD_URL=`echo "${RELEASE_RESPONSE}" | jq -r '.upload_url' | cut -d'{' -f1` 54 | 55 | for RELEASE_FILE in *.{cid,sha512} 56 | do 57 | echo "Uploading release bundle: ${RELEASE_FILE}" 58 | curl \ 59 | --fail \ 60 | --request POST \ 61 | --header "Authorization: token ${GITHUB_TOKEN}" \ 62 | --header "Content-Type: application/octet-stream" \ 63 | --data-binary "@${RELEASE_FILE}" \ 64 | "$RELEASE_UPLOAD_URL?name=$(basename "${RELEASE_FILE}")" 65 | 66 | echo "Release bundle uploaded: ${RELEASE_FILE}" 67 | done 68 | 69 | popd 70 | 71 | miscellaneous=( 72 | "README.md" 73 | "LICENSE-MIT" 74 | "LICENSE-APACHE" 75 | ) 76 | for MISC in "${miscellaneous[@]}" 77 | do 78 | echo "Uploading release bundle: ${MISC}" 79 | curl \ 80 | --request POST \ 81 | --header "Authorization: token ${GITHUB_TOKEN}" \ 82 | --header "Content-Type: application/octet-stream" \ 83 | --data-binary "@${MISC}" \ 84 | "$RELEASE_UPLOAD_URL?name=$(basename "${MISC}")" 85 | 86 | echo "Release bundle uploaded: ${MISC}" 87 | done 88 | -------------------------------------------------------------------------------- /types/api.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/multiformats/go-multiaddr" 5 | ) 6 | 7 | type APIEndpoint multiaddr.Multiaddr 8 | -------------------------------------------------------------------------------- /types/logs.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/ipfs/go-cid" 5 | "go.uber.org/zap/zapcore" 6 | ) 7 | 8 | type LogCids []cid.Cid 9 | 10 | var _ zapcore.ArrayMarshaler = (*LogCids)(nil) 11 | 12 | func (cids LogCids) MarshalLogArray(ae zapcore.ArrayEncoder) error { 13 | for _, c := range cids { 14 | ae.AppendString(c.String()) 15 | } 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /types/miner.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/filecoin-project/go-address" 8 | "github.com/filecoin-project/go-state-types/abi" 9 | ) 10 | 11 | type NetworkName string 12 | 13 | type MinerInfo struct { 14 | Addr address.Address 15 | Id string 16 | Name string 17 | OpenMining bool 18 | } 19 | 20 | type MinerState struct { 21 | Addr address.Address 22 | IsMining bool 23 | Err []string 24 | } 25 | 26 | type SimpleWinInfo struct { 27 | Epoch abi.ChainEpoch `json:"epoch"` 28 | WinCount int64 `json:"winCount"` 29 | Msg string `json:"msg"` 30 | } 31 | 32 | type CountWinners struct { 33 | Miner address.Address `json:"miner"` 34 | TotalWinCount int64 `json:"totalWinCount"` 35 | Msg string `json:"msg"` 36 | WinEpochList []SimpleWinInfo `json:"winEpochList"` 37 | } 38 | 39 | type StateMining int 40 | 41 | const ( 42 | Mining StateMining = iota 43 | Success 44 | Timeout 45 | ChainForked 46 | Error 47 | ) 48 | 49 | func (sm StateMining) String() string { 50 | switch sm { 51 | case Mining: 52 | return "Mining" 53 | case Success: 54 | return "Success" 55 | case Timeout: 56 | return "TimeOut" 57 | case ChainForked: 58 | return "ChainForked" 59 | case Error: 60 | return "Error" 61 | default: 62 | return "unknown" 63 | } 64 | } 65 | 66 | type MinedBlock struct { 67 | ParentEpoch int64 `gorm:"column:parent_epoch;type:bigint(20);default:0;NOT NULL"` 68 | ParentKey string `gorm:"column:parent_key;type:varchar(2048);default:'';NOT NULL"` 69 | 70 | Epoch int64 `gorm:"column:epoch;type:bigint(20);NOT NULL;primary_key"` 71 | Miner string `gorm:"column:miner;type:varchar(256);NOT NULL;primary_key"` 72 | Cid string `gorm:"column:cid;type:varchar(256);default:''"` 73 | 74 | WinningAt time.Time `gorm:"column:winning_at;type:datetime"` 75 | MineState StateMining `gorm:"column:mine_state;type:tinyint(4);default:0;comment:0-mining,1-success,2-timeout,3-chain forked,4-error;NOT NULL"` 76 | Consuming int64 `gorm:"column:consuming;type:bigint(10);default:0;NOT NULL"` // reserved 77 | } 78 | 79 | func (m *MinedBlock) TableName() string { 80 | return "miner_blocks" 81 | } 82 | 83 | type BlocksQueryParams struct { 84 | Miners []address.Address 85 | Limit int 86 | Offset int 87 | } 88 | 89 | type QueryRecordParams struct { 90 | Miner address.Address 91 | Epoch abi.ChainEpoch 92 | Limit uint 93 | } 94 | 95 | type ErrorCode int 96 | 97 | const ( 98 | ConnectGatewayError ErrorCode = iota 99 | CallNodeRPCError 100 | WalletSignError 101 | ) 102 | 103 | func (e ErrorCode) String() string { 104 | switch e { 105 | case ConnectGatewayError: 106 | return "ConnectGatewayError" 107 | case CallNodeRPCError: 108 | return "CallNodeRPCError" 109 | case WalletSignError: 110 | return "WalletSignError" 111 | default: 112 | return "unknown" 113 | } 114 | } 115 | 116 | func (e ErrorCode) Error() string { 117 | return fmt.Sprintf("%d", e) 118 | } 119 | -------------------------------------------------------------------------------- /types/shutdown.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // ShutdownChan is a channel to which you send a value if you intend to shut 4 | // down the daemon (or miner), including the node and RPC server. 5 | type ShutdownChan chan struct{} 6 | -------------------------------------------------------------------------------- /types/storage.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "github.com/ipfs/go-datastore" 5 | ) 6 | 7 | // MetadataDS stores metadata 8 | // dy default it's namespaced under /metadata in main repo datastore 9 | type MetadataDS datastore.Batching 10 | --------------------------------------------------------------------------------