From 3879c80e6fa67d25a5fe2c98422168639ed29b6d Mon Sep 17 00:00:00 2001 From: Phi-rjan Date: Tue, 14 Feb 2023 11:13:55 +0100 Subject: [PATCH 01/70] Add link to discussion and FIPs in `New Issue` In our current issue flow, we ask a lot checklist questions : `This is not a question or a support request. If you have any lotus related questions, please ask in the lotus forum.` `This is not a new feature or an enhancement to the Filecoin protocol. If it is, please open an FIP issue. *` This information is better displayed in the `New Issue` chooser, to guide users to the relevant place for posting. This file will add two new entries in the issue chooser: - A entry that links to the Lotus Github Discussion pager for Lotus questions or troubleshooting - A entry that links to the FIPs repo for Filecoin protocol feature or enhancements --- .github/ISSUE_TEMPLATE/config.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..e5ae608b3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Ask a question about Lotus or get support + url: https://github.com/filecoin-project/lotus/discussions/new/choose + about: Ask a question or request support for using Lotus + - name: Filecoin protocol feature or enhancement + url: https://github.com/filecoin-project/FIPs/discussions/new/choose + about: Write a discussion in the Filecoin Improvement Proposal repo From d3e715594b9cf16798d981b719023b54a8a1bc74 Mon Sep 17 00:00:00 2001 From: Phi Date: Thu, 16 Feb 2023 10:59:45 +0100 Subject: [PATCH 02/70] chore: github: Update bug_report template Update bug_report template: - Removing unnecessary checklist options. - Some of these has been move to the Github New Issue UI to reduce overhead - Remove deprecated components options - Add FVM/FEVM component --- .github/ISSUE_TEMPLATE/bug_report.yml | 55 ++++++++++----------------- 1 file changed, 20 insertions(+), 35 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 981a80256..e4c0c7f26 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -9,15 +9,9 @@ body: options: - label: This is **not** a security-related bug/issue. If it is, please follow please follow the [security policy](https://github.com/filecoin-project/lotus/security/policy). required: true - - label: This is **not** a question or a support request. If you have any lotus related questions, please ask in the [lotus forum](https://github.com/filecoin-project/lotus/discussions). - required: true - - label: This is **not** a new feature request. If it is, please file a [feature request](https://github.com/filecoin-project/lotus/issues/new?assignees=&labels=need%2Ftriage%2Ckind%2Ffeature&template=feature_request.yml) instead. - required: true - - label: This is **not** an enhancement request. If it is, please file a [improvement suggestion](https://github.com/filecoin-project/lotus/issues/new?assignees=&labels=need%2Ftriage%2Ckind%2Fenhancement&template=enhancement.yml) instead. - required: true - label: I **have** searched on the [issue tracker](https://github.com/filecoin-project/lotus/issues) and the [lotus forum](https://github.com/filecoin-project/lotus/discussions), and there is no existing related issue or discussion. required: true - - label: I am running the [`Latest release`](https://github.com/filecoin-project/lotus/releases), or the most recent RC(release canadiate) for the upcoming release or the dev branch(master), or have an issue updating to any of these. + - label: I am running the [`Latest release`](https://github.com/filecoin-project/lotus/releases), the most recent RC(release canadiate) for the upcoming release or the dev branch(master), or have an issue updating to any of these. required: true - label: I did not make any code changes to lotus. required: false @@ -28,19 +22,11 @@ body: options: - label: lotus daemon - chain sync required: false - - label: lotus miner - mining and block production + - label: lotus fvm/fevm - Lotus FVM and FEVM interactions required: false - label: lotus miner/worker - sealing required: false - - label: lotus miner - proving(WindowPoSt) - required: false - - label: lotus miner/market - storage deal - required: false - - label: lotus miner/market - retrieval deal - required: false - - label: lotus miner/market - data transfer - required: false - - label: lotus client + - label: lotus miner - proving(WindowPoSt/WinningPoSt) required: false - label: lotus JSON-RPC API required: false @@ -56,22 +42,33 @@ body: description: Enter the output of `lotus version` and `lotus-miner version` if applicable. placeholder: | e.g. - Daemon:1.11.0-rc2+debug+git.0519cd371.dirty+api1.3.0 - Local: lotus version 1.11.0-rc2+debug+git.0519cd371.dirty + Daemon: 1.19.0+mainnet+git.64059ca87+api1.5.0 + Local: lotus-miner version 1.19.0+mainnet+git.64059ca87 validations: required: true +- type: textarea + id: ReproSteps + attributes: + label: Repro Steps + description: "Steps to reproduce the behavior" + value: | + 1. Run '...' + 2. Do '...' + 3. See error '...' + ... + validations: + required: false - type: textarea id: Description attributes: label: Describe the Bug description: | This is where you get to tell us what went wrong, when doing so, please try to provide a clear and concise description of the bug with all related information: - * What you were doding when you experienced the bug? + * What you were doing when you experienced the bug? * Any *error* messages you saw, *where* you saw them, and what you believe may have caused them (if you have any ideas). * What is the expected behaviour? * For sealing issues, include the output of `lotus-miner sectors status --log ` for the failed sector(s). * For proving issues, include the output of `lotus-miner proving` info. - * For deal making issues, include the output of `lotus client list-deals -v` and/or `lotus-miner storage-deals|retrieval-deals|data-transfers list [-v]` commands for the deal(s) in question. validations: required: true - type: textarea @@ -83,18 +80,6 @@ body: Please provide debug logs of the problem, remember you can get set log level control for: * lotus: use `lotus log list` to get all log systems available and set level by `lotus log set-level`. An example can be found [here](https://lotus.filecoin.io/lotus/configure/defaults/#log-level-control). * lotus-miner:`lotus-miner log list` to get all log systems available and set level by `lotus-miner log set-level - If you don't provide detailed logs when you raise the issue it will almost certainly be the first request I make before furthur diagnosing the problem. + If you don't provide detailed logs when you raise the issue it will almost certainly be the first request we make before furthur diagnosing the problem. validations: - required: true -- type: textarea - id: RepoSteps - attributes: - label: Repo Steps - description: "Steps to reproduce the behavior" - value: | - 1. Run '...' - 2. Do '...' - 3. See error '...' - ... - validations: - required: false + required: true \ No newline at end of file From 27d4d02fe5ef7c23440d35454c61fddc533814a3 Mon Sep 17 00:00:00 2001 From: Phi Date: Thu, 16 Feb 2023 13:11:10 +0100 Subject: [PATCH 03/70] Update enhancement and feature templates Update enhancement and feature_request templates: - Removing unnecessary checklist options. - These have been move to the Github New Issue UI to reduce text overhead: #10268 - Remove deprecated components options - Add FVM/FEVM component --- .github/ISSUE_TEMPLATE/enhancement.yml | 34 +++++++++------------- .github/ISSUE_TEMPLATE/feature_request.yml | 17 ++--------- 2 files changed, 17 insertions(+), 34 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml index da662688d..d367feeac 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.yml +++ b/.github/ISSUE_TEMPLATE/enhancement.yml @@ -7,13 +7,7 @@ body: label: Checklist description: Please check off the following boxes before continuing to create an improvement suggestion! options: - - label: This is **not** a new feature or an enhancement to the Filecoin protocol. If it is, please open an [FIP issue](https://github.com/filecoin-project/FIPs/blob/master/FIPS/fip-0001.md). - required: true - - label: This is **not** a new feature request. If it is, please file a [feature request](https://github.com/filecoin-project/lotus/issues/new?assignees=&labels=need%2Ftriage%2Ckind%2Ffeature&template=feature_request.yml) instead. - required: true - - label: This is **not** brainstorming ideas. If you have an idea you'd like to discuss, please open a new discussion on [the lotus forum](https://github.com/filecoin-project/lotus/discussions/categories/ideas) and select the category as `Ideas`. - required: true - - label: I **have** a specific, actionable, and well motivated improvement to propose. + - label: I **have** a specific, actionable, and well motivated improvement to an existing lotus feature. required: true - type: checkboxes attributes: @@ -22,19 +16,11 @@ body: options: - label: lotus daemon - chain sync required: false - - label: lotus miner - mining and block production + - label: lotus fvm/fevm - Lotus FVM and FEVM interactions required: false - label: lotus miner/worker - sealing required: false - - label: lotus miner - proving(WindowPoSt) - required: false - - label: lotus miner/market - storage deal - required: false - - label: lotus miner/market - retrieval deal - required: false - - label: lotus miner/market - data transfer - required: false - - label: lotus client + - label: lotus miner - proving(WindowPoSt/WinningPoSt) required: false - label: lotus JSON-RPC API required: false @@ -45,9 +31,17 @@ body: - type: textarea id: request attributes: - label: Improvement Suggestion - description: A clear and concise description of what the motivation or the current problem is and what is the suggested improvement? - placeholder: Ex. Currently lotus... However, as a storage provider, I'd like... + label: Enhancement Suggestion + description: A clear and concise description of the suggested enhancement? + placeholder: Ex. Currently lotus... However it would be great if [enhancement] was implemented... With the ability to... + validations: + required: true +- type: textarea + id: request + attributes: + label: Use-Case + description: How would this enhancement help you? + placeholder: Ex. With the [enhancement] node operators would be able to... For Storage Providers it would enable... validations: required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 210549095..76493e9c1 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -7,8 +7,6 @@ body: label: Checklist description: Please check off the following boxes before continuing to create a new feature request! options: - - label: This is **not** a new feature or an enhancement to the Filecoin protocol. If it is, please open an [FIP issue](https://github.com/filecoin-project/FIPs/blob/master/FIPS/fip-0001.md). - required: true - label: This is **not** brainstorming ideas. If you have an idea you'd like to discuss, please open a new discussion on [the lotus forum](https://github.com/filecoin-project/lotus/discussions/categories/ideas) and select the category as `Ideas`. required: true - label: I **have** a specific, actionable, and well motivated feature request to propose. @@ -20,19 +18,11 @@ body: options: - label: lotus daemon - chain sync required: false - - label: lotus miner - mining and block production + - label: lotus fvm/fevm - Lotus FVM and FEVM interactions required: false - label: lotus miner/worker - sealing required: false - - label: lotus miner - proving(WindowPoSt) - required: false - - label: lotus miner/market - storage deal - required: false - - label: lotus miner/market - retrieval deal - required: false - - label: lotus miner/market - data transfer - required: false - - label: lotus client + - label: lotus miner - proving(WindowPoSt/WinningPoSt) required: false - label: lotus JSON-RPC API required: false @@ -56,7 +46,7 @@ body: validations: required: true - type: textarea - id: alternates + id: alternatives attributes: label: Describe alternatives you've considered description: A clear and concise description of any alternative solutions or features you've considered. @@ -69,4 +59,3 @@ body: description: Add any other context, design docs or screenshots about the feature request here. validations: required: false - From 7f6fbe22c611490009a7d7b464cfe1f187738a89 Mon Sep 17 00:00:00 2001 From: Phi Date: Fri, 17 Feb 2023 10:33:52 +0100 Subject: [PATCH 04/70] Add initial service/dev bug template Initial service/dev bug template --- .../service_developer_bug_report.yml | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/service_developer_bug_report.yml diff --git a/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml b/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml new file mode 100644 index 000000000..949d7850d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml @@ -0,0 +1,82 @@ +name: "Bug Report - developer/service provider" +description: "File a bug report related to FEVM/FVM to help us improve" +labels: [need/triage, kind/bug] +body: +- type: checkboxes + attributes: + label: Checklist + description: Please check off the following boxes before continuing to file a bug report! + options: + - label: This is **not** a security-related bug/issue. If it is, please follow please follow the [security policy](https://github.com/filecoin-project/lotus/security/policy). + required: true + - label: I **have** searched on the [issue tracker](https://github.com/filecoin-project/lotus/issues) and the [lotus forum](https://github.com/filecoin-project/lotus/discussions), and there is no existing related issue or discussion. + required: true + - label: I did not make any code changes to lotus. + required: false +- type: checkboxes + attributes: + label: Lotus component + description: Please select the lotus component you are filing a bug for + options: + - label: lotus Ethereum RPC + required: false + - label: lotus evm - Lotus EVM interactions + required: false + - label: Other + required: false +- type: textarea + id: version + attributes: + label: Lotus Version + render: text + description: Enter the output of `lotus version` if applicable. + placeholder: | + e.g. + Daemon: 1.19.0+mainnet+git.64059ca87+api1.5.0 + Local: lotus-miner version 1.19.0+mainnet+git.64059ca87 + validations: + required: true +- type: textarea + id: Repro Steps + attributes: + label: Repro Steps + description: "Steps to reproduce the behavior" + value: | + 1. Run '...' + 2. Do '...' + 3. See error '...' + ... + validations: + required: false +- type: textarea + id: Description + attributes: + label: Describe the Bug + description: | + This is where you get to tell us what went wrong, when doing so, please try to provide a clear and concise description of the bug with all related information: + * What you were doing when you experienced the bug? + * Any *error* messages you saw, *where* you saw them, and what you believe may have caused them (if you have any ideas). + * What is the expected behaviour? + validations: + required: true +- type: textarea + id: extraInfo + attributes: + label: Logging Information + render: text + description: | + Please provide debug logs of the problem, remember you can get set log level control for: + * lotus: use `lotus log list` to get all log systems available and set level by `lotus log set-level`. An example can be found [here](https://lotus.filecoin.io/lotus/configure/defaults/#log-level-control). + If you don't provide detailed logs when you raise the issue it will almost certainly be the first request I make before furthur diagnosing the problem. + validations: + required: true +- type: textarea + id: extraInfo + attributes: + label: Configuration Options + render: text + description: | + Please provide your updated FEVM related configuration options, or custome enviroment variables related to Lotus FEVM + * lotus: use `lotus config updated` to get your configuration options, and copy the [FEVM] section + validations: + required: true From 03f2efd0f9bb1e4d32bb79a9df492a8c78dc203f Mon Sep 17 00:00:00 2001 From: Phi Date: Tue, 21 Feb 2023 08:52:47 +0100 Subject: [PATCH 05/70] Add tooling textbox Add tooling textbox to template --- .../service_developer_bug_report.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml b/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml index 949d7850d..93abadf4f 100644 --- a/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml @@ -1,6 +1,6 @@ name: "Bug Report - developer/service provider" -description: "File a bug report related to FEVM/FVM to help us improve" -labels: [need/triage, kind/bug] +description: "Bug report template about FEVM/FVM for developers/service providers" +labels: [need/triage, kind/bug, area/fevm] body: - type: checkboxes attributes: @@ -55,19 +55,18 @@ body: description: | This is where you get to tell us what went wrong, when doing so, please try to provide a clear and concise description of the bug with all related information: * What you were doing when you experienced the bug? - * Any *error* messages you saw, *where* you saw them, and what you believe may have caused them (if you have any ideas). - * What is the expected behaviour? + * Any *error* messages and logs you saw, *where* you saw them, and what you believe may have caused them (if you have any ideas). + * What is the expected behaviour? Links to the actual code? validations: required: true - type: textarea id: extraInfo attributes: - label: Logging Information + label: Tooling render: text description: | - Please provide debug logs of the problem, remember you can get set log level control for: - * lotus: use `lotus log list` to get all log systems available and set level by `lotus log set-level`. An example can be found [here](https://lotus.filecoin.io/lotus/configure/defaults/#log-level-control). - If you don't provide detailed logs when you raise the issue it will almost certainly be the first request I make before furthur diagnosing the problem. + What kind of tooling are you using: + * Are you using ether.js, Alchemy, Hardhat, etc. validations: required: true - type: textarea From 745476c9ab714a94d351d26f95716ca7dec39837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Mon, 27 Feb 2023 17:44:38 +0100 Subject: [PATCH 06/70] feat: sched: Assigner experiments --- storage/sealer/sched.go | 10 +- storage/sealer/sched_assigner_darts.go | 88 +++++++++++++ storage/sealer/sched_assigner_spread.go | 118 ++++++++++-------- storage/sealer/sched_assigner_spread_tasks.go | 98 +++++++++++++++ storage/sealer/sched_assigner_utilization.go | 1 + storage/sealer/sched_resources.go | 41 ++++++ 6 files changed, 300 insertions(+), 56 deletions(-) create mode 100644 storage/sealer/sched_assigner_darts.go create mode 100644 storage/sealer/sched_assigner_spread_tasks.go diff --git a/storage/sealer/sched.go b/storage/sealer/sched.go index c2b7d6a2d..4f167eae3 100644 --- a/storage/sealer/sched.go +++ b/storage/sealer/sched.go @@ -157,7 +157,15 @@ func newScheduler(ctx context.Context, assigner string) (*Scheduler, error) { case "", "utilization": a = NewLowestUtilizationAssigner() case "spread": - a = NewSpreadAssigner() + a = NewSpreadAssigner(false) + case "experiment-spread-qcount": + a = NewSpreadAssigner(true) + case "experiment-spread-tasks": + a = NewSpreadTasksAssigner(false) + case "experiment-spread-tasks-qcount": + a = NewSpreadTasksAssigner(true) + case "experiment-random": + a = NewRandomAssigner() default: return nil, xerrors.Errorf("unknown assigner '%s'", assigner) } diff --git a/storage/sealer/sched_assigner_darts.go b/storage/sealer/sched_assigner_darts.go new file mode 100644 index 000000000..e28b70e78 --- /dev/null +++ b/storage/sealer/sched_assigner_darts.go @@ -0,0 +1,88 @@ +package sealer + +import ( + "math/rand" + + "github.com/filecoin-project/lotus/storage/sealer/storiface" +) + +func NewRandomAssigner() Assigner { + return &AssignerCommon{ + WindowSel: RandomWS, + } +} + +func RandomWS(sh *Scheduler, queueLen int, acceptableWindows [][]int, windows []SchedWindow) int { + scheduled := 0 + rmQueue := make([]int, 0, queueLen) + + for sqi := 0; sqi < queueLen; sqi++ { + task := (*sh.SchedQueue)[sqi] + + //bestAssigned := math.MaxInt // smaller = better + + type choice struct { + selectedWindow int + needRes storiface.Resources + info storiface.WorkerInfo + bestWid storiface.WorkerID + } + choices := make([]choice, 0, len(acceptableWindows[task.IndexHeap])) + + for i, wnd := range acceptableWindows[task.IndexHeap] { + wid := sh.OpenWindows[wnd].Worker + w := sh.Workers[wid] + + res := w.Info.Resources.ResourceSpec(task.Sector.ProofType, task.TaskType) + + log.Debugf("SCHED try assign sqi:%d sector %d to window %d (awi:%d)", sqi, task.Sector.ID.Number, wnd, i) + + if !windows[wnd].Allocated.CanHandleRequest(task.SealTask(), res, wid, "schedAssign", w.Info) { + continue + } + + choices = append(choices, choice{ + selectedWindow: wnd, + needRes: res, + info: w.Info, + bestWid: wid, + }) + + } + + if len(choices) == 0 { + // all windows full + continue + } + + // chose randomly + randIndex := rand.Intn(len(choices)) + selectedWindow := choices[randIndex].selectedWindow + needRes := choices[randIndex].needRes + info := choices[randIndex].info + bestWid := choices[randIndex].bestWid + + log.Debugw("SCHED ASSIGNED", + "assigner", "darts", + "sqi", sqi, + "sector", task.Sector.ID.Number, + "task", task.TaskType, + "window", selectedWindow, + "worker", bestWid, + "choices", len(choices)) + + windows[selectedWindow].Allocated.Add(task.SealTask(), info.Resources, needRes) + windows[selectedWindow].Todo = append(windows[selectedWindow].Todo, task) + + rmQueue = append(rmQueue, sqi) + scheduled++ + } + + if len(rmQueue) > 0 { + for i := len(rmQueue) - 1; i >= 0; i-- { + sh.SchedQueue.Remove(rmQueue[i]) + } + } + + return scheduled +} diff --git a/storage/sealer/sched_assigner_spread.go b/storage/sealer/sched_assigner_spread.go index f00d24d82..0a62b7406 100644 --- a/storage/sealer/sched_assigner_spread.go +++ b/storage/sealer/sched_assigner_spread.go @@ -6,76 +6,84 @@ import ( "github.com/filecoin-project/lotus/storage/sealer/storiface" ) -func NewSpreadAssigner() Assigner { +func NewSpreadAssigner(queued bool) Assigner { return &AssignerCommon{ - WindowSel: SpreadWS, + WindowSel: SpreadWS(queued), } } -func SpreadWS(sh *Scheduler, queueLen int, acceptableWindows [][]int, windows []SchedWindow) int { - scheduled := 0 - rmQueue := make([]int, 0, queueLen) - workerAssigned := map[storiface.WorkerID]int{} +func SpreadWS(queued bool) func(sh *Scheduler, queueLen int, acceptableWindows [][]int, windows []SchedWindow) int { + return func(sh *Scheduler, queueLen int, acceptableWindows [][]int, windows []SchedWindow) int { + scheduled := 0 + rmQueue := make([]int, 0, queueLen) + workerAssigned := map[storiface.WorkerID]int{} - for sqi := 0; sqi < queueLen; sqi++ { - task := (*sh.SchedQueue)[sqi] + for sqi := 0; sqi < queueLen; sqi++ { + task := (*sh.SchedQueue)[sqi] - selectedWindow := -1 - var needRes storiface.Resources - var info storiface.WorkerInfo - var bestWid storiface.WorkerID - bestAssigned := math.MaxInt // smaller = better + selectedWindow := -1 + var needRes storiface.Resources + var info storiface.WorkerInfo + var bestWid storiface.WorkerID + bestAssigned := math.MaxInt // smaller = better - for i, wnd := range acceptableWindows[task.IndexHeap] { - wid := sh.OpenWindows[wnd].Worker - w := sh.Workers[wid] + for i, wnd := range acceptableWindows[task.IndexHeap] { + wid := sh.OpenWindows[wnd].Worker + w := sh.Workers[wid] - res := w.Info.Resources.ResourceSpec(task.Sector.ProofType, task.TaskType) + res := w.Info.Resources.ResourceSpec(task.Sector.ProofType, task.TaskType) - log.Debugf("SCHED try assign sqi:%d sector %d to window %d (awi:%d)", sqi, task.Sector.ID.Number, wnd, i) + log.Debugf("SCHED try assign sqi:%d sector %d to window %d (awi:%d)", sqi, task.Sector.ID.Number, wnd, i) - if !windows[wnd].Allocated.CanHandleRequest(task.SealTask(), res, wid, "schedAssign", w.Info) { + if !windows[wnd].Allocated.CanHandleRequest(task.SealTask(), res, wid, "schedAssign", w.Info) { + continue + } + + wu, found := workerAssigned[wid] + if !found && queued { + wu = w.TaskCounts() + workerAssigned[wid] = wu + } + if wu >= bestAssigned { + continue + } + + info = w.Info + needRes = res + bestWid = wid + selectedWindow = wnd + bestAssigned = wu + } + + if selectedWindow < 0 { + // all windows full continue } - wu, _ := workerAssigned[wid] - if wu >= bestAssigned { - continue + log.Debugw("SCHED ASSIGNED", + "assigner", "spread", + "spread-queued", queued, + "sqi", sqi, + "sector", task.Sector.ID.Number, + "task", task.TaskType, + "window", selectedWindow, + "worker", bestWid, + "assigned", bestAssigned) + + workerAssigned[bestWid]++ + windows[selectedWindow].Allocated.Add(task.SealTask(), info.Resources, needRes) + windows[selectedWindow].Todo = append(windows[selectedWindow].Todo, task) + + rmQueue = append(rmQueue, sqi) + scheduled++ + } + + if len(rmQueue) > 0 { + for i := len(rmQueue) - 1; i >= 0; i-- { + sh.SchedQueue.Remove(rmQueue[i]) } - - info = w.Info - needRes = res - bestWid = wid - selectedWindow = wnd - bestAssigned = wu } - if selectedWindow < 0 { - // all windows full - continue - } - - log.Debugw("SCHED ASSIGNED", - "sqi", sqi, - "sector", task.Sector.ID.Number, - "task", task.TaskType, - "window", selectedWindow, - "worker", bestWid, - "assigned", bestAssigned) - - workerAssigned[bestWid]++ - windows[selectedWindow].Allocated.Add(task.SealTask(), info.Resources, needRes) - windows[selectedWindow].Todo = append(windows[selectedWindow].Todo, task) - - rmQueue = append(rmQueue, sqi) - scheduled++ + return scheduled } - - if len(rmQueue) > 0 { - for i := len(rmQueue) - 1; i >= 0; i-- { - sh.SchedQueue.Remove(rmQueue[i]) - } - } - - return scheduled } diff --git a/storage/sealer/sched_assigner_spread_tasks.go b/storage/sealer/sched_assigner_spread_tasks.go new file mode 100644 index 000000000..09cf98046 --- /dev/null +++ b/storage/sealer/sched_assigner_spread_tasks.go @@ -0,0 +1,98 @@ +package sealer + +import ( + "math" + + "github.com/filecoin-project/lotus/storage/sealer/sealtasks" + "github.com/filecoin-project/lotus/storage/sealer/storiface" +) + +func NewSpreadTasksAssigner(queued bool) Assigner { + return &AssignerCommon{ + WindowSel: SpreadTasksWS(queued), + } +} + +type widTask struct { + wid storiface.WorkerID + tt sealtasks.TaskType +} + +func SpreadTasksWS(queued bool) func(sh *Scheduler, queueLen int, acceptableWindows [][]int, windows []SchedWindow) int { + return func(sh *Scheduler, queueLen int, acceptableWindows [][]int, windows []SchedWindow) int { + scheduled := 0 + rmQueue := make([]int, 0, queueLen) + workerAssigned := map[widTask]int{} + + for sqi := 0; sqi < queueLen; sqi++ { + task := (*sh.SchedQueue)[sqi] + + selectedWindow := -1 + var needRes storiface.Resources + var info storiface.WorkerInfo + var bestWid widTask + bestAssigned := math.MaxInt // smaller = better + + for i, wnd := range acceptableWindows[task.IndexHeap] { + wid := sh.OpenWindows[wnd].Worker + w := sh.Workers[wid] + + res := w.Info.Resources.ResourceSpec(task.Sector.ProofType, task.TaskType) + + log.Debugf("SCHED try assign sqi:%d sector %d to window %d (awi:%d)", sqi, task.Sector.ID.Number, wnd, i) + + if !windows[wnd].Allocated.CanHandleRequest(task.SealTask(), res, wid, "schedAssign", w.Info) { + continue + } + + wt := widTask{wid: wid, tt: task.TaskType} + + wu, found := workerAssigned[wt] + if !found && queued { + st := task.SealTask() + wu = w.TaskCount(&st) + workerAssigned[wt] = wu + } + if wu >= bestAssigned { + continue + } + + info = w.Info + needRes = res + bestWid = wt + selectedWindow = wnd + bestAssigned = wu + } + + if selectedWindow < 0 { + // all windows full + continue + } + + log.Debugw("SCHED ASSIGNED", + "assigner", "spread-tasks", + "spread-queued", queued, + "sqi", sqi, + "sector", task.Sector.ID.Number, + "task", task.TaskType, + "window", selectedWindow, + "worker", bestWid, + "assigned", bestAssigned) + + workerAssigned[bestWid]++ + windows[selectedWindow].Allocated.Add(task.SealTask(), info.Resources, needRes) + windows[selectedWindow].Todo = append(windows[selectedWindow].Todo, task) + + rmQueue = append(rmQueue, sqi) + scheduled++ + } + + if len(rmQueue) > 0 { + for i := len(rmQueue) - 1; i >= 0; i-- { + sh.SchedQueue.Remove(rmQueue[i]) + } + } + + return scheduled + } +} diff --git a/storage/sealer/sched_assigner_utilization.go b/storage/sealer/sched_assigner_utilization.go index 2d051d000..1e75d904a 100644 --- a/storage/sealer/sched_assigner_utilization.go +++ b/storage/sealer/sched_assigner_utilization.go @@ -74,6 +74,7 @@ func LowestUtilizationWS(sh *Scheduler, queueLen int, acceptableWindows [][]int, } log.Debugw("SCHED ASSIGNED", + "assigner", "util", "sqi", sqi, "sector", task.Sector.ID.Number, "task", task.TaskType, diff --git a/storage/sealer/sched_resources.go b/storage/sealer/sched_resources.go index 487e294a2..c4ad7991d 100644 --- a/storage/sealer/sched_resources.go +++ b/storage/sealer/sched_resources.go @@ -170,6 +170,19 @@ func (a *ActiveResources) utilization(wr storiface.WorkerResources) float64 { // return max } +func (a *ActiveResources) taskCount(tt *sealtasks.SealTaskType) int { + // nil means all tasks + if tt == nil { + var count int + for _, c := range a.taskCounters { + count += c + } + return count + } + + return a.taskCounters[*tt] +} + func (wh *WorkerHandle) Utilization() float64 { wh.lk.Lock() u := wh.active.utilization(wh.Info.Resources) @@ -183,3 +196,31 @@ func (wh *WorkerHandle) Utilization() float64 { return u } + +func (wh *WorkerHandle) TaskCounts() int { + wh.lk.Lock() + u := wh.active.taskCount(nil) + u += wh.preparing.taskCount(nil) + wh.lk.Unlock() + wh.wndLk.Lock() + for _, window := range wh.activeWindows { + u += window.Allocated.taskCount(nil) + } + wh.wndLk.Unlock() + + return u +} + +func (wh *WorkerHandle) TaskCount(tt *sealtasks.SealTaskType) int { + wh.lk.Lock() + u := wh.active.taskCount(tt) + u += wh.preparing.taskCount(tt) + wh.lk.Unlock() + wh.wndLk.Lock() + for _, window := range wh.activeWindows { + u += window.Allocated.taskCount(tt) + } + wh.wndLk.Unlock() + + return u +} From 2316363f7a485bd1e16dce7b7d2f1deaade2b4b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Tue, 28 Feb 2023 09:02:18 +0100 Subject: [PATCH 07/70] sched: Share active/preparing task counters --- storage/sealer/sched_assigner_common.go | 2 +- storage/sealer/sched_resources.go | 72 ++++++++++++++++++++----- storage/sealer/sched_test.go | 8 +-- storage/sealer/sched_worker.go | 6 ++- storage/sealer/stats.go | 4 +- 5 files changed, 70 insertions(+), 22 deletions(-) diff --git a/storage/sealer/sched_assigner_common.go b/storage/sealer/sched_assigner_common.go index bf92dbf15..d676d410d 100644 --- a/storage/sealer/sched_assigner_common.go +++ b/storage/sealer/sched_assigner_common.go @@ -58,7 +58,7 @@ func (a *AssignerCommon) TrySched(sh *Scheduler) { windows := make([]SchedWindow, windowsLen) for i := range windows { - windows[i].Allocated = *NewActiveResources() + windows[i].Allocated = *NewActiveResources(newTaskCounter()) } acceptableWindows := make([][]int, queueLen) // QueueIndex -> []OpenWindowIndex diff --git a/storage/sealer/sched_resources.go b/storage/sealer/sched_resources.go index c4ad7991d..597f36dbe 100644 --- a/storage/sealer/sched_resources.go +++ b/storage/sealer/sched_resources.go @@ -13,18 +13,68 @@ type ActiveResources struct { gpuUsed float64 cpuUse uint64 - taskCounters map[sealtasks.SealTaskType]int + taskCounters *taskCounter cond *sync.Cond waiting int } -func NewActiveResources() *ActiveResources { - return &ActiveResources{ +type taskCounter struct { + taskCounters map[sealtasks.SealTaskType]int + + // this lock is technically redundant, as ActiveResources is always accessed + // with the worker lock, but let's not panic if we ever change that + lk sync.Mutex +} + +func newTaskCounter() *taskCounter { + return &taskCounter{ taskCounters: map[sealtasks.SealTaskType]int{}, } } +func (tc *taskCounter) Add(tt sealtasks.SealTaskType) { + tc.lk.Lock() + defer tc.lk.Unlock() + tc.taskCounters[tt]++ +} + +func (tc *taskCounter) Free(tt sealtasks.SealTaskType) { + tc.lk.Lock() + defer tc.lk.Unlock() + tc.taskCounters[tt]-- +} + +func (tc *taskCounter) Get(tt sealtasks.SealTaskType) int { + tc.lk.Lock() + defer tc.lk.Unlock() + return tc.taskCounters[tt] +} + +func (tc *taskCounter) Sum() int { + tc.lk.Lock() + defer tc.lk.Unlock() + sum := 0 + for _, v := range tc.taskCounters { + sum += v + } + return sum +} + +func (tc *taskCounter) ForEach(cb func(tt sealtasks.SealTaskType, count int)) { + tc.lk.Lock() + defer tc.lk.Unlock() + for tt, count := range tc.taskCounters { + cb(tt, count) + } +} + +func NewActiveResources(tc *taskCounter) *ActiveResources { + return &ActiveResources{ + taskCounters: tc, + } +} + func (a *ActiveResources) withResources(id storiface.WorkerID, wr storiface.WorkerInfo, tt sealtasks.SealTaskType, r storiface.Resources, locker sync.Locker, cb func() error) error { for !a.CanHandleRequest(tt, r, id, "withResources", wr) { if a.cond == nil { @@ -59,7 +109,7 @@ func (a *ActiveResources) Add(tt sealtasks.SealTaskType, wr storiface.WorkerReso a.cpuUse += r.Threads(wr.CPUs, len(wr.GPUs)) a.memUsedMin += r.MinMemory a.memUsedMax += r.MaxMemory - a.taskCounters[tt]++ + a.taskCounters.Add(tt) return a.utilization(wr) - startUtil } @@ -71,7 +121,7 @@ func (a *ActiveResources) Free(tt sealtasks.SealTaskType, wr storiface.WorkerRes a.cpuUse -= r.Threads(wr.CPUs, len(wr.GPUs)) a.memUsedMin -= r.MinMemory a.memUsedMax -= r.MaxMemory - a.taskCounters[tt]-- + a.taskCounters.Free(tt) if a.cond != nil { a.cond.Broadcast() @@ -82,8 +132,8 @@ func (a *ActiveResources) Free(tt sealtasks.SealTaskType, wr storiface.WorkerRes // handle the request. func (a *ActiveResources) CanHandleRequest(tt sealtasks.SealTaskType, needRes storiface.Resources, wid storiface.WorkerID, caller string, info storiface.WorkerInfo) bool { if needRes.MaxConcurrent > 0 { - if a.taskCounters[tt] >= needRes.MaxConcurrent { - log.Debugf("sched: not scheduling on worker %s for %s; at task limit tt=%s, curcount=%d", wid, caller, tt, a.taskCounters[tt]) + if a.taskCounters.Get(tt) >= needRes.MaxConcurrent { + log.Debugf("sched: not scheduling on worker %s for %s; at task limit tt=%s, curcount=%d", wid, caller, tt, a.taskCounters.Get(tt)) return false } } @@ -173,14 +223,10 @@ func (a *ActiveResources) utilization(wr storiface.WorkerResources) float64 { // func (a *ActiveResources) taskCount(tt *sealtasks.SealTaskType) int { // nil means all tasks if tt == nil { - var count int - for _, c := range a.taskCounters { - count += c - } - return count + return a.taskCounters.Sum() } - return a.taskCounters[*tt] + return a.taskCounters.Get(*tt) } func (wh *WorkerHandle) Utilization() float64 { diff --git a/storage/sealer/sched_test.go b/storage/sealer/sched_test.go index 2eed1ce73..5cf1a35b1 100644 --- a/storage/sealer/sched_test.go +++ b/storage/sealer/sched_test.go @@ -639,8 +639,8 @@ func BenchmarkTrySched(b *testing.B) { Resources: decentWorkerResources, }, Enabled: true, - preparing: NewActiveResources(), - active: NewActiveResources(), + preparing: NewActiveResources(newTaskCounter()), + active: NewActiveResources(newTaskCounter()), } for i := 0; i < windows; i++ { @@ -685,7 +685,7 @@ func TestWindowCompact(t *testing.T) { for _, windowTasks := range start { window := &SchedWindow{ - Allocated: *NewActiveResources(), + Allocated: *NewActiveResources(newTaskCounter()), } for _, task := range windowTasks { @@ -708,7 +708,7 @@ func TestWindowCompact(t *testing.T) { require.Equal(t, len(start)-len(expect), -sw.windowsRequested) for wi, tasks := range expect { - expectRes := NewActiveResources() + expectRes := NewActiveResources(newTaskCounter()) for ti, task := range tasks { require.Equal(t, task, wh.activeWindows[wi].Todo[ti].TaskType, "%d, %d", wi, ti) diff --git a/storage/sealer/sched_worker.go b/storage/sealer/sched_worker.go index e6e1f62da..20918e774 100644 --- a/storage/sealer/sched_worker.go +++ b/storage/sealer/sched_worker.go @@ -30,12 +30,14 @@ func newWorkerHandle(ctx context.Context, w Worker) (*WorkerHandle, error) { return nil, xerrors.Errorf("getting worker info: %w", err) } + tc := newTaskCounter() + worker := &WorkerHandle{ workerRpc: w, Info: info, - preparing: NewActiveResources(), - active: NewActiveResources(), + preparing: NewActiveResources(tc), + active: NewActiveResources(tc), Enabled: true, closingMgr: make(chan struct{}), diff --git a/storage/sealer/stats.go b/storage/sealer/stats.go index f34109d78..90b6287c3 100644 --- a/storage/sealer/stats.go +++ b/storage/sealer/stats.go @@ -43,9 +43,9 @@ func (m *Manager) WorkerStats(ctx context.Context) map[uuid.UUID]storiface.Worke TaskCounts: map[string]int{}, } - for tt, count := range handle.active.taskCounters { + handle.active.taskCounters.ForEach(func(tt sealtasks.SealTaskType, count int) { out[uuid.UUID(id)].TaskCounts[tt.String()] = count - } + }) handle.lk.Unlock() } From 04fe9062a6f6c20df7715c2ae0da2a41e4977fe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Tue, 28 Feb 2023 09:48:17 +0100 Subject: [PATCH 08/70] fix: fsm: shutdown removed sectors FSMs --- storage/pipeline/fsm.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/storage/pipeline/fsm.go b/storage/pipeline/fsm.go index 25fd6fcef..8ae18a9fd 100644 --- a/storage/pipeline/fsm.go +++ b/storage/pipeline/fsm.go @@ -6,8 +6,10 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "net/http" + "os" "reflect" "time" @@ -19,9 +21,15 @@ import ( "github.com/filecoin-project/lotus/api" ) +var errSectorRemoved = errors.New("sector removed") + func (m *Sealing) Plan(events []statemachine.Event, user interface{}) (interface{}, uint64, error) { next, processed, err := m.plan(events, user.(*SectorInfo)) if err != nil || next == nil { + if err == errSectorRemoved && os.Getenv("LOTUS_KEEP_REMOVED_FSM_ACTIVE") != "1" { + return nil, processed, statemachine.ErrTerminated + } + l := Log{ Timestamp: uint64(time.Now().Unix()), Message: fmt.Sprintf("state machine error: %s", err), @@ -601,7 +609,7 @@ func (m *Sealing) plan(events []statemachine.Event, state *SectorInfo) (func(sta case Removing: return m.handleRemoving, processed, nil case Removed: - return nil, processed, nil + return nil, processed, errSectorRemoved case RemoveFailed: return m.handleRemoveFailed, processed, nil @@ -615,13 +623,14 @@ func (m *Sealing) plan(events []statemachine.Event, state *SectorInfo) (func(sta // Fatal errors case UndefinedSectorState: log.Error("sector update with undefined state!") + return nil, processed, xerrors.Errorf("sector update with undefined state") case FailedUnrecoverable: log.Errorf("sector %d failed unrecoverably", state.SectorNumber) + return nil, processed, xerrors.Errorf("sector %d failed unrecoverably", state.SectorNumber) default: log.Errorf("unexpected sector update state: %s", state.State) + return nil, processed, xerrors.Errorf("unexpected sector update state: %s", state.State) } - - return nil, processed, nil } func (m *Sealing) onUpdateSector(ctx context.Context, state *SectorInfo) error { From c484c387350c2e2294d8b8fe1b901925bb12f4db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Tue, 28 Feb 2023 10:33:15 +0100 Subject: [PATCH 09/70] worker sched: Separate resource def for preparing window --- storage/sealer/manager.go | 37 +++++++++++++++++++----------- storage/sealer/sched.go | 15 ++++++++++-- storage/sealer/sched_test.go | 31 ++++++++++++++----------- storage/sealer/sched_worker.go | 13 ++++++----- storage/sealer/sealtasks/task.go | 2 ++ storage/sealer/storiface/worker.go | 14 +++++++++++ 6 files changed, 77 insertions(+), 35 deletions(-) diff --git a/storage/sealer/manager.go b/storage/sealer/manager.go index 336664ca8..db5f9a589 100644 --- a/storage/sealer/manager.go +++ b/storage/sealer/manager.go @@ -289,14 +289,20 @@ func (m *Manager) ServeHTTP(w http.ResponseWriter, r *http.Request) { m.remoteHnd.ServeHTTP(w, r) } -func schedNop(context.Context, Worker) error { - return nil +var schedNop = PrepareAction{ + Action: func(ctx context.Context, w Worker) error { + return nil + }, + PrepType: sealtasks.TTNoop, } -func (m *Manager) schedFetch(sector storiface.SectorRef, ft storiface.SectorFileType, ptype storiface.PathType, am storiface.AcquireMode) func(context.Context, Worker) error { - return func(ctx context.Context, worker Worker) error { - _, err := m.waitSimpleCall(ctx)(worker.Fetch(ctx, sector, ft, ptype, am)) - return err +func (m *Manager) schedFetch(sector storiface.SectorRef, ft storiface.SectorFileType, ptype storiface.PathType, am storiface.AcquireMode) PrepareAction { + return PrepareAction{ + Action: func(ctx context.Context, worker Worker) error { + _, err := m.waitSimpleCall(ctx)(worker.Fetch(ctx, sector, ft, ptype, am)) + return err + }, + PrepType: sealtasks.TTFetch, } } @@ -315,16 +321,19 @@ func (m *Manager) SectorsUnsealPiece(ctx context.Context, sector storiface.Secto // if the selected worker does NOT have the sealed files for the sector, instruct it to fetch it from a worker that has them and // put it in the sealing scratch space. - sealFetch := func(ctx context.Context, worker Worker) error { - log.Debugf("copy sealed/cache sector data for sector %d", sector.ID) - _, err := m.waitSimpleCall(ctx)(worker.Fetch(ctx, sector, storiface.FTSealed|storiface.FTCache, storiface.PathSealing, storiface.AcquireCopy)) - _, err2 := m.waitSimpleCall(ctx)(worker.Fetch(ctx, sector, storiface.FTUpdate|storiface.FTUpdateCache, storiface.PathSealing, storiface.AcquireCopy)) + sealFetch := PrepareAction{ + Action: func(ctx context.Context, worker Worker) error { + log.Debugf("copy sealed/cache sector data for sector %d", sector.ID) + _, err := m.waitSimpleCall(ctx)(worker.Fetch(ctx, sector, storiface.FTSealed|storiface.FTCache, storiface.PathSealing, storiface.AcquireCopy)) + _, err2 := m.waitSimpleCall(ctx)(worker.Fetch(ctx, sector, storiface.FTUpdate|storiface.FTUpdateCache, storiface.PathSealing, storiface.AcquireCopy)) - if err != nil && err2 != nil { - return xerrors.Errorf("cannot unseal piece. error fetching sealed data: %w. error fetching replica data: %w", err, err2) - } + if err != nil && err2 != nil { + return xerrors.Errorf("cannot unseal piece. error fetching sealed data: %w. error fetching replica data: %w", err, err2) + } - return nil + return nil + }, + PrepType: sealtasks.TTFetch, } if unsealed == nil { diff --git a/storage/sealer/sched.go b/storage/sealer/sched.go index 4f167eae3..c0ac11bcf 100644 --- a/storage/sealer/sched.go +++ b/storage/sealer/sched.go @@ -42,6 +42,10 @@ func WithPriority(ctx context.Context, priority int) context.Context { const mib = 1 << 20 type WorkerAction func(ctx context.Context, w Worker) error +type PrepareAction struct { + Action WorkerAction + PrepType sealtasks.TaskType +} type SchedWorker interface { TaskTypes(context.Context) (map[sealtasks.TaskType]struct{}, error) @@ -130,7 +134,7 @@ type WorkerRequest struct { Sel WorkerSelector SchedId uuid.UUID - prepare WorkerAction + prepare PrepareAction work WorkerAction start time.Time @@ -197,7 +201,7 @@ func newScheduler(ctx context.Context, assigner string) (*Scheduler, error) { }, nil } -func (sh *Scheduler) Schedule(ctx context.Context, sector storiface.SectorRef, taskType sealtasks.TaskType, sel WorkerSelector, prepare WorkerAction, work WorkerAction) error { +func (sh *Scheduler) Schedule(ctx context.Context, sector storiface.SectorRef, taskType sealtasks.TaskType, sel WorkerSelector, prepare PrepareAction, work WorkerAction) error { ret := make(chan workerResponse) select { @@ -247,6 +251,13 @@ func (r *WorkerRequest) SealTask() sealtasks.SealTaskType { } } +func (r *WorkerRequest) PrepSealTask() sealtasks.SealTaskType { + return sealtasks.SealTaskType{ + TaskType: r.prepare.PrepType, + RegisteredSealProof: r.Sector.ProofType, + } +} + type SchedDiagRequestInfo struct { Sector abi.SectorID TaskType sealtasks.TaskType diff --git a/storage/sealer/sched_test.go b/storage/sealer/sched_test.go index 5cf1a35b1..07731e934 100644 --- a/storage/sealer/sched_test.go +++ b/storage/sealer/sched_test.go @@ -288,25 +288,30 @@ func TestSched(t *testing.T) { ProofType: spt, } - err := sched.Schedule(ctx, sectorRef, taskType, sel, func(ctx context.Context, w Worker) error { - wi, err := w.Info(ctx) - require.NoError(t, err) + prep := PrepareAction{ + Action: func(ctx context.Context, w Worker) error { + wi, err := w.Info(ctx) + require.NoError(t, err) - require.Equal(t, expectWorker, wi.Hostname) + require.Equal(t, expectWorker, wi.Hostname) - log.Info("IN ", taskName) + log.Info("IN ", taskName) - for { - _, ok := <-done - if !ok { - break + for { + _, ok := <-done + if !ok { + break + } } - } - log.Info("OUT ", taskName) + log.Info("OUT ", taskName) - return nil - }, noopAction) + return nil + }, + PrepType: taskType, + } + + err := sched.Schedule(ctx, sectorRef, taskType, sel, prep, noopAction) if err != context.Canceled { require.NoError(t, err, fmt.Sprint(l, l2)) } diff --git a/storage/sealer/sched_worker.go b/storage/sealer/sched_worker.go index 20918e774..b6efc851a 100644 --- a/storage/sealer/sched_worker.go +++ b/storage/sealer/sched_worker.go @@ -354,8 +354,8 @@ assignLoop: worker.lk.Lock() for t, todo := range firstWindow.Todo { - needRes := worker.Info.Resources.ResourceSpec(todo.Sector.ProofType, todo.TaskType) - if worker.preparing.CanHandleRequest(todo.SealTask(), needRes, sw.wid, "startPreparing", worker.Info) { + needResPrep := worker.Info.Resources.PrepResourceSpec(todo.Sector.ProofType, todo.TaskType, todo.prepare.PrepType) + if worker.preparing.CanHandleRequest(todo.PrepSealTask(), needResPrep, sw.wid, "startPreparing", worker.Info) { tidx = t break } @@ -454,20 +454,21 @@ func (sw *schedWorker) startProcessingTask(req *WorkerRequest) error { w, sh := sw.worker, sw.sched needRes := w.Info.Resources.ResourceSpec(req.Sector.ProofType, req.TaskType) + needResPrep := w.Info.Resources.PrepResourceSpec(req.Sector.ProofType, req.TaskType, req.prepare.PrepType) w.lk.Lock() - w.preparing.Add(req.SealTask(), w.Info.Resources, needRes) + w.preparing.Add(req.PrepSealTask(), w.Info.Resources, needResPrep) w.lk.Unlock() go func() { // first run the prepare step (e.g. fetching sector data from other worker) tw := sh.workTracker.worker(sw.wid, w.Info, w.workerRpc) tw.start() - err := req.prepare(req.Ctx, tw) + err := req.prepare.Action(req.Ctx, tw) w.lk.Lock() if err != nil { - w.preparing.Free(req.SealTask(), w.Info.Resources, needRes) + w.preparing.Free(req.PrepSealTask(), w.Info.Resources, needResPrep) w.lk.Unlock() select { @@ -497,7 +498,7 @@ func (sw *schedWorker) startProcessingTask(req *WorkerRequest) error { // wait (if needed) for resources in the 'active' window err = w.active.withResources(sw.wid, w.Info, req.SealTask(), needRes, &w.lk, func() error { - w.preparing.Free(req.SealTask(), w.Info.Resources, needRes) + w.preparing.Free(req.PrepSealTask(), w.Info.Resources, needResPrep) w.lk.Unlock() defer w.lk.Lock() // we MUST return locked from this function diff --git a/storage/sealer/sealtasks/task.go b/storage/sealer/sealtasks/task.go index bbf33b159..2e134f3d3 100644 --- a/storage/sealer/sealtasks/task.go +++ b/storage/sealer/sealtasks/task.go @@ -36,6 +36,8 @@ const ( TTGenerateWindowPoSt TaskType = "post/v0/windowproof" TTGenerateWinningPoSt TaskType = "post/v0/winningproof" + + TTNoop TaskType = "" ) var order = map[TaskType]int{ diff --git a/storage/sealer/storiface/worker.go b/storage/sealer/storiface/worker.go index 3cbf9f737..2badad292 100644 --- a/storage/sealer/storiface/worker.go +++ b/storage/sealer/storiface/worker.go @@ -65,6 +65,20 @@ func (wr WorkerResources) ResourceSpec(spt abi.RegisteredSealProof, tt sealtasks return res } +// PrepResourceSpec is like ResourceSpec, but meant for use limiting parallel preparing +// tasks. +func (wr WorkerResources) PrepResourceSpec(spt abi.RegisteredSealProof, tt, prepTT sealtasks.TaskType) Resources { + res := wr.ResourceSpec(spt, tt) + + if prepTT != tt && prepTT != sealtasks.TTNoop { + prepRes := wr.ResourceSpec(spt, prepTT) + res.MaxConcurrent = prepRes.MaxConcurrent + } + + // otherwise, use the default resource table + return res +} + type WorkerStats struct { Info WorkerInfo Tasks []sealtasks.TaskType From 814c146626458f141a1e3a22926cf03e21cdec45 Mon Sep 17 00:00:00 2001 From: Travis Person Date: Mon, 27 Feb 2023 17:27:48 +0000 Subject: [PATCH 10/70] feat: stmgr: cache migrated stateroots --- chain/gen/gen.go | 2 +- chain/stmgr/forks.go | 14 +++- chain/stmgr/forks_test.go | 108 ++++++++++++++++++++++++- chain/stmgr/stmgr.go | 56 ++++++++++++- chain/store/store_test.go | 5 +- cmd/lotus-bench/import.go | 2 +- cmd/lotus-shed/balances.go | 4 +- cmd/lotus-shed/gas-estimation.go | 4 +- cmd/lotus-shed/invariants.go | 2 +- cmd/lotus-shed/migrations.go | 4 +- cmd/lotus-shed/state-stats.go | 2 +- cmd/lotus-sim/simulation/node.go | 4 +- cmd/lotus-sim/simulation/simulation.go | 2 +- cmd/lotus/daemon.go | 2 +- conformance/driver.go | 2 +- node/modules/chain.go | 2 +- node/modules/stmgr.go | 5 +- 17 files changed, 194 insertions(+), 26 deletions(-) diff --git a/chain/gen/gen.go b/chain/gen/gen.go index 673a299e3..de2df97c2 100644 --- a/chain/gen/gen.go +++ b/chain/gen/gen.go @@ -256,7 +256,7 @@ func NewGeneratorWithSectorsAndUpgradeSchedule(numSectors int, us stmgr.UpgradeS //return nil, xerrors.Errorf("creating drand beacon: %w", err) //} - sm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), sys, us, beac) + sm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), sys, us, beac, ds) if err != nil { return nil, xerrors.Errorf("initing stmgr: %w", err) } diff --git a/chain/stmgr/forks.go b/chain/stmgr/forks.go index 5a38fbada..987c3ba5c 100644 --- a/chain/stmgr/forks.go +++ b/chain/stmgr/forks.go @@ -174,9 +174,16 @@ func (us UpgradeSchedule) GetNtwkVersion(e abi.ChainEpoch) (network.Version, err func (sm *StateManager) HandleStateForks(ctx context.Context, root cid.Cid, height abi.ChainEpoch, cb ExecMonitor, ts *types.TipSet) (cid.Cid, error) { retCid := root - var err error u := sm.stateMigrations[height] if u != nil && u.upgrade != nil { + migCid, ok, err := u.resultCache.Result(ctx, root) + if err == nil && ok { + log.Warnw("CACHED migration", "height", height, "from", root, "to", migCid) + return migCid, nil + } else if err != nil { + log.Errorw("failed to lookup previous migration result", "err", err) + } + startTime := time.Now() log.Warnw("STARTING migration", "height", height, "from", root) // Yes, we clone the cache, even for the final upgrade epoch. Why? Reverts. We may @@ -197,6 +204,11 @@ func (sm *StateManager) HandleStateForks(ctx context.Context, root cid.Cid, heig "to", retCid, "duration", time.Since(startTime), ) + + // Only set if migration ran, we do not want a root => root mapping + if err := u.resultCache.Store(ctx, root, retCid); err != nil { + log.Errorw("failed to store migration result", "err", err) + } } return retCid, nil diff --git a/chain/stmgr/forks_test.go b/chain/stmgr/forks_test.go index 52cfe5fce..a904172cd 100644 --- a/chain/stmgr/forks_test.go +++ b/chain/stmgr/forks_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/ipfs/go-cid" + "github.com/ipfs/go-datastore" ipldcbor "github.com/ipfs/go-ipld-cbor" logging "github.com/ipfs/go-log/v2" "github.com/stretchr/testify/require" @@ -35,6 +36,7 @@ import ( "github.com/filecoin-project/lotus/chain/consensus" "github.com/filecoin-project/lotus/chain/consensus/filcns" "github.com/filecoin-project/lotus/chain/gen" + "github.com/filecoin-project/lotus/chain/stmgr" . "github.com/filecoin-project/lotus/chain/stmgr" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/vm" @@ -166,7 +168,7 @@ func TestForkHeightTriggers(t *testing.T) { } return st.Flush(ctx) - }}}, cg.BeaconSchedule()) + }}}, cg.BeaconSchedule(), datastore.NewMapDatastore()) if err != nil { t.Fatal(err) } @@ -284,7 +286,7 @@ func testForkRefuseCall(t *testing.T, nullsBefore, nullsAfter int) { root cid.Cid, height abi.ChainEpoch, ts *types.TipSet) (cid.Cid, error) { migrationCount++ return root, nil - }}}, cg.BeaconSchedule()) + }}}, cg.BeaconSchedule(), datastore.NewMapDatastore()) if err != nil { t.Fatal(err) } @@ -502,7 +504,7 @@ func TestForkPreMigration(t *testing.T) { return nil }, }}}, - }, cg.BeaconSchedule()) + }, cg.BeaconSchedule(), datastore.NewMapDatastore()) if err != nil { t.Fatal(err) } @@ -576,6 +578,7 @@ func TestDisablePreMigration(t *testing.T) { }}}, }, cg.BeaconSchedule(), + datastore.NewMapDatastore(), ) require.NoError(t, err) require.NoError(t, sm.Start(context.Background())) @@ -603,3 +606,102 @@ func TestDisablePreMigration(t *testing.T) { require.Equal(t, 1, len(counter)) } + +func TestMigrtionCache(t *testing.T) { + logging.SetAllLoggers(logging.LevelInfo) + + cg, err := gen.NewGenerator() + require.NoError(t, err) + + counter := make(chan struct{}, 10) + metadataDs := datastore.NewMapDatastore() + + sm, err := NewStateManager( + cg.ChainStore(), + consensus.NewTipSetExecutor(filcns.RewardFunc), + cg.StateManager().VMSys(), + UpgradeSchedule{{ + Network: network.Version1, + Height: testForkHeight, + Migration: func(_ context.Context, _ *StateManager, _ MigrationCache, _ ExecMonitor, + root cid.Cid, _ abi.ChainEpoch, _ *types.TipSet) (cid.Cid, error) { + + counter <- struct{}{} + + return root, nil + }}, + }, + cg.BeaconSchedule(), + metadataDs, + ) + require.NoError(t, err) + require.NoError(t, sm.Start(context.Background())) + defer func() { + require.NoError(t, sm.Stop(context.Background())) + }() + + inv := consensus.NewActorRegistry() + registry := builtin.MakeRegistryLegacy([]rtt.VMActor{testActor{}}) + inv.Register(actorstypes.Version0, nil, registry) + + sm.SetVMConstructor(func(ctx context.Context, vmopt *vm.VMOpts) (vm.Interface, error) { + nvm, err := vm.NewLegacyVM(ctx, vmopt) + require.NoError(t, err) + nvm.SetInvoker(inv) + return nvm, nil + }) + + cg.SetStateManager(sm) + + for i := 0; i < 50; i++ { + _, err := cg.NextTipSet() + require.NoError(t, err) + } + + ts, err := cg.ChainStore().GetTipsetByHeight(context.Background(), testForkHeight, nil, false) + require.NoError(t, err) + + root, _, err := stmgr.ComputeState(context.Background(), sm, testForkHeight+1, []*types.Message{}, ts) + require.NoError(t, err) + t.Log(root) + + require.Equal(t, 1, len(counter)) + + { + sm, err := NewStateManager( + cg.ChainStore(), + consensus.NewTipSetExecutor(filcns.RewardFunc), + cg.StateManager().VMSys(), + UpgradeSchedule{{ + Network: network.Version1, + Height: testForkHeight, + Migration: func(_ context.Context, _ *StateManager, _ MigrationCache, _ ExecMonitor, + root cid.Cid, _ abi.ChainEpoch, _ *types.TipSet) (cid.Cid, error) { + + counter <- struct{}{} + + return root, nil + }}, + }, + cg.BeaconSchedule(), + metadataDs, + ) + require.NoError(t, err) + sm.SetVMConstructor(func(ctx context.Context, vmopt *vm.VMOpts) (vm.Interface, error) { + nvm, err := vm.NewLegacyVM(ctx, vmopt) + require.NoError(t, err) + nvm.SetInvoker(inv) + return nvm, nil + }) + + ctx := context.Background() + + base, _, err := sm.ExecutionTrace(ctx, ts) + require.NoError(t, err) + _, err = sm.HandleStateForks(context.Background(), base, ts.Height(), nil, ts) + require.NoError(t, err) + + // Should not have increased as we should be using the cached results in the metadataDs + require.Equal(t, 1, len(counter)) + } +} diff --git a/chain/stmgr/stmgr.go b/chain/stmgr/stmgr.go index ee9338e63..d0e4cbb19 100644 --- a/chain/stmgr/stmgr.go +++ b/chain/stmgr/stmgr.go @@ -2,9 +2,11 @@ package stmgr import ( "context" + "fmt" "sync" "github.com/ipfs/go-cid" + dstore "github.com/ipfs/go-datastore" cbor "github.com/ipfs/go-ipld-cbor" logging "github.com/ipfs/go-log/v2" "golang.org/x/xerrors" @@ -54,6 +56,48 @@ type migration struct { upgrade MigrationFunc preMigrations []PreMigration cache *nv16.MemMigrationCache + resultCache *resultCache +} + +type resultCache struct { + ds dstore.Batching + keyPrefix string +} + +func (m *resultCache) Result(ctx context.Context, root cid.Cid) (cid.Cid, bool, error) { + kStr := fmt.Sprintf("%s-%s", m.keyPrefix, root) + k := dstore.NewKey(kStr) + + found, err := m.ds.Has(ctx, k) + if err != nil { + return cid.Undef, false, xerrors.Errorf("error looking up migration result: %w", err) + } + + if !found { + return cid.Undef, false, nil + } + + bs, err := m.ds.Get(ctx, k) + if err != nil { + return cid.Undef, false, xerrors.Errorf("error loading migration result: %w", err) + } + + c, err := cid.Parse(bs) + if err != nil { + return cid.Undef, false, xerrors.Errorf("error parsing migration result: %w", err) + } + + return c, true, nil +} + +func (m *resultCache) Store(ctx context.Context, root cid.Cid, resultCid cid.Cid) error { + kStr := fmt.Sprintf("%s-%s", m.keyPrefix, root) + k := dstore.NewKey(kStr) + if err := m.ds.Put(ctx, k, resultCid.Bytes()); err != nil { + return err + } + + return nil } type Executor interface { @@ -103,7 +147,7 @@ type treeCache struct { tree *state.StateTree } -func NewStateManager(cs *store.ChainStore, exec Executor, sys vm.SyscallBuilder, us UpgradeSchedule, beacon beacon.Schedule) (*StateManager, error) { +func NewStateManager(cs *store.ChainStore, exec Executor, sys vm.SyscallBuilder, us UpgradeSchedule, beacon beacon.Schedule, metadataDs dstore.Batching) (*StateManager, error) { // If we have upgrades, make sure they're in-order and make sense. if err := us.Validate(); err != nil { return nil, err @@ -122,12 +166,18 @@ func NewStateManager(cs *store.ChainStore, exec Executor, sys vm.SyscallBuilder, upgrade: upgrade.Migration, preMigrations: upgrade.PreMigrations, cache: nv16.NewMemMigrationCache(), + resultCache: &resultCache{ + keyPrefix: fmt.Sprintf("nv%d-%d", upgrade.Network, upgrade.Height), + ds: metadataDs, + }, } + stateMigrations[upgrade.Height] = migration } if upgrade.Expensive { expensiveUpgrades[upgrade.Height] = struct{}{} } + networkVersions = append(networkVersions, versionSpec{ networkVersion: lastVersion, atOrBelow: upgrade.Height, @@ -155,8 +205,8 @@ func NewStateManager(cs *store.ChainStore, exec Executor, sys vm.SyscallBuilder, }, nil } -func NewStateManagerWithUpgradeScheduleAndMonitor(cs *store.ChainStore, exec Executor, sys vm.SyscallBuilder, us UpgradeSchedule, b beacon.Schedule, em ExecMonitor) (*StateManager, error) { - sm, err := NewStateManager(cs, exec, sys, us, b) +func NewStateManagerWithUpgradeScheduleAndMonitor(cs *store.ChainStore, exec Executor, sys vm.SyscallBuilder, us UpgradeSchedule, b beacon.Schedule, em ExecMonitor, metadataDs dstore.Batching) (*StateManager, error) { + sm, err := NewStateManager(cs, exec, sys, us, b, metadataDs) if err != nil { return nil, err } diff --git a/chain/store/store_test.go b/chain/store/store_test.go index f5765fddc..cc72acc95 100644 --- a/chain/store/store_test.go +++ b/chain/store/store_test.go @@ -196,7 +196,8 @@ func TestChainExportImportFull(t *testing.T) { } nbs := blockstore.NewMemorySync() - cs := store.NewChainStore(nbs, nbs, datastore.NewMapDatastore(), filcns.Weight, nil) + ds := datastore.NewMapDatastore() + cs := store.NewChainStore(nbs, nbs, ds, filcns.Weight, nil) defer cs.Close() //nolint:errcheck root, err := cs.Import(context.TODO(), buf) @@ -213,7 +214,7 @@ func TestChainExportImportFull(t *testing.T) { t.Fatal("imported chain differed from exported chain") } - sm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), nil, filcns.DefaultUpgradeSchedule(), cg.BeaconSchedule()) + sm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), nil, filcns.DefaultUpgradeSchedule(), cg.BeaconSchedule(), ds) if err != nil { t.Fatal(err) } diff --git a/cmd/lotus-bench/import.go b/cmd/lotus-bench/import.go index bb7dc6907..94c8b32b5 100644 --- a/cmd/lotus-bench/import.go +++ b/cmd/lotus-bench/import.go @@ -229,7 +229,7 @@ var importBenchCmd = &cli.Command{ defer cs.Close() //nolint:errcheck // TODO: We need to supply the actual beacon after v14 - stm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(verifier), filcns.DefaultUpgradeSchedule(), nil) + stm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(verifier), filcns.DefaultUpgradeSchedule(), nil, metadataDs) if err != nil { return err } diff --git a/cmd/lotus-shed/balances.go b/cmd/lotus-shed/balances.go index d678c7977..bae281583 100644 --- a/cmd/lotus-shed/balances.go +++ b/cmd/lotus-shed/balances.go @@ -513,7 +513,7 @@ var chainBalanceStateCmd = &cli.Command{ cst := cbor.NewCborStore(bs) store := adt.WrapStore(ctx, cst) - sm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(ffiwrapper.ProofVerifier), filcns.DefaultUpgradeSchedule(), nil) + sm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(ffiwrapper.ProofVerifier), filcns.DefaultUpgradeSchedule(), nil, mds) if err != nil { return err } @@ -737,7 +737,7 @@ var chainPledgeCmd = &cli.Command{ cst := cbor.NewCborStore(bs) store := adt.WrapStore(ctx, cst) - sm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(ffiwrapper.ProofVerifier), filcns.DefaultUpgradeSchedule(), nil) + sm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(ffiwrapper.ProofVerifier), filcns.DefaultUpgradeSchedule(), nil, mds) if err != nil { return err } diff --git a/cmd/lotus-shed/gas-estimation.go b/cmd/lotus-shed/gas-estimation.go index 26c4663bf..fe8428d1e 100644 --- a/cmd/lotus-shed/gas-estimation.go +++ b/cmd/lotus-shed/gas-estimation.go @@ -111,7 +111,7 @@ var gasTraceCmd = &cli.Command{ cs := store.NewChainStore(bs, bs, mds, filcns.Weight, nil) defer cs.Close() //nolint:errcheck - sm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(ffiwrapper.ProofVerifier), filcns.DefaultUpgradeSchedule(), shd) + sm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(ffiwrapper.ProofVerifier), filcns.DefaultUpgradeSchedule(), shd, mds) if err != nil { return err } @@ -212,7 +212,7 @@ var replayOfflineCmd = &cli.Command{ cs := store.NewChainStore(bs, bs, mds, filcns.Weight, nil) defer cs.Close() //nolint:errcheck - sm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(ffiwrapper.ProofVerifier), filcns.DefaultUpgradeSchedule(), shd) + sm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(ffiwrapper.ProofVerifier), filcns.DefaultUpgradeSchedule(), shd, mds) if err != nil { return err } diff --git a/cmd/lotus-shed/invariants.go b/cmd/lotus-shed/invariants.go index 9798d111a..b759e2c2c 100644 --- a/cmd/lotus-shed/invariants.go +++ b/cmd/lotus-shed/invariants.go @@ -90,7 +90,7 @@ var invariantsCmd = &cli.Command{ cs := store.NewChainStore(bs, bs, mds, filcns.Weight, nil) defer cs.Close() //nolint:errcheck - sm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(ffiwrapper.ProofVerifier), filcns.DefaultUpgradeSchedule(), nil) + sm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(ffiwrapper.ProofVerifier), filcns.DefaultUpgradeSchedule(), nil, mds) if err != nil { return err } diff --git a/cmd/lotus-shed/migrations.go b/cmd/lotus-shed/migrations.go index e305ba7e1..d9d29f50c 100644 --- a/cmd/lotus-shed/migrations.go +++ b/cmd/lotus-shed/migrations.go @@ -8,6 +8,7 @@ import ( "time" "github.com/ipfs/go-cid" + "github.com/ipfs/go-datastore" "github.com/urfave/cli/v2" cbg "github.com/whyrusleeping/cbor-gen" "golang.org/x/xerrors" @@ -120,7 +121,8 @@ var migrationsCmd = &cli.Command{ cs := store.NewChainStore(bs, bs, mds, filcns.Weight, nil) defer cs.Close() //nolint:errcheck - sm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(ffiwrapper.ProofVerifier), filcns.DefaultUpgradeSchedule(), nil) + // Note: we use a map datastore for the metadata to avoid writing / using cached migration results in the metadata store + sm, err := stmgr.NewStateManager(cs, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(ffiwrapper.ProofVerifier), filcns.DefaultUpgradeSchedule(), nil, datastore.NewMapDatastore()) if err != nil { return err } diff --git a/cmd/lotus-shed/state-stats.go b/cmd/lotus-shed/state-stats.go index 8ec4a0ff4..521e32c79 100644 --- a/cmd/lotus-shed/state-stats.go +++ b/cmd/lotus-shed/state-stats.go @@ -308,7 +308,7 @@ to reduce the number of decode operations performed by caching the decoded objec } tsExec := consensus.NewTipSetExecutor(filcns.RewardFunc) - sm, err := stmgr.NewStateManager(cs, tsExec, vm.Syscalls(ffiwrapper.ProofVerifier), filcns.DefaultUpgradeSchedule(), nil) + sm, err := stmgr.NewStateManager(cs, tsExec, vm.Syscalls(ffiwrapper.ProofVerifier), filcns.DefaultUpgradeSchedule(), nil, mds) if err != nil { return err } diff --git a/cmd/lotus-sim/simulation/node.go b/cmd/lotus-sim/simulation/node.go index 06e837f8f..9b37da6c8 100644 --- a/cmd/lotus-sim/simulation/node.go +++ b/cmd/lotus-sim/simulation/node.go @@ -106,7 +106,7 @@ func (nd *Node) LoadSim(ctx context.Context, name string) (*Simulation, error) { if err != nil { return nil, xerrors.Errorf("failed to create upgrade schedule for simulation %s: %w", name, err) } - sim.StateManager, err = stmgr.NewStateManager(nd.Chainstore, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(mock.Verifier), us, nil) + sim.StateManager, err = stmgr.NewStateManager(nd.Chainstore, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(mock.Verifier), us, nil, nd.MetadataDS) if err != nil { return nil, xerrors.Errorf("failed to create state manager for simulation %s: %w", name, err) } @@ -125,7 +125,7 @@ func (nd *Node) CreateSim(ctx context.Context, name string, head *types.TipSet) if err != nil { return nil, err } - sm, err := stmgr.NewStateManager(nd.Chainstore, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(mock.Verifier), filcns.DefaultUpgradeSchedule(), nil) + sm, err := stmgr.NewStateManager(nd.Chainstore, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(mock.Verifier), filcns.DefaultUpgradeSchedule(), nil, nd.MetadataDS) if err != nil { return nil, xerrors.Errorf("creating state manager: %w", err) } diff --git a/cmd/lotus-sim/simulation/simulation.go b/cmd/lotus-sim/simulation/simulation.go index 19ab391dc..294f4cfbc 100644 --- a/cmd/lotus-sim/simulation/simulation.go +++ b/cmd/lotus-sim/simulation/simulation.go @@ -201,7 +201,7 @@ func (sim *Simulation) SetUpgradeHeight(nv network.Version, epoch abi.ChainEpoch if err != nil { return err } - sm, err := stmgr.NewStateManager(sim.Node.Chainstore, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(mock.Verifier), newUpgradeSchedule, nil) + sm, err := stmgr.NewStateManager(sim.Node.Chainstore, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(mock.Verifier), newUpgradeSchedule, nil, sim.Node.MetadataDS) if err != nil { return err } diff --git a/cmd/lotus/daemon.go b/cmd/lotus/daemon.go index 4a4a691ee..b3341bd79 100644 --- a/cmd/lotus/daemon.go +++ b/cmd/lotus/daemon.go @@ -540,7 +540,7 @@ func ImportChain(ctx context.Context, r repo.Repo, fname string, snapshot bool) } // TODO: We need to supply the actual beacon after v14 - stm, err := stmgr.NewStateManager(cst, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(ffiwrapper.ProofVerifier), filcns.DefaultUpgradeSchedule(), nil) + stm, err := stmgr.NewStateManager(cst, consensus.NewTipSetExecutor(filcns.RewardFunc), vm.Syscalls(ffiwrapper.ProofVerifier), filcns.DefaultUpgradeSchedule(), nil, mds) if err != nil { return err } diff --git a/conformance/driver.go b/conformance/driver.go index 3a32c7604..2680a7154 100644 --- a/conformance/driver.go +++ b/conformance/driver.go @@ -108,7 +108,7 @@ func (d *Driver) ExecuteTipset(bs blockstore.Blockstore, ds ds.Batching, params cs = store.NewChainStore(bs, bs, ds, filcns.Weight, nil) tse = consensus.NewTipSetExecutor(filcns.RewardFunc) - sm, err = stmgr.NewStateManager(cs, tse, syscalls, filcns.DefaultUpgradeSchedule(), nil) + sm, err = stmgr.NewStateManager(cs, tse, syscalls, filcns.DefaultUpgradeSchedule(), nil, ds) ) if err != nil { return nil, err diff --git a/node/modules/chain.go b/node/modules/chain.go index f304ab135..0c3bad2c7 100644 --- a/node/modules/chain.go +++ b/node/modules/chain.go @@ -123,7 +123,7 @@ func NetworkName(mctx helpers.MetricsCtx, ctx := helpers.LifecycleCtx(mctx, lc) - sm, err := stmgr.NewStateManager(cs, tsexec, syscalls, us, nil) + sm, err := stmgr.NewStateManager(cs, tsexec, syscalls, us, nil, nil) if err != nil { return "", err } diff --git a/node/modules/stmgr.go b/node/modules/stmgr.go index dd3d90f10..b8f6f4776 100644 --- a/node/modules/stmgr.go +++ b/node/modules/stmgr.go @@ -7,10 +7,11 @@ import ( "github.com/filecoin-project/lotus/chain/stmgr" "github.com/filecoin-project/lotus/chain/store" "github.com/filecoin-project/lotus/chain/vm" + "github.com/filecoin-project/lotus/node/modules/dtypes" ) -func StateManager(lc fx.Lifecycle, cs *store.ChainStore, exec stmgr.Executor, sys vm.SyscallBuilder, us stmgr.UpgradeSchedule, b beacon.Schedule) (*stmgr.StateManager, error) { - sm, err := stmgr.NewStateManager(cs, exec, sys, us, b) +func StateManager(lc fx.Lifecycle, cs *store.ChainStore, exec stmgr.Executor, sys vm.SyscallBuilder, us stmgr.UpgradeSchedule, b beacon.Schedule, metadataDs dtypes.MetadataDS) (*stmgr.StateManager, error) { + sm, err := stmgr.NewStateManager(cs, exec, sys, us, b, metadataDs) if err != nil { return nil, err } From 668b24fb23bad33652e11e51f5f92f5d8dd7443c Mon Sep 17 00:00:00 2001 From: beck <1504068285@qq.com> Date: Sat, 4 Mar 2023 15:00:44 +0800 Subject: [PATCH 11/70] make debugging windowPoSt-failures human readable --- cmd/lotus-miner/proving.go | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/cmd/lotus-miner/proving.go b/cmd/lotus-miner/proving.go index 9ae8bdd48..9cc7105ee 100644 --- a/cmd/lotus-miner/proving.go +++ b/cmd/lotus-miner/proving.go @@ -19,6 +19,7 @@ import ( "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-bitfield" "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/proof" "github.com/filecoin-project/lotus/blockstore" "github.com/filecoin-project/lotus/build" @@ -640,7 +641,42 @@ It will not send any messages to the chain.`, if err != nil { return err } - jr, err := json.Marshal(res) + + //convert sector information into easily readable information + type PoStPartition struct { + Index uint64 + Skipped []uint64 + } + type SubmitWindowedPoStParams struct { + Deadline uint64 + Partitions []PoStPartition + Proofs []proof.PoStProof + ChainCommitEpoch abi.ChainEpoch + ChainCommitRand abi.Randomness + } + var postParams []SubmitWindowedPoStParams + for _, i := range res { + var postParam SubmitWindowedPoStParams + postParam.Deadline = i.Deadline + for id, part := range i.Partitions { + postParam.Partitions[id].Index = part.Index + count, err := part.Skipped.Count() + if err != nil { + return err + } + sectors, err := part.Skipped.All(count) + if err != nil { + return err + } + postParam.Partitions[id].Skipped = sectors + } + postParam.Proofs = i.Proofs + postParam.ChainCommitEpoch = i.ChainCommitEpoch + postParam.ChainCommitRand = i.ChainCommitRand + postParams = append(postParams, postParam) + } + + jr, err := json.Marshal(postParams) if err != nil { return err } From f0ba0ebb642d701e7067e5a4c55add28a6f64abd Mon Sep 17 00:00:00 2001 From: beck <1504068285@qq.com> Date: Mon, 6 Mar 2023 18:04:55 +0800 Subject: [PATCH 12/70] indent output --- cmd/lotus-miner/proving.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/lotus-miner/proving.go b/cmd/lotus-miner/proving.go index 9cc7105ee..d58ed1250 100644 --- a/cmd/lotus-miner/proving.go +++ b/cmd/lotus-miner/proving.go @@ -676,7 +676,7 @@ It will not send any messages to the chain.`, postParams = append(postParams, postParam) } - jr, err := json.Marshal(postParams) + jr, err := json.MarshalIndent(postParams, "", " ") if err != nil { return err } From dd998d6b2452e1d63eb5e64e52d2093c0aa45fe5 Mon Sep 17 00:00:00 2001 From: zenground0 Date: Fri, 3 Mar 2023 08:53:23 -0500 Subject: [PATCH 13/70] Begin account for size during walks --- blockstore/splitstore/splitstore_compact.go | 113 ++++++++++++-------- blockstore/splitstore/splitstore_reify.go | 2 +- 2 files changed, 71 insertions(+), 44 deletions(-) diff --git a/blockstore/splitstore/splitstore_compact.go b/blockstore/splitstore/splitstore_compact.go index 59bdd515d..79afe126c 100644 --- a/blockstore/splitstore/splitstore_compact.go +++ b/blockstore/splitstore/splitstore_compact.go @@ -201,7 +201,7 @@ func (s *SplitStore) markLiveRefs(cids []cid.Cid) { count := new(int32) visitor := newConcurrentVisitor() - walkObject := func(c cid.Cid) error { + walkObject := func(c cid.Cid) (int, error) { return s.walkObjectIncomplete(c, visitor, func(c cid.Cid) error { if isUnitaryObject(c) { @@ -228,7 +228,7 @@ func (s *SplitStore) markLiveRefs(cids []cid.Cid) { // optimize the common case of single put if len(cids) == 1 { - if err := walkObject(cids[0]); err != nil { + if _, err := walkObject(cids[0]); err != nil { log.Errorf("error marking tipset refs: %s", err) } log.Debugw("marking live refs done", "took", time.Since(startMark), "marked", *count) @@ -243,7 +243,7 @@ func (s *SplitStore) markLiveRefs(cids []cid.Cid) { worker := func() error { for c := range workch { - if err := walkObject(c); err != nil { + if _, err := walkObject(c); err != nil { return err } } @@ -361,6 +361,7 @@ func (s *SplitStore) protectTxnRefs(markSet MarkSet) error { log.Infow("protecting transactional references", "refs", len(txnRefs)) count := 0 + sz := new(int64) workch := make(chan cid.Cid, len(txnRefs)) startProtect := time.Now() @@ -393,10 +394,11 @@ func (s *SplitStore) protectTxnRefs(markSet MarkSet) error { worker := func() error { for c := range workch { - err := s.doTxnProtect(c, markSet) + szTxn, err := s.doTxnProtect(c, markSet) if err != nil { return xerrors.Errorf("error protecting transactional references to %s: %w", c, err) } + atomic.AddInt64(sz, int64(szTxn)) } return nil } @@ -410,15 +412,15 @@ func (s *SplitStore) protectTxnRefs(markSet MarkSet) error { return err } - log.Infow("protecting transactional refs done", "took", time.Since(startProtect), "protected", count) + log.Infow("protecting transactional refs done", "took", time.Since(startProtect), "protected", count, "protected size", sz) } } // transactionally protect a reference by walking the object and marking. // concurrent markings are short circuited by checking the markset. -func (s *SplitStore) doTxnProtect(root cid.Cid, markSet MarkSet) error { +func (s *SplitStore) doTxnProtect(root cid.Cid, markSet MarkSet) (int, error) { if err := s.checkClosing(); err != nil { - return err + return 0, err } // Note: cold objects are deleted heaviest first, so the consituents of an object @@ -907,6 +909,7 @@ func (s *SplitStore) walkChain(ts *types.TipSet, inclState, inclMsgs abi.ChainEp copy(toWalk, ts.Cids()) walkCnt := new(int64) scanCnt := new(int64) + szWalk := new(int64) tsRef := func(blkCids []cid.Cid) (cid.Cid, error) { return types.NewTipSetKey(blkCids...).Cid() @@ -942,8 +945,10 @@ func (s *SplitStore) walkChain(ts *types.TipSet, inclState, inclMsgs abi.ChainEp if err != nil { return xerrors.Errorf("error computing cid reference to parent tipset") } - if err := s.walkObjectIncomplete(pRef, visitor, fHot, stopWalk); err != nil { + if sz, err := s.walkObjectIncomplete(pRef, visitor, fHot, stopWalk); err != nil { return xerrors.Errorf("error walking parent tipset cid reference") + } else { + atomic.AddInt64(szWalk, int64(sz)) } // message are retained if within the inclMsgs boundary @@ -951,38 +956,52 @@ func (s *SplitStore) walkChain(ts *types.TipSet, inclState, inclMsgs abi.ChainEp if inclMsgs < inclState { // we need to use walkObjectIncomplete here, as messages/receipts may be missing early on if we // synced from snapshot and have a long HotStoreMessageRetentionPolicy. - if err := s.walkObjectIncomplete(hdr.Messages, visitor, fHot, stopWalk); err != nil { + if sz, err := s.walkObjectIncomplete(hdr.Messages, visitor, fHot, stopWalk); err != nil { return xerrors.Errorf("error walking messages (cid: %s): %w", hdr.Messages, err) + } else { + atomic.AddInt64(szWalk, int64(sz)) } - if err := s.walkObjectIncomplete(hdr.ParentMessageReceipts, visitor, fHot, stopWalk); err != nil { + if sz, err := s.walkObjectIncomplete(hdr.ParentMessageReceipts, visitor, fHot, stopWalk); err != nil { return xerrors.Errorf("error walking messages receipts (cid: %s): %w", hdr.ParentMessageReceipts, err) + } else { + atomic.AddInt64(szWalk, int64(sz)) } } else { - if err := s.walkObject(hdr.Messages, visitor, fHot); err != nil { + if sz, err := s.walkObject(hdr.Messages, visitor, fHot); err != nil { return xerrors.Errorf("error walking messages (cid: %s): %w", hdr.Messages, err) + } else { + atomic.AddInt64(szWalk, int64(sz)) } - if err := s.walkObject(hdr.ParentMessageReceipts, visitor, fHot); err != nil { + if sz, err := s.walkObject(hdr.ParentMessageReceipts, visitor, fHot); err != nil { return xerrors.Errorf("error walking message receipts (cid: %s): %w", hdr.ParentMessageReceipts, err) + } else { + atomic.AddInt64(szWalk, int64(sz)) } } } // messages and receipts outside of inclMsgs are included in the cold store if hdr.Height < inclMsgs && hdr.Height > 0 { - if err := s.walkObjectIncomplete(hdr.Messages, visitor, fCold, stopWalk); err != nil { + if sz, err := s.walkObjectIncomplete(hdr.Messages, visitor, fCold, stopWalk); err != nil { return xerrors.Errorf("error walking messages (cid: %s): %w", hdr.Messages, err) + } else { + atomic.AddInt64(szWalk, int64(sz)) } - if err := s.walkObjectIncomplete(hdr.ParentMessageReceipts, visitor, fCold, stopWalk); err != nil { + if sz, err := s.walkObjectIncomplete(hdr.ParentMessageReceipts, visitor, fCold, stopWalk); err != nil { return xerrors.Errorf("error walking messages receipts (cid: %s): %w", hdr.ParentMessageReceipts, err) + } else { + atomic.AddInt64(szWalk, int64(sz)) } } // state is only retained if within the inclState boundary, with the exception of genesis if hdr.Height >= inclState || hdr.Height == 0 { - if err := s.walkObject(hdr.ParentStateRoot, visitor, fHot); err != nil { + if sz, err := s.walkObject(hdr.ParentStateRoot, visitor, fHot); err != nil { return xerrors.Errorf("error walking state root (cid: %s): %w", hdr.ParentStateRoot, err) + } else { + atomic.AddInt64(szWalk, int64(sz)) } atomic.AddInt64(scanCnt, 1) } @@ -1001,8 +1020,10 @@ func (s *SplitStore) walkChain(ts *types.TipSet, inclState, inclMsgs abi.ChainEp if err != nil { return xerrors.Errorf("error computing cid reference to parent tipset") } - if err := s.walkObjectIncomplete(hRef, visitor, fHot, stopWalk); err != nil { + if sz, err := s.walkObjectIncomplete(hRef, visitor, fHot, stopWalk); err != nil { return xerrors.Errorf("error walking parent tipset cid reference") + } else { + atomic.AddInt64(szWalk, int64(sz)) } for len(toWalk) > 0 { @@ -1047,123 +1068,129 @@ func (s *SplitStore) walkChain(ts *types.TipSet, inclState, inclMsgs abi.ChainEp } } - log.Infow("chain walk done", "walked", *walkCnt, "scanned", *scanCnt) + log.Infow("chain walk done", "walked", *walkCnt, "scanned", *scanCnt, "walk size", szWalk) return nil } -func (s *SplitStore) walkObject(c cid.Cid, visitor ObjectVisitor, f func(cid.Cid) error) error { +func (s *SplitStore) walkObject(c cid.Cid, visitor ObjectVisitor, f func(cid.Cid) error) (int, error) { + var sz int visit, err := visitor.Visit(c) if err != nil { - return xerrors.Errorf("error visiting object: %w", err) + return 0, xerrors.Errorf("error visiting object: %w", err) } if !visit { - return nil + return sz, nil } if err := f(c); err != nil { if err == errStopWalk { - return nil + return sz, nil } - return err + return 0, err } if c.Prefix().Codec != cid.DagCBOR { - return nil + return sz, nil } // check this before recursing if err := s.checkClosing(); err != nil { - return err + return 0, err } var links []cid.Cid err = s.view(c, func(data []byte) error { + sz += len(data) return cbg.ScanForLinks(bytes.NewReader(data), func(c cid.Cid) { links = append(links, c) }) }) if err != nil { - return xerrors.Errorf("error scanning linked block (cid: %s): %w", c, err) + return 0, xerrors.Errorf("error scanning linked block (cid: %s): %w", c, err) } for _, c := range links { - err := s.walkObject(c, visitor, f) + szLink, err := s.walkObject(c, visitor, f) if err != nil { - return xerrors.Errorf("error walking link (cid: %s): %w", c, err) + return 0, xerrors.Errorf("error walking link (cid: %s): %w", c, err) } + sz += szLink } - return nil + return sz, nil } // like walkObject, but the object may be potentially incomplete (references missing) -func (s *SplitStore) walkObjectIncomplete(c cid.Cid, visitor ObjectVisitor, f, missing func(cid.Cid) error) error { +func (s *SplitStore) walkObjectIncomplete(c cid.Cid, visitor ObjectVisitor, f, missing func(cid.Cid) error) (int, error) { + sz := 0 visit, err := visitor.Visit(c) if err != nil { - return xerrors.Errorf("error visiting object: %w", err) + return 0, xerrors.Errorf("error visiting object: %w", err) } if !visit { - return nil + return sz, nil } // occurs check -- only for DAGs if c.Prefix().Codec == cid.DagCBOR { has, err := s.has(c) if err != nil { - return xerrors.Errorf("error occur checking %s: %w", c, err) + return 0, xerrors.Errorf("error occur checking %s: %w", c, err) } if !has { err = missing(c) if err == errStopWalk { - return nil + return sz, nil } - return err + return 0, err } } if err := f(c); err != nil { if err == errStopWalk { - return nil + return sz, nil } - return err + return 0, err } if c.Prefix().Codec != cid.DagCBOR { - return nil + return sz, nil } // check this before recursing if err := s.checkClosing(); err != nil { - return err + return sz, err } var links []cid.Cid err = s.view(c, func(data []byte) error { + sz += len(data) return cbg.ScanForLinks(bytes.NewReader(data), func(c cid.Cid) { links = append(links, c) }) }) if err != nil { - return xerrors.Errorf("error scanning linked block (cid: %s): %w", c, err) + return 0, xerrors.Errorf("error scanning linked block (cid: %s): %w", c, err) } for _, c := range links { - err := s.walkObjectIncomplete(c, visitor, f, missing) + szLink, err := s.walkObjectIncomplete(c, visitor, f, missing) if err != nil { - return xerrors.Errorf("error walking link (cid: %s): %w", c, err) + return 0, xerrors.Errorf("error walking link (cid: %s): %w", c, err) } + sz += szLink } - return nil + return sz, nil } // internal version used during compaction and related operations @@ -1528,7 +1555,7 @@ func (s *SplitStore) waitForMissingRefs(markSet MarkSet) { missing = make(map[cid.Cid]struct{}) for c := range towalk { - err := s.walkObjectIncomplete(c, visitor, + _, err := s.walkObjectIncomplete(c, visitor, func(c cid.Cid) error { if isUnitaryObject(c) { return errStopWalk diff --git a/blockstore/splitstore/splitstore_reify.go b/blockstore/splitstore/splitstore_reify.go index aa14f090a..07efedead 100644 --- a/blockstore/splitstore/splitstore_reify.go +++ b/blockstore/splitstore/splitstore_reify.go @@ -101,7 +101,7 @@ func (s *SplitStore) doReify(c cid.Cid) { defer s.txnLk.RUnlock() count := 0 - err := s.walkObjectIncomplete(c, newTmpVisitor(), + _, err := s.walkObjectIncomplete(c, newTmpVisitor(), func(c cid.Cid) error { if isUnitaryObject(c) { return errStopWalk From 5d9739a863b41bfd1d1563082509f9700e3bfeda Mon Sep 17 00:00:00 2001 From: zenground0 Date: Sat, 4 Mar 2023 09:38:18 -0700 Subject: [PATCH 14/70] Track size of dags relevant to compaction --- blockstore/splitstore/splitstore.go | 11 +++++++ blockstore/splitstore/splitstore_check.go | 4 +-- blockstore/splitstore/splitstore_compact.go | 33 ++++++++++++++++----- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/blockstore/splitstore/splitstore.go b/blockstore/splitstore/splitstore.go index 0afb15c11..43d6d9349 100644 --- a/blockstore/splitstore/splitstore.go +++ b/blockstore/splitstore/splitstore.go @@ -195,6 +195,17 @@ type SplitStore struct { // registered protectors protectors []func(func(cid.Cid) error) error + + // dag sizes measured during latest compaction + // logged and used for GC strategy + + // protected by compaction lock + szWalk int64 + szProtectedTxns int64 + szToPurge int64 // expected purges before critical section protections and live marking + + // protected by txnLk + szMarkedLiveRefs int64 } var _ bstore.Blockstore = (*SplitStore)(nil) diff --git a/blockstore/splitstore/splitstore_check.go b/blockstore/splitstore/splitstore_check.go index 336515980..f57703866 100644 --- a/blockstore/splitstore/splitstore_check.go +++ b/blockstore/splitstore/splitstore_check.go @@ -95,7 +95,7 @@ func (s *SplitStore) doCheck(curTs *types.TipSet) error { } defer visitor.Close() //nolint - err = s.walkChain(curTs, boundaryEpoch, boundaryEpoch, visitor, + size, err := s.walkChain(curTs, boundaryEpoch, boundaryEpoch, visitor, func(c cid.Cid) error { if isUnitaryObject(c) { return errStopWalk @@ -133,7 +133,7 @@ func (s *SplitStore) doCheck(curTs *types.TipSet) error { return err } - log.Infow("check done", "cold", *coldCnt, "missing", *missingCnt) + log.Infow("check done", "cold", *coldCnt, "missing", *missingCnt, "walk size", size) write("--") write("cold: %d missing: %d", *coldCnt, *missingCnt) write("DONE") diff --git a/blockstore/splitstore/splitstore_compact.go b/blockstore/splitstore/splitstore_compact.go index 79afe126c..bddb9e1df 100644 --- a/blockstore/splitstore/splitstore_compact.go +++ b/blockstore/splitstore/splitstore_compact.go @@ -199,6 +199,8 @@ func (s *SplitStore) markLiveRefs(cids []cid.Cid) { log.Debugf("marking %d live refs", len(cids)) startMark := time.Now() + szMarked := new(int64) + count := new(int32) visitor := newConcurrentVisitor() walkObject := func(c cid.Cid) (int, error) { @@ -228,10 +230,12 @@ func (s *SplitStore) markLiveRefs(cids []cid.Cid) { // optimize the common case of single put if len(cids) == 1 { - if _, err := walkObject(cids[0]); err != nil { + sz, err := walkObject(cids[0]) + if err != nil { log.Errorf("error marking tipset refs: %s", err) } log.Debugw("marking live refs done", "took", time.Since(startMark), "marked", *count) + atomic.AddInt64(szMarked, int64(sz)) return } @@ -243,9 +247,11 @@ func (s *SplitStore) markLiveRefs(cids []cid.Cid) { worker := func() error { for c := range workch { - if _, err := walkObject(c); err != nil { + sz, err := walkObject(c) + if err != nil { return err } + atomic.AddInt64(szMarked, int64(sz)) } return nil @@ -268,7 +274,8 @@ func (s *SplitStore) markLiveRefs(cids []cid.Cid) { log.Errorf("error marking tipset refs: %s", err) } - log.Debugw("marking live refs done", "took", time.Since(startMark), "marked", *count) + log.Debugw("marking live refs done", "took", time.Since(startMark), "marked", *count, "size marked", *szMarked) + s.szMarkedLiveRefs += *szMarked } // transactionally protect a view @@ -600,7 +607,6 @@ func (s *SplitStore) doCompact(curTs *types.TipSet) error { } err = s.walkChain(curTs, boundaryEpoch, inclMsgsEpoch, &noopVisitor{}, fHot, fCold) - if err != nil { return xerrors.Errorf("error marking: %w", err) } @@ -640,7 +646,7 @@ func (s *SplitStore) doCompact(curTs *types.TipSet) error { defer purgew.Close() //nolint:errcheck // some stats for logging - var hotCnt, coldCnt, purgeCnt int + var hotCnt, coldCnt, purgeCnt, szPurge int err = s.hot.ForEachKey(func(c cid.Cid) error { // was it marked? mark, err := markSet.Has(c) @@ -652,6 +658,16 @@ func (s *SplitStore) doCompact(curTs *types.TipSet) error { hotCnt++ return nil } + if sz, err := s.hot.GetSize(s.ctx, c); err != nil { + if ipld.IsNotFound(err) { + log.Warnf("hotstore missing expected block %s", c) + return nil + } + + return xerrors.Errorf("error retrieving block %s from hotstore: %w", c, err) + } else { + szPurge += sz + } // it needs to be removed from hot store, mark it as candidate for purge if err := purgew.Write(c); err != nil { @@ -691,7 +707,8 @@ func (s *SplitStore) doCompact(curTs *types.TipSet) error { log.Infow("cold collection done", "took", time.Since(startCollect)) - log.Infow("compaction stats", "hot", hotCnt, "cold", coldCnt, "purge", purgeCnt) + log.Infow("compaction stats", "hot", hotCnt, "cold", coldCnt, "purge", purgeCnt, "purge size", szPurge) + s.szToPurge = int64(szPurge) stats.Record(s.ctx, metrics.SplitstoreCompactionHot.M(int64(hotCnt))) stats.Record(s.ctx, metrics.SplitstoreCompactionCold.M(int64(coldCnt))) @@ -775,8 +792,8 @@ func (s *SplitStore) doCompact(curTs *types.TipSet) error { return xerrors.Errorf("error purging cold objects: %w", err) } log.Infow("purging cold objects from hotstore done", "took", time.Since(startPurge)) - s.endCriticalSection() + log.Infow("total protected size", s.szProtectedTxns, "total marked live size", s.szMarkedLiveRefs) if err := checkpoint.Close(); err != nil { log.Warnf("error closing checkpoint: %s", err) @@ -1069,7 +1086,7 @@ func (s *SplitStore) walkChain(ts *types.TipSet, inclState, inclMsgs abi.ChainEp } log.Infow("chain walk done", "walked", *walkCnt, "scanned", *scanCnt, "walk size", szWalk) - + s.szWalk = *szWalk return nil } From 0fe91846cd924c3f90907acd3ddcbe14856bee7a Mon Sep 17 00:00:00 2001 From: zenground0 Date: Sat, 4 Mar 2023 09:46:38 -0700 Subject: [PATCH 15/70] Plan out moving GC limiting --- blockstore/splitstore/splitstore_gc.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/blockstore/splitstore/splitstore_gc.go b/blockstore/splitstore/splitstore_gc.go index 3b53b8042..c70440cd4 100644 --- a/blockstore/splitstore/splitstore_gc.go +++ b/blockstore/splitstore/splitstore_gc.go @@ -8,6 +8,13 @@ import ( ) func (s *SplitStore) gcHotAfterCompaction() { + // TODO size aware GC + // 1. Add a config value to specify targetted max number of bytes M + // 2. Use measurement of marked hotstore size H (we now have this), actual hostore size T (need to compute this), total move size H + T, approximate purged size P + // 3. Trigger moving GC whenever H + T is within 50 GB of M + // 4. if H + T > M use aggressive online threshold + // 5. Use threshold that covers 3 std devs of vlogs when doing aggresive online. Mean == (H + P) / T, assume normal distribution + // 6. Use threshold that covers 1 or 2 std devs of vlogs when doing regular online GC var opts []bstore.BlockstoreGCOption if s.cfg.HotStoreFullGCFrequency > 0 && s.compactionIndex%int64(s.cfg.HotStoreFullGCFrequency) == 0 { opts = append(opts, bstore.WithFullGC(true)) From a994153e272f061b423ea420e3ca21967b14867f Mon Sep 17 00:00:00 2001 From: zenground0 Date: Tue, 7 Mar 2023 07:38:27 -0700 Subject: [PATCH 16/70] GC respects target for max hotstore space --- blockstore/splitstore/splitstore.go | 10 ++++ blockstore/splitstore/splitstore_check.go | 2 +- blockstore/splitstore/splitstore_compact.go | 18 +++++-- blockstore/splitstore/splitstore_gc.go | 55 ++++++++++++++++++--- documentation/en/default-lotus-config.toml | 11 +++++ node/config/doc_gen.go | 11 +++++ node/config/types.go | 7 +++ node/modules/blockstore.go | 1 + 8 files changed, 103 insertions(+), 12 deletions(-) diff --git a/blockstore/splitstore/splitstore.go b/blockstore/splitstore/splitstore.go index 43d6d9349..cefb86ebd 100644 --- a/blockstore/splitstore/splitstore.go +++ b/blockstore/splitstore/splitstore.go @@ -115,6 +115,14 @@ type Config struct { // A positive value is the number of compactions before a full GC is performed; // a value of 1 will perform full GC in every compaction. HotStoreFullGCFrequency uint64 + + // HotstoreMaxSpaceTarget suggests the max allowed space the hotstore can take. + // This is not a hard limit, it is possible for the hotstore to exceed the target + // for example if state grows massively between compactions. The splitstore + // will make a best effort to avoid overflowing the target and in practice should + // never overflow. This field is used when doing GC at the end of a compaction to + // adaptively choose moving GC + HotstoreMaxSpaceTarget uint64 } // ChainAccessor allows the Splitstore to access the chain. It will most likely @@ -165,6 +173,7 @@ type SplitStore struct { compactionIndex int64 pruneIndex int64 + onlineGCCnt int64 ctx context.Context cancel func() @@ -203,6 +212,7 @@ type SplitStore struct { szWalk int64 szProtectedTxns int64 szToPurge int64 // expected purges before critical section protections and live marking + szKeys int64 // approximate, not counting keys protected when entering critical section // protected by txnLk szMarkedLiveRefs int64 diff --git a/blockstore/splitstore/splitstore_check.go b/blockstore/splitstore/splitstore_check.go index f57703866..2645c78c5 100644 --- a/blockstore/splitstore/splitstore_check.go +++ b/blockstore/splitstore/splitstore_check.go @@ -95,7 +95,7 @@ func (s *SplitStore) doCheck(curTs *types.TipSet) error { } defer visitor.Close() //nolint - size, err := s.walkChain(curTs, boundaryEpoch, boundaryEpoch, visitor, + size := s.walkChain(curTs, boundaryEpoch, boundaryEpoch, visitor, func(c cid.Cid) error { if isUnitaryObject(c) { return errStopWalk diff --git a/blockstore/splitstore/splitstore_compact.go b/blockstore/splitstore/splitstore_compact.go index bddb9e1df..60003bbd2 100644 --- a/blockstore/splitstore/splitstore_compact.go +++ b/blockstore/splitstore/splitstore_compact.go @@ -66,7 +66,8 @@ var ( ) const ( - batchSize = 16384 + batchSize = 16384 + cidKeySize = 32 ) func (s *SplitStore) HeadChange(_, apply []*types.TipSet) error { @@ -518,6 +519,7 @@ func (s *SplitStore) doCompact(curTs *types.TipSet) error { // might be potentially inconsistent; abort compaction and notify the user to intervene. return xerrors.Errorf("checkpoint exists; aborting compaction") } + s.clearSizeMeasurements() currentEpoch := curTs.Height() boundaryEpoch := currentEpoch - CompactionBoundary @@ -709,6 +711,7 @@ func (s *SplitStore) doCompact(curTs *types.TipSet) error { log.Infow("compaction stats", "hot", hotCnt, "cold", coldCnt, "purge", purgeCnt, "purge size", szPurge) s.szToPurge = int64(szPurge) + s.szKeys = int64(hotCnt) * cidKeySize stats.Record(s.ctx, metrics.SplitstoreCompactionHot.M(int64(hotCnt))) stats.Record(s.ctx, metrics.SplitstoreCompactionCold.M(int64(coldCnt))) @@ -1473,8 +1476,9 @@ func (s *SplitStore) completeCompaction() error { } s.compactType = none - // Note: at this point we can start the splitstore; a compaction should run on - // the first head change, which will trigger gc on the hotstore. + // Note: at this point we can start the splitstore; base epoch is not + // incremented here so a compaction should run on the first head + // change, which will trigger gc on the hotstore. // We don't mind the second (back-to-back) compaction as the head will // have advanced during marking and coldset accumulation. return nil @@ -1532,6 +1536,14 @@ func (s *SplitStore) completePurge(coldr *ColdSetReader, checkpoint *Checkpoint, return nil } +func (s *SplitStore) clearSizeMeasurements() { + s.szKeys = 0 + s.szMarkedLiveRefs = 0 + s.szProtectedTxns = 0 + s.szToPurge = 0 + s.szWalk = 0 +} + // I really don't like having this code, but we seem to have some occasional DAG references with // missing constituents. During testing in mainnet *some* of these references *sometimes* appeared // after a little bit. diff --git a/blockstore/splitstore/splitstore_gc.go b/blockstore/splitstore/splitstore_gc.go index c70440cd4..70ad9fb75 100644 --- a/blockstore/splitstore/splitstore_gc.go +++ b/blockstore/splitstore/splitstore_gc.go @@ -7,17 +7,56 @@ import ( bstore "github.com/filecoin-project/lotus/blockstore" ) +const ( + // When < 150 GB of space would remain during moving GC, trigger moving GC + targetThreshold = 150_000_000_000 + // Don't attempt moving GC with 50 GB or less would remain during moving GC + targetBuffer = 50_000_000_000 + // Fraction of garbage in badger vlog for online GC traversal to collect garbage + aggressiveOnlineGCThreshold = 0.0001 +) + func (s *SplitStore) gcHotAfterCompaction() { - // TODO size aware GC - // 1. Add a config value to specify targetted max number of bytes M - // 2. Use measurement of marked hotstore size H (we now have this), actual hostore size T (need to compute this), total move size H + T, approximate purged size P - // 3. Trigger moving GC whenever H + T is within 50 GB of M - // 4. if H + T > M use aggressive online threshold - // 5. Use threshold that covers 3 std devs of vlogs when doing aggresive online. Mean == (H + P) / T, assume normal distribution - // 6. Use threshold that covers 1 or 2 std devs of vlogs when doing regular online GC + // Measure hotstore size, determine if we should do full GC, determine if we can do full GC. + // We should do full GC if + // FullGCFrequency is specified and compaction index matches frequency + // OR HotstoreMaxSpaceTarget is specified and total moving space is within 150 GB of target + // We can do full if + // HotstoreMaxSpaceTarget is not specified + // OR total moving space would not exceed 50 GB below target + // + // a) If we should not do full GC => online GC + // b) If we should do full GC and can => moving GC + // c) If we should do full GC and can't => aggressive online GC + var hotSize int64 + var err error + sizer, ok := s.hot.(bstore.BlockstoreSize) + if ok { + hotSize, err = sizer.Size() + if err != nil { + log.Warnf("error getting hotstore size: %s, estimating empty hot store for targeting", err) + hotSize = 0 + } + } else { + hotSize = 0 + } + + copySizeApprox := s.szKeys + s.szMarkedLiveRefs + s.szProtectedTxns + s.szWalk + shouldTarget := s.cfg.HotstoreMaxSpaceTarget > 0 && hotSize+copySizeApprox > int64(s.cfg.HotstoreMaxSpaceTarget)-targetThreshold + shouldFreq := s.cfg.HotStoreFullGCFrequency > 0 && s.compactionIndex%int64(s.cfg.HotStoreFullGCFrequency) == 0 + shouldDoFull := shouldTarget || shouldFreq + canDoFull := s.cfg.HotstoreMaxSpaceTarget == 0 || hotSize+copySizeApprox < int64(s.cfg.HotstoreMaxSpaceTarget)-targetBuffer + log.Infof("measured hot store size: %d, approximate new size: %d, should do full %t, can do full %t", hotSize, copySizeApprox, shouldDoFull, canDoFull) + var opts []bstore.BlockstoreGCOption - if s.cfg.HotStoreFullGCFrequency > 0 && s.compactionIndex%int64(s.cfg.HotStoreFullGCFrequency) == 0 { + if shouldDoFull && canDoFull { opts = append(opts, bstore.WithFullGC(true)) + } else if shouldDoFull && !canDoFull { + log.Warnf("Attention! Estimated moving GC size %d is not within safety buffer %d of target max %d, performing aggressive online GC to attempt to bring hotstore size down safely", copySizeApprox, targetBuffer, s.cfg.HotstoreMaxSpaceTarget) + log.Warn("If problem continues you can 1) temporarily allocate more disk space to hotstore and 2) reflect in HotstoreMaxSpaceTarget OR trigger manual move with `lotus chain prune hot-moving`") + log.Warn("If problem continues and you do not have any more disk space you can run continue to manually trigger online GC at agressive thresholds (< 0.01) with `lotus chain prune hot`") + + opts = append(opts, bstore.WithThreshold(aggressiveOnlineGCThreshold)) } if err := s.gcBlockstore(s.hot, opts); err != nil { diff --git a/documentation/en/default-lotus-config.toml b/documentation/en/default-lotus-config.toml index 41d7e6aca..a447350ff 100644 --- a/documentation/en/default-lotus-config.toml +++ b/documentation/en/default-lotus-config.toml @@ -230,6 +230,17 @@ # env var: LOTUS_CHAINSTORE_SPLITSTORE_HOTSTOREFULLGCFREQUENCY #HotStoreFullGCFrequency = 20 + # HotStoreMaxSpaceTarget sets a target max disk size for the hotstore. Splitstore GC + # will run moving GC if disk utilization gets within a threshold (150 GB) of the target. + # Splitstore GC will NOT run moving GC if the total size of the move would get + # within 50 GB of the target, and instead will run a more aggressive online GC. + # If both HotStoreFullGCFrequency and HotStoreMaxSpaceTarget are set then splitstore + # GC will trigger moving GC if either configuration condition is met. + # + # type: uint64 + # env var: LOTUS_CHAINSTORE_SPLITSTORE_HOTSTOREMAXSPACETARGET + #HotStoreMaxSpaceTarget = 0 + [Cluster] # EXPERIMENTAL. config to enabled node cluster with raft consensus diff --git a/node/config/doc_gen.go b/node/config/doc_gen.go index 8b79bed4f..53c305be6 100644 --- a/node/config/doc_gen.go +++ b/node/config/doc_gen.go @@ -1286,6 +1286,17 @@ the compaction boundary; default is 0.`, A value of 0 disables, while a value 1 will do full GC in every compaction. Default is 20 (about once a week).`, }, + { + Name: "HotStoreMaxSpaceTarget", + Type: "uint64", + + Comment: `HotStoreMaxSpaceTarget sets a target max disk size for the hotstore. Splitstore GC +will run moving GC if disk utilization gets within a threshold (150 GB) of the target. +Splitstore GC will NOT run moving GC if the total size of the move would get +within 50 GB of the target, and instead will run a more aggressive online GC. +If both HotStoreFullGCFrequency and HotStoreMaxSpaceTarget are set then splitstore +GC will trigger moving GC if either configuration condition is met.`, + }, }, "StorageMiner": []DocField{ { diff --git a/node/config/types.go b/node/config/types.go index 690e8caee..123714794 100644 --- a/node/config/types.go +++ b/node/config/types.go @@ -601,6 +601,13 @@ type Splitstore struct { // A value of 0 disables, while a value 1 will do full GC in every compaction. // Default is 20 (about once a week). HotStoreFullGCFrequency uint64 + // HotStoreMaxSpaceTarget sets a target max disk size for the hotstore. Splitstore GC + // will run moving GC if disk utilization gets within a threshold (150 GB) of the target. + // Splitstore GC will NOT run moving GC if the total size of the move would get + // within 50 GB of the target, and instead will run a more aggressive online GC. + // If both HotStoreFullGCFrequency and HotStoreMaxSpaceTarget are set then splitstore + // GC will trigger moving GC if either configuration condition is met. + HotStoreMaxSpaceTarget uint64 } // // Full Node diff --git a/node/modules/blockstore.go b/node/modules/blockstore.go index 90b7b6183..f4211bbe2 100644 --- a/node/modules/blockstore.go +++ b/node/modules/blockstore.go @@ -87,6 +87,7 @@ func SplitBlockstore(cfg *config.Chainstore) func(lc fx.Lifecycle, r repo.Locked UniversalColdBlocks: cfg.Splitstore.ColdStoreType == "universal", HotStoreMessageRetention: cfg.Splitstore.HotStoreMessageRetention, HotStoreFullGCFrequency: cfg.Splitstore.HotStoreFullGCFrequency, + HotstoreMaxSpaceTarget: cfg.Splitstore.HotStoreMaxSpaceTarget, } ss, err := splitstore.Open(path, ds, hot, cold, cfg) if err != nil { From bf29d4199366b36e2e7f7940127a6d2d6e7606db Mon Sep 17 00:00:00 2001 From: zenground0 Date: Tue, 7 Mar 2023 08:21:15 -0700 Subject: [PATCH 17/70] lint --- blockstore/splitstore/splitstore_compact.go | 60 ++++++++++----------- blockstore/splitstore/splitstore_gc.go | 2 +- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/blockstore/splitstore/splitstore_compact.go b/blockstore/splitstore/splitstore_compact.go index 60003bbd2..d8a7c2c1d 100644 --- a/blockstore/splitstore/splitstore_compact.go +++ b/blockstore/splitstore/splitstore_compact.go @@ -660,16 +660,16 @@ func (s *SplitStore) doCompact(curTs *types.TipSet) error { hotCnt++ return nil } - if sz, err := s.hot.GetSize(s.ctx, c); err != nil { + sz, err := s.hot.GetSize(s.ctx, c) + if err != nil { if ipld.IsNotFound(err) { log.Warnf("hotstore missing expected block %s", c) return nil } return xerrors.Errorf("error retrieving block %s from hotstore: %w", c, err) - } else { - szPurge += sz } + szPurge += sz // it needs to be removed from hot store, mark it as candidate for purge if err := purgew.Write(c); err != nil { @@ -965,64 +965,64 @@ func (s *SplitStore) walkChain(ts *types.TipSet, inclState, inclMsgs abi.ChainEp if err != nil { return xerrors.Errorf("error computing cid reference to parent tipset") } - if sz, err := s.walkObjectIncomplete(pRef, visitor, fHot, stopWalk); err != nil { + sz, err := s.walkObjectIncomplete(pRef, visitor, fHot, stopWalk) + if err != nil { return xerrors.Errorf("error walking parent tipset cid reference") - } else { - atomic.AddInt64(szWalk, int64(sz)) } + atomic.AddInt64(szWalk, int64(sz)) // message are retained if within the inclMsgs boundary if hdr.Height >= inclMsgs && hdr.Height > 0 { if inclMsgs < inclState { // we need to use walkObjectIncomplete here, as messages/receipts may be missing early on if we // synced from snapshot and have a long HotStoreMessageRetentionPolicy. - if sz, err := s.walkObjectIncomplete(hdr.Messages, visitor, fHot, stopWalk); err != nil { + sz, err := s.walkObjectIncomplete(hdr.Messages, visitor, fHot, stopWalk) + if err != nil { return xerrors.Errorf("error walking messages (cid: %s): %w", hdr.Messages, err) - } else { - atomic.AddInt64(szWalk, int64(sz)) } + atomic.AddInt64(szWalk, int64(sz)) - if sz, err := s.walkObjectIncomplete(hdr.ParentMessageReceipts, visitor, fHot, stopWalk); err != nil { + sz, err = s.walkObjectIncomplete(hdr.ParentMessageReceipts, visitor, fHot, stopWalk) + if err != nil { return xerrors.Errorf("error walking messages receipts (cid: %s): %w", hdr.ParentMessageReceipts, err) - } else { - atomic.AddInt64(szWalk, int64(sz)) } + atomic.AddInt64(szWalk, int64(sz)) } else { - if sz, err := s.walkObject(hdr.Messages, visitor, fHot); err != nil { + sz, err = s.walkObject(hdr.Messages, visitor, fHot) + if err != nil { return xerrors.Errorf("error walking messages (cid: %s): %w", hdr.Messages, err) - } else { - atomic.AddInt64(szWalk, int64(sz)) } + atomic.AddInt64(szWalk, int64(sz)) - if sz, err := s.walkObject(hdr.ParentMessageReceipts, visitor, fHot); err != nil { + sz, err := s.walkObject(hdr.ParentMessageReceipts, visitor, fHot) + if err != nil { return xerrors.Errorf("error walking message receipts (cid: %s): %w", hdr.ParentMessageReceipts, err) - } else { - atomic.AddInt64(szWalk, int64(sz)) } + atomic.AddInt64(szWalk, int64(sz)) } } // messages and receipts outside of inclMsgs are included in the cold store if hdr.Height < inclMsgs && hdr.Height > 0 { - if sz, err := s.walkObjectIncomplete(hdr.Messages, visitor, fCold, stopWalk); err != nil { + sz, err := s.walkObjectIncomplete(hdr.Messages, visitor, fCold, stopWalk) + if err != nil { return xerrors.Errorf("error walking messages (cid: %s): %w", hdr.Messages, err) - } else { - atomic.AddInt64(szWalk, int64(sz)) } - if sz, err := s.walkObjectIncomplete(hdr.ParentMessageReceipts, visitor, fCold, stopWalk); err != nil { + atomic.AddInt64(szWalk, int64(sz)) + sz, err = s.walkObjectIncomplete(hdr.ParentMessageReceipts, visitor, fCold, stopWalk) + if err != nil { return xerrors.Errorf("error walking messages receipts (cid: %s): %w", hdr.ParentMessageReceipts, err) - } else { - atomic.AddInt64(szWalk, int64(sz)) } + atomic.AddInt64(szWalk, int64(sz)) } // state is only retained if within the inclState boundary, with the exception of genesis if hdr.Height >= inclState || hdr.Height == 0 { - if sz, err := s.walkObject(hdr.ParentStateRoot, visitor, fHot); err != nil { + sz, err := s.walkObject(hdr.ParentStateRoot, visitor, fHot) + if err != nil { return xerrors.Errorf("error walking state root (cid: %s): %w", hdr.ParentStateRoot, err) - } else { - atomic.AddInt64(szWalk, int64(sz)) } + atomic.AddInt64(szWalk, int64(sz)) atomic.AddInt64(scanCnt, 1) } @@ -1040,11 +1040,11 @@ func (s *SplitStore) walkChain(ts *types.TipSet, inclState, inclMsgs abi.ChainEp if err != nil { return xerrors.Errorf("error computing cid reference to parent tipset") } - if sz, err := s.walkObjectIncomplete(hRef, visitor, fHot, stopWalk); err != nil { + sz, err := s.walkObjectIncomplete(hRef, visitor, fHot, stopWalk) + if err != nil { return xerrors.Errorf("error walking parent tipset cid reference") - } else { - atomic.AddInt64(szWalk, int64(sz)) } + atomic.AddInt64(szWalk, int64(sz)) for len(toWalk) > 0 { // walking can take a while, so check this with every opportunity diff --git a/blockstore/splitstore/splitstore_gc.go b/blockstore/splitstore/splitstore_gc.go index 70ad9fb75..cdd866b16 100644 --- a/blockstore/splitstore/splitstore_gc.go +++ b/blockstore/splitstore/splitstore_gc.go @@ -54,7 +54,7 @@ func (s *SplitStore) gcHotAfterCompaction() { } else if shouldDoFull && !canDoFull { log.Warnf("Attention! Estimated moving GC size %d is not within safety buffer %d of target max %d, performing aggressive online GC to attempt to bring hotstore size down safely", copySizeApprox, targetBuffer, s.cfg.HotstoreMaxSpaceTarget) log.Warn("If problem continues you can 1) temporarily allocate more disk space to hotstore and 2) reflect in HotstoreMaxSpaceTarget OR trigger manual move with `lotus chain prune hot-moving`") - log.Warn("If problem continues and you do not have any more disk space you can run continue to manually trigger online GC at agressive thresholds (< 0.01) with `lotus chain prune hot`") + log.Warn("If problem continues and you do not have any more disk space you can run continue to manually trigger online GC at aggressive thresholds (< 0.01) with `lotus chain prune hot`") opts = append(opts, bstore.WithThreshold(aggressiveOnlineGCThreshold)) } From f2a652f29f3604f708de658af704208d67631178 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Tue, 7 Mar 2023 16:48:09 +0100 Subject: [PATCH 18/70] rpcenc: More reliably failing TestReaderRedirectDrop --- lib/rpcenc/reader_test.go | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/lib/rpcenc/reader_test.go b/lib/rpcenc/reader_test.go index da436068f..8f191a761 100644 --- a/lib/rpcenc/reader_test.go +++ b/lib/rpcenc/reader_test.go @@ -3,8 +3,8 @@ package rpcenc import ( "context" + "fmt" "io" - "io/ioutil" "net/http/httptest" "strings" "sync" @@ -77,7 +77,12 @@ func (h *ReaderHandler) CloseReader(ctx context.Context, r io.Reader) error { } func (h *ReaderHandler) ReadAll(ctx context.Context, r io.Reader) ([]byte, error) { - return ioutil.ReadAll(r) + b, err := io.ReadAll(r) + if err != nil { + return nil, xerrors.Errorf("readall: %w", err) + } + + return b, nil } func (h *ReaderHandler) ReadNullLen(ctx context.Context, r io.Reader) (int64, error) { @@ -219,9 +224,15 @@ func TestReaderRedirect(t *testing.T) { } func TestReaderRedirectDrop(t *testing.T) { + for i := 0; i < 10; i++ { + t.Run(fmt.Sprintf("test %d", i), testReaderRedirectDrop) + } +} + +func testReaderRedirectDrop(t *testing.T) { // lower timeout so that the dangling connection between client and reader is dropped quickly // after the test. Otherwise httptest.Close is blocked. - Timeout = 200 * time.Millisecond + Timeout = 90 * time.Millisecond var allClient struct { ReadAll func(ctx context.Context, r io.Reader) ([]byte, error) @@ -294,6 +305,8 @@ func TestReaderRedirectDrop(t *testing.T) { done.Wait() + fmt.Println("---------------------") + // Redir client drops before subcall done.Add(1) @@ -313,6 +326,8 @@ func TestReaderRedirectDrop(t *testing.T) { // kill redirecting server connection testServ.CloseClientConnections() + //time.Sleep(200 * time.Millisecond) + // ReadAllWaiting should fail done.Wait() @@ -322,5 +337,9 @@ func TestReaderRedirectDrop(t *testing.T) { // wait for subcall to finish <-contCh - require.ErrorContains(t, allServerHandler.subErr, "decoding params for 'ReaderHandler.ReadAll' (param: 0; custom decoder): context canceled") + estr := allServerHandler.subErr.Error() + + require.True(t, + strings.Contains(estr, "decoding params for 'ReaderHandler.ReadAll' (param: 0; custom decoder): context canceled") || + strings.Contains(estr, "readall: unexpected EOF"), "unexpected error: %s", estr) } From ec5fde466a0c231a9b84ec1e67c49496593329ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Tue, 7 Mar 2023 16:48:33 +0100 Subject: [PATCH 19/70] rpcenc: Propagate closeOnce in beginPost --- lib/rpcenc/reader.go | 4 +++- lib/rpcenc/reader_test.go | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/rpcenc/reader.go b/lib/rpcenc/reader.go index 64dcc4b49..34b9fcfb4 100644 --- a/lib/rpcenc/reader.go +++ b/lib/rpcenc/reader.go @@ -221,7 +221,7 @@ type RpcReader struct { res chan readRes beginOnce *sync.Once - closeOnce sync.Once + closeOnce *sync.Once } var ErrHasBody = errors.New("RPCReader has body, either already read from or from a client with no redirect support") @@ -265,6 +265,7 @@ func (w *RpcReader) beginPost() { w.postBody = nr.postBody w.res = nr.res w.beginOnce = nr.beginOnce + w.closeOnce = nr.closeOnce } } @@ -355,6 +356,7 @@ func ReaderParamDecoder() (http.HandlerFunc, jsonrpc.ServerOption) { res: make(chan readRes), next: ch, beginOnce: &sync.Once{}, + closeOnce: &sync.Once{}, } switch req.Method { diff --git a/lib/rpcenc/reader_test.go b/lib/rpcenc/reader_test.go index 8f191a761..3a554a0ca 100644 --- a/lib/rpcenc/reader_test.go +++ b/lib/rpcenc/reader_test.go @@ -326,8 +326,6 @@ func testReaderRedirectDrop(t *testing.T) { // kill redirecting server connection testServ.CloseClientConnections() - //time.Sleep(200 * time.Millisecond) - // ReadAllWaiting should fail done.Wait() From f1b1eb8b367dd15d9ada6f540562d9fd942339d1 Mon Sep 17 00:00:00 2001 From: Aayush Date: Tue, 7 Mar 2023 13:40:39 -0500 Subject: [PATCH 20/70] refactor: EthAPI: Drop unnecessary param from newEthTxReceipt --- node/impl/full/eth.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index 907adf8cd..af8595362 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -433,7 +433,7 @@ func (a *EthModule) EthGetTransactionReceipt(ctx context.Context, txHash ethtype } } - receipt, err := newEthTxReceipt(ctx, tx, msgLookup, replay, events, a.StateAPI) + receipt, err := newEthTxReceipt(ctx, tx, replay, events, a.StateAPI) if err != nil { return nil, nil } @@ -2030,7 +2030,7 @@ func newEthTxFromMessageLookup(ctx context.Context, msgLookup *api.MsgLookup, tx return tx, nil } -func newEthTxReceipt(ctx context.Context, tx ethtypes.EthTx, lookup *api.MsgLookup, replay *api.InvocResult, events []types.Event, sa StateAPI) (api.EthTxReceipt, error) { +func newEthTxReceipt(ctx context.Context, tx ethtypes.EthTx, replay *api.InvocResult, events []types.Event, sa StateAPI) (api.EthTxReceipt, error) { var ( transactionIndex ethtypes.EthUint64 blockHash ethtypes.EthHash @@ -2059,25 +2059,25 @@ func newEthTxReceipt(ctx context.Context, tx ethtypes.EthTx, lookup *api.MsgLook LogsBloom: ethtypes.EmptyEthBloom[:], } - if lookup.Receipt.ExitCode.IsSuccess() { + if replay.MsgRct.ExitCode.IsSuccess() { receipt.Status = 1 } - if lookup.Receipt.ExitCode.IsError() { + if replay.MsgRct.ExitCode.IsError() { receipt.Status = 0 } - receipt.GasUsed = ethtypes.EthUint64(lookup.Receipt.GasUsed) + receipt.GasUsed = ethtypes.EthUint64(replay.MsgRct.GasUsed) // TODO: handle CumulativeGasUsed receipt.CumulativeGasUsed = ethtypes.EmptyEthInt - effectiveGasPrice := big.Div(replay.GasCost.TotalCost, big.NewInt(lookup.Receipt.GasUsed)) + effectiveGasPrice := big.Div(replay.GasCost.TotalCost, big.NewInt(replay.MsgRct.GasUsed)) receipt.EffectiveGasPrice = ethtypes.EthBigInt(effectiveGasPrice) - if receipt.To == nil && lookup.Receipt.ExitCode.IsSuccess() { + if receipt.To == nil && replay.MsgRct.ExitCode.IsSuccess() { // Create and Create2 return the same things. var ret eam.CreateExternalReturn - if err := ret.UnmarshalCBOR(bytes.NewReader(lookup.Receipt.Return)); err != nil { + if err := ret.UnmarshalCBOR(bytes.NewReader(replay.MsgRct.Return)); err != nil { return api.EthTxReceipt{}, xerrors.Errorf("failed to parse contract creation result: %w", err) } addr := ethtypes.EthAddress(ret.EthAddress) From 04a0fddb95c15da2f976e20e1dfe294ccf96ee0d Mon Sep 17 00:00:00 2001 From: Jiaying Wang <42981373+jennijuju@users.noreply.github.com> Date: Wed, 8 Mar 2023 00:06:36 -0500 Subject: [PATCH 21/70] Update .github/ISSUE_TEMPLATE/service_developer_bug_report.yml --- .github/ISSUE_TEMPLATE/service_developer_bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml b/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml index 93abadf4f..927f2ce8f 100644 --- a/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml @@ -20,7 +20,7 @@ body: options: - label: lotus Ethereum RPC required: false - - label: lotus evm - Lotus EVM interactions + - label: lotus FVM - Lotus FVM interactions required: false - label: Other required: false From a896d88b515d1aafa4e8d13aeab375491bc69cdf Mon Sep 17 00:00:00 2001 From: Jiaying Wang <42981373+jennijuju@users.noreply.github.com> Date: Wed, 8 Mar 2023 00:06:41 -0500 Subject: [PATCH 22/70] Update .github/ISSUE_TEMPLATE/service_developer_bug_report.yml --- .github/ISSUE_TEMPLATE/service_developer_bug_report.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml b/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml index 927f2ce8f..4b314b683 100644 --- a/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml @@ -22,6 +22,8 @@ body: required: false - label: lotus FVM - Lotus FVM interactions required: false + - label: FEVM tooling + required: false - label: Other required: false - type: textarea From e5055c25b03c8c8a518d4ce31e96c01800806681 Mon Sep 17 00:00:00 2001 From: Jiaying Wang <42981373+jennijuju@users.noreply.github.com> Date: Wed, 8 Mar 2023 00:07:48 -0500 Subject: [PATCH 23/70] Update service_developer_bug_report.yml --- .github/ISSUE_TEMPLATE/service_developer_bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml b/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml index 4b314b683..9b9d524be 100644 --- a/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml @@ -56,7 +56,7 @@ body: label: Describe the Bug description: | This is where you get to tell us what went wrong, when doing so, please try to provide a clear and concise description of the bug with all related information: - * What you were doing when you experienced the bug? + * What you were doing when you experienced the bug? What are you trying to build? * Any *error* messages and logs you saw, *where* you saw them, and what you believe may have caused them (if you have any ideas). * What is the expected behaviour? Links to the actual code? validations: From 925a385200a97b67f9361a616ab5a8ee38eb4136 Mon Sep 17 00:00:00 2001 From: Jiaying Wang <42981373+jennijuju@users.noreply.github.com> Date: Wed, 8 Mar 2023 00:10:41 -0500 Subject: [PATCH 24/70] Update service_developer_bug_report.yml --- .github/ISSUE_TEMPLATE/service_developer_bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml b/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml index 9b9d524be..d19a92bce 100644 --- a/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml @@ -62,7 +62,7 @@ body: validations: required: true - type: textarea - id: extraInfo + id: toolingInfo attributes: label: Tooling render: text From a0a1d371a25f4f0896d9ec2c88349b245999844b Mon Sep 17 00:00:00 2001 From: Jiaying Wang <42981373+jennijuju@users.noreply.github.com> Date: Wed, 8 Mar 2023 00:12:18 -0500 Subject: [PATCH 25/70] Update service_developer_bug_report.yml --- .github/ISSUE_TEMPLATE/service_developer_bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml b/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml index d19a92bce..8174d13f5 100644 --- a/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/service_developer_bug_report.yml @@ -39,7 +39,7 @@ body: validations: required: true - type: textarea - id: Repro Steps + id: repro attributes: label: Repro Steps description: "Steps to reproduce the behavior" From b67e6db52730b7d74dbed248e06012caaaed6118 Mon Sep 17 00:00:00 2001 From: Arsenii Petrovich Date: Tue, 7 Mar 2023 19:36:11 +0200 Subject: [PATCH 26/70] try to add statenetworkname method --- api/api_gateway.go | 2 ++ api/proxy_gen.go | 31 +++++++++++++++++++++---------- api/v0api/gateway.go | 2 ++ api/v0api/proxy_gen.go | 23 +++++++++++++++++------ gateway/node.go | 2 ++ 5 files changed, 44 insertions(+), 16 deletions(-) diff --git a/api/api_gateway.go b/api/api_gateway.go index 2e877fb1a..62f4be87f 100644 --- a/api/api_gateway.go +++ b/api/api_gateway.go @@ -12,6 +12,7 @@ import ( "github.com/filecoin-project/go-state-types/builtin/v9/miner" "github.com/filecoin-project/go-state-types/dline" + "github.com/filecoin-project/lotus/node/modules/dtypes" apitypes "github.com/filecoin-project/lotus/api/types" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/types/ethtypes" @@ -63,6 +64,7 @@ type Gateway interface { StateMinerInfo(ctx context.Context, actor address.Address, tsk types.TipSetKey) (MinerInfo, error) StateMinerProvingDeadline(ctx context.Context, addr address.Address, tsk types.TipSetKey) (*dline.Info, error) StateMinerPower(context.Context, address.Address, types.TipSetKey) (*MinerPower, error) + StateNetworkName(context.Context) (dtypes.NetworkName, error) StateNetworkVersion(context.Context, types.TipSetKey) (apitypes.NetworkVersion, error) StateSectorGetInfo(ctx context.Context, maddr address.Address, n abi.SectorNumber, tsk types.TipSetKey) (*miner.SectorOnChainInfo, error) StateVerifiedClientStatus(ctx context.Context, addr address.Address, tsk types.TipSetKey) (*abi.StoragePower, error) diff --git a/api/proxy_gen.go b/api/proxy_gen.go index 17be4088d..0a4490c37 100644 --- a/api/proxy_gen.go +++ b/api/proxy_gen.go @@ -7,15 +7,6 @@ import ( "encoding/json" "time" - "github.com/google/uuid" - "github.com/ipfs/go-cid" - blocks "github.com/ipfs/go-libipfs/blocks" - "github.com/libp2p/go-libp2p/core/metrics" - "github.com/libp2p/go-libp2p/core/network" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/libp2p/go-libp2p/core/protocol" - "golang.org/x/xerrors" - "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-bitfield" datatransfer "github.com/filecoin-project/go-data-transfer" @@ -32,7 +23,6 @@ import ( "github.com/filecoin-project/go-state-types/dline" abinetwork "github.com/filecoin-project/go-state-types/network" "github.com/filecoin-project/go-state-types/proof" - apitypes "github.com/filecoin-project/lotus/api/types" builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin" lminer "github.com/filecoin-project/lotus/chain/actors/builtin/miner" @@ -45,6 +35,14 @@ import ( "github.com/filecoin-project/lotus/storage/sealer/fsutil" "github.com/filecoin-project/lotus/storage/sealer/sealtasks" "github.com/filecoin-project/lotus/storage/sealer/storiface" + "github.com/google/uuid" + "github.com/ipfs/go-cid" + blocks "github.com/ipfs/go-libipfs/blocks" + "github.com/libp2p/go-libp2p/core/metrics" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/protocol" + "golang.org/x/xerrors" ) var ErrNotSupported = xerrors.New("method not supported") @@ -758,6 +756,8 @@ type GatewayMethods struct { StateMinerProvingDeadline func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (*dline.Info, error) `` + StateNetworkName func(p0 context.Context) (dtypes.NetworkName, error) `` + StateNetworkVersion func(p0 context.Context, p1 types.TipSetKey) (apitypes.NetworkVersion, error) `` StateReadState func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (*ActorState, error) `perm:"read"` @@ -4776,6 +4776,17 @@ func (s *GatewayStub) StateMinerProvingDeadline(p0 context.Context, p1 address.A return nil, ErrNotSupported } +func (s *GatewayStruct) StateNetworkName(p0 context.Context) (dtypes.NetworkName, error) { + if s.Internal.StateNetworkName == nil { + return *new(dtypes.NetworkName), ErrNotSupported + } + return s.Internal.StateNetworkName(p0) +} + +func (s *GatewayStub) StateNetworkName(p0 context.Context) (dtypes.NetworkName, error) { + return *new(dtypes.NetworkName), ErrNotSupported +} + func (s *GatewayStruct) StateNetworkVersion(p0 context.Context, p1 types.TipSetKey) (apitypes.NetworkVersion, error) { if s.Internal.StateNetworkVersion == nil { return *new(apitypes.NetworkVersion), ErrNotSupported diff --git a/api/v0api/gateway.go b/api/v0api/gateway.go index 674371c14..fcbc714b3 100644 --- a/api/v0api/gateway.go +++ b/api/v0api/gateway.go @@ -12,6 +12,7 @@ import ( "github.com/filecoin-project/go-state-types/dline" abinetwork "github.com/filecoin-project/go-state-types/network" + "github.com/filecoin-project/lotus/node/modules/dtypes" "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/chain/types" ) @@ -59,6 +60,7 @@ type Gateway interface { StateMinerInfo(ctx context.Context, actor address.Address, tsk types.TipSetKey) (api.MinerInfo, error) StateMinerProvingDeadline(ctx context.Context, addr address.Address, tsk types.TipSetKey) (*dline.Info, error) StateMinerPower(context.Context, address.Address, types.TipSetKey) (*api.MinerPower, error) + StateNetworkName(context.Context) (dtypes.NetworkName, error) StateNetworkVersion(context.Context, types.TipSetKey) (abinetwork.Version, error) StateSearchMsg(ctx context.Context, msg cid.Cid) (*api.MsgLookup, error) StateSectorGetInfo(ctx context.Context, maddr address.Address, n abi.SectorNumber, tsk types.TipSetKey) (*miner.SectorOnChainInfo, error) diff --git a/api/v0api/proxy_gen.go b/api/v0api/proxy_gen.go index 5fa0d949c..b8359fb42 100644 --- a/api/v0api/proxy_gen.go +++ b/api/v0api/proxy_gen.go @@ -5,11 +5,6 @@ package v0api import ( "context" - "github.com/ipfs/go-cid" - blocks "github.com/ipfs/go-libipfs/blocks" - "github.com/libp2p/go-libp2p/core/peer" - "golang.org/x/xerrors" - "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-bitfield" datatransfer "github.com/filecoin-project/go-data-transfer" @@ -22,7 +17,6 @@ import ( "github.com/filecoin-project/go-state-types/crypto" "github.com/filecoin-project/go-state-types/dline" abinetwork "github.com/filecoin-project/go-state-types/network" - "github.com/filecoin-project/lotus/api" apitypes "github.com/filecoin-project/lotus/api/types" lminer "github.com/filecoin-project/lotus/chain/actors/builtin/miner" @@ -30,6 +24,10 @@ import ( marketevents "github.com/filecoin-project/lotus/markets/loggers" "github.com/filecoin-project/lotus/node/modules/dtypes" "github.com/filecoin-project/lotus/node/repo/imports" + "github.com/ipfs/go-cid" + blocks "github.com/ipfs/go-libipfs/blocks" + "github.com/libp2p/go-libp2p/core/peer" + "golang.org/x/xerrors" ) var ErrNotSupported = xerrors.New("method not supported") @@ -481,6 +479,8 @@ type GatewayMethods struct { StateMinerProvingDeadline func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (*dline.Info, error) `` + StateNetworkName func(p0 context.Context) (dtypes.NetworkName, error) `` + StateNetworkVersion func(p0 context.Context, p1 types.TipSetKey) (abinetwork.Version, error) `` StateSearchMsg func(p0 context.Context, p1 cid.Cid) (*api.MsgLookup, error) `` @@ -2842,6 +2842,17 @@ func (s *GatewayStub) StateMinerProvingDeadline(p0 context.Context, p1 address.A return nil, ErrNotSupported } +func (s *GatewayStruct) StateNetworkName(p0 context.Context) (dtypes.NetworkName, error) { + if s.Internal.StateNetworkName == nil { + return *new(dtypes.NetworkName), ErrNotSupported + } + return s.Internal.StateNetworkName(p0) +} + +func (s *GatewayStub) StateNetworkName(p0 context.Context) (dtypes.NetworkName, error) { + return *new(dtypes.NetworkName), ErrNotSupported +} + func (s *GatewayStruct) StateNetworkVersion(p0 context.Context, p1 types.TipSetKey) (abinetwork.Version, error) { if s.Internal.StateNetworkVersion == nil { return *new(abinetwork.Version), ErrNotSupported diff --git a/gateway/node.go b/gateway/node.go index 90a6812b5..ac83f12bc 100644 --- a/gateway/node.go +++ b/gateway/node.go @@ -25,6 +25,7 @@ import ( _ "github.com/filecoin-project/lotus/lib/sigs/bls" _ "github.com/filecoin-project/lotus/lib/sigs/delegated" _ "github.com/filecoin-project/lotus/lib/sigs/secp" + "github.com/filecoin-project/lotus/node/modules/dtypes" "github.com/filecoin-project/lotus/metrics" "github.com/filecoin-project/lotus/node/impl/full" ) @@ -71,6 +72,7 @@ type TargetAPI interface { StateListMiners(ctx context.Context, tsk types.TipSetKey) ([]address.Address, error) StateMarketBalance(ctx context.Context, addr address.Address, tsk types.TipSetKey) (api.MarketBalance, error) StateMarketStorageDeal(ctx context.Context, dealId abi.DealID, tsk types.TipSetKey) (*api.MarketDeal, error) + StateNetworkName(context.Context) (dtypes.NetworkName, error) StateNetworkVersion(context.Context, types.TipSetKey) (network.Version, error) StateSearchMsg(ctx context.Context, from types.TipSetKey, msg cid.Cid, limit abi.ChainEpoch, allowReplaced bool) (*api.MsgLookup, error) StateWaitMsg(ctx context.Context, cid cid.Cid, confidence uint64, limit abi.ChainEpoch, allowReplaced bool) (*api.MsgLookup, error) From b8aaec6c37271cf45780ca39b654b5aa039f5d9b Mon Sep 17 00:00:00 2001 From: Arsenii Petrovich Date: Tue, 7 Mar 2023 21:29:49 +0200 Subject: [PATCH 27/70] add method implementation --- gateway/proxy_fil.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gateway/proxy_fil.go b/gateway/proxy_fil.go index 1f6ee2ccc..ba0c98579 100644 --- a/gateway/proxy_fil.go +++ b/gateway/proxy_fil.go @@ -16,6 +16,7 @@ import ( "github.com/filecoin-project/lotus/api" apitypes "github.com/filecoin-project/lotus/api/types" + "github.com/filecoin-project/lotus/node/modules/dtypes" "github.com/filecoin-project/lotus/build" "github.com/filecoin-project/lotus/chain/actors/builtin/miner" "github.com/filecoin-project/lotus/chain/types" @@ -308,6 +309,13 @@ func (gw *Node) StateMarketStorageDeal(ctx context.Context, dealId abi.DealID, t return gw.target.StateMarketStorageDeal(ctx, dealId, tsk) } +func (gw *Node) StateNetworkName(ctx context.Context) (dtypes.NetworkName, error) { + if err := gw.limit(ctx, stateRateLimitTokens); err != nil { + return *new(dtypes.NetworkName), err + } + return gw.target.StateNetworkName(ctx) +} + func (gw *Node) StateNetworkVersion(ctx context.Context, tsk types.TipSetKey) (network.Version, error) { if err := gw.limit(ctx, stateRateLimitTokens); err != nil { return network.VersionMax, err From 17ca3a1ac90c7c000b87f7cdf72ba59edc0d64ed Mon Sep 17 00:00:00 2001 From: Arsenii Petrovich Date: Tue, 7 Mar 2023 22:19:20 +0200 Subject: [PATCH 28/70] add MPoolGetNonce --- api/api_gateway.go | 1 + api/proxy_gen.go | 13 +++++++++++++ api/v0api/gateway.go | 1 + api/v0api/proxy_gen.go | 13 +++++++++++++ gateway/node.go | 1 + gateway/proxy_fil.go | 7 +++++++ 6 files changed, 36 insertions(+) diff --git a/api/api_gateway.go b/api/api_gateway.go index 62f4be87f..8de636516 100644 --- a/api/api_gateway.go +++ b/api/api_gateway.go @@ -48,6 +48,7 @@ type Gateway interface { ChainReadObj(context.Context, cid.Cid) ([]byte, error) ChainGetGenesis(context.Context) (*types.TipSet, error) GasEstimateMessageGas(ctx context.Context, msg *types.Message, spec *MessageSendSpec, tsk types.TipSetKey) (*types.Message, error) + MpoolGetNonce(ctx context.Context, addr address.Address) (uint64, error) MpoolPush(ctx context.Context, sm *types.SignedMessage) (cid.Cid, error) MsigGetAvailableBalance(ctx context.Context, addr address.Address, tsk types.TipSetKey) (types.BigInt, error) MsigGetPending(context.Context, address.Address, types.TipSetKey) ([]*MsigTransaction, error) diff --git a/api/proxy_gen.go b/api/proxy_gen.go index 0a4490c37..09de70561 100644 --- a/api/proxy_gen.go +++ b/api/proxy_gen.go @@ -722,6 +722,8 @@ type GatewayMethods struct { GasEstimateMessageGas func(p0 context.Context, p1 *types.Message, p2 *MessageSendSpec, p3 types.TipSetKey) (*types.Message, error) `` + MpoolGetNonce func(p0 context.Context, p1 address.Address) (uint64, error) `` + MpoolPush func(p0 context.Context, p1 *types.SignedMessage) (cid.Cid, error) `` MsigGetAvailableBalance func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (types.BigInt, error) `` @@ -4589,6 +4591,17 @@ func (s *GatewayStub) GasEstimateMessageGas(p0 context.Context, p1 *types.Messag return nil, ErrNotSupported } +func (s *GatewayStruct) MpoolGetNonce(p0 context.Context, p1 address.Address) (uint64, error) { + if s.Internal.MpoolGetNonce == nil { + return 0, ErrNotSupported + } + return s.Internal.MpoolGetNonce(p0, p1) +} + +func (s *GatewayStub) MpoolGetNonce(p0 context.Context, p1 address.Address) (uint64, error) { + return 0, ErrNotSupported +} + func (s *GatewayStruct) MpoolPush(p0 context.Context, p1 *types.SignedMessage) (cid.Cid, error) { if s.Internal.MpoolPush == nil { return *new(cid.Cid), ErrNotSupported diff --git a/api/v0api/gateway.go b/api/v0api/gateway.go index fcbc714b3..f157617d9 100644 --- a/api/v0api/gateway.go +++ b/api/v0api/gateway.go @@ -45,6 +45,7 @@ type Gateway interface { ChainNotify(context.Context) (<-chan []*api.HeadChange, error) ChainReadObj(context.Context, cid.Cid) ([]byte, error) GasEstimateMessageGas(ctx context.Context, msg *types.Message, spec *api.MessageSendSpec, tsk types.TipSetKey) (*types.Message, error) + MpoolGetNonce(ctx context.Context, addr address.Address) (uint64, error) MpoolPush(ctx context.Context, sm *types.SignedMessage) (cid.Cid, error) MsigGetAvailableBalance(ctx context.Context, addr address.Address, tsk types.TipSetKey) (types.BigInt, error) MsigGetVested(ctx context.Context, addr address.Address, start types.TipSetKey, end types.TipSetKey) (types.BigInt, error) diff --git a/api/v0api/proxy_gen.go b/api/v0api/proxy_gen.go index b8359fb42..88d3def63 100644 --- a/api/v0api/proxy_gen.go +++ b/api/v0api/proxy_gen.go @@ -449,6 +449,8 @@ type GatewayMethods struct { GasEstimateMessageGas func(p0 context.Context, p1 *types.Message, p2 *api.MessageSendSpec, p3 types.TipSetKey) (*types.Message, error) `` + MpoolGetNonce func(p0 context.Context, p1 address.Address) (uint64, error) `` + MpoolPush func(p0 context.Context, p1 *types.SignedMessage) (cid.Cid, error) `` MsigGetAvailableBalance func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (types.BigInt, error) `` @@ -2677,6 +2679,17 @@ func (s *GatewayStub) GasEstimateMessageGas(p0 context.Context, p1 *types.Messag return nil, ErrNotSupported } +func (s *GatewayStruct) MpoolGetNonce(p0 context.Context, p1 address.Address) (uint64, error) { + if s.Internal.MpoolGetNonce == nil { + return 0, ErrNotSupported + } + return s.Internal.MpoolGetNonce(p0, p1) +} + +func (s *GatewayStub) MpoolGetNonce(p0 context.Context, p1 address.Address) (uint64, error) { + return 0, ErrNotSupported +} + func (s *GatewayStruct) MpoolPush(p0 context.Context, p1 *types.SignedMessage) (cid.Cid, error) { if s.Internal.MpoolPush == nil { return *new(cid.Cid), ErrNotSupported diff --git a/gateway/node.go b/gateway/node.go index ac83f12bc..5a51fe62c 100644 --- a/gateway/node.go +++ b/gateway/node.go @@ -60,6 +60,7 @@ type TargetAPI interface { ChainPutObj(context.Context, blocks.Block) error ChainGetGenesis(context.Context) (*types.TipSet, error) GasEstimateMessageGas(ctx context.Context, msg *types.Message, spec *api.MessageSendSpec, tsk types.TipSetKey) (*types.Message, error) + MpoolGetNonce(ctx context.Context, addr address.Address) (uint64, error) MpoolPushUntrusted(ctx context.Context, sm *types.SignedMessage) (cid.Cid, error) MsigGetAvailableBalance(ctx context.Context, addr address.Address, tsk types.TipSetKey) (types.BigInt, error) MsigGetVested(ctx context.Context, addr address.Address, start types.TipSetKey, end types.TipSetKey) (types.BigInt, error) diff --git a/gateway/proxy_fil.go b/gateway/proxy_fil.go index ba0c98579..50da42cba 100644 --- a/gateway/proxy_fil.go +++ b/gateway/proxy_fil.go @@ -188,6 +188,13 @@ func (gw *Node) GasEstimateMessageGas(ctx context.Context, msg *types.Message, s return gw.target.GasEstimateMessageGas(ctx, msg, spec, tsk) } +func (gw *Node) MpoolGetNonce(ctx context.Context, addr address.Address) (uint64, error) { + if err := gw.limit(ctx, stateRateLimitTokens); err != nil { + return 0, err + } + return gw.target.MpoolGetNonce(ctx, addr) +} + func (gw *Node) MpoolPush(ctx context.Context, sm *types.SignedMessage) (cid.Cid, error) { if err := gw.limit(ctx, stateRateLimitTokens); err != nil { return cid.Cid{}, err From fb5c24b2b5d068e7164b07486e41a2787cd8379b Mon Sep 17 00:00:00 2001 From: Arsenii Petrovich Date: Wed, 8 Mar 2023 03:15:52 +0200 Subject: [PATCH 29/70] add two more methods: StateCall and StateDecodeParams --- api/api_gateway.go | 4 +++- api/proxy_gen.go | 26 ++++++++++++++++++++++++++ api/v0api/gateway.go | 4 +++- api/v0api/proxy_gen.go | 30 +++++++++++++++++++++++++++++- gateway/node.go | 4 +++- gateway/proxy_fil.go | 20 ++++++++++++++++++++ 6 files changed, 84 insertions(+), 4 deletions(-) diff --git a/api/api_gateway.go b/api/api_gateway.go index 8de636516..575050623 100644 --- a/api/api_gateway.go +++ b/api/api_gateway.go @@ -55,9 +55,11 @@ type Gateway interface { MsigGetVested(ctx context.Context, addr address.Address, start types.TipSetKey, end types.TipSetKey) (types.BigInt, error) MsigGetVestingSchedule(ctx context.Context, addr address.Address, tsk types.TipSetKey) (MsigVesting, error) StateAccountKey(ctx context.Context, addr address.Address, tsk types.TipSetKey) (address.Address, error) + StateCall(ctx context.Context, msg *types.Message, tsk types.TipSetKey) (*InvocResult, error) StateDealProviderCollateralBounds(ctx context.Context, size abi.PaddedPieceSize, verified bool, tsk types.TipSetKey) (DealCollateralBounds, error) + StateDecodeParams(ctx context.Context, toAddr address.Address, method abi.MethodNum, params []byte, tsk types.TipSetKey) (interface{}, error) StateGetActor(ctx context.Context, actor address.Address, ts types.TipSetKey) (*types.Actor, error) - StateReadState(ctx context.Context, actor address.Address, tsk types.TipSetKey) (*ActorState, error) //perm:read + StateReadState(ctx context.Context, actor address.Address, tsk types.TipSetKey) (*ActorState, error) StateListMiners(ctx context.Context, tsk types.TipSetKey) ([]address.Address, error) StateLookupID(ctx context.Context, addr address.Address, tsk types.TipSetKey) (address.Address, error) StateMarketBalance(ctx context.Context, addr address.Address, tsk types.TipSetKey) (MarketBalance, error) diff --git a/api/proxy_gen.go b/api/proxy_gen.go index 09de70561..7a602a696 100644 --- a/api/proxy_gen.go +++ b/api/proxy_gen.go @@ -740,8 +740,12 @@ type GatewayMethods struct { StateAccountKey func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (address.Address, error) `` + StateCall func(p0 context.Context, p1 *types.Message, p2 types.TipSetKey) (*InvocResult, error) `` + StateDealProviderCollateralBounds func(p0 context.Context, p1 abi.PaddedPieceSize, p2 bool, p3 types.TipSetKey) (DealCollateralBounds, error) `` + StateDecodeParams func(p0 context.Context, p1 address.Address, p2 abi.MethodNum, p3 []byte, p4 types.TipSetKey) (interface{}, error) `` + StateGetActor func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (*types.Actor, error) `` StateListMiners func(p0 context.Context, p1 types.TipSetKey) ([]address.Address, error) `` @@ -4690,6 +4694,17 @@ func (s *GatewayStub) StateAccountKey(p0 context.Context, p1 address.Address, p2 return *new(address.Address), ErrNotSupported } +func (s *GatewayStruct) StateCall(p0 context.Context, p1 *types.Message, p2 types.TipSetKey) (*InvocResult, error) { + if s.Internal.StateCall == nil { + return nil, ErrNotSupported + } + return s.Internal.StateCall(p0, p1, p2) +} + +func (s *GatewayStub) StateCall(p0 context.Context, p1 *types.Message, p2 types.TipSetKey) (*InvocResult, error) { + return nil, ErrNotSupported +} + func (s *GatewayStruct) StateDealProviderCollateralBounds(p0 context.Context, p1 abi.PaddedPieceSize, p2 bool, p3 types.TipSetKey) (DealCollateralBounds, error) { if s.Internal.StateDealProviderCollateralBounds == nil { return *new(DealCollateralBounds), ErrNotSupported @@ -4701,6 +4716,17 @@ func (s *GatewayStub) StateDealProviderCollateralBounds(p0 context.Context, p1 a return *new(DealCollateralBounds), ErrNotSupported } +func (s *GatewayStruct) StateDecodeParams(p0 context.Context, p1 address.Address, p2 abi.MethodNum, p3 []byte, p4 types.TipSetKey) (interface{}, error) { + if s.Internal.StateDecodeParams == nil { + return nil, ErrNotSupported + } + return s.Internal.StateDecodeParams(p0, p1, p2, p3, p4) +} + +func (s *GatewayStub) StateDecodeParams(p0 context.Context, p1 address.Address, p2 abi.MethodNum, p3 []byte, p4 types.TipSetKey) (interface{}, error) { + return nil, ErrNotSupported +} + func (s *GatewayStruct) StateGetActor(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (*types.Actor, error) { if s.Internal.StateGetActor == nil { return nil, ErrNotSupported diff --git a/api/v0api/gateway.go b/api/v0api/gateway.go index f157617d9..b83dd0539 100644 --- a/api/v0api/gateway.go +++ b/api/v0api/gateway.go @@ -51,7 +51,9 @@ type Gateway interface { MsigGetVested(ctx context.Context, addr address.Address, start types.TipSetKey, end types.TipSetKey) (types.BigInt, error) MsigGetPending(context.Context, address.Address, types.TipSetKey) ([]*api.MsigTransaction, error) StateAccountKey(ctx context.Context, addr address.Address, tsk types.TipSetKey) (address.Address, error) - StateDealProviderCollateralBounds(ctx context.Context, size abi.PaddedPieceSize, verified bool, tsk types.TipSetKey) (api.DealCollateralBounds, error) + StateCall(ctx context.Context, msg *types.Message, tsk types.TipSetKey) (*api.InvocResult, error) + StateDealProviderCollateralBounds(ctx context.Context, size abi.PaddedPieceSize, verified bool, tsk types.TipSetKey) (api.DealCollateralBounds, error) + StateDecodeParams(ctx context.Context, toAddr address.Address, method abi.MethodNum, params []byte, tsk types.TipSetKey) (interface{}, error) StateGetActor(ctx context.Context, actor address.Address, ts types.TipSetKey) (*types.Actor, error) StateGetReceipt(context.Context, cid.Cid, types.TipSetKey) (*types.MessageReceipt, error) StateListMiners(ctx context.Context, tsk types.TipSetKey) ([]address.Address, error) diff --git a/api/v0api/proxy_gen.go b/api/v0api/proxy_gen.go index 88d3def63..fc321f594 100644 --- a/api/v0api/proxy_gen.go +++ b/api/v0api/proxy_gen.go @@ -461,7 +461,11 @@ type GatewayMethods struct { StateAccountKey func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (address.Address, error) `` - StateDealProviderCollateralBounds func(p0 context.Context, p1 abi.PaddedPieceSize, p2 bool, p3 types.TipSetKey) (api.DealCollateralBounds, error) `` + StateCall func(p0 context.Context, p1 *types.Message, p2 types.TipSetKey) (*api.InvocResult, error) `` + + StateDealProviderCollateralBounds func(p0 context.Context, p1 abi.PaddedPieceSize, p2 bool, p3 types.TipSetKey) (api.DealCollateralBounds, error) `` + + StateDecodeParams func(p0 context.Context, p1 address.Address, p2 abi.MethodNum, p3 []byte, p4 types.TipSetKey) (interface{}, error) `` StateGetActor func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (*types.Actor, error) `` @@ -2734,6 +2738,8 @@ func (s *GatewayStub) MsigGetVested(p0 context.Context, p1 address.Address, p2 t return *new(types.BigInt), ErrNotSupported } + + func (s *GatewayStruct) StateAccountKey(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (address.Address, error) { if s.Internal.StateAccountKey == nil { return *new(address.Address), ErrNotSupported @@ -2745,6 +2751,17 @@ func (s *GatewayStub) StateAccountKey(p0 context.Context, p1 address.Address, p2 return *new(address.Address), ErrNotSupported } +func (s *GatewayStruct) StateCall(p0 context.Context, p1 *types.Message, p2 types.TipSetKey) (*api.InvocResult, error) { + if s.Internal.StateCall == nil { + return nil, ErrNotSupported + } + return s.Internal.StateCall(p0, p1, p2) +} + +func (s *GatewayStub) StateCall(p0 context.Context, p1 *types.Message, p2 types.TipSetKey) (*api.InvocResult, error) { + return nil, ErrNotSupported +} + func (s *GatewayStruct) StateDealProviderCollateralBounds(p0 context.Context, p1 abi.PaddedPieceSize, p2 bool, p3 types.TipSetKey) (api.DealCollateralBounds, error) { if s.Internal.StateDealProviderCollateralBounds == nil { return *new(api.DealCollateralBounds), ErrNotSupported @@ -2756,6 +2773,17 @@ func (s *GatewayStub) StateDealProviderCollateralBounds(p0 context.Context, p1 a return *new(api.DealCollateralBounds), ErrNotSupported } +func (s *GatewayStruct) StateDecodeParams(p0 context.Context, p1 address.Address, p2 abi.MethodNum, p3 []byte, p4 types.TipSetKey) (interface{}, error) { + if s.Internal.StateDecodeParams == nil { + return nil, ErrNotSupported + } + return s.Internal.StateDecodeParams(p0, p1, p2, p3, p4) +} + +func (s *GatewayStub) StateDecodeParams(p0 context.Context, p1 address.Address, p2 abi.MethodNum, p3 []byte, p4 types.TipSetKey) (interface{}, error) { + return nil, ErrNotSupported +} + func (s *GatewayStruct) StateGetActor(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (*types.Actor, error) { if s.Internal.StateGetActor == nil { return nil, ErrNotSupported diff --git a/gateway/node.go b/gateway/node.go index 5a51fe62c..babf844a2 100644 --- a/gateway/node.go +++ b/gateway/node.go @@ -67,7 +67,9 @@ type TargetAPI interface { MsigGetVestingSchedule(context.Context, address.Address, types.TipSetKey) (api.MsigVesting, error) MsigGetPending(ctx context.Context, addr address.Address, ts types.TipSetKey) ([]*api.MsigTransaction, error) StateAccountKey(ctx context.Context, addr address.Address, tsk types.TipSetKey) (address.Address, error) - StateDealProviderCollateralBounds(ctx context.Context, size abi.PaddedPieceSize, verified bool, tsk types.TipSetKey) (api.DealCollateralBounds, error) + StateCall(ctx context.Context, msg *types.Message, tsk types.TipSetKey) (*api.InvocResult, error) + StateDealProviderCollateralBounds(ctx context.Context, size abi.PaddedPieceSize, verified bool, tsk types.TipSetKey) (api.DealCollateralBounds, error) + StateDecodeParams(ctx context.Context, toAddr address.Address, method abi.MethodNum, params []byte, tsk types.TipSetKey) (interface{}, error) StateGetActor(ctx context.Context, actor address.Address, ts types.TipSetKey) (*types.Actor, error) StateLookupID(ctx context.Context, addr address.Address, tsk types.TipSetKey) (address.Address, error) StateListMiners(ctx context.Context, tsk types.TipSetKey) ([]address.Address, error) diff --git a/gateway/proxy_fil.go b/gateway/proxy_fil.go index 50da42cba..e3e8d5312 100644 --- a/gateway/proxy_fil.go +++ b/gateway/proxy_fil.go @@ -256,6 +256,16 @@ func (gw *Node) StateAccountKey(ctx context.Context, addr address.Address, tsk t return gw.target.StateAccountKey(ctx, addr, tsk) } +func (gw *Node) StateCall(ctx context.Context, msg *types.Message, tsk types.TipSetKey) (*api.InvocResult, error) { + if err := gw.limit(ctx, stateRateLimitTokens); err != nil { + return nil, err + } + if err := gw.checkTipsetKey(ctx, tsk); err != nil { + return nil, err + } + return gw.target.StateCall(ctx, msg, tsk) +} + func (gw *Node) StateDealProviderCollateralBounds(ctx context.Context, size abi.PaddedPieceSize, verified bool, tsk types.TipSetKey) (api.DealCollateralBounds, error) { if err := gw.limit(ctx, stateRateLimitTokens); err != nil { return api.DealCollateralBounds{}, err @@ -266,6 +276,16 @@ func (gw *Node) StateDealProviderCollateralBounds(ctx context.Context, size abi. return gw.target.StateDealProviderCollateralBounds(ctx, size, verified, tsk) } +func (gw *Node) StateDecodeParams(ctx context.Context, toAddr address.Address, method abi.MethodNum, params []byte, tsk types.TipSetKey) (interface{}, error) { + if err := gw.limit(ctx, stateRateLimitTokens); err != nil { + return nil, err + } + if err := gw.checkTipsetKey(ctx, tsk); err != nil { + return nil, err + } + return gw.target.StateDecodeParams(ctx, toAddr, method, params, tsk) +} + func (gw *Node) StateGetActor(ctx context.Context, actor address.Address, tsk types.TipSetKey) (*types.Actor, error) { if err := gw.limit(ctx, stateRateLimitTokens); err != nil { return nil, err From e17ec37d10ebdb84f83954cd913b59cd64aad13d Mon Sep 17 00:00:00 2001 From: zenground0 Date: Wed, 8 Mar 2023 09:13:00 -0700 Subject: [PATCH 30/70] fix logging --- blockstore/splitstore/splitstore_compact.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blockstore/splitstore/splitstore_compact.go b/blockstore/splitstore/splitstore_compact.go index d8a7c2c1d..841211524 100644 --- a/blockstore/splitstore/splitstore_compact.go +++ b/blockstore/splitstore/splitstore_compact.go @@ -796,7 +796,7 @@ func (s *SplitStore) doCompact(curTs *types.TipSet) error { } log.Infow("purging cold objects from hotstore done", "took", time.Since(startPurge)) s.endCriticalSection() - log.Infow("total protected size", s.szProtectedTxns, "total marked live size", s.szMarkedLiveRefs) + log.Infow("critical section done", "total protected size", s.szProtectedTxns, "total marked live size", s.szMarkedLiveRefs) if err := checkpoint.Close(); err != nil { log.Warnf("error closing checkpoint: %s", err) From efbc0ff12e4cdf508d8cab01afc5740c00da08e5 Mon Sep 17 00:00:00 2001 From: zenground0 Date: Wed, 8 Mar 2023 09:43:37 -0700 Subject: [PATCH 31/70] Fix up approximation and logging --- blockstore/splitstore/splitstore_compact.go | 2 +- blockstore/splitstore/splitstore_gc.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/blockstore/splitstore/splitstore_compact.go b/blockstore/splitstore/splitstore_compact.go index 841211524..bd73aa570 100644 --- a/blockstore/splitstore/splitstore_compact.go +++ b/blockstore/splitstore/splitstore_compact.go @@ -419,7 +419,7 @@ func (s *SplitStore) protectTxnRefs(markSet MarkSet) error { if err := g.Wait(); err != nil { return err } - + s.szProtectedTxns += *sz log.Infow("protecting transactional refs done", "took", time.Since(startProtect), "protected", count, "protected size", sz) } } diff --git a/blockstore/splitstore/splitstore_gc.go b/blockstore/splitstore/splitstore_gc.go index cdd866b16..2e23b993a 100644 --- a/blockstore/splitstore/splitstore_gc.go +++ b/blockstore/splitstore/splitstore_gc.go @@ -46,6 +46,7 @@ func (s *SplitStore) gcHotAfterCompaction() { shouldFreq := s.cfg.HotStoreFullGCFrequency > 0 && s.compactionIndex%int64(s.cfg.HotStoreFullGCFrequency) == 0 shouldDoFull := shouldTarget || shouldFreq canDoFull := s.cfg.HotstoreMaxSpaceTarget == 0 || hotSize+copySizeApprox < int64(s.cfg.HotstoreMaxSpaceTarget)-targetBuffer + log.Debugw("approximating new hot store size", "key size", s.szKeys, "marked live refs", s.szMarkedLiveRefs, "protected txns", s.szProtectedTxns, "walked DAG", s.szWalk) log.Infof("measured hot store size: %d, approximate new size: %d, should do full %t, can do full %t", hotSize, copySizeApprox, shouldDoFull, canDoFull) var opts []bstore.BlockstoreGCOption From 0ccef4e5c0d2a9723c459d536706e8dff6fc1438 Mon Sep 17 00:00:00 2001 From: Arsenii Petrovich Date: Wed, 8 Mar 2023 18:53:19 +0200 Subject: [PATCH 32/70] run make gen --- api/api_gateway.go | 4 +-- api/proxy_gen.go | 38 +++++++++++---------- api/v0api/gateway.go | 8 ++--- api/v0api/proxy_gen.go | 60 +++++++++++++++++----------------- build/openrpc/full.json.gz | Bin 33611 -> 33618 bytes build/openrpc/gateway.json.gz | Bin 8480 -> 9246 bytes build/openrpc/miner.json.gz | Bin 16043 -> 16043 bytes build/openrpc/worker.json.gz | Bin 5225 -> 5224 bytes gateway/node.go | 12 +++---- gateway/proxy_fil.go | 24 +++++++------- 10 files changed, 74 insertions(+), 72 deletions(-) diff --git a/api/api_gateway.go b/api/api_gateway.go index 575050623..9bc69cc0f 100644 --- a/api/api_gateway.go +++ b/api/api_gateway.go @@ -12,10 +12,10 @@ import ( "github.com/filecoin-project/go-state-types/builtin/v9/miner" "github.com/filecoin-project/go-state-types/dline" - "github.com/filecoin-project/lotus/node/modules/dtypes" apitypes "github.com/filecoin-project/lotus/api/types" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/types/ethtypes" + "github.com/filecoin-project/lotus/node/modules/dtypes" ) // MODIFYING THE API INTERFACE @@ -48,7 +48,7 @@ type Gateway interface { ChainReadObj(context.Context, cid.Cid) ([]byte, error) ChainGetGenesis(context.Context) (*types.TipSet, error) GasEstimateMessageGas(ctx context.Context, msg *types.Message, spec *MessageSendSpec, tsk types.TipSetKey) (*types.Message, error) - MpoolGetNonce(ctx context.Context, addr address.Address) (uint64, error) + MpoolGetNonce(ctx context.Context, addr address.Address) (uint64, error) MpoolPush(ctx context.Context, sm *types.SignedMessage) (cid.Cid, error) MsigGetAvailableBalance(ctx context.Context, addr address.Address, tsk types.TipSetKey) (types.BigInt, error) MsigGetPending(context.Context, address.Address, types.TipSetKey) ([]*MsigTransaction, error) diff --git a/api/proxy_gen.go b/api/proxy_gen.go index 7a602a696..33e6362bd 100644 --- a/api/proxy_gen.go +++ b/api/proxy_gen.go @@ -7,6 +7,15 @@ import ( "encoding/json" "time" + "github.com/google/uuid" + "github.com/ipfs/go-cid" + blocks "github.com/ipfs/go-libipfs/blocks" + "github.com/libp2p/go-libp2p/core/metrics" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/protocol" + "golang.org/x/xerrors" + "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-bitfield" datatransfer "github.com/filecoin-project/go-data-transfer" @@ -23,6 +32,7 @@ import ( "github.com/filecoin-project/go-state-types/dline" abinetwork "github.com/filecoin-project/go-state-types/network" "github.com/filecoin-project/go-state-types/proof" + apitypes "github.com/filecoin-project/lotus/api/types" builtinactors "github.com/filecoin-project/lotus/chain/actors/builtin" lminer "github.com/filecoin-project/lotus/chain/actors/builtin/miner" @@ -35,14 +45,6 @@ import ( "github.com/filecoin-project/lotus/storage/sealer/fsutil" "github.com/filecoin-project/lotus/storage/sealer/sealtasks" "github.com/filecoin-project/lotus/storage/sealer/storiface" - "github.com/google/uuid" - "github.com/ipfs/go-cid" - blocks "github.com/ipfs/go-libipfs/blocks" - "github.com/libp2p/go-libp2p/core/metrics" - "github.com/libp2p/go-libp2p/core/network" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/libp2p/go-libp2p/core/protocol" - "golang.org/x/xerrors" ) var ErrNotSupported = xerrors.New("method not supported") @@ -722,7 +724,7 @@ type GatewayMethods struct { GasEstimateMessageGas func(p0 context.Context, p1 *types.Message, p2 *MessageSendSpec, p3 types.TipSetKey) (*types.Message, error) `` - MpoolGetNonce func(p0 context.Context, p1 address.Address) (uint64, error) `` + MpoolGetNonce func(p0 context.Context, p1 address.Address) (uint64, error) `` MpoolPush func(p0 context.Context, p1 *types.SignedMessage) (cid.Cid, error) `` @@ -740,11 +742,11 @@ type GatewayMethods struct { StateAccountKey func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (address.Address, error) `` - StateCall func(p0 context.Context, p1 *types.Message, p2 types.TipSetKey) (*InvocResult, error) `` + StateCall func(p0 context.Context, p1 *types.Message, p2 types.TipSetKey) (*InvocResult, error) `` StateDealProviderCollateralBounds func(p0 context.Context, p1 abi.PaddedPieceSize, p2 bool, p3 types.TipSetKey) (DealCollateralBounds, error) `` - StateDecodeParams func(p0 context.Context, p1 address.Address, p2 abi.MethodNum, p3 []byte, p4 types.TipSetKey) (interface{}, error) `` + StateDecodeParams func(p0 context.Context, p1 address.Address, p2 abi.MethodNum, p3 []byte, p4 types.TipSetKey) (interface{}, error) `` StateGetActor func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (*types.Actor, error) `` @@ -762,11 +764,11 @@ type GatewayMethods struct { StateMinerProvingDeadline func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (*dline.Info, error) `` - StateNetworkName func(p0 context.Context) (dtypes.NetworkName, error) `` + StateNetworkName func(p0 context.Context) (dtypes.NetworkName, error) `` StateNetworkVersion func(p0 context.Context, p1 types.TipSetKey) (apitypes.NetworkVersion, error) `` - StateReadState func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (*ActorState, error) `perm:"read"` + StateReadState func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (*ActorState, error) `` StateSearchMsg func(p0 context.Context, p1 types.TipSetKey, p2 cid.Cid, p3 abi.ChainEpoch, p4 bool) (*MsgLookup, error) `` @@ -4717,14 +4719,14 @@ func (s *GatewayStub) StateDealProviderCollateralBounds(p0 context.Context, p1 a } func (s *GatewayStruct) StateDecodeParams(p0 context.Context, p1 address.Address, p2 abi.MethodNum, p3 []byte, p4 types.TipSetKey) (interface{}, error) { - if s.Internal.StateDecodeParams == nil { - return nil, ErrNotSupported - } - return s.Internal.StateDecodeParams(p0, p1, p2, p3, p4) + if s.Internal.StateDecodeParams == nil { + return nil, ErrNotSupported + } + return s.Internal.StateDecodeParams(p0, p1, p2, p3, p4) } func (s *GatewayStub) StateDecodeParams(p0 context.Context, p1 address.Address, p2 abi.MethodNum, p3 []byte, p4 types.TipSetKey) (interface{}, error) { - return nil, ErrNotSupported + return nil, ErrNotSupported } func (s *GatewayStruct) StateGetActor(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (*types.Actor, error) { diff --git a/api/v0api/gateway.go b/api/v0api/gateway.go index b83dd0539..2a0bfb2f7 100644 --- a/api/v0api/gateway.go +++ b/api/v0api/gateway.go @@ -12,9 +12,9 @@ import ( "github.com/filecoin-project/go-state-types/dline" abinetwork "github.com/filecoin-project/go-state-types/network" - "github.com/filecoin-project/lotus/node/modules/dtypes" "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/node/modules/dtypes" ) // MODIFYING THE API INTERFACE @@ -51,9 +51,9 @@ type Gateway interface { MsigGetVested(ctx context.Context, addr address.Address, start types.TipSetKey, end types.TipSetKey) (types.BigInt, error) MsigGetPending(context.Context, address.Address, types.TipSetKey) ([]*api.MsigTransaction, error) StateAccountKey(ctx context.Context, addr address.Address, tsk types.TipSetKey) (address.Address, error) - StateCall(ctx context.Context, msg *types.Message, tsk types.TipSetKey) (*api.InvocResult, error) - StateDealProviderCollateralBounds(ctx context.Context, size abi.PaddedPieceSize, verified bool, tsk types.TipSetKey) (api.DealCollateralBounds, error) - StateDecodeParams(ctx context.Context, toAddr address.Address, method abi.MethodNum, params []byte, tsk types.TipSetKey) (interface{}, error) + StateCall(ctx context.Context, msg *types.Message, tsk types.TipSetKey) (*api.InvocResult, error) + StateDealProviderCollateralBounds(ctx context.Context, size abi.PaddedPieceSize, verified bool, tsk types.TipSetKey) (api.DealCollateralBounds, error) + StateDecodeParams(ctx context.Context, toAddr address.Address, method abi.MethodNum, params []byte, tsk types.TipSetKey) (interface{}, error) StateGetActor(ctx context.Context, actor address.Address, ts types.TipSetKey) (*types.Actor, error) StateGetReceipt(context.Context, cid.Cid, types.TipSetKey) (*types.MessageReceipt, error) StateListMiners(ctx context.Context, tsk types.TipSetKey) ([]address.Address, error) diff --git a/api/v0api/proxy_gen.go b/api/v0api/proxy_gen.go index fc321f594..17a1ae84a 100644 --- a/api/v0api/proxy_gen.go +++ b/api/v0api/proxy_gen.go @@ -5,6 +5,11 @@ package v0api import ( "context" + "github.com/ipfs/go-cid" + blocks "github.com/ipfs/go-libipfs/blocks" + "github.com/libp2p/go-libp2p/core/peer" + "golang.org/x/xerrors" + "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-bitfield" datatransfer "github.com/filecoin-project/go-data-transfer" @@ -17,6 +22,7 @@ import ( "github.com/filecoin-project/go-state-types/crypto" "github.com/filecoin-project/go-state-types/dline" abinetwork "github.com/filecoin-project/go-state-types/network" + "github.com/filecoin-project/lotus/api" apitypes "github.com/filecoin-project/lotus/api/types" lminer "github.com/filecoin-project/lotus/chain/actors/builtin/miner" @@ -24,10 +30,6 @@ import ( marketevents "github.com/filecoin-project/lotus/markets/loggers" "github.com/filecoin-project/lotus/node/modules/dtypes" "github.com/filecoin-project/lotus/node/repo/imports" - "github.com/ipfs/go-cid" - blocks "github.com/ipfs/go-libipfs/blocks" - "github.com/libp2p/go-libp2p/core/peer" - "golang.org/x/xerrors" ) var ErrNotSupported = xerrors.New("method not supported") @@ -461,11 +463,11 @@ type GatewayMethods struct { StateAccountKey func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (address.Address, error) `` - StateCall func(p0 context.Context, p1 *types.Message, p2 types.TipSetKey) (*api.InvocResult, error) `` + StateCall func(p0 context.Context, p1 *types.Message, p2 types.TipSetKey) (*api.InvocResult, error) `` - StateDealProviderCollateralBounds func(p0 context.Context, p1 abi.PaddedPieceSize, p2 bool, p3 types.TipSetKey) (api.DealCollateralBounds, error) `` + StateDealProviderCollateralBounds func(p0 context.Context, p1 abi.PaddedPieceSize, p2 bool, p3 types.TipSetKey) (api.DealCollateralBounds, error) `` - StateDecodeParams func(p0 context.Context, p1 address.Address, p2 abi.MethodNum, p3 []byte, p4 types.TipSetKey) (interface{}, error) `` + StateDecodeParams func(p0 context.Context, p1 address.Address, p2 abi.MethodNum, p3 []byte, p4 types.TipSetKey) (interface{}, error) `` StateGetActor func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (*types.Actor, error) `` @@ -485,7 +487,7 @@ type GatewayMethods struct { StateMinerProvingDeadline func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (*dline.Info, error) `` - StateNetworkName func(p0 context.Context) (dtypes.NetworkName, error) `` + StateNetworkName func(p0 context.Context) (dtypes.NetworkName, error) `` StateNetworkVersion func(p0 context.Context, p1 types.TipSetKey) (abinetwork.Version, error) `` @@ -2684,14 +2686,14 @@ func (s *GatewayStub) GasEstimateMessageGas(p0 context.Context, p1 *types.Messag } func (s *GatewayStruct) MpoolGetNonce(p0 context.Context, p1 address.Address) (uint64, error) { - if s.Internal.MpoolGetNonce == nil { - return 0, ErrNotSupported - } - return s.Internal.MpoolGetNonce(p0, p1) + if s.Internal.MpoolGetNonce == nil { + return 0, ErrNotSupported + } + return s.Internal.MpoolGetNonce(p0, p1) } func (s *GatewayStub) MpoolGetNonce(p0 context.Context, p1 address.Address) (uint64, error) { - return 0, ErrNotSupported + return 0, ErrNotSupported } func (s *GatewayStruct) MpoolPush(p0 context.Context, p1 *types.SignedMessage) (cid.Cid, error) { @@ -2738,8 +2740,6 @@ func (s *GatewayStub) MsigGetVested(p0 context.Context, p1 address.Address, p2 t return *new(types.BigInt), ErrNotSupported } - - func (s *GatewayStruct) StateAccountKey(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (address.Address, error) { if s.Internal.StateAccountKey == nil { return *new(address.Address), ErrNotSupported @@ -2752,14 +2752,14 @@ func (s *GatewayStub) StateAccountKey(p0 context.Context, p1 address.Address, p2 } func (s *GatewayStruct) StateCall(p0 context.Context, p1 *types.Message, p2 types.TipSetKey) (*api.InvocResult, error) { - if s.Internal.StateCall == nil { - return nil, ErrNotSupported - } - return s.Internal.StateCall(p0, p1, p2) + if s.Internal.StateCall == nil { + return nil, ErrNotSupported + } + return s.Internal.StateCall(p0, p1, p2) } func (s *GatewayStub) StateCall(p0 context.Context, p1 *types.Message, p2 types.TipSetKey) (*api.InvocResult, error) { - return nil, ErrNotSupported + return nil, ErrNotSupported } func (s *GatewayStruct) StateDealProviderCollateralBounds(p0 context.Context, p1 abi.PaddedPieceSize, p2 bool, p3 types.TipSetKey) (api.DealCollateralBounds, error) { @@ -2774,14 +2774,14 @@ func (s *GatewayStub) StateDealProviderCollateralBounds(p0 context.Context, p1 a } func (s *GatewayStruct) StateDecodeParams(p0 context.Context, p1 address.Address, p2 abi.MethodNum, p3 []byte, p4 types.TipSetKey) (interface{}, error) { - if s.Internal.StateDecodeParams == nil { - return nil, ErrNotSupported - } - return s.Internal.StateDecodeParams(p0, p1, p2, p3, p4) + if s.Internal.StateDecodeParams == nil { + return nil, ErrNotSupported + } + return s.Internal.StateDecodeParams(p0, p1, p2, p3, p4) } func (s *GatewayStub) StateDecodeParams(p0 context.Context, p1 address.Address, p2 abi.MethodNum, p3 []byte, p4 types.TipSetKey) (interface{}, error) { - return nil, ErrNotSupported + return nil, ErrNotSupported } func (s *GatewayStruct) StateGetActor(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (*types.Actor, error) { @@ -2884,14 +2884,14 @@ func (s *GatewayStub) StateMinerProvingDeadline(p0 context.Context, p1 address.A } func (s *GatewayStruct) StateNetworkName(p0 context.Context) (dtypes.NetworkName, error) { - if s.Internal.StateNetworkName == nil { - return *new(dtypes.NetworkName), ErrNotSupported - } - return s.Internal.StateNetworkName(p0) + if s.Internal.StateNetworkName == nil { + return *new(dtypes.NetworkName), ErrNotSupported + } + return s.Internal.StateNetworkName(p0) } func (s *GatewayStub) StateNetworkName(p0 context.Context) (dtypes.NetworkName, error) { - return *new(dtypes.NetworkName), ErrNotSupported + return *new(dtypes.NetworkName), ErrNotSupported } func (s *GatewayStruct) StateNetworkVersion(p0 context.Context, p1 types.TipSetKey) (abinetwork.Version, error) { diff --git a/build/openrpc/full.json.gz b/build/openrpc/full.json.gz index bb1567be5e8c8f42cc72492e907c0c0c2cae9b06..0aaef1effe5c0418ff34300bf0984baa65ab8ead 100644 GIT binary patch delta 30995 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ȋ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 S{Qm#|0RR6Tivf_Ws|Emat=YZ+ delta 30984 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 A>;M1& literal 8480 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Уa&tDsZhY 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 F0{{ua4DJ8` diff --git a/build/openrpc/worker.json.gz b/build/openrpc/worker.json.gz index 1495059f96c61edbfefa4082c11a9776a8161700..aeb928561373e0427dcebdc71552d170dac4937b 100644 GIT binary patch delta 4417 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 Htu6ro=Ol(0 delta 4418 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 IL#-|W07RLA2><{9 diff --git a/gateway/node.go b/gateway/node.go index babf844a2..a97778eac 100644 --- a/gateway/node.go +++ b/gateway/node.go @@ -25,9 +25,9 @@ import ( _ "github.com/filecoin-project/lotus/lib/sigs/bls" _ "github.com/filecoin-project/lotus/lib/sigs/delegated" _ "github.com/filecoin-project/lotus/lib/sigs/secp" - "github.com/filecoin-project/lotus/node/modules/dtypes" "github.com/filecoin-project/lotus/metrics" "github.com/filecoin-project/lotus/node/impl/full" + "github.com/filecoin-project/lotus/node/modules/dtypes" ) const ( @@ -60,22 +60,22 @@ type TargetAPI interface { ChainPutObj(context.Context, blocks.Block) error ChainGetGenesis(context.Context) (*types.TipSet, error) GasEstimateMessageGas(ctx context.Context, msg *types.Message, spec *api.MessageSendSpec, tsk types.TipSetKey) (*types.Message, error) - MpoolGetNonce(ctx context.Context, addr address.Address) (uint64, error) + MpoolGetNonce(ctx context.Context, addr address.Address) (uint64, error) MpoolPushUntrusted(ctx context.Context, sm *types.SignedMessage) (cid.Cid, error) MsigGetAvailableBalance(ctx context.Context, addr address.Address, tsk types.TipSetKey) (types.BigInt, error) MsigGetVested(ctx context.Context, addr address.Address, start types.TipSetKey, end types.TipSetKey) (types.BigInt, error) MsigGetVestingSchedule(context.Context, address.Address, types.TipSetKey) (api.MsigVesting, error) MsigGetPending(ctx context.Context, addr address.Address, ts types.TipSetKey) ([]*api.MsigTransaction, error) StateAccountKey(ctx context.Context, addr address.Address, tsk types.TipSetKey) (address.Address, error) - StateCall(ctx context.Context, msg *types.Message, tsk types.TipSetKey) (*api.InvocResult, error) - StateDealProviderCollateralBounds(ctx context.Context, size abi.PaddedPieceSize, verified bool, tsk types.TipSetKey) (api.DealCollateralBounds, error) - StateDecodeParams(ctx context.Context, toAddr address.Address, method abi.MethodNum, params []byte, tsk types.TipSetKey) (interface{}, error) + StateCall(ctx context.Context, msg *types.Message, tsk types.TipSetKey) (*api.InvocResult, error) + StateDealProviderCollateralBounds(ctx context.Context, size abi.PaddedPieceSize, verified bool, tsk types.TipSetKey) (api.DealCollateralBounds, error) + StateDecodeParams(ctx context.Context, toAddr address.Address, method abi.MethodNum, params []byte, tsk types.TipSetKey) (interface{}, error) StateGetActor(ctx context.Context, actor address.Address, ts types.TipSetKey) (*types.Actor, error) StateLookupID(ctx context.Context, addr address.Address, tsk types.TipSetKey) (address.Address, error) StateListMiners(ctx context.Context, tsk types.TipSetKey) ([]address.Address, error) StateMarketBalance(ctx context.Context, addr address.Address, tsk types.TipSetKey) (api.MarketBalance, error) StateMarketStorageDeal(ctx context.Context, dealId abi.DealID, tsk types.TipSetKey) (*api.MarketDeal, error) - StateNetworkName(context.Context) (dtypes.NetworkName, error) + StateNetworkName(context.Context) (dtypes.NetworkName, error) StateNetworkVersion(context.Context, types.TipSetKey) (network.Version, error) StateSearchMsg(ctx context.Context, from types.TipSetKey, msg cid.Cid, limit abi.ChainEpoch, allowReplaced bool) (*api.MsgLookup, error) StateWaitMsg(ctx context.Context, cid cid.Cid, confidence uint64, limit abi.ChainEpoch, allowReplaced bool) (*api.MsgLookup, error) diff --git a/gateway/proxy_fil.go b/gateway/proxy_fil.go index e3e8d5312..bf6c2dff8 100644 --- a/gateway/proxy_fil.go +++ b/gateway/proxy_fil.go @@ -16,11 +16,11 @@ import ( "github.com/filecoin-project/lotus/api" apitypes "github.com/filecoin-project/lotus/api/types" - "github.com/filecoin-project/lotus/node/modules/dtypes" "github.com/filecoin-project/lotus/build" "github.com/filecoin-project/lotus/chain/actors/builtin/miner" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/lib/sigs" + "github.com/filecoin-project/lotus/node/modules/dtypes" ) func (gw *Node) Discover(ctx context.Context) (apitypes.OpenRPCDocument, error) { @@ -189,10 +189,10 @@ func (gw *Node) GasEstimateMessageGas(ctx context.Context, msg *types.Message, s } func (gw *Node) MpoolGetNonce(ctx context.Context, addr address.Address) (uint64, error) { - if err := gw.limit(ctx, stateRateLimitTokens); err != nil { - return 0, err - } - return gw.target.MpoolGetNonce(ctx, addr) + if err := gw.limit(ctx, stateRateLimitTokens); err != nil { + return 0, err + } + return gw.target.MpoolGetNonce(ctx, addr) } func (gw *Node) MpoolPush(ctx context.Context, sm *types.SignedMessage) (cid.Cid, error) { @@ -277,13 +277,13 @@ func (gw *Node) StateDealProviderCollateralBounds(ctx context.Context, size abi. } func (gw *Node) StateDecodeParams(ctx context.Context, toAddr address.Address, method abi.MethodNum, params []byte, tsk types.TipSetKey) (interface{}, error) { - if err := gw.limit(ctx, stateRateLimitTokens); err != nil { - return nil, err - } - if err := gw.checkTipsetKey(ctx, tsk); err != nil { - return nil, err - } - return gw.target.StateDecodeParams(ctx, toAddr, method, params, tsk) + if err := gw.limit(ctx, stateRateLimitTokens); err != nil { + return nil, err + } + if err := gw.checkTipsetKey(ctx, tsk); err != nil { + return nil, err + } + return gw.target.StateDecodeParams(ctx, toAddr, method, params, tsk) } func (gw *Node) StateGetActor(ctx context.Context, actor address.Address, tsk types.TipSetKey) (*types.Actor, error) { From a450e749546bf53121b396a3c273c917bff4ec09 Mon Sep 17 00:00:00 2001 From: Arsenii Petrovich <23522179+ArseniiPetrovich@users.noreply.github.com> Date: Wed, 8 Mar 2023 18:55:51 +0200 Subject: [PATCH 33/70] small doc patch on how to make gen after api changes --- api/api_gateway.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/api_gateway.go b/api/api_gateway.go index 2e877fb1a..9a1ed1eb8 100644 --- a/api/api_gateway.go +++ b/api/api_gateway.go @@ -25,7 +25,7 @@ import ( // When adding / changing methods in this file: // * Do the change here // * Adjust implementation in `node/impl/` -// * Run `make gen` - this will: +// * Run `make clean && make all && make gen` - this will: // * Generate proxy structs // * Generate mocks // * Generate markdown docs From cef416e2b0667d1748146238eb8d5cc60245481d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Kripalani?= Date: Wed, 8 Mar 2023 20:38:09 +0000 Subject: [PATCH 34/70] Eth API: make block parameter parsing sounder. --- node/impl/full/eth.go | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index 907adf8cd..7af9e7000 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -153,6 +153,8 @@ type EthAPI struct { EthEventAPI } +var ErrNullRound = errors.New("requested epoch was a null round") + func (a *EthModule) StateNetworkName(ctx context.Context) (dtypes.NetworkName, error) { return stmgr.GetNetworkName(ctx, a.StateManager, a.Chain.GetHeaviestTipSet().ParentState()) } @@ -231,7 +233,7 @@ func (a *EthModule) EthGetBlockByHash(ctx context.Context, blkHash ethtypes.EthH return newEthBlockFromFilecoinTipSet(ctx, ts, fullTxInfo, a.Chain, a.StateAPI) } -func (a *EthModule) parseBlkParam(ctx context.Context, blkParam string) (tipset *types.TipSet, err error) { +func (a *EthModule) parseBlkParam(ctx context.Context, blkParam string, strict bool) (tipset *types.TipSet, err error) { if blkParam == "earliest" { return nil, fmt.Errorf("block param \"earliest\" is not supported") } @@ -252,16 +254,19 @@ func (a *EthModule) parseBlkParam(ctx context.Context, blkParam string) (tipset if err != nil { return nil, fmt.Errorf("cannot parse block number: %v", err) } - ts, err := a.Chain.GetTipsetByHeight(ctx, abi.ChainEpoch(num), nil, false) + ts, err := a.Chain.GetTipsetByHeight(ctx, abi.ChainEpoch(num), nil, true) if err != nil { return nil, fmt.Errorf("cannot get tipset at height: %v", num) } + if strict && ts.Height() != abi.ChainEpoch(num) { + return nil, ErrNullRound + } return ts, nil } } func (a *EthModule) EthGetBlockByNumber(ctx context.Context, blkParam string, fullTxInfo bool) (ethtypes.EthBlock, error) { - ts, err := a.parseBlkParam(ctx, blkParam) + ts, err := a.parseBlkParam(ctx, blkParam, true) if err != nil { return ethtypes.EthBlock{}, err } @@ -367,7 +372,7 @@ func (a *EthModule) EthGetTransactionCount(ctx context.Context, sender ethtypes. return ethtypes.EthUint64(0), nil } - ts, err := a.parseBlkParam(ctx, blkParam) + ts, err := a.parseBlkParam(ctx, blkParam, false) if err != nil { return ethtypes.EthUint64(0), xerrors.Errorf("cannot parse block param: %s", blkParam) } @@ -456,7 +461,7 @@ func (a *EthModule) EthGetCode(ctx context.Context, ethAddr ethtypes.EthAddress, return nil, xerrors.Errorf("cannot get Filecoin address: %w", err) } - ts, err := a.parseBlkParam(ctx, blkParam) + ts, err := a.parseBlkParam(ctx, blkParam, false) if err != nil { return nil, xerrors.Errorf("cannot parse block param: %s", blkParam) } @@ -535,7 +540,7 @@ func (a *EthModule) EthGetCode(ctx context.Context, ethAddr ethtypes.EthAddress, } func (a *EthModule) EthGetStorageAt(ctx context.Context, ethAddr ethtypes.EthAddress, position ethtypes.EthBytes, blkParam string) (ethtypes.EthBytes, error) { - ts, err := a.parseBlkParam(ctx, blkParam) + ts, err := a.parseBlkParam(ctx, blkParam, false) if err != nil { return nil, xerrors.Errorf("cannot parse block param: %s", blkParam) } @@ -631,7 +636,7 @@ func (a *EthModule) EthGetBalance(ctx context.Context, address ethtypes.EthAddre return ethtypes.EthBigInt{}, err } - ts, err := a.parseBlkParam(ctx, blkParam) + ts, err := a.parseBlkParam(ctx, blkParam, false) if err != nil { return ethtypes.EthBigInt{}, xerrors.Errorf("cannot parse block param: %s", blkParam) } @@ -676,7 +681,7 @@ func (a *EthModule) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (eth } } - ts, err := a.parseBlkParam(ctx, params.NewestBlkNum) + ts, err := a.parseBlkParam(ctx, params.NewestBlkNum, false) if err != nil { return ethtypes.EthFeeHistory{}, fmt.Errorf("bad block parameter %s: %s", params.NewestBlkNum, err) } @@ -1066,7 +1071,7 @@ func (a *EthModule) EthCall(ctx context.Context, tx ethtypes.EthCall, blkParam s return nil, xerrors.Errorf("failed to convert ethcall to filecoin message: %w", err) } - ts, err := a.parseBlkParam(ctx, blkParam) + ts, err := a.parseBlkParam(ctx, blkParam, false) if err != nil { return nil, xerrors.Errorf("cannot parse block param: %s", blkParam) } From 28d8b4cd6588a185a888bc24744920e02d7f9493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Kripalani?= Date: Wed, 8 Mar 2023 20:49:28 +0000 Subject: [PATCH 35/70] Eth API: fail when requesting future epochs. --- node/impl/full/eth.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index 7af9e7000..919380ad1 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -254,6 +254,9 @@ func (a *EthModule) parseBlkParam(ctx context.Context, blkParam string, strict b if err != nil { return nil, fmt.Errorf("cannot parse block number: %v", err) } + if abi.ChainEpoch(num) > head.Height()-1 { + return nil, fmt.Errorf("requested a future epoch (beyond 'latest')") + } ts, err := a.Chain.GetTipsetByHeight(ctx, abi.ChainEpoch(num), nil, true) if err != nil { return nil, fmt.Errorf("cannot get tipset at height: %v", num) From b6dc0d20024e054a65b189ce65e31395e7a8b41a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Kripalani?= Date: Wed, 8 Mar 2023 21:15:04 +0000 Subject: [PATCH 36/70] add a test to verify block parameter soundness. --- itests/eth_transactions_test.go | 71 +++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/itests/eth_transactions_test.go b/itests/eth_transactions_test.go index 9afeb7bd5..82de0c6c0 100644 --- a/itests/eth_transactions_test.go +++ b/itests/eth_transactions_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/filecoin-project/lotus/chain/store" "github.com/stretchr/testify/require" "github.com/filecoin-project/go-state-types/big" @@ -270,6 +271,76 @@ func TestContractInvocation(t *testing.T) { require.EqualValues(t, ethtypes.EthUint64(0x1), receipt.Status) } +func TestGetBlockByNumber(t *testing.T) { + blockTime := 100 * time.Millisecond + client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC()) + + bms := ens.InterconnectAll().BeginMining(blockTime) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + // install contract + contractHex, err := os.ReadFile("./contracts/SimpleCoin.hex") + require.NoError(t, err) + + contract, err := hex.DecodeString(string(contractHex)) + require.NoError(t, err) + + // create a new Ethereum account + key, ethAddr, deployer := client.EVM().NewAccount() + // send some funds to the f410 address + kit.SendFunds(ctx, t, client, deployer, types.FromFil(10)) + + // DEPLOY CONTRACT + tx, err := deployContractTx(ctx, client, ethAddr, contract) + require.NoError(t, err) + + client.EVM().SignTransaction(tx, key.PrivateKey) + hash := client.EVM().SubmitTransaction(ctx, tx) + + receipt, err := waitForEthTxReceipt(ctx, client, hash) + require.NoError(t, err) + require.NotNil(t, receipt) + require.EqualValues(t, ethtypes.EthUint64(0x1), receipt.Status) + + latest, err := client.EthBlockNumber(ctx) + require.NoError(t, err) + + // can get the latest block + _, err = client.EthGetBlockByNumber(ctx, latest.Hex(), true) + require.NoError(t, err) + + // fail to get a future block + _, err = client.EthGetBlockByNumber(ctx, (latest + 10000).Hex(), true) + require.Error(t, err) + + // inject 10 null rounds + bms[0].InjectNulls(10) + + // wait until we produce blocks again + tctx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + ch, err := client.ChainNotify(tctx) + require.NoError(t, err) + hc := <-ch // current + hc = <-ch // wait for next block + require.Equal(t, store.HCApply, hc[0].Type) + + afterNullHeight := hc[0].Val.Height() + + // Fail when trying to fetch a null round. + _, err = client.EthGetBlockByNumber(ctx, (ethtypes.EthUint64(afterNullHeight - 1)).Hex(), true) + require.Error(t, err) + + // Fetch balance on a null round; should not fail and should return previous balance. + // Should be lower than original balance. + bal, err := client.EthGetBalance(ctx, ethAddr, (ethtypes.EthUint64(afterNullHeight - 1)).Hex()) + require.NoError(t, err) + require.NotEqual(t, big.Zero(), bal) + require.Equal(t, -1, bal.Cmp(types.FromFil(10).Int)) +} + func deployContractTx(ctx context.Context, client *kit.TestFullNode, ethAddr ethtypes.EthAddress, contract []byte) (*ethtypes.EthTxArgs, error) { gaslimit, err := client.EthEstimateGas(ctx, ethtypes.EthCall{ From: ðAddr, From 4427986ac18fe694c1dd4e16694d9716f457f088 Mon Sep 17 00:00:00 2001 From: Arsenii Petrovich <23522179+ArseniiPetrovich@users.noreply.github.com> Date: Thu, 9 Mar 2023 02:10:22 +0200 Subject: [PATCH 37/70] replace make all with make deps according to review --- api/api_gateway.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/api_gateway.go b/api/api_gateway.go index 9a1ed1eb8..ad50d4ac6 100644 --- a/api/api_gateway.go +++ b/api/api_gateway.go @@ -25,7 +25,7 @@ import ( // When adding / changing methods in this file: // * Do the change here // * Adjust implementation in `node/impl/` -// * Run `make clean && make all && make gen` - this will: +// * Run `make clean && make deps && make gen` - this will: // * Generate proxy structs // * Generate mocks // * Generate markdown docs From 0a0df61d779d817537f4aa5f2115000481a96cb4 Mon Sep 17 00:00:00 2001 From: ychiao Date: Wed, 8 Mar 2023 19:28:17 -0500 Subject: [PATCH 38/70] fix: EthAPI: use StateCompute for feeHistory; apply minimum gas premium (#10413) --- itests/eth_fee_history_test.go | 135 ++++++++++++++++++++++++++++----- node/impl/full/eth.go | 37 ++++----- node/impl/full/eth_test.go | 2 +- 3 files changed, 135 insertions(+), 39 deletions(-) diff --git a/itests/eth_fee_history_test.go b/itests/eth_fee_history_test.go index 72302f298..a792c7f0e 100644 --- a/itests/eth_fee_history_test.go +++ b/itests/eth_fee_history_test.go @@ -3,18 +3,42 @@ package itests import ( "context" "encoding/json" + "sort" "testing" "time" "github.com/stretchr/testify/require" "github.com/filecoin-project/go-jsonrpc" + "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/types/ethtypes" "github.com/filecoin-project/lotus/itests/kit" "github.com/filecoin-project/lotus/lib/result" + "github.com/filecoin-project/lotus/node/impl/full" ) +// calculateExpectations calculates the expected number of items to be included in the response +// of eth_feeHistory. It takes care of null rounds by finding the closet tipset with height +// smaller than startHeight, and then looks back at requestAmount of items. It also considers +// scenarios where there are not enough items to look back. +func calculateExpectations(tsHeights []int, requestAmount, startHeight int) (count, oldestHeight int) { + latestIdx := sort.SearchInts(tsHeights, startHeight) + // SearchInts returns the index of the number that's larger than the target if the target + // doesn't exist. However, we're looking for the closet number that's smaller that the target + for tsHeights[latestIdx] > startHeight { + latestIdx-- + } + cnt := requestAmount + oldestIdx := latestIdx - requestAmount + 1 + if oldestIdx < 0 { + cnt = latestIdx + 1 + oldestIdx = 0 + } + return cnt, tsHeights[oldestIdx] +} + func TestEthFeeHistory(t *testing.T) { require := require.New(t) @@ -22,70 +46,136 @@ func TestEthFeeHistory(t *testing.T) { blockTime := 100 * time.Millisecond client, _, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC()) - ens.InterconnectAll().BeginMining(blockTime) ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - // Wait for the network to create 20 blocks + heads, err := client.ChainNotify(ctx) + require.NoError(err) + + // Save the full view of the tipsets to calculate the answer when there are null rounds + tsHeights := []int{1} + go func() { + for chg := range heads { + for _, c := range chg { + tsHeights = append(tsHeights, int(c.Val.Height())) + } + } + }() + + miner := ens.InterconnectAll().BeginMining(blockTime) + + client.WaitTillChain(ctx, kit.HeightAtLeast(7)) + miner[0].InjectNulls(abi.ChainEpoch(5)) + + // Wait for the network to create at least 20 tipsets client.WaitTillChain(ctx, kit.HeightAtLeast(20)) + for _, m := range miner { + m.Pause() + } + + ch, err := client.ChainNotify(ctx) + require.NoError(err) + + // Wait for 5 seconds of inactivity + func() { + for { + select { + case <-ch: + continue + case <-time.After(5 * time.Second): + return + } + } + }() + + sort.Ints(tsHeights) + + // because of the deferred execution, the last tipset is not executed yet, + // and the one before the last one is the last executed tipset, + // which corresponds to the "latest" tag in EthGetBlockByNumber + latestBlk := ethtypes.EthUint64(tsHeights[len(tsHeights)-2]) + blk, err := client.EthGetBlockByNumber(ctx, "latest", false) + require.NoError(err) + require.Equal(blk.Number, latestBlk) + + assertHistory := func(history *ethtypes.EthFeeHistory, requestAmount, startHeight int) { + amount, oldest := calculateExpectations(tsHeights, requestAmount, startHeight) + require.Equal(amount+1, len(history.BaseFeePerGas)) + require.Equal(amount, len(history.GasUsedRatio)) + require.Equal(ethtypes.EthUint64(oldest), history.OldestBlock) + } history, err := client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams]( json.Marshal([]interface{}{5, "0x10"}), ).Assert(require.NoError)) require.NoError(err) - require.Equal(6, len(history.BaseFeePerGas)) - require.Equal(5, len(history.GasUsedRatio)) - require.Equal(ethtypes.EthUint64(16-5+1), history.OldestBlock) + assertHistory(&history, 5, 16) require.Nil(history.Reward) history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams]( json.Marshal([]interface{}{"5", "0x10"}), ).Assert(require.NoError)) require.NoError(err) - require.Equal(6, len(history.BaseFeePerGas)) - require.Equal(5, len(history.GasUsedRatio)) - require.Equal(ethtypes.EthUint64(16-5+1), history.OldestBlock) + assertHistory(&history, 5, 16) + require.Nil(history.Reward) + + history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams]( + json.Marshal([]interface{}{5, "latest"}), + ).Assert(require.NoError)) + require.NoError(err) + assertHistory(&history, 5, int(latestBlk)) require.Nil(history.Reward) history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams]( json.Marshal([]interface{}{"0x10", "0x12"}), ).Assert(require.NoError)) require.NoError(err) - require.Equal(17, len(history.BaseFeePerGas)) - require.Equal(16, len(history.GasUsedRatio)) - require.Equal(ethtypes.EthUint64(18-16+1), history.OldestBlock) + assertHistory(&history, 16, 18) require.Nil(history.Reward) history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams]( json.Marshal([]interface{}{5, "0x10"}), ).Assert(require.NoError)) require.NoError(err) - require.Equal(6, len(history.BaseFeePerGas)) - require.Equal(5, len(history.GasUsedRatio)) - require.Equal(ethtypes.EthUint64(16-5+1), history.OldestBlock) + assertHistory(&history, 5, 16) require.Nil(history.Reward) history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams]( json.Marshal([]interface{}{5, "10"}), ).Assert(require.NoError)) require.NoError(err) - require.Equal(6, len(history.BaseFeePerGas)) - require.Equal(5, len(history.GasUsedRatio)) - require.Equal(ethtypes.EthUint64(10-5+1), history.OldestBlock) + assertHistory(&history, 5, 10) + require.Nil(history.Reward) + + // test when the requested number of blocks is longer than chain length + history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams]( + json.Marshal([]interface{}{"0x30", "latest"}), + ).Assert(require.NoError)) + require.NoError(err) + assertHistory(&history, 48, int(latestBlk)) + require.Nil(history.Reward) + + // test when the requested number of blocks is longer than chain length + history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams]( + json.Marshal([]interface{}{"0x30", "10"}), + ).Assert(require.NoError)) + require.NoError(err) + assertHistory(&history, 48, 10) require.Nil(history.Reward) history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams]( json.Marshal([]interface{}{5, "10", &[]float64{25, 50, 75}}), ).Assert(require.NoError)) require.NoError(err) - require.Equal(6, len(history.BaseFeePerGas)) - require.Equal(5, len(history.GasUsedRatio)) - require.Equal(ethtypes.EthUint64(10-5+1), history.OldestBlock) + assertHistory(&history, 5, 10) require.NotNil(history.Reward) require.Equal(5, len(*history.Reward)) for _, arr := range *history.Reward { require.Equal(3, len(arr)) + for _, item := range arr { + require.Equal(ethtypes.EthBigInt(types.NewInt(full.MinGasPremium)), item) + } } history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams]( @@ -93,6 +183,11 @@ func TestEthFeeHistory(t *testing.T) { ).Assert(require.NoError)) require.Error(err) + history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams]( + json.Marshal([]interface{}{5, "10", &[]float64{75, 50}}), + ).Assert(require.NoError)) + require.Error(err) + history, err = client.EthFeeHistory(ctx, result.Wrap[jsonrpc.RawParams]( json.Marshal([]interface{}{5, "10", &[]float64{}}), ).Assert(require.NoError)) diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index 907adf8cd..64e16ca08 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -252,7 +252,7 @@ func (a *EthModule) parseBlkParam(ctx context.Context, blkParam string) (tipset if err != nil { return nil, fmt.Errorf("cannot parse block number: %v", err) } - ts, err := a.Chain.GetTipsetByHeight(ctx, abi.ChainEpoch(num), nil, false) + ts, err := a.Chain.GetTipsetByHeight(ctx, abi.ChainEpoch(num), nil, true) if err != nil { return nil, fmt.Errorf("cannot get tipset at height: %v", num) } @@ -681,11 +681,7 @@ func (a *EthModule) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (eth return ethtypes.EthFeeHistory{}, fmt.Errorf("bad block parameter %s: %s", params.NewestBlkNum, err) } - // Deal with the case that the chain is shorter than the number of requested blocks. oldestBlkHeight := uint64(1) - if abi.ChainEpoch(params.BlkCount) <= ts.Height() { - oldestBlkHeight = uint64(ts.Height()) - uint64(params.BlkCount) + 1 - } // NOTE: baseFeePerGas should include the next block after the newest of the returned range, // because the next base fee can be inferred from the messages in the newest block. @@ -695,29 +691,32 @@ func (a *EthModule) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (eth gasUsedRatioArray := []float64{} rewardsArray := make([][]ethtypes.EthBigInt, 0) - for ts.Height() >= abi.ChainEpoch(oldestBlkHeight) { - // Unfortunately we need to rebuild the full message view so we can - // totalize gas used in the tipset. - msgs, err := a.Chain.MessagesForTipset(ctx, ts) + blocksIncluded := 0 + for blocksIncluded < int(params.BlkCount) && ts.Height() > 0 { + compOutput, err := a.StateCompute(ctx, ts.Height(), nil, ts.Key()) if err != nil { - return ethtypes.EthFeeHistory{}, xerrors.Errorf("error loading messages for tipset: %v: %w", ts, err) + return ethtypes.EthFeeHistory{}, xerrors.Errorf("cannot lookup the status of tipset: %v: %w", ts, err) } txGasRewards := gasRewardSorter{} - for txIdx, msg := range msgs { - msgLookup, err := a.StateAPI.StateSearchMsg(ctx, types.EmptyTSK, msg.Cid(), api.LookbackNoLimit, false) - if err != nil || msgLookup == nil { - return ethtypes.EthFeeHistory{}, nil + for _, msg := range compOutput.Trace { + if msg.Msg.From == builtintypes.SystemActorAddr { + continue } - tx, err := newEthTxFromMessageLookup(ctx, msgLookup, txIdx, a.Chain, a.StateAPI) + smsgCid, err := getSignedMessage(ctx, a.Chain, msg.MsgCid) if err != nil { - return ethtypes.EthFeeHistory{}, nil + return ethtypes.EthFeeHistory{}, xerrors.Errorf("failed to get signed msg %s: %w", msg.MsgCid, err) + } + + tx, err := newEthTxFromSignedMessage(ctx, smsgCid, a.StateAPI) + if err != nil { + return ethtypes.EthFeeHistory{}, err } txGasRewards = append(txGasRewards, gasRewardTuple{ reward: tx.Reward(ts.Blocks()[0].ParentBaseFee), - gas: uint64(msgLookup.Receipt.GasUsed), + gas: uint64(msg.MsgRct.GasUsed), }) } @@ -727,6 +726,8 @@ func (a *EthModule) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (eth baseFeeArray = append(baseFeeArray, ethtypes.EthBigInt(ts.Blocks()[0].ParentBaseFee)) gasUsedRatioArray = append(gasUsedRatioArray, float64(totalGasUsed)/float64(build.BlockGasLimit)) rewardsArray = append(rewardsArray, rewards) + oldestBlkHeight = uint64(ts.Height()) + blocksIncluded++ parentTsKey := ts.Parents() ts, err = a.Chain.LoadTipSet(ctx, parentTsKey) @@ -2343,7 +2344,7 @@ func calculateRewardsAndGasUsed(rewardPercentiles []float64, txGasRewards gasRew rewards := make([]ethtypes.EthBigInt, len(rewardPercentiles)) for i := range rewards { - rewards[i] = ethtypes.EthBigIntZero + rewards[i] = ethtypes.EthBigInt(types.NewInt(MinGasPremium)) } if len(txGasRewards) == 0 { diff --git a/node/impl/full/eth_test.go b/node/impl/full/eth_test.go index 67a8b0500..4cf3b5c76 100644 --- a/node/impl/full/eth_test.go +++ b/node/impl/full/eth_test.go @@ -135,7 +135,7 @@ func TestRewardPercentiles(t *testing.T) { { percentiles: []float64{25, 50, 75}, txGasRewards: []gasRewardTuple{}, - answer: []int64{0, 0, 0}, + answer: []int64{MinGasPremium, MinGasPremium, MinGasPremium}, }, { percentiles: []float64{25, 50, 75, 100}, From e5553554d17caf050daba140b14f0f73f08da281 Mon Sep 17 00:00:00 2001 From: zenground0 Date: Wed, 8 Mar 2023 17:30:42 -0700 Subject: [PATCH 39/70] cid key size --- blockstore/splitstore/splitstore_compact.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blockstore/splitstore/splitstore_compact.go b/blockstore/splitstore/splitstore_compact.go index bd73aa570..7ac00cadf 100644 --- a/blockstore/splitstore/splitstore_compact.go +++ b/blockstore/splitstore/splitstore_compact.go @@ -67,7 +67,7 @@ var ( const ( batchSize = 16384 - cidKeySize = 32 + cidKeySize = 128 ) func (s *SplitStore) HeadChange(_, apply []*types.TipSet) error { From ee9ff563d20a30ea071f72e07dee888202e1b636 Mon Sep 17 00:00:00 2001 From: zenground0 Date: Wed, 8 Mar 2023 17:51:59 -0700 Subject: [PATCH 40/70] computing szPurge leads to deadlock and unneeded, remove --- blockstore/splitstore/splitstore.go | 1 - blockstore/splitstore/splitstore_compact.go | 18 +++--------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/blockstore/splitstore/splitstore.go b/blockstore/splitstore/splitstore.go index cefb86ebd..ad19f5dff 100644 --- a/blockstore/splitstore/splitstore.go +++ b/blockstore/splitstore/splitstore.go @@ -211,7 +211,6 @@ type SplitStore struct { // protected by compaction lock szWalk int64 szProtectedTxns int64 - szToPurge int64 // expected purges before critical section protections and live marking szKeys int64 // approximate, not counting keys protected when entering critical section // protected by txnLk diff --git a/blockstore/splitstore/splitstore_compact.go b/blockstore/splitstore/splitstore_compact.go index 7ac00cadf..db1b35176 100644 --- a/blockstore/splitstore/splitstore_compact.go +++ b/blockstore/splitstore/splitstore_compact.go @@ -648,7 +648,7 @@ func (s *SplitStore) doCompact(curTs *types.TipSet) error { defer purgew.Close() //nolint:errcheck // some stats for logging - var hotCnt, coldCnt, purgeCnt, szPurge int + var hotCnt, coldCnt, purgeCnt int64 err = s.hot.ForEachKey(func(c cid.Cid) error { // was it marked? mark, err := markSet.Has(c) @@ -660,16 +660,6 @@ func (s *SplitStore) doCompact(curTs *types.TipSet) error { hotCnt++ return nil } - sz, err := s.hot.GetSize(s.ctx, c) - if err != nil { - if ipld.IsNotFound(err) { - log.Warnf("hotstore missing expected block %s", c) - return nil - } - - return xerrors.Errorf("error retrieving block %s from hotstore: %w", c, err) - } - szPurge += sz // it needs to be removed from hot store, mark it as candidate for purge if err := purgew.Write(c); err != nil { @@ -709,9 +699,8 @@ func (s *SplitStore) doCompact(curTs *types.TipSet) error { log.Infow("cold collection done", "took", time.Since(startCollect)) - log.Infow("compaction stats", "hot", hotCnt, "cold", coldCnt, "purge", purgeCnt, "purge size", szPurge) - s.szToPurge = int64(szPurge) - s.szKeys = int64(hotCnt) * cidKeySize + log.Infow("compaction stats", "hot", hotCnt, "cold", coldCnt, "purge", purgeCnt) + s.szKeys = hotCnt * cidKeySize stats.Record(s.ctx, metrics.SplitstoreCompactionHot.M(int64(hotCnt))) stats.Record(s.ctx, metrics.SplitstoreCompactionCold.M(int64(coldCnt))) @@ -1540,7 +1529,6 @@ func (s *SplitStore) clearSizeMeasurements() { s.szKeys = 0 s.szMarkedLiveRefs = 0 s.szProtectedTxns = 0 - s.szToPurge = 0 s.szWalk = 0 } From efbb63582e9ab1e5da679726045b9a5b661c4bb8 Mon Sep 17 00:00:00 2001 From: zenground0 Date: Wed, 8 Mar 2023 18:11:39 -0700 Subject: [PATCH 41/70] Review Response --- blockstore/splitstore/splitstore_compact.go | 16 +++++++------- blockstore/splitstore/splitstore_gc.go | 24 ++++++++++++--------- node/config/doc_gen.go | 5 ++++- node/config/types.go | 3 +++ 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/blockstore/splitstore/splitstore_compact.go b/blockstore/splitstore/splitstore_compact.go index db1b35176..68845a598 100644 --- a/blockstore/splitstore/splitstore_compact.go +++ b/blockstore/splitstore/splitstore_compact.go @@ -204,7 +204,7 @@ func (s *SplitStore) markLiveRefs(cids []cid.Cid) { count := new(int32) visitor := newConcurrentVisitor() - walkObject := func(c cid.Cid) (int, error) { + walkObject := func(c cid.Cid) (int64, error) { return s.walkObjectIncomplete(c, visitor, func(c cid.Cid) error { if isUnitaryObject(c) { @@ -426,7 +426,7 @@ func (s *SplitStore) protectTxnRefs(markSet MarkSet) error { // transactionally protect a reference by walking the object and marking. // concurrent markings are short circuited by checking the markset. -func (s *SplitStore) doTxnProtect(root cid.Cid, markSet MarkSet) (int, error) { +func (s *SplitStore) doTxnProtect(root cid.Cid, markSet MarkSet) (int64, error) { if err := s.checkClosing(); err != nil { return 0, err } @@ -1082,8 +1082,8 @@ func (s *SplitStore) walkChain(ts *types.TipSet, inclState, inclMsgs abi.ChainEp return nil } -func (s *SplitStore) walkObject(c cid.Cid, visitor ObjectVisitor, f func(cid.Cid) error) (int, error) { - var sz int +func (s *SplitStore) walkObject(c cid.Cid, visitor ObjectVisitor, f func(cid.Cid) error) (int64, error) { + var sz int64 visit, err := visitor.Visit(c) if err != nil { return 0, xerrors.Errorf("error visiting object: %w", err) @@ -1112,7 +1112,7 @@ func (s *SplitStore) walkObject(c cid.Cid, visitor ObjectVisitor, f func(cid.Cid var links []cid.Cid err = s.view(c, func(data []byte) error { - sz += len(data) + sz += int64(len(data)) return cbg.ScanForLinks(bytes.NewReader(data), func(c cid.Cid) { links = append(links, c) }) @@ -1134,8 +1134,8 @@ func (s *SplitStore) walkObject(c cid.Cid, visitor ObjectVisitor, f func(cid.Cid } // like walkObject, but the object may be potentially incomplete (references missing) -func (s *SplitStore) walkObjectIncomplete(c cid.Cid, visitor ObjectVisitor, f, missing func(cid.Cid) error) (int, error) { - sz := 0 +func (s *SplitStore) walkObjectIncomplete(c cid.Cid, visitor ObjectVisitor, f, missing func(cid.Cid) error) (int64, error) { + sz := int64(0) visit, err := visitor.Visit(c) if err != nil { return 0, xerrors.Errorf("error visiting object: %w", err) @@ -1181,7 +1181,7 @@ func (s *SplitStore) walkObjectIncomplete(c cid.Cid, visitor ObjectVisitor, f, m var links []cid.Cid err = s.view(c, func(data []byte) error { - sz += len(data) + sz += int64(len(data)) return cbg.ScanForLinks(bytes.NewReader(data), func(c cid.Cid) { links = append(links, c) }) diff --git a/blockstore/splitstore/splitstore_gc.go b/blockstore/splitstore/splitstore_gc.go index 2e23b993a..c415b74dc 100644 --- a/blockstore/splitstore/splitstore_gc.go +++ b/blockstore/splitstore/splitstore_gc.go @@ -28,18 +28,21 @@ func (s *SplitStore) gcHotAfterCompaction() { // a) If we should not do full GC => online GC // b) If we should do full GC and can => moving GC // c) If we should do full GC and can't => aggressive online GC - var hotSize int64 - var err error - sizer, ok := s.hot.(bstore.BlockstoreSize) - if ok { - hotSize, err = sizer.Size() - if err != nil { - log.Warnf("error getting hotstore size: %s, estimating empty hot store for targeting", err) - hotSize = 0 + getSize := func() int64 { + sizer, ok := s.hot.(bstore.BlockstoreSize) + if ok { + size, err := sizer.Size() + if err != nil { + log.Warnf("error getting hotstore size: %s, estimating empty hot store for targeting", err) + return 0 + } + return size + } else { + log.Errorf("Could not measure hotstore size, assuming it is 0 bytes, which it is not") + return 0 } - } else { - hotSize = 0 } + hotSize := getSize() copySizeApprox := s.szKeys + s.szMarkedLiveRefs + s.szProtectedTxns + s.szWalk shouldTarget := s.cfg.HotstoreMaxSpaceTarget > 0 && hotSize+copySizeApprox > int64(s.cfg.HotstoreMaxSpaceTarget)-targetThreshold @@ -63,6 +66,7 @@ func (s *SplitStore) gcHotAfterCompaction() { if err := s.gcBlockstore(s.hot, opts); err != nil { log.Warnf("error garbage collecting hostore: %s", err) } + log.Infof("measured hot store size after GC: %d", getSize()) } func (s *SplitStore) gcBlockstore(b bstore.Blockstore, opts []bstore.BlockstoreGCOption) error { diff --git a/node/config/doc_gen.go b/node/config/doc_gen.go index 53c305be6..f5a89293c 100644 --- a/node/config/doc_gen.go +++ b/node/config/doc_gen.go @@ -1295,7 +1295,10 @@ will run moving GC if disk utilization gets within a threshold (150 GB) of the t Splitstore GC will NOT run moving GC if the total size of the move would get within 50 GB of the target, and instead will run a more aggressive online GC. If both HotStoreFullGCFrequency and HotStoreMaxSpaceTarget are set then splitstore -GC will trigger moving GC if either configuration condition is met.`, +GC will trigger moving GC if either configuration condition is met. +A reasonable minimum is 2x fully GCed hotstore size + 50 G buffer. +At this minimum size moving GC happens every time, any smaller and moving GC won't +be able to run. In spring 2023 this minimum is ~550 GB.`, }, }, "StorageMiner": []DocField{ diff --git a/node/config/types.go b/node/config/types.go index 123714794..68cd35123 100644 --- a/node/config/types.go +++ b/node/config/types.go @@ -607,6 +607,9 @@ type Splitstore struct { // within 50 GB of the target, and instead will run a more aggressive online GC. // If both HotStoreFullGCFrequency and HotStoreMaxSpaceTarget are set then splitstore // GC will trigger moving GC if either configuration condition is met. + // A reasonable minimum is 2x fully GCed hotstore size + 50 G buffer. + // At this minimum size moving GC happens every time, any smaller and moving GC won't + // be able to run. In spring 2023 this minimum is ~550 GB. HotStoreMaxSpaceTarget uint64 } From 793141473555c66b16aec169f3f87c971040a52c Mon Sep 17 00:00:00 2001 From: zenground0 Date: Wed, 8 Mar 2023 18:20:27 -0700 Subject: [PATCH 42/70] Lint --- blockstore/splitstore/splitstore_compact.go | 6 +++--- blockstore/splitstore/splitstore_gc.go | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/blockstore/splitstore/splitstore_compact.go b/blockstore/splitstore/splitstore_compact.go index 68845a598..c113ec72f 100644 --- a/blockstore/splitstore/splitstore_compact.go +++ b/blockstore/splitstore/splitstore_compact.go @@ -236,7 +236,7 @@ func (s *SplitStore) markLiveRefs(cids []cid.Cid) { log.Errorf("error marking tipset refs: %s", err) } log.Debugw("marking live refs done", "took", time.Since(startMark), "marked", *count) - atomic.AddInt64(szMarked, int64(sz)) + atomic.AddInt64(szMarked, sz) return } @@ -252,7 +252,7 @@ func (s *SplitStore) markLiveRefs(cids []cid.Cid) { if err != nil { return err } - atomic.AddInt64(szMarked, int64(sz)) + atomic.AddInt64(szMarked, sz) } return nil @@ -969,7 +969,7 @@ func (s *SplitStore) walkChain(ts *types.TipSet, inclState, inclMsgs abi.ChainEp if err != nil { return xerrors.Errorf("error walking messages (cid: %s): %w", hdr.Messages, err) } - atomic.AddInt64(szWalk, int64(sz)) + atomic.AddInt64(szWalk, sz) sz, err = s.walkObjectIncomplete(hdr.ParentMessageReceipts, visitor, fHot, stopWalk) if err != nil { diff --git a/blockstore/splitstore/splitstore_gc.go b/blockstore/splitstore/splitstore_gc.go index c415b74dc..43832a585 100644 --- a/blockstore/splitstore/splitstore_gc.go +++ b/blockstore/splitstore/splitstore_gc.go @@ -37,10 +37,9 @@ func (s *SplitStore) gcHotAfterCompaction() { return 0 } return size - } else { - log.Errorf("Could not measure hotstore size, assuming it is 0 bytes, which it is not") - return 0 } + log.Errorf("Could not measure hotstore size, assuming it is 0 bytes, which it is not") + return 0 } hotSize := getSize() From 0b0913f2f3b8a38a6c4e94d83aaade48eb0dbc0a Mon Sep 17 00:00:00 2001 From: zenground0 Date: Wed, 8 Mar 2023 18:40:23 -0700 Subject: [PATCH 43/70] lint --- blockstore/splitstore/splitstore_compact.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blockstore/splitstore/splitstore_compact.go b/blockstore/splitstore/splitstore_compact.go index c113ec72f..b3873f659 100644 --- a/blockstore/splitstore/splitstore_compact.go +++ b/blockstore/splitstore/splitstore_compact.go @@ -997,7 +997,7 @@ func (s *SplitStore) walkChain(ts *types.TipSet, inclState, inclMsgs abi.ChainEp if err != nil { return xerrors.Errorf("error walking messages (cid: %s): %w", hdr.Messages, err) } - atomic.AddInt64(szWalk, int64(sz)) + atomic.AddInt64(szWalk, sz) sz, err = s.walkObjectIncomplete(hdr.ParentMessageReceipts, visitor, fCold, stopWalk) if err != nil { return xerrors.Errorf("error walking messages receipts (cid: %s): %w", hdr.ParentMessageReceipts, err) @@ -1011,7 +1011,7 @@ func (s *SplitStore) walkChain(ts *types.TipSet, inclState, inclMsgs abi.ChainEp if err != nil { return xerrors.Errorf("error walking state root (cid: %s): %w", hdr.ParentStateRoot, err) } - atomic.AddInt64(szWalk, int64(sz)) + atomic.AddInt64(szWalk, sz) atomic.AddInt64(scanCnt, 1) } From 14af4b27cd36d4cee4dd77c0c8818c5007c2a62a Mon Sep 17 00:00:00 2001 From: zenground0 Date: Wed, 8 Mar 2023 18:41:31 -0700 Subject: [PATCH 44/70] docsgen-cli --- documentation/en/default-lotus-config.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/documentation/en/default-lotus-config.toml b/documentation/en/default-lotus-config.toml index a447350ff..f772e316c 100644 --- a/documentation/en/default-lotus-config.toml +++ b/documentation/en/default-lotus-config.toml @@ -236,6 +236,9 @@ # within 50 GB of the target, and instead will run a more aggressive online GC. # If both HotStoreFullGCFrequency and HotStoreMaxSpaceTarget are set then splitstore # GC will trigger moving GC if either configuration condition is met. + # A reasonable minimum is 2x fully GCed hotstore size + 50 G buffer. + # At this minimum size moving GC happens every time, any smaller and moving GC won't + # be able to run. In spring 2023 this minimum is ~550 GB. # # type: uint64 # env var: LOTUS_CHAINSTORE_SPLITSTORE_HOTSTOREMAXSPACETARGET From 140f2099bd8423f0a68a6eb6bd49aa55a49bee46 Mon Sep 17 00:00:00 2001 From: zenground0 Date: Wed, 8 Mar 2023 22:49:30 -0700 Subject: [PATCH 45/70] lint --- blockstore/splitstore/splitstore_compact.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blockstore/splitstore/splitstore_compact.go b/blockstore/splitstore/splitstore_compact.go index b3873f659..96542fcf4 100644 --- a/blockstore/splitstore/splitstore_compact.go +++ b/blockstore/splitstore/splitstore_compact.go @@ -701,8 +701,8 @@ func (s *SplitStore) doCompact(curTs *types.TipSet) error { log.Infow("compaction stats", "hot", hotCnt, "cold", coldCnt, "purge", purgeCnt) s.szKeys = hotCnt * cidKeySize - stats.Record(s.ctx, metrics.SplitstoreCompactionHot.M(int64(hotCnt))) - stats.Record(s.ctx, metrics.SplitstoreCompactionCold.M(int64(coldCnt))) + stats.Record(s.ctx, metrics.SplitstoreCompactionHot.M(hotCnt)) + stats.Record(s.ctx, metrics.SplitstoreCompactionCold.M(coldCnt)) if err := s.checkClosing(); err != nil { return err @@ -975,7 +975,7 @@ func (s *SplitStore) walkChain(ts *types.TipSet, inclState, inclMsgs abi.ChainEp if err != nil { return xerrors.Errorf("error walking messages receipts (cid: %s): %w", hdr.ParentMessageReceipts, err) } - atomic.AddInt64(szWalk, int64(sz)) + atomic.AddInt64(szWalk, sz) } else { sz, err = s.walkObject(hdr.Messages, visitor, fHot) if err != nil { From ab91ab100f4e8523fe2703d4c416ffdf44c6a45f Mon Sep 17 00:00:00 2001 From: Aayush Rajasekaran Date: Thu, 9 Mar 2023 07:14:57 -0500 Subject: [PATCH 46/70] fix: eth API: return correct txIdx around null blocks (#10419) --- node/impl/full/eth.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index 64e16ca08..46a445aeb 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -1797,12 +1797,16 @@ func newEthBlockFromFilecoinTipSet(ctx context.Context, ts *types.TipSet, fullTx return ethtypes.EthBlock{}, xerrors.Errorf("failed to compute state: %w", err) } - for txIdx, msg := range compOutput.Trace { + txIdx := 0 + for _, msg := range compOutput.Trace { // skip system messages like reward application and cron if msg.Msg.From == builtintypes.SystemActorAddr { continue } + ti := ethtypes.EthUint64(txIdx) + txIdx++ + gasUsed += msg.MsgRct.GasUsed smsgCid, err := getSignedMessage(ctx, cs, msg.MsgCid) if err != nil { @@ -1813,8 +1817,6 @@ func newEthBlockFromFilecoinTipSet(ctx context.Context, ts *types.TipSet, fullTx return ethtypes.EthBlock{}, xerrors.Errorf("failed to convert msg to ethTx: %w", err) } - ti := ethtypes.EthUint64(txIdx) - tx.ChainID = ethtypes.EthUint64(build.Eip155ChainId) tx.BlockHash = &blkHash tx.BlockNumber = &bn From fd3ddc860ed1cae8b9f6087ff5fc21afd0dfba29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Kripalani?= Date: Thu, 9 Mar 2023 12:15:35 +0000 Subject: [PATCH 47/70] fmt. --- itests/eth_transactions_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/itests/eth_transactions_test.go b/itests/eth_transactions_test.go index 82de0c6c0..5cc829907 100644 --- a/itests/eth_transactions_test.go +++ b/itests/eth_transactions_test.go @@ -7,7 +7,6 @@ import ( "testing" "time" - "github.com/filecoin-project/lotus/chain/store" "github.com/stretchr/testify/require" "github.com/filecoin-project/go-state-types/big" @@ -15,6 +14,7 @@ import ( "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/build" + "github.com/filecoin-project/lotus/chain/store" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/types/ethtypes" "github.com/filecoin-project/lotus/itests/kit" From fb7eb086246f572323244721f4a2d31723034ff0 Mon Sep 17 00:00:00 2001 From: zenground0 Date: Thu, 9 Mar 2023 06:36:35 -0700 Subject: [PATCH 48/70] lint --- blockstore/splitstore/splitstore_compact.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/blockstore/splitstore/splitstore_compact.go b/blockstore/splitstore/splitstore_compact.go index 96542fcf4..e01ce684c 100644 --- a/blockstore/splitstore/splitstore_compact.go +++ b/blockstore/splitstore/splitstore_compact.go @@ -406,7 +406,7 @@ func (s *SplitStore) protectTxnRefs(markSet MarkSet) error { if err != nil { return xerrors.Errorf("error protecting transactional references to %s: %w", c, err) } - atomic.AddInt64(sz, int64(szTxn)) + atomic.AddInt64(sz, szTxn) } return nil } @@ -958,7 +958,7 @@ func (s *SplitStore) walkChain(ts *types.TipSet, inclState, inclMsgs abi.ChainEp if err != nil { return xerrors.Errorf("error walking parent tipset cid reference") } - atomic.AddInt64(szWalk, int64(sz)) + atomic.AddInt64(szWalk, sz) // message are retained if within the inclMsgs boundary if hdr.Height >= inclMsgs && hdr.Height > 0 { @@ -981,13 +981,13 @@ func (s *SplitStore) walkChain(ts *types.TipSet, inclState, inclMsgs abi.ChainEp if err != nil { return xerrors.Errorf("error walking messages (cid: %s): %w", hdr.Messages, err) } - atomic.AddInt64(szWalk, int64(sz)) + atomic.AddInt64(szWalk, sz) sz, err := s.walkObject(hdr.ParentMessageReceipts, visitor, fHot) if err != nil { return xerrors.Errorf("error walking message receipts (cid: %s): %w", hdr.ParentMessageReceipts, err) } - atomic.AddInt64(szWalk, int64(sz)) + atomic.AddInt64(szWalk, sz) } } @@ -1002,7 +1002,7 @@ func (s *SplitStore) walkChain(ts *types.TipSet, inclState, inclMsgs abi.ChainEp if err != nil { return xerrors.Errorf("error walking messages receipts (cid: %s): %w", hdr.ParentMessageReceipts, err) } - atomic.AddInt64(szWalk, int64(sz)) + atomic.AddInt64(szWalk, sz) } // state is only retained if within the inclState boundary, with the exception of genesis @@ -1033,7 +1033,7 @@ func (s *SplitStore) walkChain(ts *types.TipSet, inclState, inclMsgs abi.ChainEp if err != nil { return xerrors.Errorf("error walking parent tipset cid reference") } - atomic.AddInt64(szWalk, int64(sz)) + atomic.AddInt64(szWalk, sz) for len(toWalk) > 0 { // walking can take a while, so check this with every opportunity @@ -1135,7 +1135,7 @@ func (s *SplitStore) walkObject(c cid.Cid, visitor ObjectVisitor, f func(cid.Cid // like walkObject, but the object may be potentially incomplete (references missing) func (s *SplitStore) walkObjectIncomplete(c cid.Cid, visitor ObjectVisitor, f, missing func(cid.Cid) error) (int64, error) { - sz := int64(0) + var sz int64 visit, err := visitor.Visit(c) if err != nil { return 0, xerrors.Errorf("error visiting object: %w", err) From c80783dbead9429902df7f857b70b44fadb08885 Mon Sep 17 00:00:00 2001 From: ZenGround0 <5515260+ZenGround0@users.noreply.github.com> Date: Thu, 9 Mar 2023 08:37:34 -0500 Subject: [PATCH 49/70] feat:splitstore:Splitstore enabled by default (#10429) Discard mode --------- Co-authored-by: zenground0 --- documentation/en/default-lotus-config.toml | 4 ++-- node/config/def.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/documentation/en/default-lotus-config.toml b/documentation/en/default-lotus-config.toml index 41d7e6aca..903573ed0 100644 --- a/documentation/en/default-lotus-config.toml +++ b/documentation/en/default-lotus-config.toml @@ -191,7 +191,7 @@ [Chainstore] # type: bool # env var: LOTUS_CHAINSTORE_ENABLESPLITSTORE - #EnableSplitstore = false + #EnableSplitstore = true [Chainstore.Splitstore] # ColdStoreType specifies the type of the coldstore. @@ -199,7 +199,7 @@ # # type: string # env var: LOTUS_CHAINSTORE_SPLITSTORE_COLDSTORETYPE - #ColdStoreType = "messages" + #ColdStoreType = "discard" # HotStoreType specifies the type of the hotstore. # Only currently supported value is "badger". diff --git a/node/config/def.go b/node/config/def.go index f50e9575a..fc8451ee3 100644 --- a/node/config/def.go +++ b/node/config/def.go @@ -89,9 +89,9 @@ func DefaultFullNode() *FullNode { SimultaneousTransfersForRetrieval: DefaultSimultaneousTransfers, }, Chainstore: Chainstore{ - EnableSplitstore: false, + EnableSplitstore: true, Splitstore: Splitstore{ - ColdStoreType: "messages", + ColdStoreType: "discard", HotStoreType: "badger", MarkSetType: "badger", From aac30cd840ddf409bf794a093ffe2d4dd5327245 Mon Sep 17 00:00:00 2001 From: zenground0 Date: Thu, 9 Mar 2023 07:57:35 -0700 Subject: [PATCH 50/70] Review Response --- blockstore/splitstore/splitstore_compact.go | 6 +++--- blockstore/splitstore/splitstore_gc.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/blockstore/splitstore/splitstore_compact.go b/blockstore/splitstore/splitstore_compact.go index e01ce684c..12d0ff899 100644 --- a/blockstore/splitstore/splitstore_compact.go +++ b/blockstore/splitstore/splitstore_compact.go @@ -276,7 +276,7 @@ func (s *SplitStore) markLiveRefs(cids []cid.Cid) { } log.Debugw("marking live refs done", "took", time.Since(startMark), "marked", *count, "size marked", *szMarked) - s.szMarkedLiveRefs += *szMarked + s.szMarkedLiveRefs += atomic.LoadInt64(szMarked) } // transactionally protect a view @@ -419,7 +419,7 @@ func (s *SplitStore) protectTxnRefs(markSet MarkSet) error { if err := g.Wait(); err != nil { return err } - s.szProtectedTxns += *sz + s.szProtectedTxns += atomic.LoadInt64(sz) log.Infow("protecting transactional refs done", "took", time.Since(startProtect), "protected", count, "protected size", sz) } } @@ -1078,7 +1078,7 @@ func (s *SplitStore) walkChain(ts *types.TipSet, inclState, inclMsgs abi.ChainEp } log.Infow("chain walk done", "walked", *walkCnt, "scanned", *scanCnt, "walk size", szWalk) - s.szWalk = *szWalk + s.szWalk = atomic.LoadInt64(szWalk) return nil } diff --git a/blockstore/splitstore/splitstore_gc.go b/blockstore/splitstore/splitstore_gc.go index 43832a585..1a5fbda68 100644 --- a/blockstore/splitstore/splitstore_gc.go +++ b/blockstore/splitstore/splitstore_gc.go @@ -13,7 +13,7 @@ const ( // Don't attempt moving GC with 50 GB or less would remain during moving GC targetBuffer = 50_000_000_000 // Fraction of garbage in badger vlog for online GC traversal to collect garbage - aggressiveOnlineGCThreshold = 0.0001 + AggressiveOnlineGCThreshold = 0.0001 ) func (s *SplitStore) gcHotAfterCompaction() { @@ -59,7 +59,7 @@ func (s *SplitStore) gcHotAfterCompaction() { log.Warn("If problem continues you can 1) temporarily allocate more disk space to hotstore and 2) reflect in HotstoreMaxSpaceTarget OR trigger manual move with `lotus chain prune hot-moving`") log.Warn("If problem continues and you do not have any more disk space you can run continue to manually trigger online GC at aggressive thresholds (< 0.01) with `lotus chain prune hot`") - opts = append(opts, bstore.WithThreshold(aggressiveOnlineGCThreshold)) + opts = append(opts, bstore.WithThreshold(AggressiveOnlineGCThreshold)) } if err := s.gcBlockstore(s.hot, opts); err != nil { From d38bdcebfdbba3f86ced7f03a7b0aecb7c12def1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Kripalani?= Date: Thu, 9 Mar 2023 14:59:56 +0000 Subject: [PATCH 51/70] fix lint. --- itests/eth_transactions_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/itests/eth_transactions_test.go b/itests/eth_transactions_test.go index 5cc829907..9d6c742dc 100644 --- a/itests/eth_transactions_test.go +++ b/itests/eth_transactions_test.go @@ -323,8 +323,8 @@ func TestGetBlockByNumber(t *testing.T) { defer cancel() ch, err := client.ChainNotify(tctx) require.NoError(t, err) - hc := <-ch // current - hc = <-ch // wait for next block + <-ch // current + hc := <-ch // wait for next block require.Equal(t, store.HCApply, hc[0].Type) afterNullHeight := hc[0].Val.Height() From bd0c010be6f1d0b85f5ba3c214a5f3911674f9f6 Mon Sep 17 00:00:00 2001 From: zenground0 Date: Thu, 9 Mar 2023 08:40:14 -0700 Subject: [PATCH 52/70] Configur buffer and threshold --- blockstore/splitstore/splitstore.go | 9 +++++++++ blockstore/splitstore/splitstore_gc.go | 4 ++-- documentation/en/default-lotus-config.toml | 6 ++++++ node/config/def.go | 4 +++- node/config/types.go | 9 +++++++++ node/modules/blockstore.go | 14 ++++++++------ 6 files changed, 37 insertions(+), 9 deletions(-) diff --git a/blockstore/splitstore/splitstore.go b/blockstore/splitstore/splitstore.go index ad19f5dff..410cc50df 100644 --- a/blockstore/splitstore/splitstore.go +++ b/blockstore/splitstore/splitstore.go @@ -123,6 +123,15 @@ type Config struct { // never overflow. This field is used when doing GC at the end of a compaction to // adaptively choose moving GC HotstoreMaxSpaceTarget uint64 + + // Moving GC will be triggered when total moving size exceeds + // HotstoreMaxSpaceTarget - HotstoreMaxSpaceThreshold + HotstoreMaxSpaceThreshold uint64 + + // Safety buffer to prevent moving GC from overflowing disk. + // Moving GC will not occur when total moving size exceeds + // HotstoreMaxSpaceTarget - HotstoreMaxSpaceSafetyBuffer + HotstoreMaxSpaceSafetyBuffer uint64 } // ChainAccessor allows the Splitstore to access the chain. It will most likely diff --git a/blockstore/splitstore/splitstore_gc.go b/blockstore/splitstore/splitstore_gc.go index 1a5fbda68..f6087205d 100644 --- a/blockstore/splitstore/splitstore_gc.go +++ b/blockstore/splitstore/splitstore_gc.go @@ -44,10 +44,10 @@ func (s *SplitStore) gcHotAfterCompaction() { hotSize := getSize() copySizeApprox := s.szKeys + s.szMarkedLiveRefs + s.szProtectedTxns + s.szWalk - shouldTarget := s.cfg.HotstoreMaxSpaceTarget > 0 && hotSize+copySizeApprox > int64(s.cfg.HotstoreMaxSpaceTarget)-targetThreshold + shouldTarget := s.cfg.HotstoreMaxSpaceTarget > 0 && hotSize+copySizeApprox > int64(s.cfg.HotstoreMaxSpaceTarget)-int64(s.cfg.HotstoreMaxSpaceThreshold) shouldFreq := s.cfg.HotStoreFullGCFrequency > 0 && s.compactionIndex%int64(s.cfg.HotStoreFullGCFrequency) == 0 shouldDoFull := shouldTarget || shouldFreq - canDoFull := s.cfg.HotstoreMaxSpaceTarget == 0 || hotSize+copySizeApprox < int64(s.cfg.HotstoreMaxSpaceTarget)-targetBuffer + canDoFull := s.cfg.HotstoreMaxSpaceTarget == 0 || hotSize+copySizeApprox < int64(s.cfg.HotstoreMaxSpaceTarget)-int64(s.cfg.HotstoreMaxSpaceSafetyBuffer) log.Debugw("approximating new hot store size", "key size", s.szKeys, "marked live refs", s.szMarkedLiveRefs, "protected txns", s.szProtectedTxns, "walked DAG", s.szWalk) log.Infof("measured hot store size: %d, approximate new size: %d, should do full %t, can do full %t", hotSize, copySizeApprox, shouldDoFull, canDoFull) diff --git a/documentation/en/default-lotus-config.toml b/documentation/en/default-lotus-config.toml index f772e316c..e17448a6c 100644 --- a/documentation/en/default-lotus-config.toml +++ b/documentation/en/default-lotus-config.toml @@ -244,6 +244,12 @@ # env var: LOTUS_CHAINSTORE_SPLITSTORE_HOTSTOREMAXSPACETARGET #HotStoreMaxSpaceTarget = 0 + # env var: LOTUS_CHAINSTORE_SPLITSTORE_HOTSTOREMAXSPACETHRESHOLD + #HotStoreMaxSpaceThreshold = 150000000000 + + # env var: LOTUS_CHAINSTORE_SPLITSTORE_HOTSTOREMAXSPACESAFETYBUFFER + #HotstoreMaxSpaceSafetyBuffer = 50000000000 + [Cluster] # EXPERIMENTAL. config to enabled node cluster with raft consensus diff --git a/node/config/def.go b/node/config/def.go index f50e9575a..a5c532d14 100644 --- a/node/config/def.go +++ b/node/config/def.go @@ -95,7 +95,9 @@ func DefaultFullNode() *FullNode { HotStoreType: "badger", MarkSetType: "badger", - HotStoreFullGCFrequency: 20, + HotStoreFullGCFrequency: 20, + HotStoreMaxSpaceThreshold: 150_000_000_000, + HotstoreMaxSpaceSafetyBuffer: 50_000_000_000, }, }, Cluster: *DefaultUserRaftConfig(), diff --git a/node/config/types.go b/node/config/types.go index 68cd35123..5b952d35e 100644 --- a/node/config/types.go +++ b/node/config/types.go @@ -611,6 +611,15 @@ type Splitstore struct { // At this minimum size moving GC happens every time, any smaller and moving GC won't // be able to run. In spring 2023 this minimum is ~550 GB. HotStoreMaxSpaceTarget uint64 + + // When HotStoreMaxSpaceTarget is set Moving GC will be triggered when total moving size + // exceeds HotstoreMaxSpaceTarget - HotstoreMaxSpaceThreshold + HotStoreMaxSpaceThreshold uint64 + + // Safety buffer to prevent moving GC from overflowing disk when HotStoreMaxSpaceTarget + // is set. Moving GC will not occur when total moving size exceeds + // HotstoreMaxSpaceTarget - HotstoreMaxSpaceSafetyBuffer + HotstoreMaxSpaceSafetyBuffer uint64 } // // Full Node diff --git a/node/modules/blockstore.go b/node/modules/blockstore.go index f4211bbe2..f96fd0db4 100644 --- a/node/modules/blockstore.go +++ b/node/modules/blockstore.go @@ -82,12 +82,14 @@ func SplitBlockstore(cfg *config.Chainstore) func(lc fx.Lifecycle, r repo.Locked } cfg := &splitstore.Config{ - MarkSetType: cfg.Splitstore.MarkSetType, - DiscardColdBlocks: cfg.Splitstore.ColdStoreType == "discard", - UniversalColdBlocks: cfg.Splitstore.ColdStoreType == "universal", - HotStoreMessageRetention: cfg.Splitstore.HotStoreMessageRetention, - HotStoreFullGCFrequency: cfg.Splitstore.HotStoreFullGCFrequency, - HotstoreMaxSpaceTarget: cfg.Splitstore.HotStoreMaxSpaceTarget, + MarkSetType: cfg.Splitstore.MarkSetType, + DiscardColdBlocks: cfg.Splitstore.ColdStoreType == "discard", + UniversalColdBlocks: cfg.Splitstore.ColdStoreType == "universal", + HotStoreMessageRetention: cfg.Splitstore.HotStoreMessageRetention, + HotStoreFullGCFrequency: cfg.Splitstore.HotStoreFullGCFrequency, + HotstoreMaxSpaceTarget: cfg.Splitstore.HotStoreMaxSpaceTarget, + HotstoreMaxSpaceThreshold: cfg.Splitstore.HotStoreMaxSpaceThreshold, + HotstoreMaxSpaceSafetyBuffer: cfg.Splitstore.HotstoreMaxSpaceSafetyBuffer, } ss, err := splitstore.Open(path, ds, hot, cold, cfg) if err != nil { From 87d5a3723ffc7a3c584cbd14a889a5c9128dafa8 Mon Sep 17 00:00:00 2001 From: zenground0 Date: Thu, 9 Mar 2023 08:57:14 -0700 Subject: [PATCH 53/70] lint --- blockstore/splitstore/splitstore_gc.go | 6 +----- node/config/doc_gen.go | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/blockstore/splitstore/splitstore_gc.go b/blockstore/splitstore/splitstore_gc.go index f6087205d..2ddb7d404 100644 --- a/blockstore/splitstore/splitstore_gc.go +++ b/blockstore/splitstore/splitstore_gc.go @@ -8,10 +8,6 @@ import ( ) const ( - // When < 150 GB of space would remain during moving GC, trigger moving GC - targetThreshold = 150_000_000_000 - // Don't attempt moving GC with 50 GB or less would remain during moving GC - targetBuffer = 50_000_000_000 // Fraction of garbage in badger vlog for online GC traversal to collect garbage AggressiveOnlineGCThreshold = 0.0001 ) @@ -55,7 +51,7 @@ func (s *SplitStore) gcHotAfterCompaction() { if shouldDoFull && canDoFull { opts = append(opts, bstore.WithFullGC(true)) } else if shouldDoFull && !canDoFull { - log.Warnf("Attention! Estimated moving GC size %d is not within safety buffer %d of target max %d, performing aggressive online GC to attempt to bring hotstore size down safely", copySizeApprox, targetBuffer, s.cfg.HotstoreMaxSpaceTarget) + log.Warnf("Attention! Estimated moving GC size %d is not within safety buffer %d of target max %d, performing aggressive online GC to attempt to bring hotstore size down safely", copySizeApprox, s.cfg.HotstoreMaxSpaceSafetyBuffer, s.cfg.HotstoreMaxSpaceTarget) log.Warn("If problem continues you can 1) temporarily allocate more disk space to hotstore and 2) reflect in HotstoreMaxSpaceTarget OR trigger manual move with `lotus chain prune hot-moving`") log.Warn("If problem continues and you do not have any more disk space you can run continue to manually trigger online GC at aggressive thresholds (< 0.01) with `lotus chain prune hot`") diff --git a/node/config/doc_gen.go b/node/config/doc_gen.go index f5a89293c..c62084708 100644 --- a/node/config/doc_gen.go +++ b/node/config/doc_gen.go @@ -1300,6 +1300,21 @@ A reasonable minimum is 2x fully GCed hotstore size + 50 G buffer. At this minimum size moving GC happens every time, any smaller and moving GC won't be able to run. In spring 2023 this minimum is ~550 GB.`, }, + { + Name: "HotStoreMaxSpaceThreshold", + Type: "uint64", + + Comment: `When HotStoreMaxSpaceTarget is set Moving GC will be triggered when total moving size +exceeds HotstoreMaxSpaceTarget - HotstoreMaxSpaceThreshold`, + }, + { + Name: "HotstoreMaxSpaceSafetyBuffer", + Type: "uint64", + + Comment: `Safety buffer to prevent moving GC from overflowing disk when HotStoreMaxSpaceTarget +is set. Moving GC will not occur when total moving size exceeds +HotstoreMaxSpaceTarget - HotstoreMaxSpaceSafetyBuffer`, + }, }, "StorageMiner": []DocField{ { From 366ebe3155a28ee08597886ae943366cc0f86d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Thu, 9 Mar 2023 17:18:04 +0100 Subject: [PATCH 54/70] make gen --- documentation/en/default-lotus-config.toml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/documentation/en/default-lotus-config.toml b/documentation/en/default-lotus-config.toml index e17448a6c..8e757d2c3 100644 --- a/documentation/en/default-lotus-config.toml +++ b/documentation/en/default-lotus-config.toml @@ -244,9 +244,18 @@ # env var: LOTUS_CHAINSTORE_SPLITSTORE_HOTSTOREMAXSPACETARGET #HotStoreMaxSpaceTarget = 0 + # When HotStoreMaxSpaceTarget is set Moving GC will be triggered when total moving size + # exceeds HotstoreMaxSpaceTarget - HotstoreMaxSpaceThreshold + # + # type: uint64 # env var: LOTUS_CHAINSTORE_SPLITSTORE_HOTSTOREMAXSPACETHRESHOLD #HotStoreMaxSpaceThreshold = 150000000000 + # Safety buffer to prevent moving GC from overflowing disk when HotStoreMaxSpaceTarget + # is set. Moving GC will not occur when total moving size exceeds + # HotstoreMaxSpaceTarget - HotstoreMaxSpaceSafetyBuffer + # + # type: uint64 # env var: LOTUS_CHAINSTORE_SPLITSTORE_HOTSTOREMAXSPACESAFETYBUFFER #HotstoreMaxSpaceSafetyBuffer = 50000000000 From 13d6211ed039d2e8a6ed3d1d9a1736dfc63af860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Kripalani?= Date: Thu, 9 Mar 2023 17:14:23 +0000 Subject: [PATCH 55/70] fix TestEthBlockHashesCorrect_MultiBlockTipset: skip null rounds. --- itests/eth_block_hash_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/itests/eth_block_hash_test.go b/itests/eth_block_hash_test.go index ac6506bb2..7cc766ebc 100644 --- a/itests/eth_block_hash_test.go +++ b/itests/eth_block_hash_test.go @@ -3,6 +3,7 @@ package itests import ( "context" "fmt" + "strings" "testing" "time" @@ -55,6 +56,10 @@ func TestEthBlockHashesCorrect_MultiBlockTipset(t *testing.T) { hex := fmt.Sprintf("0x%x", i) ethBlockA, err := n2.EthGetBlockByNumber(ctx, hex, true) + // Cannot use static ErrFullRound error for comparison since it gets reserialized as a JSON RPC error. + if err != nil && strings.Contains(err.Error(), "null round") { + continue + } require.NoError(t, err) ethBlockB, err := n2.EthGetBlockByHash(ctx, ethBlockA.Hash, true) From fce0813821d0fc1329570f1a49eb1f6a26430ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Kripalani?= Date: Thu, 9 Mar 2023 18:51:38 +0000 Subject: [PATCH 56/70] switch to ChainAPI#ChainGetTipSetByHeight. --- node/impl/full/eth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index ace52f489..c7de18900 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -257,7 +257,7 @@ func (a *EthModule) parseBlkParam(ctx context.Context, blkParam string, strict b if abi.ChainEpoch(num) > head.Height()-1 { return nil, fmt.Errorf("requested a future epoch (beyond 'latest')") } - ts, err := a.Chain.GetTipsetByHeight(ctx, abi.ChainEpoch(num), nil, true) + ts, err := a.ChainAPI.ChainGetTipSetByHeight(ctx, abi.ChainEpoch(num), head.Key()) if err != nil { return nil, fmt.Errorf("cannot get tipset at height: %v", num) } From b4e589a0f76f0ad7dcac62bf4ec232c1f9eb9a48 Mon Sep 17 00:00:00 2001 From: Travis Person Date: Tue, 7 Mar 2023 19:35:40 +0000 Subject: [PATCH 57/70] review comments --- chain/stmgr/forks.go | 6 +++--- chain/stmgr/stmgr.go | 41 +++++++++++++++++++---------------------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/chain/stmgr/forks.go b/chain/stmgr/forks.go index 987c3ba5c..82b3acce4 100644 --- a/chain/stmgr/forks.go +++ b/chain/stmgr/forks.go @@ -176,9 +176,9 @@ func (sm *StateManager) HandleStateForks(ctx context.Context, root cid.Cid, heig retCid := root u := sm.stateMigrations[height] if u != nil && u.upgrade != nil { - migCid, ok, err := u.resultCache.Result(ctx, root) + migCid, ok, err := u.migrationResultCache.Get(ctx, root) if err == nil && ok { - log.Warnw("CACHED migration", "height", height, "from", root, "to", migCid) + log.Infow("CACHED migration", "height", height, "from", root, "to", migCid) return migCid, nil } else if err != nil { log.Errorw("failed to lookup previous migration result", "err", err) @@ -206,7 +206,7 @@ func (sm *StateManager) HandleStateForks(ctx context.Context, root cid.Cid, heig ) // Only set if migration ran, we do not want a root => root mapping - if err := u.resultCache.Store(ctx, root, retCid); err != nil { + if err := u.migrationResultCache.Store(ctx, root, retCid); err != nil { log.Errorw("failed to store migration result", "err", err) } } diff --git a/chain/stmgr/stmgr.go b/chain/stmgr/stmgr.go index d0e4cbb19..b9f8d81bf 100644 --- a/chain/stmgr/stmgr.go +++ b/chain/stmgr/stmgr.go @@ -8,6 +8,7 @@ import ( "github.com/ipfs/go-cid" dstore "github.com/ipfs/go-datastore" cbor "github.com/ipfs/go-ipld-cbor" + ipld "github.com/ipfs/go-ipld-format" logging "github.com/ipfs/go-log/v2" "golang.org/x/xerrors" @@ -53,32 +54,29 @@ type versionSpec struct { } type migration struct { - upgrade MigrationFunc - preMigrations []PreMigration - cache *nv16.MemMigrationCache - resultCache *resultCache + upgrade MigrationFunc + preMigrations []PreMigration + cache *nv16.MemMigrationCache + migrationResultCache *migrationResultCache } -type resultCache struct { +type migrationResultCache struct { ds dstore.Batching keyPrefix string } -func (m *resultCache) Result(ctx context.Context, root cid.Cid) (cid.Cid, bool, error) { - kStr := fmt.Sprintf("%s-%s", m.keyPrefix, root) - k := dstore.NewKey(kStr) +func (m *migrationResultCache) keyForMigration(root cid.Cid) dstore.Key { + kStr := fmt.Sprintf("%s/%s", m.keyPrefix, root) + return dstore.NewKey(kStr) +} - found, err := m.ds.Has(ctx, k) - if err != nil { - return cid.Undef, false, xerrors.Errorf("error looking up migration result: %w", err) - } - - if !found { - return cid.Undef, false, nil - } +func (m *migrationResultCache) Get(ctx context.Context, root cid.Cid) (cid.Cid, bool, error) { + k := m.keyForMigration(root) bs, err := m.ds.Get(ctx, k) - if err != nil { + if ipld.IsNotFound(err) { + return cid.Undef, false, nil + } else if err != nil { return cid.Undef, false, xerrors.Errorf("error loading migration result: %w", err) } @@ -90,9 +88,8 @@ func (m *resultCache) Result(ctx context.Context, root cid.Cid) (cid.Cid, bool, return c, true, nil } -func (m *resultCache) Store(ctx context.Context, root cid.Cid, resultCid cid.Cid) error { - kStr := fmt.Sprintf("%s-%s", m.keyPrefix, root) - k := dstore.NewKey(kStr) +func (m *migrationResultCache) Store(ctx context.Context, root cid.Cid, resultCid cid.Cid) error { + k := m.keyForMigration(root) if err := m.ds.Put(ctx, k, resultCid.Bytes()); err != nil { return err } @@ -166,8 +163,8 @@ func NewStateManager(cs *store.ChainStore, exec Executor, sys vm.SyscallBuilder, upgrade: upgrade.Migration, preMigrations: upgrade.PreMigrations, cache: nv16.NewMemMigrationCache(), - resultCache: &resultCache{ - keyPrefix: fmt.Sprintf("nv%d-%d", upgrade.Network, upgrade.Height), + migrationResultCache: &migrationResultCache{ + keyPrefix: fmt.Sprintf("/migration-cache/nv%d", upgrade.Network), ds: metadataDs, }, } From 3d21d71231c290a275908afc5a2dec34e149f997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Kripalani?= Date: Thu, 9 Mar 2023 19:05:31 +0000 Subject: [PATCH 58/70] simplify test. --- itests/eth_transactions_test.go | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/itests/eth_transactions_test.go b/itests/eth_transactions_test.go index 9d6c742dc..8d0df0433 100644 --- a/itests/eth_transactions_test.go +++ b/itests/eth_transactions_test.go @@ -280,29 +280,10 @@ func TestGetBlockByNumber(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - // install contract - contractHex, err := os.ReadFile("./contracts/SimpleCoin.hex") - require.NoError(t, err) - - contract, err := hex.DecodeString(string(contractHex)) - require.NoError(t, err) - // create a new Ethereum account - key, ethAddr, deployer := client.EVM().NewAccount() + _, ethAddr, filAddr := client.EVM().NewAccount() // send some funds to the f410 address - kit.SendFunds(ctx, t, client, deployer, types.FromFil(10)) - - // DEPLOY CONTRACT - tx, err := deployContractTx(ctx, client, ethAddr, contract) - require.NoError(t, err) - - client.EVM().SignTransaction(tx, key.PrivateKey) - hash := client.EVM().SubmitTransaction(ctx, tx) - - receipt, err := waitForEthTxReceipt(ctx, client, hash) - require.NoError(t, err) - require.NotNil(t, receipt) - require.EqualValues(t, ethtypes.EthUint64(0x1), receipt.Status) + kit.SendFunds(ctx, t, client, filAddr, types.FromFil(10)) latest, err := client.EthBlockNumber(ctx) require.NoError(t, err) @@ -338,7 +319,7 @@ func TestGetBlockByNumber(t *testing.T) { bal, err := client.EthGetBalance(ctx, ethAddr, (ethtypes.EthUint64(afterNullHeight - 1)).Hex()) require.NoError(t, err) require.NotEqual(t, big.Zero(), bal) - require.Equal(t, -1, bal.Cmp(types.FromFil(10).Int)) + require.Equal(t, types.FromFil(10).Int, bal.Int) } func deployContractTx(ctx context.Context, client *kit.TestFullNode, ethAddr ethtypes.EthAddress, contract []byte) (*ethtypes.EthTxArgs, error) { From 90c89288b62c3343e4885ac1dfb8498fbd863fea Mon Sep 17 00:00:00 2001 From: Aayush Date: Wed, 8 Mar 2023 11:36:17 -0500 Subject: [PATCH 59/70] feat: chain: make fetching tipset by height 1000x faster --- chain/store/index.go | 56 +++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/chain/store/index.go b/chain/store/index.go index fe8f399ee..620cb2dee 100644 --- a/chain/store/index.go +++ b/chain/store/index.go @@ -4,8 +4,8 @@ import ( "context" "os" "strconv" + "sync" - lru "github.com/hashicorp/golang-lru" "golang.org/x/xerrors" "github.com/filecoin-project/go-state-types/abi" @@ -13,7 +13,7 @@ import ( "github.com/filecoin-project/lotus/chain/types" ) -var DefaultChainIndexCacheSize = 32 << 10 +var DefaultChainIndexCacheSize = 32 << 15 func init() { if s := os.Getenv("LOTUS_CHAIN_INDEX_CACHE"); s != "" { @@ -27,7 +27,8 @@ func init() { } type ChainIndex struct { - skipCache *lru.ARCCache + indexCacheLk sync.Mutex + indexCache map[types.TipSetKey]*lbEntry loadTipSet loadTipSetFunc @@ -36,17 +37,14 @@ type ChainIndex struct { type loadTipSetFunc func(context.Context, types.TipSetKey) (*types.TipSet, error) func NewChainIndex(lts loadTipSetFunc) *ChainIndex { - sc, _ := lru.NewARC(DefaultChainIndexCacheSize) return &ChainIndex{ - skipCache: sc, + indexCache: make(map[types.TipSetKey]*lbEntry, DefaultChainIndexCacheSize), loadTipSet: lts, skipLength: 20, } } type lbEntry struct { - ts *types.TipSet - parentHeight abi.ChainEpoch targetHeight abi.ChainEpoch target types.TipSetKey } @@ -58,25 +56,36 @@ func (ci *ChainIndex) GetTipsetByHeight(ctx context.Context, from *types.TipSet, rounded, err := ci.roundDown(ctx, from) if err != nil { - return nil, err + return nil, xerrors.Errorf("failed to round down: %w", err) } + ci.indexCacheLk.Lock() + defer ci.indexCacheLk.Unlock() cur := rounded.Key() for { - cval, ok := ci.skipCache.Get(cur) + lbe, ok := ci.indexCache[cur] if !ok { fc, err := ci.fillCache(ctx, cur) if err != nil { - return nil, err + return nil, xerrors.Errorf("failed to fill cache: %w", err) } - cval = fc + lbe = fc } - lbe := cval.(*lbEntry) - if lbe.ts.Height() == to || lbe.parentHeight < to { - return lbe.ts, nil - } else if to > lbe.targetHeight { - return ci.walkBack(ctx, lbe.ts, to) + if to == lbe.targetHeight { + ts, err := ci.loadTipSet(ctx, lbe.target) + if err != nil { + return nil, xerrors.Errorf("failed to load tipset: %w", err) + } + + return ts, nil + } + if to > lbe.targetHeight { + ts, err := ci.loadTipSet(ctx, cur) + if err != nil { + return nil, xerrors.Errorf("failed to load tipset: %w", err) + } + return ci.walkBack(ctx, ts, to) } cur = lbe.target @@ -87,16 +96,17 @@ func (ci *ChainIndex) GetTipsetByHeightWithoutCache(ctx context.Context, from *t return ci.walkBack(ctx, from, to) } +// Caller must hold indexCacheLk func (ci *ChainIndex) fillCache(ctx context.Context, tsk types.TipSetKey) (*lbEntry, error) { ts, err := ci.loadTipSet(ctx, tsk) if err != nil { - return nil, err + return nil, xerrors.Errorf("failed to load tipset: %w", err) } if ts.Height() == 0 { return &lbEntry{ - ts: ts, - parentHeight: 0, + targetHeight: 0, + target: tsk, }, nil } @@ -124,12 +134,10 @@ func (ci *ChainIndex) fillCache(ctx context.Context, tsk types.TipSetKey) (*lbEn } lbe := &lbEntry{ - ts: ts, - parentHeight: parent.Height(), targetHeight: skipTarget.Height(), target: skipTarget.Key(), } - ci.skipCache.Add(tsk, lbe) + ci.indexCache[tsk] = lbe return lbe, nil } @@ -144,7 +152,7 @@ func (ci *ChainIndex) roundDown(ctx context.Context, ts *types.TipSet) (*types.T rounded, err := ci.walkBack(ctx, ts, target) if err != nil { - return nil, err + return nil, xerrors.Errorf("failed to walk back: %w", err) } return rounded, nil @@ -164,7 +172,7 @@ func (ci *ChainIndex) walkBack(ctx context.Context, from *types.TipSet, to abi.C for { pts, err := ci.loadTipSet(ctx, ts.Parents()) if err != nil { - return nil, err + return nil, xerrors.Errorf("failed to load tipset: %w", err) } if to > pts.Height() { From 58900a70333a11a903cf9fe3f29e6a5c309cb000 Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Thu, 9 Mar 2023 13:17:17 -0800 Subject: [PATCH 60/70] feat: mempool: Reduce minimum replace fee from 1.25x to 1.1x (#10416) However, we're leaving the default at 1.25x for backwards compatibility, for now. Also: 1. Actually use the configured replace fee ratio. 2. Store said ratios as percentages instead of floats. 1.25, or 1+1/(2^2), can be represented as a float. 1.1, or 1 + 1/(2 * 5), cannot. fixes #10415 --- api/docgen/docgen.go | 4 ++ build/openrpc/full.json.gz | Bin 33618 -> 33620 bytes chain/messagepool/config.go | 14 ++++--- chain/messagepool/messagepool.go | 14 ++++--- chain/types/mpool.go | 2 +- chain/types/percent.go | 39 ++++++++++++++++++++ chain/types/percent_test.go | 34 +++++++++++++++++ cli/mpool.go | 9 ++++- cli/mpool_test.go | 7 +++- documentation/en/api-v0-methods.md | 4 +- documentation/en/api-v1-unstable-methods.md | 4 +- 11 files changed, 112 insertions(+), 19 deletions(-) create mode 100644 chain/types/percent.go create mode 100644 chain/types/percent_test.go diff --git a/api/docgen/docgen.go b/api/docgen/docgen.go index 3793f2fc2..10e9c2fce 100644 --- a/api/docgen/docgen.go +++ b/api/docgen/docgen.go @@ -401,6 +401,10 @@ func init() { FromBlock: pstring("2301220"), Address: []ethtypes.EthAddress{ethaddr}, }) + + percent := types.Percent(123) + addExample(percent) + addExample(&percent) } func GetAPIType(name, pkg string) (i interface{}, t reflect.Type, permStruct []reflect.Type) { diff --git a/build/openrpc/full.json.gz b/build/openrpc/full.json.gz index 0aaef1effe5c0418ff34300bf0984baa65ab8ead..64a555389d85bd25842fb733c6fccda66c10332f 100644 GIT binary patch delta 11263 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 delta 11261 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 1.00). +type Percent int64 + +func (p Percent) String() string { + abs := p + sign := "" + if abs < 0 { + abs = -abs + sign = "-" + } + return fmt.Sprintf(`%s%d.%d`, sign, abs/100, abs%100) +} + +func (p Percent) MarshalJSON() ([]byte, error) { + return []byte(p.String()), nil +} + +func (p *Percent) UnmarshalJSON(b []byte) error { + flt, err := strconv.ParseFloat(string(b)+"e2", 64) + if err != nil { + return xerrors.Errorf("unable to parse ratio %s: %w", string(b), err) + } + if math.Trunc(flt) != flt { + return xerrors.Errorf("ratio may only have two decimals: %s", string(b)) + } + *p = Percent(flt) + return nil +} diff --git a/chain/types/percent_test.go b/chain/types/percent_test.go new file mode 100644 index 000000000..7364c2447 --- /dev/null +++ b/chain/types/percent_test.go @@ -0,0 +1,34 @@ +package types + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPercent(t *testing.T) { + for _, tc := range []struct { + p Percent + s string + }{ + {100, "1.0"}, + {111, "1.11"}, + {12, "0.12"}, + {-12, "-0.12"}, + {1012, "10.12"}, + {-1012, "-10.12"}, + {0, "0.0"}, + } { + tc := tc + t.Run(fmt.Sprintf("%d <> %s", tc.p, tc.s), func(t *testing.T) { + m, err := tc.p.MarshalJSON() + require.NoError(t, err) + require.Equal(t, tc.s, string(m)) + var p Percent + require.NoError(t, p.UnmarshalJSON([]byte(tc.s))) + require.Equal(t, tc.p, p) + }) + } + +} diff --git a/cli/mpool.go b/cli/mpool.go index e098806cb..c83fb4b61 100644 --- a/cli/mpool.go +++ b/cli/mpool.go @@ -461,7 +461,12 @@ var MpoolReplaceCmd = &cli.Command{ msg := found.Message if cctx.Bool("auto") { - minRBF := messagepool.ComputeMinRBF(msg.GasPremium) + cfg, err := api.MpoolGetConfig(ctx) + if err != nil { + return xerrors.Errorf("failed to lookup the message pool config: %w", err) + } + + defaultRBF := messagepool.ComputeRBF(msg.GasPremium, cfg.ReplaceByFeeRatio) var mss *lapi.MessageSendSpec if cctx.IsSet("fee-limit") { @@ -482,7 +487,7 @@ var MpoolReplaceCmd = &cli.Command{ return xerrors.Errorf("failed to estimate gas values: %w", err) } - msg.GasPremium = big.Max(retm.GasPremium, minRBF) + msg.GasPremium = big.Max(retm.GasPremium, defaultRBF) msg.GasFeeCap = big.Max(retm.GasFeeCap, msg.GasPremium) mff := func() (abi.TokenAmount, error) { diff --git a/cli/mpool_test.go b/cli/mpool_test.go index 01b49d4b3..0aa055ba3 100644 --- a/cli/mpool_test.go +++ b/cli/mpool_test.go @@ -15,6 +15,7 @@ import ( "github.com/filecoin-project/go-state-types/big" "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/chain/messagepool" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/types/mock" "github.com/filecoin-project/lotus/chain/wallet" @@ -298,6 +299,7 @@ func TestReplace(t *testing.T) { mockApi.EXPECT().ChainGetMessage(ctx, sm.Cid()).Return(&sm.Message, nil), mockApi.EXPECT().ChainHead(ctx).Return(nil, nil), mockApi.EXPECT().MpoolPending(ctx, types.EmptyTSK).Return([]*types.SignedMessage{sm}, nil), + mockApi.EXPECT().MpoolGetConfig(ctx).Return(messagepool.DefaultConfig(), nil), // use gomock.any to match the message in expected api calls // since the replace function modifies the message between calls, it would be pointless to try to match the exact argument mockApi.EXPECT().GasEstimateMessageGas(ctx, gomock.Any(), &mss, types.EmptyTSK).Return(&sm.Message, nil), @@ -342,6 +344,7 @@ func TestReplace(t *testing.T) { gomock.InOrder( mockApi.EXPECT().ChainHead(ctx).Return(nil, nil), mockApi.EXPECT().MpoolPending(ctx, types.EmptyTSK).Return([]*types.SignedMessage{sm}, nil), + mockApi.EXPECT().MpoolGetConfig(ctx).Return(messagepool.DefaultConfig(), nil), // use gomock.any to match the message in expected api calls // since the replace function modifies the message between calls, it would be pointless to try to match the exact argument mockApi.EXPECT().GasEstimateMessageGas(ctx, gomock.Any(), &mss, types.EmptyTSK).Return(&sm.Message, nil), @@ -538,7 +541,7 @@ func TestConfig(t *testing.T) { t.Fatal(err) } - mpoolCfg := &types.MpoolConfig{PriorityAddrs: []address.Address{senderAddr}, SizeLimitHigh: 1234567, SizeLimitLow: 6, ReplaceByFeeRatio: 0.25} + mpoolCfg := &types.MpoolConfig{PriorityAddrs: []address.Address{senderAddr}, SizeLimitHigh: 1234567, SizeLimitLow: 6, ReplaceByFeeRatio: types.Percent(25)} gomock.InOrder( mockApi.EXPECT().MpoolGetConfig(ctx).Return(mpoolCfg, nil), ) @@ -566,7 +569,7 @@ func TestConfig(t *testing.T) { t.Fatal(err) } - mpoolCfg := &types.MpoolConfig{PriorityAddrs: []address.Address{senderAddr}, SizeLimitHigh: 234567, SizeLimitLow: 3, ReplaceByFeeRatio: 0.33} + mpoolCfg := &types.MpoolConfig{PriorityAddrs: []address.Address{senderAddr}, SizeLimitHigh: 234567, SizeLimitLow: 3, ReplaceByFeeRatio: types.Percent(33)} gomock.InOrder( mockApi.EXPECT().MpoolSetConfig(ctx, mpoolCfg).Return(nil), ) diff --git a/documentation/en/api-v0-methods.md b/documentation/en/api-v0-methods.md index d254c14e7..b73244fa8 100644 --- a/documentation/en/api-v0-methods.md +++ b/documentation/en/api-v0-methods.md @@ -2879,7 +2879,7 @@ Response: ], "SizeLimitHigh": 123, "SizeLimitLow": 123, - "ReplaceByFeeRatio": 12.3, + "ReplaceByFeeRatio": 1.23, "PruneCooldown": 60000000000, "GasLimitOverestimation": 12.3 } @@ -3167,7 +3167,7 @@ Inputs: ], "SizeLimitHigh": 123, "SizeLimitLow": 123, - "ReplaceByFeeRatio": 12.3, + "ReplaceByFeeRatio": 1.23, "PruneCooldown": 60000000000, "GasLimitOverestimation": 12.3 } diff --git a/documentation/en/api-v1-unstable-methods.md b/documentation/en/api-v1-unstable-methods.md index 5516acb44..aedfbfce0 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -3882,7 +3882,7 @@ Response: ], "SizeLimitHigh": 123, "SizeLimitLow": 123, - "ReplaceByFeeRatio": 12.3, + "ReplaceByFeeRatio": 1.23, "PruneCooldown": 60000000000, "GasLimitOverestimation": 12.3 } @@ -4170,7 +4170,7 @@ Inputs: ], "SizeLimitHigh": 123, "SizeLimitLow": 123, - "ReplaceByFeeRatio": 12.3, + "ReplaceByFeeRatio": 1.23, "PruneCooldown": 60000000000, "GasLimitOverestimation": 12.3 } From 7a2eb86dd5724a7fd8835824415529442faba854 Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Fri, 10 Mar 2023 10:46:30 -0800 Subject: [PATCH 61/70] feat: cli: Add an EVM command to fetch a contract's bytecode --- cli/evm.go | 49 +++++++++++++++++++++++++++++++++++ documentation/en/cli-lotus.md | 14 ++++++++++ 2 files changed, 63 insertions(+) diff --git a/cli/evm.go b/cli/evm.go index d153e7212..84cbf8c61 100644 --- a/cli/evm.go +++ b/cli/evm.go @@ -35,6 +35,7 @@ var EvmCmd = &cli.Command{ EvmGetInfoCmd, EvmCallSimulateCmd, EvmGetContractAddress, + EvmGetBytecode, }, } @@ -486,3 +487,51 @@ func ethAddrFromFilecoinAddress(ctx context.Context, addr address.Address, fnapi return ethAddr, faddr, nil } + +var EvmGetBytecode = &cli.Command{ + Name: "bytecode", + Usage: "Write the bytecode of a smart contract to a file", + ArgsUsage: "[contract-address] [file-name]", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "bin", + Usage: "write the bytecode as raw binary and don't hex-encode", + }, + }, + Action: func(cctx *cli.Context) error { + + if cctx.NArg() != 2 { + return IncorrectNumArgs(cctx) + } + + contractAddr, err := ethtypes.ParseEthAddress(cctx.Args().Get(0)) + if err != nil { + return err + } + + fileName := cctx.Args().Get(1) + + api, closer, err := GetFullNodeAPIV1(cctx) + if err != nil { + return err + } + defer closer() + ctx := ReqContext(cctx) + + code, err := api.EthGetCode(ctx, contractAddr, "latest") + if err != nil { + return err + } + if !cctx.Bool("bin") { + newCode := make([]byte, hex.EncodedLen(len(code))) + hex.Encode(newCode, code) + code = newCode + } + if err := os.WriteFile(fileName, code, 0o666); err != nil { + return xerrors.Errorf("failed to write bytecode to file %s: %w", fileName, err) + } + + fmt.Printf("Code for %s written to %s\n", contractAddr, fileName) + return nil + }, +} diff --git a/documentation/en/cli-lotus.md b/documentation/en/cli-lotus.md index 4458599ab..325b2738d 100644 --- a/documentation/en/cli-lotus.md +++ b/documentation/en/cli-lotus.md @@ -2647,6 +2647,7 @@ COMMANDS: stat Print eth/filecoin addrs and code cid call Simulate an eth contract call contract-address Generate contract address from smart contract code + bytecode Write the bytecode of a smart contract to a file help, h Shows a list of commands or help for one command OPTIONS: @@ -2721,6 +2722,19 @@ OPTIONS: ``` +### lotus evm bytecode +``` +NAME: + lotus evm bytecode - Write the bytecode of a smart contract to a file + +USAGE: + lotus evm bytecode [command options] [contract-address] [file-name] + +OPTIONS: + --bin write the bytecode as raw binary and don't hex-encode (default: false) + +``` + ## lotus net ``` NAME: From f7603f6c134df680b1aa6aeace0f696847f6ee21 Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Thu, 9 Mar 2023 18:35:04 -0800 Subject: [PATCH 62/70] feat: eth API: reject masked ID addresses embedded in f410f payloads We'll never get an actor/account deployed to one of these addresses (although we might get a placeholder). However, converting such an address to an f4 address is definitely wrong. --- chain/types/ethtypes/eth_types.go | 23 +++++++++++++++++------ chain/types/ethtypes/eth_types_test.go | 15 +++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/chain/types/ethtypes/eth_types.go b/chain/types/ethtypes/eth_types.go index ddda91ea0..f157c7f94 100644 --- a/chain/types/ethtypes/eth_types.go +++ b/chain/types/ethtypes/eth_types.go @@ -295,17 +295,21 @@ func EthAddressFromPubKey(pubk []byte) ([]byte, error) { return ethAddr, nil } +var maskedIDPrefix = [20 - 8]byte{0xff} + func IsEthAddress(addr address.Address) bool { if addr.Protocol() != address.Delegated { return false } payload := addr.Payload() - namespace, _, err := varint.FromUvarint(payload) + namespace, offset, err := varint.FromUvarint(payload) if err != nil { return false } - return namespace == builtintypes.EthereumAddressManagerActorID + payload = payload[offset:] + + return namespace == builtintypes.EthereumAddressManagerActorID && len(payload) == 20 && !bytes.HasPrefix(payload, maskedIDPrefix[:]) } func EthAddressFromFilecoinAddress(addr address.Address) (EthAddress, error) { @@ -326,9 +330,17 @@ func EthAddressFromFilecoinAddress(addr address.Address) (EthAddress, error) { return EthAddress{}, xerrors.Errorf("invalid delegated address namespace in: %s", addr) } payload = payload[n:] - if namespace == builtintypes.EthereumAddressManagerActorID { - return CastEthAddress(payload) + if namespace != builtintypes.EthereumAddressManagerActorID { + return EthAddress{}, ErrInvalidAddress } + ethAddr, err := CastEthAddress(payload) + if err != nil { + return EthAddress{}, err + } + if ethAddr.IsMaskedID() { + return EthAddress{}, xerrors.Errorf("f410f addresses cannot embed masked-ID payloads: %s", ethAddr) + } + return ethAddr, nil } return EthAddress{}, ErrInvalidAddress } @@ -376,8 +388,7 @@ func (ea *EthAddress) UnmarshalJSON(b []byte) error { } func (ea EthAddress) IsMaskedID() bool { - idmask := [12]byte{0xff} - return bytes.Equal(ea[:12], idmask[:]) + return bytes.HasPrefix(ea[:], maskedIDPrefix[:]) } func (ea EthAddress) ToFilecoinAddress() (address.Address, error) { diff --git a/chain/types/ethtypes/eth_types_test.go b/chain/types/ethtypes/eth_types_test.go index 118fbc901..4a73184c2 100644 --- a/chain/types/ethtypes/eth_types_test.go +++ b/chain/types/ethtypes/eth_types_test.go @@ -9,6 +9,7 @@ import ( "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-state-types/big" + "github.com/filecoin-project/go-state-types/builtin" ) type TestCase struct { @@ -178,6 +179,20 @@ func TestParseEthAddr(t *testing.T) { } } +func TestMaskedIDInF4(t *testing.T) { + addr, err := address.NewIDAddress(100) + require.NoError(t, err) + + eaddr, err := EthAddressFromFilecoinAddress(addr) + require.NoError(t, err) + + badaddr, err := address.NewDelegatedAddress(builtin.EthereumAddressManagerActorID, eaddr[:]) + require.NoError(t, err) + + _, err = EthAddressFromFilecoinAddress(badaddr) + require.Error(t, err) +} + func TestUnmarshalEthCall(t *testing.T) { data := `{"from":"0x4D6D86b31a112a05A473c4aE84afaF873f632325","to":"0xFe01CC39f5Ae8553D6914DBb9dC27D219fa22D7f","gas":"0x5","gasPrice":"0x6","value":"0x123","data":""}` From 92bca485b62df90396f359da001403e16c2c584f Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Fri, 10 Mar 2023 12:59:23 -0800 Subject: [PATCH 63/70] feat: stmgr: skip tipset execution when possible --- chain/stmgr/execute.go | 52 ++++++++++++++++++++++++++++++++++++++++++ chain/types/tipset.go | 4 ++++ 2 files changed, 56 insertions(+) diff --git a/chain/stmgr/execute.go b/chain/stmgr/execute.go index e67398299..57ff205b9 100644 --- a/chain/stmgr/execute.go +++ b/chain/stmgr/execute.go @@ -8,6 +8,7 @@ import ( "go.opencensus.io/trace" "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/chain/store" "github.com/filecoin-project/lotus/chain/types" ) @@ -52,6 +53,12 @@ func (sm *StateManager) TipSetState(ctx context.Context, ts *types.TipSet) (st c sm.stlk.Unlock() + // First, try to find the tipset in the current chain. If found, we can avoid re-executing + // it. + if st, rec, found := tryLookupTipsetState(ctx, sm.cs, ts); found { + return st, rec, nil + } + st, rec, err = sm.tsExec.ExecuteTipSet(ctx, sm, ts, sm.tsExecMonitor, false) if err != nil { return cid.Undef, cid.Undef, err @@ -60,6 +67,51 @@ func (sm *StateManager) TipSetState(ctx context.Context, ts *types.TipSet) (st c return st, rec, nil } +// Try to lookup a state & receipt CID for a given tipset by walking the chain instead of executing +// it. This will only successfully return the state/receipt CIDs if they're found in the state +// store. +// +// NOTE: This _won't_ recursively walk the receipt/state trees. It assumes that having the root +// implies having the rest of the tree. However, lotus generally makes that assumption anyways. +func tryLookupTipsetState(ctx context.Context, cs *store.ChainStore, ts *types.TipSet) (cid.Cid, cid.Cid, bool) { + nextTs, err := cs.GetTipsetByHeight(ctx, ts.Height()+1, nil, false) + if err != nil { + // Nothing to see here. The requested height may be beyond the current head. + return cid.Undef, cid.Undef, false + } + + // Make sure we're on the correct fork. + if nextTs.Parents() != ts.Key() { + // Also nothing to see here. This just means that the requested tipset is on a + // different fork. + return cid.Undef, cid.Undef, false + } + + stateCid := nextTs.ParentState() + receiptCid := nextTs.ParentMessageReceipts() + + // Make sure we have the parent state. + if hasState, err := cs.StateBlockstore().Has(ctx, stateCid); err != nil { + log.Errorw("failed to lookup state-root in blockstore", "cid", stateCid, "error", err) + return cid.Undef, cid.Undef, false + } else if !hasState { + // We have the chain but don't have the state. It looks like we need to try + // executing? + return cid.Undef, cid.Undef, false + } + + // Make sure we have the receipts. + if hasReceipts, err := cs.ChainBlockstore().Has(ctx, receiptCid); err != nil { + log.Errorw("failed to lookup receipts in blockstore", "cid", receiptCid, "error", err) + return cid.Undef, cid.Undef, false + } else if !hasReceipts { + // If we don't have the receipts, re-execute and try again. + return cid.Undef, cid.Undef, false + } + + return stateCid, receiptCid, true +} + func (sm *StateManager) ExecutionTraceWithMonitor(ctx context.Context, ts *types.TipSet, em ExecMonitor) (cid.Cid, error) { st, _, err := sm.tsExec.ExecuteTipSet(ctx, sm, ts, em, true) return st, err diff --git a/chain/types/tipset.go b/chain/types/tipset.go index c1aa90fc9..047a1c00e 100644 --- a/chain/types/tipset.go +++ b/chain/types/tipset.go @@ -234,6 +234,10 @@ func (ts *TipSet) MinTicketBlock() *BlockHeader { return min } +func (ts *TipSet) ParentMessageReceipts() cid.Cid { + return ts.blks[0].ParentMessageReceipts +} + func (ts *TipSet) ParentState() cid.Cid { return ts.blks[0].ParentStateRoot } From 1cf57ffe2d083432a52e4572346243d121c3a06f Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Fri, 10 Mar 2023 15:33:45 -0800 Subject: [PATCH 64/70] feat: eth: optimize receipt reading This optimizes the eth APIs (except the fee history one) to lookup the tipset state/receipts instead of computing the state. --- chain/store/messages.go | 20 ++++++++++++++++++ node/impl/full/eth.go | 46 ++++++++++++++++++++++++++--------------- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/chain/store/messages.go b/chain/store/messages.go index 5ac62d394..33d0c579a 100644 --- a/chain/store/messages.go +++ b/chain/store/messages.go @@ -237,6 +237,26 @@ func (cs *ChainStore) ReadMsgMetaCids(ctx context.Context, mmc cid.Cid) ([]cid.C return blscids, secpkcids, nil } +func (cs *ChainStore) ReadReceipts(ctx context.Context, root cid.Cid) ([]types.MessageReceipt, error) { + a, err := blockadt.AsArray(cs.ActorStore(ctx), root) + if err != nil { + return nil, err + } + + receipts := make([]types.MessageReceipt, 0, a.Length()) + var rcpt types.MessageReceipt + if err := a.ForEach(&rcpt, func(i int64) error { + if int64(len(receipts)) != i { + return xerrors.Errorf("missing receipt %d", i) + } + receipts = append(receipts, rcpt) + return nil + }); err != nil { + return nil, err + } + return receipts, nil +} + func (cs *ChainStore) MessagesForBlock(ctx context.Context, b *types.BlockHeader) ([]*types.Message, []*types.SignedMessage, error) { blscids, secpkcids, err := cs.ReadMsgMetaCids(ctx, b.Messages) if err != nil { diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index d8271fcdb..d08b17e41 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -1797,30 +1797,42 @@ func newEthBlockFromFilecoinTipSet(ctx context.Context, ts *types.TipSet, fullTx return ethtypes.EthBlock{}, xerrors.Errorf("error loading messages for tipset: %v: %w", ts, err) } - block := ethtypes.NewEthBlock(len(msgs) > 0) - - gasUsed := int64(0) - compOutput, err := sa.StateCompute(ctx, ts.Height(), nil, ts.Key()) + _, rcptRoot, err := sa.StateManager.TipSetState(ctx, ts) if err != nil { return ethtypes.EthBlock{}, xerrors.Errorf("failed to compute state: %w", err) } - txIdx := 0 - for _, msg := range compOutput.Trace { - // skip system messages like reward application and cron - if msg.Msg.From == builtintypes.SystemActorAddr { - continue - } + rcpts, err := cs.ReadReceipts(ctx, rcptRoot) + if err != nil { + return ethtypes.EthBlock{}, xerrors.Errorf("error loading receipts for tipset: %v: %w", ts, err) + } - ti := ethtypes.EthUint64(txIdx) - txIdx++ + if len(msgs) != len(rcpts) { + return ethtypes.EthBlock{}, xerrors.Errorf("receipts and message array lengths didn't match for tipset: %v: %w", ts, err) + } - gasUsed += msg.MsgRct.GasUsed - smsgCid, err := getSignedMessage(ctx, cs, msg.MsgCid) - if err != nil { - return ethtypes.EthBlock{}, xerrors.Errorf("failed to get signed msg %s: %w", msg.MsgCid, err) + block := ethtypes.NewEthBlock(len(msgs) > 0) + + gasUsed := int64(0) + for i, msg := range msgs { + rcpt := rcpts[i] + ti := ethtypes.EthUint64(i) + gasUsed += rcpt.GasUsed + var smsg *types.SignedMessage + switch msg := msg.(type) { + case *types.SignedMessage: + smsg = msg + case *types.Message: + smsg = &types.SignedMessage{ + Message: *msg, + Signature: crypto.Signature{ + Type: crypto.SigTypeBLS, + }, + } + default: + return ethtypes.EthBlock{}, xerrors.Errorf("failed to get signed msg %s: %w", msg.Cid(), err) } - tx, err := newEthTxFromSignedMessage(ctx, smsgCid, sa) + tx, err := newEthTxFromSignedMessage(ctx, smsg, sa) if err != nil { return ethtypes.EthBlock{}, xerrors.Errorf("failed to convert msg to ethTx: %w", err) } From eba270d1e2f2bb6282498cd6578c911aef8b569d Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Fri, 10 Mar 2023 16:01:34 -0800 Subject: [PATCH 65/70] feat: api: optimize ChainGetParentReceipts Read the receipts all at once instead of fetching them one-by-one. --- node/impl/full/chain.go | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/node/impl/full/chain.go b/node/impl/full/chain.go index 465b680ef..d0f36ce48 100644 --- a/node/impl/full/chain.go +++ b/node/impl/full/chain.go @@ -193,25 +193,14 @@ func (a *ChainAPI) ChainGetParentReceipts(ctx context.Context, bcid cid.Cid) ([] return nil, nil } - // TODO: need to get the number of messages better than this - pts, err := a.Chain.LoadTipSet(ctx, types.NewTipSetKey(b.Parents...)) + receipts, err := a.Chain.ReadReceipts(ctx, b.ParentMessageReceipts) if err != nil { return nil, err } - cm, err := a.Chain.MessagesForTipset(ctx, pts) - if err != nil { - return nil, err - } - - var out []*types.MessageReceipt - for i := 0; i < len(cm); i++ { - r, err := a.Chain.GetParentReceipt(ctx, b, i) - if err != nil { - return nil, err - } - - out = append(out, r) + out := make([]*types.MessageReceipt, len(receipts)) + for i := range receipts { + out[i] = &receipts[i] } return out, nil From 59bebf8a350ca50d1438514eabe25167407fbf5a Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Fri, 10 Mar 2023 16:29:43 -0800 Subject: [PATCH 66/70] test: eth: reduce chances of chain-reorgs affecting the test Now that this API is _much_ faster, we're more likely to "catch up" to the head faster than it can stabilize. I'm pretty sure the test was intended to be written this way anyways. --- itests/eth_block_hash_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/itests/eth_block_hash_test.go b/itests/eth_block_hash_test.go index 7cc766ebc..db21375c5 100644 --- a/itests/eth_block_hash_test.go +++ b/itests/eth_block_hash_test.go @@ -49,9 +49,6 @@ func TestEthBlockHashesCorrect_MultiBlockTipset(t *testing.T) { // let the chain run a little bit longer to minimise the chance of reorgs n2.WaitTillChain(ctx, kit.HeightAtLeast(head.Height()+50)) - head, err = n2.ChainHead(context.Background()) - require.NoError(t, err) - for i := 1; i <= int(head.Height()); i++ { hex := fmt.Sprintf("0x%x", i) From f7a979d825eccdee3ab849412ccb7c27459a383a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Kripalani?= Date: Sat, 11 Mar 2023 17:24:56 +0000 Subject: [PATCH 67/70] eth_feeHistory: migrate to using TipSetState. --- chain/types/ethtypes/eth_transactions.go | 8 -- chain/types/message.go | 13 +++ node/impl/full/eth.go | 111 ++++++++++++----------- node/impl/full/eth_test.go | 21 ++--- 4 files changed, 79 insertions(+), 74 deletions(-) diff --git a/chain/types/ethtypes/eth_transactions.go b/chain/types/ethtypes/eth_transactions.go index 7afde4bd2..6c13c5bf6 100644 --- a/chain/types/ethtypes/eth_transactions.go +++ b/chain/types/ethtypes/eth_transactions.go @@ -44,14 +44,6 @@ type EthTx struct { S EthBigInt `json:"s"` } -func (tx *EthTx) Reward(blkBaseFee big.Int) EthBigInt { - availablePriorityFee := big.Sub(big.Int(tx.MaxFeePerGas), blkBaseFee) - if big.Cmp(big.Int(tx.MaxPriorityFeePerGas), availablePriorityFee) <= 0 { - return tx.MaxPriorityFeePerGas - } - return EthBigInt(availablePriorityFee) -} - type EthTxArgs struct { ChainID int `json:"chainId"` Nonce int `json:"nonce"` diff --git a/chain/types/message.go b/chain/types/message.go index becd2c010..4304ba659 100644 --- a/chain/types/message.go +++ b/chain/types/message.go @@ -219,4 +219,17 @@ func (m *Message) ValidForBlockInclusion(minGas int64, version network.Version) return nil } +// EffectiveGasPremium returns the effective gas premium claimable by the miner +// given the supplied base fee. +// +// Filecoin clamps the gas premium at GasFeeCap - BaseFee, if lower than the +// specified premium. +func (m *Message) EffectiveGasPremium(baseFee abi.TokenAmount) abi.TokenAmount { + available := big.Sub(m.GasFeeCap, baseFee) + if big.Cmp(m.GasPremium, available) <= 0 { + return m.GasPremium + } + return available +} + const TestGasLimit = 100e6 diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index d08b17e41..12e761fa8 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -689,49 +689,43 @@ func (a *EthModule) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (eth return ethtypes.EthFeeHistory{}, fmt.Errorf("bad block parameter %s: %s", params.NewestBlkNum, err) } - oldestBlkHeight := uint64(1) + var ( + basefee = ts.Blocks()[0].ParentBaseFee + oldestBlkHeight = uint64(1) - // NOTE: baseFeePerGas should include the next block after the newest of the returned range, - // because the next base fee can be inferred from the messages in the newest block. - // However, this is NOT the case in Filecoin due to deferred execution, so the best - // we can do is duplicate the last value. - baseFeeArray := []ethtypes.EthBigInt{ethtypes.EthBigInt(ts.Blocks()[0].ParentBaseFee)} - gasUsedRatioArray := []float64{} - rewardsArray := make([][]ethtypes.EthBigInt, 0) + // NOTE: baseFeePerGas should include the next block after the newest of the returned range, + // because the next base fee can be inferred from the messages in the newest block. + // However, this is NOT the case in Filecoin due to deferred execution, so the best + // we can do is duplicate the last value. + baseFeeArray = []ethtypes.EthBigInt{ethtypes.EthBigInt(basefee)} + rewardsArray = make([][]ethtypes.EthBigInt, 0) + gasUsedRatioArray = []float64{} + blocksIncluded int + ) - blocksIncluded := 0 for blocksIncluded < int(params.BlkCount) && ts.Height() > 0 { - compOutput, err := a.StateCompute(ctx, ts.Height(), nil, ts.Key()) + msgs, rcpts, err := messagesAndReceipts(ctx, ts, a.Chain, a.StateAPI) if err != nil { - return ethtypes.EthFeeHistory{}, xerrors.Errorf("cannot lookup the status of tipset: %v: %w", ts, err) + return ethtypes.EthFeeHistory{}, xerrors.Errorf("failed to retrieve messages and receipts for height %d: %w", ts.Height(), err) } txGasRewards := gasRewardSorter{} - for _, msg := range compOutput.Trace { - if msg.Msg.From == builtintypes.SystemActorAddr { + for i, msg := range msgs { + if msg.VMMessage().From == builtintypes.SystemActorAddr { continue } - smsgCid, err := getSignedMessage(ctx, a.Chain, msg.MsgCid) - if err != nil { - return ethtypes.EthFeeHistory{}, xerrors.Errorf("failed to get signed msg %s: %w", msg.MsgCid, err) - } - - tx, err := newEthTxFromSignedMessage(ctx, smsgCid, a.StateAPI) - if err != nil { - return ethtypes.EthFeeHistory{}, err - } - + effectivePremium := msg.VMMessage().EffectiveGasPremium(basefee) txGasRewards = append(txGasRewards, gasRewardTuple{ - reward: tx.Reward(ts.Blocks()[0].ParentBaseFee), - gas: uint64(msg.MsgRct.GasUsed), + premium: effectivePremium, + gasUsed: rcpts[i].GasUsed, }) } rewards, totalGasUsed := calculateRewardsAndGasUsed(rewardPercentiles, txGasRewards) // arrays should be reversed at the end - baseFeeArray = append(baseFeeArray, ethtypes.EthBigInt(ts.Blocks()[0].ParentBaseFee)) + baseFeeArray = append(baseFeeArray, ethtypes.EthBigInt(basefee)) gasUsedRatioArray = append(gasUsedRatioArray, float64(totalGasUsed)/float64(build.BlockGasLimit)) rewardsArray = append(rewardsArray, rewards) oldestBlkHeight = uint64(ts.Height()) @@ -1792,23 +1786,9 @@ func newEthBlockFromFilecoinTipSet(ctx context.Context, ts *types.TipSet, fullTx return ethtypes.EthBlock{}, err } - msgs, err := cs.MessagesForTipset(ctx, ts) + msgs, rcpts, err := messagesAndReceipts(ctx, ts, cs, sa) if err != nil { - return ethtypes.EthBlock{}, xerrors.Errorf("error loading messages for tipset: %v: %w", ts, err) - } - - _, rcptRoot, err := sa.StateManager.TipSetState(ctx, ts) - if err != nil { - return ethtypes.EthBlock{}, xerrors.Errorf("failed to compute state: %w", err) - } - - rcpts, err := cs.ReadReceipts(ctx, rcptRoot) - if err != nil { - return ethtypes.EthBlock{}, xerrors.Errorf("error loading receipts for tipset: %v: %w", ts, err) - } - - if len(msgs) != len(rcpts) { - return ethtypes.EthBlock{}, xerrors.Errorf("receipts and message array lengths didn't match for tipset: %v: %w", ts, err) + return ethtypes.EthBlock{}, xerrors.Errorf("failed to retrieve messages and receipts: %w", err) } block := ethtypes.NewEthBlock(len(msgs) > 0) @@ -1858,6 +1838,29 @@ func newEthBlockFromFilecoinTipSet(ctx context.Context, ts *types.TipSet, fullTx return block, nil } +func messagesAndReceipts(ctx context.Context, ts *types.TipSet, cs *store.ChainStore, sa StateAPI) ([]types.ChainMsg, []types.MessageReceipt, error) { + msgs, err := cs.MessagesForTipset(ctx, ts) + if err != nil { + return nil, nil, xerrors.Errorf("error loading messages for tipset: %v: %w", ts, err) + } + + _, rcptRoot, err := sa.StateManager.TipSetState(ctx, ts) + if err != nil { + return nil, nil, xerrors.Errorf("failed to compute state: %w", err) + } + + rcpts, err := cs.ReadReceipts(ctx, rcptRoot) + if err != nil { + return nil, nil, xerrors.Errorf("error loading receipts for tipset: %v: %w", ts, err) + } + + if len(msgs) != len(rcpts) { + return nil, nil, xerrors.Errorf("receipts and message array lengths didn't match for tipset: %v: %w", ts, err) + } + + return msgs, rcpts, nil +} + // lookupEthAddress makes its best effort at finding the Ethereum address for a // Filecoin address. It does the following: // @@ -2358,10 +2361,10 @@ func parseEthRevert(ret []byte) string { return ethtypes.EthBytes(cbytes).String() } -func calculateRewardsAndGasUsed(rewardPercentiles []float64, txGasRewards gasRewardSorter) ([]ethtypes.EthBigInt, uint64) { - var totalGasUsed uint64 +func calculateRewardsAndGasUsed(rewardPercentiles []float64, txGasRewards gasRewardSorter) ([]ethtypes.EthBigInt, int64) { + var gasUsedTotal int64 for _, tx := range txGasRewards { - totalGasUsed += tx.gas + gasUsedTotal += tx.gasUsed } rewards := make([]ethtypes.EthBigInt, len(rewardPercentiles)) @@ -2370,23 +2373,23 @@ func calculateRewardsAndGasUsed(rewardPercentiles []float64, txGasRewards gasRew } if len(txGasRewards) == 0 { - return rewards, totalGasUsed + return rewards, gasUsedTotal } sort.Stable(txGasRewards) var idx int - var sum uint64 + var sum int64 for i, percentile := range rewardPercentiles { - threshold := uint64(float64(totalGasUsed) * percentile / 100) + threshold := int64(float64(gasUsedTotal) * percentile / 100) for sum < threshold && idx < len(txGasRewards)-1 { - sum += txGasRewards[idx].gas + sum += txGasRewards[idx].gasUsed idx++ } - rewards[i] = txGasRewards[idx].reward + rewards[i] = ethtypes.EthBigInt(txGasRewards[idx].premium) } - return rewards, totalGasUsed + return rewards, gasUsedTotal } func getSignedMessage(ctx context.Context, cs *store.ChainStore, msgCid cid.Cid) (*types.SignedMessage, error) { @@ -2409,8 +2412,8 @@ func getSignedMessage(ctx context.Context, cs *store.ChainStore, msgCid cid.Cid) } type gasRewardTuple struct { - gas uint64 - reward ethtypes.EthBigInt + gasUsed int64 + premium abi.TokenAmount } // sorted in ascending order @@ -2421,5 +2424,5 @@ func (g gasRewardSorter) Swap(i, j int) { g[i], g[j] = g[j], g[i] } func (g gasRewardSorter) Less(i, j int) bool { - return g[i].reward.Int.Cmp(g[j].reward.Int) == -1 + return g[i].premium.Int.Cmp(g[j].premium.Int) == -1 } diff --git a/node/impl/full/eth_test.go b/node/impl/full/eth_test.go index 4cf3b5c76..87c0852fb 100644 --- a/node/impl/full/eth_test.go +++ b/node/impl/full/eth_test.go @@ -117,11 +117,8 @@ func TestReward(t *testing.T) { {maxFeePerGas: big.NewInt(50), maxPriorityFeePerGas: big.NewInt(200), answer: big.NewInt(-50)}, } for _, tc := range testcases { - tx := ethtypes.EthTx{ - MaxFeePerGas: ethtypes.EthBigInt(tc.maxFeePerGas), - MaxPriorityFeePerGas: ethtypes.EthBigInt(tc.maxPriorityFeePerGas), - } - reward := tx.Reward(baseFee) + msg := &types.Message{GasFeeCap: tc.maxFeePerGas, GasPremium: tc.maxPriorityFeePerGas} + reward := msg.EffectiveGasPremium(baseFee) require.Equal(t, 0, reward.Int.Cmp(tc.answer.Int), reward, tc.answer) } } @@ -140,20 +137,20 @@ func TestRewardPercentiles(t *testing.T) { { percentiles: []float64{25, 50, 75, 100}, txGasRewards: []gasRewardTuple{ - {gas: uint64(0), reward: ethtypes.EthBigInt(big.NewInt(300))}, - {gas: uint64(100), reward: ethtypes.EthBigInt(big.NewInt(200))}, - {gas: uint64(350), reward: ethtypes.EthBigInt(big.NewInt(100))}, - {gas: uint64(500), reward: ethtypes.EthBigInt(big.NewInt(600))}, - {gas: uint64(300), reward: ethtypes.EthBigInt(big.NewInt(700))}, + {gasUsed: int64(0), premium: big.NewInt(300)}, + {gasUsed: int64(100), premium: big.NewInt(200)}, + {gasUsed: int64(350), premium: big.NewInt(100)}, + {gasUsed: int64(500), premium: big.NewInt(600)}, + {gasUsed: int64(300), premium: big.NewInt(700)}, }, answer: []int64{200, 700, 700, 700}, }, } for _, tc := range testcases { rewards, totalGasUsed := calculateRewardsAndGasUsed(tc.percentiles, tc.txGasRewards) - gasUsed := uint64(0) + var gasUsed int64 for _, tx := range tc.txGasRewards { - gasUsed += tx.gas + gasUsed += tx.gasUsed } ans := []ethtypes.EthBigInt{} for _, bi := range tc.answer { From 9412753ba33c4bba7dd5df34a1cd5f9a34464e4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Kripalani?= Date: Sat, 11 Mar 2023 23:03:17 +0000 Subject: [PATCH 68/70] Eth API: drop support for 'pending' block parameter. After transitioning from using StateCompute to loading receipts, we can no longer handle the 'pending' block without forcing computation. Eth Core Devs are evaluating a proposal to remove support on their end too. --- node/impl/full/eth.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index 12e761fa8..0528c2176 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -234,14 +234,13 @@ func (a *EthModule) EthGetBlockByHash(ctx context.Context, blkHash ethtypes.EthH } func (a *EthModule) parseBlkParam(ctx context.Context, blkParam string, strict bool) (tipset *types.TipSet, err error) { - if blkParam == "earliest" { - return nil, fmt.Errorf("block param \"earliest\" is not supported") + switch blkParam { + case "earliest", "pending": + return nil, fmt.Errorf("block param %q is not supported", blkParam) } head := a.Chain.GetHeaviestTipSet() switch blkParam { - case "pending": - return head, nil case "latest": parent, err := a.Chain.GetTipSetFromKey(ctx, head.Parents()) if err != nil { From 5a4b5ff97e60e1f654839a39dad2697cab3b9a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Kripalani?= Date: Sun, 12 Mar 2023 00:44:49 +0000 Subject: [PATCH 69/70] remove superfluous filter. --- node/impl/full/eth.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/node/impl/full/eth.go b/node/impl/full/eth.go index 0528c2176..c71711fb0 100644 --- a/node/impl/full/eth.go +++ b/node/impl/full/eth.go @@ -710,10 +710,6 @@ func (a *EthModule) EthFeeHistory(ctx context.Context, p jsonrpc.RawParams) (eth txGasRewards := gasRewardSorter{} for i, msg := range msgs { - if msg.VMMessage().From == builtintypes.SystemActorAddr { - continue - } - effectivePremium := msg.VMMessage().EffectiveGasPremium(basefee) txGasRewards = append(txGasRewards, gasRewardTuple{ premium: effectivePremium, From 571a84b39075947aeebc2d377c2576a9240fc8cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Kripalani?= Date: Sun, 12 Mar 2023 00:45:02 +0000 Subject: [PATCH 70/70] drop irrelevant test. --- itests/eth_conformance_test.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/itests/eth_conformance_test.go b/itests/eth_conformance_test.go index 8a367d6b1..4d8f5c3dd 100644 --- a/itests/eth_conformance_test.go +++ b/itests/eth_conformance_test.go @@ -236,14 +236,6 @@ func TestEthOpenRPCConformance(t *testing.T) { skipReason: "earliest block is not supported", }, - { - method: "eth_getBlockByNumber", - variant: "pending", - call: func(a *ethAPIRaw) (json.RawMessage, error) { - return ethapi.EthGetBlockByNumber(context.Background(), "pending", true) - }, - }, - { method: "eth_getBlockByNumber", call: func(a *ethAPIRaw) (json.RawMessage, error) {